Selaa lähdekoodia

HUE-965 [useradmin] Improve user experience

Only superuser can access the Useradmin app.
A normal user can still modify some of its information through the logout dropdown.
Adding 'view access' permissions: perms of this format e.g. access_view:useradmin:edit_user
enable user to access the view 'edit_user' in the app 'useradmin'.
Until we get Django 1.3 and resolve returning the URL name, we just do a match of the name of the view.

Also:
Move forms into forms.py
Improve add/edit user forms
Split 'add user/add ldap user' templates
Adding proper HTTP return code to PopUpException in Useradmin
Romain Rigaux 13 vuotta sitten
vanhempi
commit
b3b40941cf

+ 282 - 0
apps/useradmin/src/useradmin/forms.py

@@ -0,0 +1,282 @@
+#!/usr/bin/env python
+# Licensed to Cloudera, Inc. under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  Cloudera, Inc. licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import logging
+import re
+
+import django.contrib.auth.forms
+from django import forms
+from django.contrib.auth.models import User, Group
+from django.forms.util import ErrorList
+from django.utils.translation import ugettext as _
+
+from desktop.lib.django_util import get_username_re_rule, get_groupname_re_rule
+
+from useradmin.models import GroupPermission, HuePermission
+from useradmin.models import get_default_user_group
+
+
+
+LOG = logging.getLogger(__name__)
+
+
+class UserChangeForm(django.contrib.auth.forms.UserChangeForm):
+  """
+  This is similar, but not quite the same as djagno.contrib.auth.forms.UserChangeForm
+  and UserCreationForm.
+  """
+  username = forms.RegexField(
+      label=_("Username"),
+      max_length=30,
+      regex='^%s$' % (get_username_re_rule(),),
+      help_text = _("Required. 30 characters or fewer. No whitespaces or colons."),
+      error_messages = {'invalid': _("Whitespaces and ':' not allowed") })
+  password1 = forms.CharField(label=_("Password"), widget=forms.PasswordInput, required=False)
+  password2 = forms.CharField(label=_("Password confirmation"), widget=forms.PasswordInput, required=False)
+  ensure_home_directory = forms.BooleanField(label=_("Create home directory"),
+                                            help_text=_("Create home directory if one doesn't already exist."),
+                                            initial=True,
+                                            required=False)
+
+  class Meta(django.contrib.auth.forms.UserChangeForm.Meta):
+    fields = ["username", "first_name", "last_name", "email", "ensure_home_directory"]
+
+  def clean_password2(self):
+    password1 = self.cleaned_data.get("password1", "")
+    password2 = self.cleaned_data["password2"]
+    if password1 != password2:
+      raise forms.ValidationError(_("Passwords do not match."))
+    return password2
+
+  def clean_password1(self):
+    password = self.cleaned_data.get("password1", "")
+    if self.instance.id is None and password == "":
+      raise forms.ValidationError(_("You must specify a password when creating a new user."))
+    return self.cleaned_data.get("password1", "")
+
+  def save(self, commit=True):
+    """
+    Update password if it's set.
+    """
+    user = super(UserChangeForm, self).save(commit=False)
+    if self.cleaned_data["password1"]:
+      user.set_password(self.cleaned_data["password1"])
+    if commit:
+      user.save()
+      # groups must be saved after the user
+      self.save_m2m()
+    return user
+
+class SuperUserChangeForm(UserChangeForm):
+  class Meta(UserChangeForm.Meta):
+    fields = ["username", "is_active"] + UserChangeForm.Meta.fields + ["is_superuser", "groups"]
+
+  def __init__(self, *args, **kwargs):
+    super(SuperUserChangeForm, self).__init__(*args, **kwargs)
+    if self.instance.id:
+      # If the user exists already, we'll use its current group memberships
+      self.initial['groups'] = set(self.instance.groups.all())
+    else:
+      # If his is a new user, suggest the default group
+      default_group = get_default_user_group()
+      if default_group is not None:
+        self.initial['groups'] = set([default_group])
+      else:
+        self.initial['groups'] = []
+
+
+class AddLdapUserForm(forms.Form):
+  username = forms.RegexField(
+      label=_("Username"),
+      max_length=64,
+      regex='^%s$' % (get_username_re_rule(),),
+      help_text=_("Required. 30 characters or fewer. No whitespaces or colons."),
+      error_messages={'invalid': _("Whitespaces and ':' not allowed")})
+  dn = forms.BooleanField(label=_("Distinguished name"),
+                          help_text=_("Whether or not the user should be imported by "
+                                    "distinguished name."),
+                          initial=False,
+                          required=False)
+  ensure_home_directory = forms.BooleanField(label=_("Create home directory"),
+                                            help_text=_("Create home directory for user if one doesn't already exist."),
+                                            initial=True,
+                                            required=False)
+
+  def clean(self):
+    cleaned_data = super(AddLdapUserForm, self).clean()
+    username = cleaned_data.get("username")
+    dn = cleaned_data.get("dn")
+
+    if not dn:
+      if username is not None and len(username) > 30:
+        msg = _('Too long: 30 characters or fewer and not %(username)s') % dict(username=len(username),)
+        errors = self._errors.setdefault('username', ErrorList())
+        errors.append(msg)
+        raise forms.ValidationError(msg)
+
+    return cleaned_data
+
+
+class AddLdapGroupForm(forms.Form):
+  name = forms.RegexField(
+      label="Name",
+      max_length=64,
+      regex='^%s$' % (get_groupname_re_rule(),),
+      help_text=_("Required. 30 characters or fewer. May only contain letters, "
+                "numbers, hyphens or underscores."),
+      error_messages={'invalid': _("Whitespaces and ':' not allowed") })
+  dn = forms.BooleanField(label=_("Distinguished name"),
+                          help_text=_("Whether or not the group should be imported by "
+                                    "distinguished name."),
+                          initial=False,
+                          required=False)
+  import_members = forms.BooleanField(label=_('Import new members'),
+                                      help_text=_('Import unimported or new users from the group.'),
+                                      initial=False,
+                                      required=False)
+  ensure_home_directories = forms.BooleanField(label=_('Create home directories'),
+                                                help_text=_('Create home directories for every member imported, if members are being imported.'),
+                                                initial=True,
+                                                required=False)
+
+  def clean(self):
+    cleaned_data = super(AddLdapGroupForm, self).clean()
+    name = cleaned_data.get("name")
+    dn = cleaned_data.get("dn")
+
+    if not dn:
+      if name is not None and len(name) > 30:
+        msg = _('Too long: 30 characters or fewer and not %(name)s') % dict(name=(len(name),))
+        errors = self._errors.setdefault('name', ErrorList())
+        errors.append(msg)
+        raise forms.ValidationError(msg)
+
+    return cleaned_data
+
+
+class GroupEditForm(forms.ModelForm):
+  """
+  Form to manipulate a group.  This manages the group name and its membership.
+  """
+  GROUPNAME = re.compile('^%s$' % get_groupname_re_rule())
+
+  class Meta:
+    model = Group
+    fields = ("name",)
+
+  def clean_name(self):
+    # Note that the superclass doesn't have a clean_name method.
+    data = self.cleaned_data["name"]
+    if not self.GROUPNAME.match(data):
+      raise forms.ValidationError(_("Group name may only contain letters, " +
+                                  "numbers, hyphens or underscores."))
+    return data
+
+  def __init__(self, *args, **kwargs):
+    super(GroupEditForm, self).__init__(*args, **kwargs)
+
+    if self.instance.id:
+      self.fields['name'].widget.attrs['readonly'] = True
+      initial_members = User.objects.filter(groups=self.instance).order_by('username')
+      initial_perms = HuePermission.objects.filter(grouppermission__group=self.instance).order_by('app','description')
+    else:
+      initial_members = []
+      initial_perms = []
+
+    self.fields["members"] = _make_model_field(_("members"), initial_members, User.objects.order_by('username'))
+    self.fields["permissions"] = _make_model_field(_("permissions"), initial_perms, HuePermission.objects.order_by('app','description'))
+
+  def _compute_diff(self, field_name):
+    current = set(self.fields[field_name].initial_objs)
+    updated = set(self.cleaned_data[field_name])
+    delete = current.difference(updated)
+    add = updated.difference(current)
+    return delete, add
+
+  def save(self):
+    super(GroupEditForm, self).save()
+    self._save_members()
+    self._save_permissions()
+
+  def _save_members(self):
+    delete_membership, add_membership = self._compute_diff("members")
+    for user in delete_membership:
+      user.groups.remove(self.instance)
+      user.save()
+    for user in add_membership:
+      user.groups.add(self.instance)
+      user.save()
+
+  def _save_permissions(self):
+    delete_permission, add_permission = self._compute_diff("permissions")
+    for perm in delete_permission:
+      GroupPermission.objects.get(group=self.instance, hue_permission=perm).delete()
+    for perm in add_permission:
+      GroupPermission.objects.create(group=self.instance, hue_permission=perm)
+
+
+class PermissionsEditForm(forms.ModelForm):
+  """
+  Form to manage the set of groups that have a particular permission.
+  """
+  def __init__(self, *args, **kwargs):
+    super(PermissionsEditForm, self).__init__(*args, **kwargs)
+
+    if self.instance.id:
+      initial_groups = Group.objects.filter(grouppermission__hue_permission=self.instance).order_by('name')
+    else:
+      initial_groups = []
+
+    self.fields["groups"] = _make_model_field(_("groups"), initial_groups, Group.objects.order_by('name'))
+
+  def _compute_diff(self, field_name):
+    current = set(self.fields[field_name].initial_objs)
+    updated = set(self.cleaned_data[field_name])
+    delete = current.difference(updated)
+    add = updated.difference(current)
+    return delete, add
+
+  def save(self):
+    self._save_permissions()
+
+  def _save_permissions(self):
+    delete_group, add_group = self._compute_diff("groups")
+    for group in delete_group:
+      GroupPermission.objects.get(group=group, hue_permission=self.instance).delete()
+    for group in add_group:
+      GroupPermission.objects.create(group=group, hue_permission=self.instance)
+
+
+def _make_model_field(label, initial, choices, multi=True):
+  """ Creates multiple choice field with given query object as choices. """
+  if multi:
+    field = forms.models.ModelMultipleChoiceField(choices, required=False)
+    field.initial_objs = initial
+    field.initial = [ obj.pk for obj in initial ]
+    field.label = label
+  else:
+    field = forms.models.ModelChoiceField(choices, required=False)
+    field.initial_obj = initial
+    if initial:
+      field.initial = initial.pk
+  return field
+
+class SyncLdapUsersGroupsForm(forms.Form):
+  ensure_home_directory = forms.BooleanField(label=_("Create Home Directories"),
+                                            help_text=_("Create home directory for every user, if one doesn't already exist."),
+                                            initial=True,
+                                            required=False)

+ 10 - 10
apps/useradmin/src/useradmin/migrations/0001_permissions_and_profiles.py

@@ -34,7 +34,7 @@ class Migration(DataMigration):
       return tuple(dependent_migrations)
 
     depends_on = classproperty(_depends_on)
-    
+
     def forwards(self, orm):
         """
         This migration has been customized to support upgrades from Cloudera
@@ -47,7 +47,7 @@ class Migration(DataMigration):
           db.delete_table('userman_grouprelations')
         except Exception:
          pass
-        
+
         try:
           db.rename_table('userman_userprofile', 'useradmin_userprofile')
           db.delete_column('useradmin_userprofile', 'primary_group_id')
@@ -64,7 +64,7 @@ class Migration(DataMigration):
               up.creation_method = UserProfile.CreationMethod.HUE
             up.save()
         except Exception:
-          db.rollback_transaction()  
+          db.rollback_transaction()
           db.start_transaction()
 
           # Adding model 'UserProfile'
@@ -83,7 +83,7 @@ class Migration(DataMigration):
           db.create_index('useradmin_grouppermission', ['group_id'])
           db.create_index('useradmin_grouppermission', ['hue_permission_id'])
         except Exception:
-          db.rollback_transaction()  
+          db.rollback_transaction()
           db.start_transaction()
 
           # Adding model 'GroupPermission'
@@ -99,7 +99,7 @@ class Migration(DataMigration):
         try:
           db.rename_table('userman_desktoppermission', 'useradmin_huepermission')
         except Exception:
-          db.rollback_transaction()  
+          db.rollback_transaction()
           db.start_transaction()
 
           # Adding model 'HuePermission'
@@ -112,9 +112,9 @@ class Migration(DataMigration):
           db.commit_transaction()
           db.start_transaction()
           db.send_create_signal('useradmin', ['HuePermission'])
-    
+
     def backwards(self, orm):
-        
+
         # Deleting model 'UserProfile'
         db.delete_table('useradmin_userprofile')
 
@@ -123,8 +123,8 @@ class Migration(DataMigration):
 
         # Deleting model 'HuePermission'
         db.delete_table('useradmin_huepermission')
-    
-    
+
+
     models = {
         'auth.group': {
             'Meta': {'object_name': 'Group'},
@@ -183,5 +183,5 @@ class Migration(DataMigration):
             'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'})
         }
     }
-    
+
     complete_apps = ['useradmin']

+ 8 - 8
apps/useradmin/src/useradmin/migrations/0002_add_ldap_support.py

@@ -9,7 +9,7 @@ from useradmin.models import create_profile_for_user
 from useradmin.models import UserProfile
 
 class Migration(DataMigration):
-    
+
     def forwards(self, orm):
         """
         This migration has been customized to support upgrades from Cloudera
@@ -19,7 +19,7 @@ class Migration(DataMigration):
           db.rename_table('userman_ldapgroup', 'useradmin_ldapgroup')
           db.delete_column('useradmin_ldapgroup', 'hidden')
         except Exception, e:
-          db.rollback_transaction()  
+          db.rollback_transaction()
           db.start_transaction()
 
           # Adding model 'LdapGroup'
@@ -37,23 +37,23 @@ class Migration(DataMigration):
           # table may have been migrated from Cloudera Enterprise, in which case
           # this column would already exist.
           pass
-    
+
         for user in User.objects.all():
           try:
             orm.UserProfile.objects.get(user=user)
           except orm.UserProfile.DoesNotExist:
             create_profile_for_user(user)
 
-    
+
     def backwards(self, orm):
-        
+
         # Deleting model 'LdapGroup'
         db.delete_table('useradmin_ldapgroup')
 
         # Deleting field 'UserProfile.creation_method'
         db.delete_column('useradmin_userprofile', 'creation_method')
-    
-    
+
+
     models = {
         'auth.group': {
             'Meta': {'object_name': 'Group'},
@@ -118,5 +118,5 @@ class Migration(DataMigration):
             'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'})
         }
     }
-    
+
     complete_apps = ['useradmin']

+ 3 - 2
apps/useradmin/src/useradmin/models.py

@@ -160,7 +160,7 @@ models.signals.post_save.connect(create_user_signal_handler, sender=auth_models.
 
 class LdapGroup(models.Model):
   """
-  Groups that come from LDAP originally will have an LdapGroup 
+  Groups that come from LDAP originally will have an LdapGroup
   record generated at creation time.
   """
   group = models.ForeignKey(auth_models.Group, related_name="group")
@@ -261,7 +261,8 @@ def update_app_permissions(**kwargs):
     default_group = get_default_user_group()
     if default_group:
       for new_dp in added:
-        GroupPermission.objects.create(group=default_group, hue_permission=new_dp)
+        if not (new_dp.app == 'useradmin' and new_dp.action == 'access'):
+          GroupPermission.objects.create(group=default_group, hue_permission=new_dp)
 
     available = HuePermission.objects.count()
 

+ 5 - 0
apps/useradmin/src/useradmin/settings.py

@@ -13,8 +13,13 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
+
 DJANGO_APPS = [ "useradmin" ]
 NICE_NAME = "User Admin"
 REQUIRES_HADOOP = False
 ICON = "/useradmin/static/art/icon_useradmin_24.png"
 MENU_INDEX = 60
+
+PERMISSION_ACTIONS = (
+  ("access_view:useradmin:edit_user", "Access to profile page on User Admin"),
+)

+ 68 - 0
apps/useradmin/src/useradmin/templates/add_ldap_user.mako

@@ -0,0 +1,68 @@
+## Licensed to Cloudera, Inc. under one
+## or more contributor license agreements.  See the NOTICE file
+## distributed with this work for additional information
+## regarding copyright ownership.  Cloudera, Inc. licenses this file
+## to you under the Apache License, Version 2.0 (the
+## "License"); you may not use this file except in compliance
+## with the License.  You may obtain a copy of the License at
+##
+##     http://www.apache.org/licenses/LICENSE-2.0
+##
+## Unless required by applicable law or agreed to in writing, software
+## distributed under the License is distributed on an "AS IS" BASIS,
+## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+## See the License for the specific language governing permissions and
+## limitations under the License.
+<%!
+from desktop.views import commonheader, commonfooter
+from desktop.lib.django_util import extract_field_data
+import urllib
+from django.utils.translation import ugettext as _
+%>
+
+<%namespace name="layout" file="layout.mako" />
+
+${commonheader(_('Hue Users'), "useradmin", user, "100px")}
+${layout.menubar(section='users', _=_)}
+
+
+<div class="container-fluid">
+   <h1>${_('Hue Users - Add/Sync LDAP user')}</h1>
+
+    <br/>
+
+	<form id="editForm" method="POST" class="form form-horizontal">
+		<fieldset>
+		      % for field in form.fields:	
+                  % if form[field].is_hidden:
+                      ${ form[field] }
+                  % else:
+                      ${ layout.render_field(form[field]) }
+                  % endif	
+		      % endfor				
+		</fieldset>
+		<br/>
+		<div class="form-actions">
+			% if username:
+				<input type="submit" class="btn btn-primary" value="${_('Update user')}"/>
+			% else:
+			    <input type="submit" class="btn btn-primary" value="${_('Add/Sync user')}"/>
+			% endif
+			<a href="/useradmin/users" class="btn">${_('Cancel')}</a>
+		</div>
+	</form>
+</div>
+
+<script type="text/javascript" charset="utf-8">
+	$(document).ready(function(){
+		$("#id_groups").jHueSelector({
+            selectAllLabel: "${_('Select all')}",
+            searchPlaceholder: "${_('Search')}",
+            noChoicesFound: "${_('No groups found.')} <a href='${url('useradmin.views.edit_group')}'>${_('Create a new group now')} &raquo;</a>",
+            width:618,
+            height:240
+        });
+	});
+</script>
+
+${commonfooter(messages)}

+ 64 - 70
apps/useradmin/src/useradmin/templates/edit_user.mako

@@ -19,92 +19,86 @@ from desktop.lib.django_util import extract_field_data
 import urllib
 from django.utils.translation import ugettext as _
 %>
+
 <%namespace name="layout" file="layout.mako" />
 
 ${commonheader(_('Hue Users'), "useradmin", user, "100px")}
-${layout.menubar(section='users', _=_)}
 
-<%def name="render_field(field)">
-  %if not field.is_hidden:
-    <% group_class = len(field.errors) and "error" or "" %>
-    <div class="control-group ${group_class}">
-      <label class="control-label" for="id_${field.html_name}">${_(field.label)}</label>
-      <div class="controls">
-		${unicode(field) | n}
-        % if len(field.errors):
-          <span class="help-inline">${unicode(field.errors) | n}</span>
-        % endif
-      </div>
-    </div>
-  %endif
-</%def>
+% if user.is_superuser:
+  ${layout.menubar(section='users', _=_)}
+% endif
 
 <div class="container-fluid">
-	% if username:
-		<h1>${_('Hue Users - Edit user: %(username)s') % dict(username=username)}</h1>
-	% else:
-		% if ldap:
-			<h1>${_('Hue Users - Add/Sync LDAP user')}</h1>
-		% else:
-			<h1>${_('Hue Users - Create user')}</h1>
-		% endif
-	% endif
+    % if username:
+        <h1>${_('Hue Users - Edit user: %(username)s') % dict(username=username)}</h1>
+    % else:
+        <h1>${_('Hue Users - Create user')}</h1>
+    % endif
 
     <br/>
 
-	<form id="editForm" action="${urllib.quote(action)}" method="POST" class="form form-horizontal">
-		<fieldset>
-			% for field in form:
-                %if field.name == "username" and "password1" in form.fields:
-                    ${render_field(form["username"])}
-                    <div class="row">
-                        <div class="span5">
-                        ${render_field(form["password1"])}
-                        </div>
-                        <div class="span4">
-                        ${render_field(form["password2"])}
-                        </div>
-                    </div>
-                %elif field.name == "first_name":
-                    <div class="row">
-                        <div class="span5">
-                        ${render_field(form["first_name"])}
-                        </div>
-                        <div class="span4">
-                        ${render_field(form["last_name"])}
-                        </div>
-                    </div>
-                %elif field.name == "last_name" or field.name == "password1" or field.name == "password2":
-                    ## skip rendering
-                %else:
-				    ${render_field(field)}
-                %endif
-			% endfor
-		</fieldset>
-		<br/>
-		<div class="form-actions">
-			% if username:
-				<input type="submit" class="btn btn-primary" value="${_('Update user')}"/>
-			% else:
-				% if ldap:
-					<input type="submit" class="btn btn-primary" value="${_('Add/Sync user')}"/>
-				% else:
-					<input type="submit" class="btn btn-primary" value="${_('Add user')}"/>
-				% endif
-			% endif
-			<a href="/useradmin/users" class="btn">${_('Cancel')}</a>
-		</div>
-	</form>
+    <form id="editForm" method="POST" class="form form-horizontal">
+        <fieldset>
+            <h3>${ _('Information') }<h3>
+
+            ${layout.render_field(form["username"])}
+
+            % if "password1" in form.fields:
+            <div class="row">
+                <div class="span5">
+                ${layout.render_field(form["password1"])}
+                </div>
+                <div class="span4">
+                ${layout.render_field(form["password2"])}
+                </div>
+            </div>
+            % endif
+
+            <h3>${ _('Optional') }<h3>
+
+            % if "first_name" in form.fields:
+            <div class="row">
+                <div class="span5">
+                ${layout.render_field(form["first_name"])}
+                </div>
+                <div class="span4">
+                ${layout.render_field(form["last_name"])}
+                </div>
+            </div>
+            % endif
+
+            ${layout.render_field(form["email"])}
+            % if user.is_superuser:
+              ${layout.render_field(form["groups"])}
+              ${layout.render_field(form["is_active"])}
+            % endif
+            ${layout.render_field(form["ensure_home_directory"])}
+            % if user.is_superuser:
+              ${'is_superuser' in form.fields and layout.render_field(form["is_superuser"])}
+            % endif
+        </fieldset>
+        <br/>
+        <div class="form-actions">
+            % if username:
+                <input type="submit" class="btn btn-primary" value="${_('Update user')}"/>
+            % else:
+                <input type="submit" class="btn btn-primary" value="${_('Add user')}"/>
+            % endif
+            <a class="btn" onclick="history.back()">${ _('Cancel') }</a>
+        </div>
+    </form>
 </div>
+
 <script type="text/javascript" charset="utf-8">
-	$(document).ready(function(){
-		$("#id_groups").jHueSelector({
+    $(document).ready(function(){
+        $("#id_groups").jHueSelector({
             selectAllLabel: "${_('Select all')}",
             searchPlaceholder: "${_('Search')}",
             noChoicesFound: "${_('No groups found.')} <a href='${url('useradmin.views.edit_group')}'>${_('Create a new group now')} &raquo;</a>",
             width:618,
             height:240
         });
-	});
+    });
 </script>
+
 ${commonfooter(messages)}

+ 15 - 0
apps/useradmin/src/useradmin/templates/layout.mako

@@ -25,6 +25,21 @@ def is_selected(section, matcher):
     return ""
 %>
 
+<%def name="render_field(field)">
+  %if not field.is_hidden:
+    <% group_class = len(field.errors) and "error" or "" %>
+    <div class="control-group ${group_class}">
+      <label class="control-label" for="id_${field.html_name}">${field.label}</label>
+      <div class="controls">
+        ${unicode(field) | n}
+        % if len(field.errors):
+          <span class="help-inline">${unicode(field.errors) | n}</span>
+        % endif
+      </div>
+    </div>
+  %endif
+</%def>
+
 <%def name="menubar(section='', _=None)">
 	<div class="subnav subnav-fixed">
 		<div class="container-fluid">

+ 62 - 269
apps/useradmin/src/useradmin/views.py

@@ -14,56 +14,65 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
+
 """
 User management application.
 """
-import re
+
 import pwd
 import grp
 import logging
 import threading
 import subprocess
 
-import django.contrib.auth.forms
-from django import forms
+import ldap_access
+
 from django.contrib.auth.models import User, Group
-from desktop.lib.django_util import get_username_re_rule, get_groupname_re_rule, render
+from desktop.lib.django_util import render
 from desktop.lib.exceptions_renderable import PopupException
 from django.core.urlresolvers import reverse
+from django.utils.translation import ugettext as _
 from django.forms.util import ErrorList
 from django.shortcuts import redirect
 
 from hadoop.fs.exceptions import WebHdfsException
-from useradmin.models import GroupPermission, HuePermission, UserProfile, LdapGroup
+from useradmin.models import HuePermission, UserProfile, LdapGroup
 from useradmin.models import get_profile, get_default_user_group
-import ldap_access
+from useradmin.forms import SyncLdapUsersGroupsForm, AddLdapGroupForm,\
+  AddLdapUserForm, PermissionsEditForm, GroupEditForm, SuperUserChangeForm,\
+  UserChangeForm
 
-from django.utils.translation import ugettext as _
 
 LOG = logging.getLogger(__name__)
 
 __users_lock = threading.Lock()
 __groups_lock = threading.Lock()
 
+
+
 def list_users(request):
   return render("list_users.mako", request, dict(users=User.objects.all(), request=request))
 
+
 def list_groups(request):
   return render("list_groups.mako", request, dict(groups=Group.objects.all()))
 
+
 def list_permissions(request):
   return render("list_permissions.mako", request, dict(permissions=HuePermission.objects.all()))
 
+
 def delete_user(request, username):
   if not request.user.is_superuser:
-    raise PopupException(_("You must be a superuser to delete users."))
+    raise PopupException(_("You must be a superuser to delete users."), error_code=401)
+
   if request.method == 'POST':
     try:
       global __users_lock
       __users_lock.acquire()
       try:
         if username == request.user.username:
-          raise PopupException(_("You cannot remove yourself."))
+          raise PopupException(_("You cannot remove yourself."), error_code=401)
         user = User.objects.get(username=username)
         user_profile = UserProfile.objects.get(user=user)
         user_profile.delete()
@@ -74,13 +83,15 @@ def delete_user(request, username):
       request.info(_('The user was deleted.'))
       return redirect(reverse(list_users))
     except User.DoesNotExist:
-      raise PopupException(_("User not found."))
+      raise PopupException(_("User not found."), error_code=404)
   else:
     return render("delete_user.mako", request, dict(path=request.path, username=username))
 
+
 def delete_group(request, name):
   if not request.user.is_superuser:
-    raise PopupException(_("You must be a superuser to delete groups."))
+    raise PopupException(_("You must be a superuser to delete groups."), error_code=401)
+
   if request.method == 'POST':
     try:
       global groups_lock
@@ -92,7 +103,7 @@ def delete_group(request, name):
         default_group = get_default_user_group()
         group = Group.objects.get(name=name)
         if default_group is not None and default_group.name == name:
-          raise PopupException(_("The default user group may not be deleted."))
+          raise PopupException(_("The default user group may not be deleted."), error_code=401)
         group.delete()
       finally:
         __groups_lock.release()
@@ -100,72 +111,10 @@ def delete_group(request, name):
       request.info(_('The group was deleted.'))
       return redirect(reverse(list_groups))
     except Group.DoesNotExist:
-      raise PopupException(_("Group not found."))
+      raise PopupException(_("Group not found."), error_code=404)
   else:
     return render("delete_group.mako", request, dict(path=request.path, groupname=name))
 
-class UserChangeForm(django.contrib.auth.forms.UserChangeForm):
-  """
-  This is similar, but not quite the same as djagno.contrib.auth.forms.UserChangeForm
-  and UserCreationForm.
-  """
-  username = forms.RegexField(
-      label=_("Username"),
-      max_length=30,
-      regex='^%s$' % (get_username_re_rule(),),
-      help_text = _("Required. 30 characters or fewer. No whitespaces or colons."),
-      error_messages = {'invalid': _("Whitespaces and ':' not allowed") })
-  password1 = forms.CharField(label=_("Password"), widget=forms.PasswordInput, required=False)
-  password2 = forms.CharField(label=_("Password confirmation"), widget=forms.PasswordInput, required=False)
-  ensure_home_directory = forms.BooleanField(label=_("Create home directory"),
-                                            help_text=_("Create home directory if one doesn't already exist."),
-                                            initial=True,
-                                            required=False)
-
-  class Meta(django.contrib.auth.forms.UserChangeForm.Meta):
-    fields = ["username", "first_name", "last_name", "email", "ensure_home_directory"]
-
-  def clean_password2(self):
-    password1 = self.cleaned_data.get("password1", "")
-    password2 = self.cleaned_data["password2"]
-    if password1 != password2:
-      raise forms.ValidationError(_("Passwords do not match."))
-    return password2
-
-  def clean_password1(self):
-    password = self.cleaned_data.get("password1", "")
-    if self.instance.id is None and password == "":
-      raise forms.ValidationError(_("You must specify a password when creating a new user."))
-    return self.cleaned_data.get("password1", "")
-
-  def save(self, commit=True):
-    """
-    Update password if it's set.
-    """
-    user = super(UserChangeForm, self).save(commit=False)
-    if self.cleaned_data["password1"]:
-      user.set_password(self.cleaned_data["password1"])
-    if commit:
-      user.save()
-      # groups must be saved after the user
-      self.save_m2m()
-    return user
-
-class SuperUserChangeForm(UserChangeForm):
-  class Meta(UserChangeForm.Meta):
-    fields = ["username", "is_active"] + UserChangeForm.Meta.fields + ["is_superuser", "groups"]
-  def __init__(self, *args, **kwargs):
-    super(SuperUserChangeForm, self).__init__(*args, **kwargs)
-    if self.instance.id:
-      # If the user exists already, we'll use its current group memberships
-      self.initial['groups'] = set(self.instance.groups.all())
-    else:
-      # If his is a new user, suggest the default group
-      default_group = get_default_user_group()
-      if default_group is not None:
-        self.initial['groups'] = set([default_group])
-      else:
-        self.initial['groups'] = []
 
 def edit_user(request, username=None):
   """
@@ -177,7 +126,8 @@ def edit_user(request, username=None):
   @param username:      Default to None, when creating a new user
   """
   if request.user.username != username and not request.user.is_superuser:
-    raise PopupException(_("You must be a superuser to add or edit another user."))
+    raise PopupException(_("You must be a superuser to add or edit another user."), error_code=401)
+
   if username is not None:
     instance = User.objects.get(username=username)
   else:
@@ -201,7 +151,7 @@ def edit_user(request, username=None):
         # (3) The last active superuser cannot demote/inactivate himself.
         #
         if request.user.username == username and not form.instance.is_active:
-          raise PopupException(_("You cannot make yourself inactive."))
+          raise PopupException(_("You cannot make yourself inactive."), error_code=401)
 
         global __users_lock
         __users_lock.acquire()
@@ -213,7 +163,7 @@ def edit_user(request, username=None):
               _check_remove_last_super(orig)
           else:
             if form.instance.is_superuser and not request.user.is_superuser:
-              raise PopupException(_("You cannot make yourself a superuser."))
+              raise PopupException(_("You cannot make yourself a superuser."), error_code=401)
 
           # All ok
           form.save()
@@ -227,7 +177,10 @@ def edit_user(request, username=None):
           ensure_home_directory(request.fs, instance.username)
         except (IOError, WebHdfsException), e:
           request.error(_('Cannot make home directory for user %s.' % instance.username))
-      return redirect(reverse(list_users))
+      if request.user.is_superuser:
+        return redirect(reverse(list_users))
+      else:
+        return redirect(reverse(edit_user, kwargs={'username': username}))
   else:
     default_user_group = get_default_user_group()
     initial = {
@@ -235,7 +188,9 @@ def edit_user(request, username=None):
       'groups': default_user_group and [default_user_group] or []
     }
     form = form_class(instance=instance, initial=initial)
-  return render('edit_user.mako', request, dict(form=form, action=request.path, username=username))
+
+  return render('edit_user.mako', request, dict(form=form, username=username))
+
 
 def edit_group(request, name=None):
   """
@@ -249,7 +204,7 @@ def edit_group(request, name=None):
   Only superusers may create a group
   """
   if not request.user.is_superuser:
-    raise PopupException(_("You must be a superuser to add or edit a group."))
+    raise PopupException(_("You must be a superuser to add or edit a group."), error_code=401)
 
   if name is not None:
     instance = Group.objects.get(name=name)
@@ -265,8 +220,10 @@ def edit_group(request, name=None):
 
   else:
     form = GroupEditForm(instance=instance)
+
   return render('edit_group.mako', request, dict(form=form, action=request.path, name=name))
 
+
 def edit_permission(request, app=None, priv=None):
   """
   edit_permission(request, app = None, priv = None) -> reply
@@ -281,7 +238,7 @@ def edit_permission(request, app=None, priv=None):
   Only superusers may modify permissions
   """
   if not request.user.is_superuser:
-    raise PopupException(_("You must be a superuser to change permissions."))
+    raise PopupException(_("You must be a superuser to change permissions."), error_code=401)
 
   instance = HuePermission.objects.get(app=app, action=priv)
 
@@ -294,39 +251,9 @@ def edit_permission(request, app=None, priv=None):
 
   else:
     form = PermissionsEditForm(instance=instance)
-  return render('edit_permissions.mako', request,
-    dict(form=form, action=request.path, app=app, priv=priv))
-
-class AddLdapUserForm(forms.Form):
-  username = forms.RegexField(
-      label=_("Username"),
-      max_length=64,
-      regex='^%s$' % (get_username_re_rule(),),
-      help_text=_("Required. 30 characters or fewer. No whitespaces or colons."),
-      error_messages={'invalid': _("Whitespaces and ':' not allowed")})
-  dn = forms.BooleanField(label=_("Distinguished name"),
-                          help_text=_("Whether or not the user should be imported by "
-                                    "distinguished name."),
-                          initial=False,
-                          required=False)
-  ensure_home_directory = forms.BooleanField(label=_("Create home directory"),
-                                            help_text=_("Create home directory for user if one doesn't already exist."),
-                                            initial=True,
-                                            required=False)
-
-  def clean(self):
-    cleaned_data = super(AddLdapUserForm, self).clean()
-    username = cleaned_data.get("username")
-    dn = cleaned_data.get("dn")
-
-    if not dn:
-      if username is not None and len(username) > 30:
-        msg = _('Too long: 30 characters or fewer and not %(username)s') % dict(username=len(username),)
-        errors = self._errors.setdefault('username', ErrorList())
-        errors.append(msg)
-        raise forms.ValidationError(msg)
-
-    return cleaned_data
+
+  return render('edit_permissions.mako', request, dict(form=form, action=request.path, app=app, priv=priv))
+
 
 def add_ldap_user(request):
   """
@@ -338,7 +265,7 @@ def add_ldap_user(request):
   If the LDAP request failed, the error message is generic right now.
   """
   if not request.user.is_superuser:
-    raise PopupException(_("You must be a superuser to add another user."))
+    raise PopupException(_("You must be a superuser to add another user."), error_code=401)
 
   if request.method == 'POST':
     form = AddLdapUserForm(request.POST)
@@ -359,43 +286,9 @@ def add_ldap_user(request):
         return redirect(reverse(list_users))
   else:
     form = AddLdapUserForm()
-  return render('edit_user.mako', request, dict(form=form, action=request.path, ldap=True))
-
-class AddLdapGroupForm(forms.Form):
-  name = forms.RegexField(
-      label="Name",
-      max_length=64,
-      regex='^%s$' % (get_groupname_re_rule(),),
-      help_text=_("Required. 30 characters or fewer. May only contain letters, "
-                "numbers, hyphens or underscores."),
-      error_messages={'invalid': _("Whitespaces and ':' not allowed") })
-  dn = forms.BooleanField(label=_("Distinguished name"),
-                          help_text=_("Whether or not the group should be imported by "
-                                    "distinguished name."),
-                          initial=False,
-                          required=False)
-  import_members = forms.BooleanField(label=_('Import new members'),
-                                      help_text=_('Import unimported or new users from the group.'),
-                                      initial=False,
-                                      required=False)
-  ensure_home_directories = forms.BooleanField(label=_('Create home directories'),
-                                                help_text=_('Create home directories for every member imported, if members are being imported.'),
-                                                initial=True,
-                                                required=False)
-
-  def clean(self):
-    cleaned_data = super(AddLdapGroupForm, self).clean()
-    name = cleaned_data.get("name")
-    dn = cleaned_data.get("dn")
-
-    if not dn:
-      if name is not None and len(name) > 30:
-        msg = _('Too long: 30 characters or fewer and not %(name)s') % dict(name=(len(name),))
-        errors = self._errors.setdefault('name', ErrorList())
-        errors.append(msg)
-        raise forms.ValidationError(msg)
-
-    return cleaned_data
+
+  return render('add_ldap_user.mako', request, dict(form=form))
+
 
 def add_ldap_group(request):
   """
@@ -408,7 +301,7 @@ def add_ldap_group(request):
   all unimported users.
   """
   if not request.user.is_superuser:
-    raise PopupException(_("You must be a superuser to add another group."))
+    raise PopupException(_("You must be a superuser to add another group."), error_code=401)
 
   if request.method == 'POST':
     form = AddLdapGroupForm(request.POST)
@@ -425,8 +318,10 @@ def add_ldap_group(request):
         return redirect(reverse(list_groups))
   else:
     form = AddLdapGroupForm()
+
   return render('edit_group.mako', request, dict(form=form, action=request.path, ldap=True))
 
+
 def sync_ldap_users_groups(request):
   """
   Handler for syncing the Hue database with LDAP users and groups.
@@ -436,7 +331,7 @@ def sync_ldap_users_groups(request):
   server's current state.
   """
   if not request.user.is_superuser:
-    raise PopupException(_("You must be a superuser to sync the LDAP users/groups."))
+    raise PopupException(_("You must be a superuser to sync the LDAP users/groups."), error_code=401)
 
   if request.method == 'POST':
     form = SyncLdapUsersGroupsForm(request.POST)
@@ -452,10 +347,12 @@ def sync_ldap_users_groups(request):
           except (IOError, WebHdfsException), e:
             raise PopupException(_("The import may not be complete, sync again."), detail=e)
       return redirect(reverse(list_users))
+  else:
+    form = SyncLdapUsersGroupsForm()
 
-  form = SyncLdapUsersGroupsForm()
   return render("sync_ldap_users_groups.mako", request, dict(path=request.path, form=form))
 
+
 def ensure_home_directory(fs, username):
   """
   Adds a users home directory if it doesn't already exist.
@@ -465,6 +362,7 @@ def ensure_home_directory(fs, username):
   home_dir = '/user/%s' % username
   fs.do_as_user(username, fs.create_home_dir, home_dir)
 
+
 def _check_remove_last_super(user_obj):
   """Raise an error if we're removing the last superuser"""
   if not user_obj.is_superuser:
@@ -476,8 +374,8 @@ def _check_remove_last_super(user_obj):
   num_active_su = all_active_su.count()
   assert num_active_su >= 1, _("No active superuser configured.")
   if num_active_su == 1:
-    raise PopupException(_("You cannot remove the last active "
-                         "superuser from the configuration."))
+    raise PopupException(_("You cannot remove the last active superuser from the configuration."), error_code=401)
+
 
 def sync_unix_users_and_groups(min_uid, max_uid, min_gid, max_gid, check_shell):
   """
@@ -539,6 +437,7 @@ def sync_unix_users_and_groups(min_uid, max_uid, min_gid, max_gid, check_shell):
   __users_lock.release()
   __groups_lock.release()
 
+
 def _import_ldap_user(username, import_by_dn=False):
   """
   Import a user from LDAP. If import_by_dn is true, this will import the user by
@@ -574,6 +473,7 @@ def _import_ldap_user(username, import_by_dn=False):
 
   return user
 
+
 def _import_ldap_group(groupname, import_members=False, import_by_dn=False):
   """
   Import a group from LDAP. If import_members is true, this will also import any
@@ -615,12 +515,15 @@ def _import_ldap_group(groupname, import_members=False, import_by_dn=False):
   group.save()
   return group
 
+
 def import_ldap_user(user, import_by_dn):
   return _import_ldap_user(user, import_by_dn)
 
+
 def import_ldap_group(group, import_members, import_by_dn):
   return _import_ldap_group(group, import_members, import_by_dn)
 
+
 def sync_ldap_users():
   """
   Syncs LDAP user information. This will not import new
@@ -633,6 +536,7 @@ def sync_ldap_users():
     _import_ldap_user(user.username)
   return users
 
+
 def sync_ldap_groups():
   """
   Syncs LDAP group memberships. This will not import new
@@ -644,114 +548,3 @@ def sync_ldap_groups():
   for group in groups:
     _import_ldap_group(group.name)
   return groups
-
-class GroupEditForm(forms.ModelForm):
-  """
-  Form to manipulate a group.  This manages the group name and its membership.
-  """
-  GROUPNAME = re.compile('^%s$' % get_groupname_re_rule())
-
-  class Meta:
-    model = Group
-    fields = ("name",)
-
-  def clean_name(self):
-    # Note that the superclass doesn't have a clean_name method.
-    data = self.cleaned_data["name"]
-    if not self.GROUPNAME.match(data):
-      raise forms.ValidationError(_("Group name may only contain letters, " +
-                                  "numbers, hyphens or underscores."))
-    return data
-
-  def __init__(self, *args, **kwargs):
-    super(GroupEditForm, self).__init__(*args, **kwargs)
-
-    if self.instance.id:
-      self.fields['name'].widget.attrs['readonly'] = True
-      initial_members = User.objects.filter(groups=self.instance).order_by('username')
-      initial_perms = HuePermission.objects.filter(grouppermission__group=self.instance).order_by('app','description')
-    else:
-      initial_members = []
-      initial_perms = []
-
-    self.fields["members"] = _make_model_field(_("members"), initial_members, User.objects.order_by('username'))
-    self.fields["permissions"] = _make_model_field(_("permissions"), initial_perms, HuePermission.objects.order_by('app','description'))
-
-  def _compute_diff(self, field_name):
-    current = set(self.fields[field_name].initial_objs)
-    updated = set(self.cleaned_data[field_name])
-    delete = current.difference(updated)
-    add = updated.difference(current)
-    return delete, add
-
-  def save(self):
-    super(GroupEditForm, self).save()
-    self._save_members()
-    self._save_permissions()
-
-  def _save_members(self):
-    delete_membership, add_membership = self._compute_diff("members")
-    for user in delete_membership:
-      user.groups.remove(self.instance)
-      user.save()
-    for user in add_membership:
-      user.groups.add(self.instance)
-      user.save()
-
-  def _save_permissions(self):
-    delete_permission, add_permission = self._compute_diff("permissions")
-    for perm in delete_permission:
-      GroupPermission.objects.get(group=self.instance, hue_permission=perm).delete()
-    for perm in add_permission:
-      GroupPermission.objects.create(group=self.instance, hue_permission=perm)
-
-class PermissionsEditForm(forms.ModelForm):
-  """
-  Form to manage the set of groups that have a particular permission.
-  """
-  def __init__(self, *args, **kwargs):
-    super(PermissionsEditForm, self).__init__(*args, **kwargs)
-
-    if self.instance.id:
-      initial_groups = Group.objects.filter(grouppermission__hue_permission=self.instance).order_by('name')
-    else:
-      initial_groups = []
-
-    self.fields["groups"] = _make_model_field(_("groups"), initial_groups, Group.objects.order_by('name'))
-
-  def _compute_diff(self, field_name):
-    current = set(self.fields[field_name].initial_objs)
-    updated = set(self.cleaned_data[field_name])
-    delete = current.difference(updated)
-    add = updated.difference(current)
-    return delete, add
-
-  def save(self):
-    self._save_permissions()
-
-  def _save_permissions(self):
-    delete_group, add_group = self._compute_diff("groups")
-    for group in delete_group:
-      GroupPermission.objects.get(group=group, hue_permission=self.instance).delete()
-    for group in add_group:
-      GroupPermission.objects.create(group=group, hue_permission=self.instance)
-
-def _make_model_field(label, initial, choices, multi=True):
-  """ Creates multiple choice field with given query object as choices. """
-  if multi:
-    field = forms.models.ModelMultipleChoiceField(choices, required=False)
-    field.initial_objs = initial
-    field.initial = [ obj.pk for obj in initial ]
-    field.label = label
-  else:
-    field = forms.models.ModelChoiceField(choices, required=False)
-    field.initial_obj = initial
-    if initial:
-      field.initial = initial.pk
-  return field
-
-class SyncLdapUsersGroupsForm(forms.Form):
-  ensure_home_directory = forms.BooleanField(label=_("Create Home Directories"),
-                                            help_text=_("Create home directory for every user, if one doesn't already exist."),
-                                            initial=True,
-                                            required=False)

+ 16 - 7
desktop/core/src/desktop/middleware.py

@@ -27,7 +27,9 @@ from django.contrib.auth import REDIRECT_FIELD_NAME, BACKEND_SESSION_KEY, authen
 from django.contrib.auth.middleware import RemoteUserMiddleware
 from django.core import exceptions, urlresolvers
 import django.db
+from django.core.urlresolvers import resolve
 from django.http import HttpResponseRedirect, HttpResponse
+from django.utils.translation import ugettext as _
 from django.utils.http import urlquote
 from django.utils.encoding import iri_to_uri
 import django.views.static
@@ -43,7 +45,6 @@ from desktop import appmanager
 from hadoop import cluster
 import simplejson
 
-from django.utils.translation import ugettext as _
 
 LOG = logging.getLogger(__name__)
 
@@ -295,11 +296,21 @@ class LoginAndPermissionMiddleware(object):
     # app.
     if request.user.is_active and request.user.is_authenticated():
       AppSpecificMiddleware.augment_request_with_app(request, view_func)
+
+      # Until we get Django 1.3 and resolve returning the URL name, we just do a match of the name of the view
+      try:
+        access_view = 'access_view:%s:%s' % (request._desktop_app, resolve(request.path)[0].__name__)
+      except Exception, e:
+        access_log(request, 'error checking view perm: %s', e, level=access_log_level)
+        access_view =''
+
       if request._desktop_app and \
           request._desktop_app != "desktop" and \
-          not request.user.has_hue_permission(action="access", app=request._desktop_app):
+          not (request.user.has_hue_permission(action="access", app=request._desktop_app) or
+               request.user.has_hue_permission(action=access_view, app=request._desktop_app)):
         access_log(request, 'permission denied', level=access_log_level)
-        return PopupException(_("You do not have permission to access the %(app_name)s application.") % {'app_name': request._desktop_app.capitalize()}).response(request)
+        return PopupException(_("You do not have permission to access the %(app_name)s application.") %
+                              {'app_name': request._desktop_app.capitalize()}, error_code=401).response(request)
       else:
         log_page_hit(request, view_func, level=access_log_level)
         return None
@@ -314,9 +325,7 @@ class LoginAndPermissionMiddleware(object):
       response[MIDDLEWARE_HEADER] = 'LOGIN_REQUIRED'
       return response
     else:
-      return HttpResponseRedirect("%s?%s=%s" % (settings.LOGIN_URL,
-        REDIRECT_FIELD_NAME,
-        urlquote(request.get_full_path())))
+      return HttpResponseRedirect("%s?%s=%s" % (settings.LOGIN_URL, REDIRECT_FIELD_NAME, urlquote(request.get_full_path())))
 
 
 class SessionOverPostMiddleware(object):
@@ -443,7 +452,7 @@ class HtmlValidationMiddleware(object):
 
     # Write the two versions of html out for offline debugging
     filename = os.path.join(self._outdir, fn_name)
-    
+
     result = "HTML tidy result: %s [%s]:" \
              "\n\t%s" \
              "\nPlease see %s.orig %s.tidy\n-------" % \

+ 26 - 2
desktop/core/src/desktop/middleware_test.py

@@ -14,13 +14,13 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-#
-# Tests for Desktop-specific middleware
 
 from desktop.lib.django_test_util import make_logged_in_client
+from desktop.lib.test_utils import add_permission
 
 from nose.tools import assert_equal
 
+
 def test_jframe_middleware():
   c = make_logged_in_client()
   path = "/about/?foo=bar&baz=3"
@@ -41,3 +41,27 @@ def test_jframe_middleware():
 
   response = c.get("/about/?")
   assert_equal("/about/", response["X-Hue-JFrame-Path"])
+
+
+def test_view_perms():
+  # Super user
+  c = make_logged_in_client()
+
+  response = c.get("/useradmin/")
+  assert_equal(200, response.status_code)
+
+  response = c.get("/useradmin/users/edit/test")
+  assert_equal(200, response.status_code)
+
+  # Normal user
+  c = make_logged_in_client('user', is_superuser=False)
+  add_permission('user', 'test-view-group', 'access_view:useradmin:edit_user', 'useradmin')
+
+  response = c.get("/useradmin/")
+  assert_equal(401, response.status_code)
+
+  response = c.get("/useradmin/users/edit/test")
+  assert_equal(401, response.status_code)
+
+  response = c.get("/useradmin/users/edit/user") # Can access his profile page
+  assert_equal(200, response.status_code, response.content)