Browse Source

HUE-608. ACLS for Hue apps

This commit introduces a number of concepts: user profiles, desktop
permissions, and group permissions.

User profiles are primarily around to store some metadata about users that is
otherwise not available through the Django user model.

Desktop permissions are introduced to manage permissions on an app-by-app
basis. It makes sense to implement them differently than Django's internal
permission infrastructure, because the Django permissions are tightly coupled
with permissions on models, i.e. creating, removing, updating models.

Finally, group permissions are a relationship between groups and permissions.
In this current implementation, users will not have direct access to
applications, but rather, they must have group membership which grants them access
to apps. Superusers will always have access to all applications.
Jon Natkins 13 years ago
parent
commit
de56884066

+ 3 - 0
apps/jobsub/src/jobsub/tests.py

@@ -34,6 +34,7 @@ from nose.plugins.attrib import attr
 from django.contrib.auth.models import User
 
 from desktop.lib.django_test_util import make_logged_in_client
+from desktop.lib.test_utils import grant_access
 
 from jobsub.views import in_process_jobsubd
 from jobsub.models import JobDesign, Submission
@@ -130,6 +131,8 @@ def test_job_design_cycle():
   # doesn't seem to work, so we use a new client.
   c.logout()
   c = make_logged_in_client("test2", is_superuser=False)
+  grant_access("test2", "test-grp", "jobsub")
+
   response = c.post("/jobsub/new/jar", 
     dict(name="test2", jarfile="myfile", arguments="x y z", submit="Save"))
   assert_true(response.context["saved"])

+ 2 - 2
apps/shell/src/shell/shellmanager.py

@@ -421,7 +421,7 @@ class ShellManager(object):
 
     shell_types_for_user = []
     for item in self.shell_types:
-      if user.has_desktop_permission('launch_%s' % (item[constants.KEY_NAME],), 'shell'):
+      if user.has_hue_permission('launch_%s' % (item[constants.KEY_NAME],), 'shell'):
         shell_types_for_user.append(item)
     return shell_types_for_user
 
@@ -500,7 +500,7 @@ class ShellManager(object):
     except KeyError:
       return { constants.NO_SUCH_USER : True }
 
-    if not user.has_desktop_permission('launch_%s' % (shell_name,), 'shell'):
+    if not user.has_hue_permission('launch_%s' % (shell_name,), 'shell'):
       return { constants.SHELL_NOT_ALLOWED : True }
 
     if not username in self._meta:

+ 115 - 0
apps/useradmin/src/useradmin/migrations/0001_permissions_and_profiles.py

@@ -0,0 +1,115 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import DataMigration
+from django.db import models
+from django.contrib.auth.models import User
+from useradmin.models import create_profile_for_user
+
+class Migration(DataMigration):
+    
+    def forwards(self, orm):
+        
+        # Adding model 'UserProfile'
+        db.create_table('useradmin_userprofile', (
+            ('home_directory', self.gf('django.db.models.fields.CharField')(max_length=1024, null=True)),
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], unique=True)),
+        ))
+        db.send_create_signal('useradmin', ['UserProfile'])
+
+        # Adding model 'GroupPermission'
+        db.create_table('useradmin_grouppermission', (
+            ('hue_permission', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['useradmin.HuePermission'])),
+            ('group', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.Group'])),
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+        ))
+        db.send_create_signal('useradmin', ['GroupPermission'])
+
+        # Adding model 'HuePermission'
+        db.create_table('useradmin_huepermission', (
+            ('action', self.gf('django.db.models.fields.CharField')(max_length=100)),
+            ('app', self.gf('django.db.models.fields.CharField')(max_length=30)),
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('description', self.gf('django.db.models.fields.CharField')(max_length=255)),
+        ))
+        db.send_create_signal('useradmin', ['HuePermission'])
+    
+        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 'UserProfile'
+        db.delete_table('useradmin_userprofile')
+
+        # Deleting model 'GroupPermission'
+        db.delete_table('useradmin_grouppermission')
+
+        # Deleting model 'HuePermission'
+        db.delete_table('useradmin_huepermission')
+    
+    
+    models = {
+        'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        'auth.permission': {
+            'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'useradmin.huepermission': {
+            'Meta': {'object_name': 'HuePermission'},
+            'action': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'app': ('django.db.models.fields.CharField', [], {'max_length': '30'}),
+            'description': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'through': "orm['useradmin.GroupPermission']", 'symmetrical': 'False'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+        },
+        'useradmin.grouppermission': {
+            'Meta': {'object_name': 'GroupPermission'},
+            'hue_permission': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['useradmin.HuePermission']"}),
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+        },
+        'useradmin.userprofile': {
+            'Meta': {'object_name': 'UserProfile'},
+            'home_directory': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'})
+        }
+    }
+    
+    complete_apps = ['useradmin']

+ 0 - 0
apps/useradmin/src/useradmin/migrations/__init__.py


+ 218 - 0
apps/useradmin/src/useradmin/models.py

@@ -14,4 +14,222 @@
 # 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.
+"""
+The core of this module adds permissions functionality to Hue applications.
+
+A "Hue Permission" (colloquially, appname.action, but stored in the
+HuePermission model) is a way to specify some action whose
+control may be restricted.  Every Hue application, by default,
+has an "access" action.  To specify extra actions, applications
+can specify them in appname.settings.PERMISSION_ACTIONS, as
+pairs of (action_name, description).
+
+Several mechanisms enforce permission.  First of all, the "access" permission
+is controlled by LoginAndPermissionMiddleware.  For eligible views
+within an application, the access permission is checked.  Second,
+views may use @desktop.decorators.hue_permission_required("action", "app")
+to annotate their function, and this decorator will check a permission.
+Thirdly, you may wish to do so manually, by using something akin to:
+
+  app = desktop.lib.apputil.get_current_app() # a string
+  dp = HuePermission.objects.get(app=pp, action=action)
+  request.user.has_hue_permission(dp)
+
+[Design note: it is questionable that a Hue permission is
+a model, instead of just being a string.  Could go either way.]
+
+Permissions may be granted to groups, but not, currently, to users.
+A user's abilities is the union of all permissions the group
+has access to.
+
+Note that Django itself has a notion of users, groups, and permissions.
+We re-use Django's notion of users and groups, but ignore its notion of
+permissions.  The permissions notion in Django is strongly tied to
+what models you may or may not edit, and there are elaborations (especially
+in Django 1.2) to manipulate this row by row.  This does not map nicely
+onto actions which may not relate to database models.
+"""
+import logging
+
+from django.db import models
+from django.contrib.auth import models as auth_models
+from desktop import appmanager
+from desktop.lib.django_util import PopupException
+
+LOG = logging.getLogger(__name__)
+
+class UserProfile(models.Model):
+  """
+  WARNING: Some of the columns in the UserProfile object have been added
+  via south migration scripts. During an upgrade that modifies this model,
+  the columns in the django ORM database will not match the
+  actual object defined here, until the latest migration has been executed.
+  The code that does the actual UserProfile population must reside in the most
+  recent migration that modifies the UserProfile model.
+
+  for user in User.objects.all():
+    try:
+      p = orm.UserProfile.objects.get(user=user)
+    except orm.UserProfile.DoesNotExist:
+      create_profile_for_user(user)
+
+  IF ADDING A MIGRATION THAT MODIFIES THIS MODEL, MAKE SURE TO MOVE THIS CODE
+  OUT OF THE CURRENT MIGRATION, AND INTO THE NEW ONE, OR UPGRADES WILL NOT WORK
+  PROPERLY
+  """
+
+  user = models.ForeignKey(auth_models.User, unique=True)
+  home_directory = models.CharField(editable=True, max_length=1024, null=True)
+
+  def get_groups(self):
+    return self.user.groups.all()
+
+  def _lookup_permission(self, app, action):
+    return HuePermission.objects.get(app=app, action=action)
+
+  def has_hue_permission(self, action=None, app=None, perm=None):
+    if perm is None:
+      try:
+        perm = self._lookup_permission(app, action)
+      except HuePermission.DoesNotExist:
+        LOG.exception("Permission object not available. Was syncdb run after installation?")
+        return self.user.is_superuser
+    if self.user.is_superuser:
+      return True
+
+    for group in self.user.groups.all():
+      if group_has_permission(group, perm):
+        return True
+    return False
+
+  def check_hue_permission(self, perm=None, app=None, action=None):
+    """
+    Raises a PopupException if permission is denied.
+
+    Either perm or both app and action are required.
+    """
+    if perm is None:
+      perm = self._lookup_permission(app, action)
+    if self.has_hue_permission(perm):
+      return
+    else:
+      raise PopupException("You do not have permissions to %s." % perm.description)
+
+def get_profile(user):
+  """
+  Caches the profile, to avoid DB queries at every call.
+  """
+  if hasattr(user, "_cached_userman_profile"):
+    return user._cached_userman_profile
+  else:
+    profile = UserProfile.objects.get(user=user)
+    user._cached_userman_profile = profile
+    return profile
+
+def group_has_permission(group, perm):
+  return GroupPermission.objects.filter(group=group, hue_permission=perm).count() > 0
+
+def group_permissions(group):
+  return HuePermission.objects.filter(grouppermission__group=group).all()
+
+# Create a user profile for the given user
+def create_profile_for_user(user):
+  p = UserProfile()
+  p.user = user
+  p.home_directory = "/user/%s" % p.user.username
+  try:
+    p.save()
+  except:
+    LOG.debug("Failed to automatically create user profile.", exc_info=True)
+
+def create_user_signal_handler(sender, **kwargs):
+  if kwargs['created']:
+    create_profile_for_user(kwargs['instance'])
+
+# Create a user profile every time a user gets created.
+models.signals.post_save.connect(create_user_signal_handler, sender=auth_models.User)
+
+class GroupPermission(models.Model):
+  """
+  Represents the permissions a group has.
+  """
+  group = models.ForeignKey(auth_models.Group)
+  hue_permission = models.ForeignKey("HuePermission")
+
+
+# Permission Management
+class HuePermission(models.Model):
+  """
+  Set of non-object specific permissions that an app supports.
+
+  For now, we only assign permissions to groups, though that may change.
+  """
+  app = models.CharField(max_length=30)
+  action = models.CharField(max_length=100)
+  description = models.CharField(max_length=255)
+
+  groups = models.ManyToManyField(auth_models.Group, through=GroupPermission)
+
+  def __str__(self):
+    return "%s.%s:%s(%d)" % (self.app, self.action, self.description, self.pk)
+
+  @classmethod
+  def get_app_permission(cls, hue_app, action):
+    return HuePermission.objects.get(app=hue_app, action=action)
+
+def update_app_permissions(**kwargs):
+  """
+  Inserts missing permissions into the database table.
+  This is a 'syncdb' callback.
+
+  We never delete permissions automatically, because apps might come and go.
+
+  Note that signing up to the "syncdb" signal is not necessarily
+  the best thing we can do, since some apps might not
+  have models, but nonetheless, "syncdb" is typically
+  run when apps are installed.
+  """
+  # Map app->action->HuePermission.
+  current = {}
+  try:
+    for dp in HuePermission.objects.all():
+      current.setdefault(dp.app, {})[dp.action] = dp
+  except:
+    return
+
+  updated = 0
+  uptodate = 0
+  added = [ ]
+
+  for app_obj in appmanager.DESKTOP_APPS:
+    app = app_obj.name
+    actions = set([("access", "launch this application")])
+    actions.update(getattr(app_obj.settings, "PERMISSION_ACTIONS", []))
+
+    if app not in current:
+      current[app] = {}
+
+    for action, description in actions:
+      c = current[app].get(action)
+      if c:
+        if c.description != description:
+          c.description = description
+          c.save()
+          updated += 1
+        else:
+          uptodate += 1
+      else:
+        new_dp = HuePermission(app=app, action=action, description=description)
+        new_dp.save()
+        added.append(new_dp)
+
+  available = HuePermission.objects.count()
+
+  LOG.info("HuePermissions: %d added, %d updated, %d up to date, %d stale" %
+           (len(added),
+            updated,
+            uptodate,
+            available - len(added) - updated - uptodate))
+
+models.signals.post_syncdb.connect(update_app_permissions)
 

+ 3 - 0
apps/useradmin/src/useradmin/templates/list_groups.mako

@@ -18,6 +18,7 @@ from desktop.views import commonheader, commonfooter
 %>
 <% import urllib %>
 <% from django.utils.translation import ugettext, ungettext, get_language, activate %>
+<% from useradmin.models import group_permissions %>
 <% _ = ugettext %>
 
 <%namespace name="layout" file="layout.mako" />
@@ -37,6 +38,7 @@ ${layout.menubar(section='groups')}
           <tr>
             <th>${_('Group Name')}</th>
             <th>${_('Members')}</th>
+            <th>${_('Permissions')}</th>
 			<th>&nbsp;</th>
           </tr>
         </head>
@@ -45,6 +47,7 @@ ${layout.menubar(section='groups')}
           <tr class="groupRow" data-search="${group.name}${', '.join([user.username for user in group.user_set.all()])}">
             <td>${group.name}</td>
             <td>${', '.join([user.username for user in group.user_set.all()])}</td>
+            <td>${', '.join([perm.app + "." + perm.action for perm in group_permissions(group)])}</td>
             <td>
               <a title="Edit ${group.name}" class="btn small" href="${ url('useradmin.views.edit_group', name=urllib.quote(group.name)) }">Edit</a>
               <a title="Delete ${group.name}" class="btn small confirmationModal" alt="Are you sure you want to delete ${group.name}?" href="javascript:void(0)" data-confirmation-url="${ url('useradmin.views.delete_group', name=urllib.quote_plus(group.name)) }">Delete</a>

+ 75 - 2
apps/useradmin/src/useradmin/tests.py

@@ -23,12 +23,15 @@ Tests for "user admin"
 
 import urllib
 
-from nose.tools import assert_true, assert_equal
+from nose.tools import assert_true, assert_equal, assert_false
 
 from desktop.lib.django_test_util import make_logged_in_client
 from django.contrib.auth.models import User, Group
 from django.utils.encoding import smart_unicode
 
+from useradmin.models import HuePermission, GroupPermission
+from useradmin.models import get_profile
+
 def reset_all_users():
   """Reset to a clean state by deleting all users"""
   for user in User.objects.all():
@@ -49,6 +52,57 @@ def test_invalid_username():
     response = c.post('/useradmin/users/new', dict(username=bad_name, password1="test", password2="test"))
     assert_true('not allowed' in response.context["form"].errors['username'][0])
 
+def test_group_permissions():
+  reset_all_users()
+  reset_all_groups()
+
+  # Get ourselves set up with a user and a group
+  c = make_logged_in_client(username="test", is_superuser=True)
+  Group.objects.create(name="test-group")
+  test_user = User.objects.get(username="test")
+  test_user.groups.add(Group.objects.get(name="test-group"))
+  test_user.save()
+
+  # Make sure that a superuser can always access applications
+  response = c.get('/useradmin/users')
+  assert_true('Hue Users' in response.content)
+
+  assert_true(len(GroupPermission.objects.all()) == 0)
+  c.post('/useradmin/groups/edit/test-group',
+         dict(name="test-group",
+         members=[User.objects.get(username="test").pk],
+         permissions=[HuePermission.objects.get(app='useradmin',action='access').pk],
+         save="Save"), follow=True)
+  assert_true(len(GroupPermission.objects.all()) == 1)
+
+  # Now test that we have limited access
+  c1 = make_logged_in_client(username="nonadmin", is_superuser=False)
+  response = c1.get('/useradmin/users')
+  assert_true('You do not have permission to access the Useradmin application.' in response.content)
+
+  # Add the non-admin to a group that should grant permissions to the app
+  test_user = User.objects.get(username="nonadmin")
+  test_user.groups.add(Group.objects.get(name='test-group'))
+  test_user.save()
+
+  # Check that we have access now
+  response = c1.get('/useradmin/users')
+  assert_true(get_profile(test_user).has_hue_permission('access','useradmin'))
+  assert_true('Hue Users' in response.content)
+
+  # And revoke access from the group
+  c.post('/useradmin/groups/edit/test-group',
+         dict(name="test-group",
+         members=[User.objects.get(username="test").pk],
+         permissions=[],
+         save="Save"), follow=True)
+  assert_true(len(GroupPermission.objects.all()) == 0)
+  assert_false(get_profile(test_user).has_hue_permission('access','useradmin'))
+
+  # We should no longer have access to the app
+  response = c1.get('/useradmin/users')
+  assert_true('You do not have permission to access the Useradmin application.' in response.content)
+
 def test_group_admin():
   reset_all_users()
   reset_all_groups()
@@ -81,13 +135,23 @@ def test_group_admin():
 
   # Test some permissions
   c2 = make_logged_in_client(username="nonadmin", is_superuser=False)
+
+  # Need to give access to the user for the rest of the test
+  group = Group.objects.create(name="access-group")
+  perm = HuePermission.objects.get(app='useradmin', action='access')
+  GroupPermission.objects.create(group=group, hue_permission=perm)
+  test_user = User.objects.get(username="nonadmin")
+  test_user.groups.add(Group.objects.get(name="access-group"))
+  test_user.save()
+
   response = c2.get('/useradmin/groups/new')
   assert_true("You must be a superuser" in response.content)
   response = c2.get('/useradmin/groups/edit/testgroup')
   assert_true("You must be a superuser" in response.content)
 
+  # Should be one group left, because we created the other group
   response = c.post('/useradmin/groups/delete/testgroup')
-  assert_true(len(Group.objects.all()) == 0)
+  assert_true(len(Group.objects.all()) == 1)
 
 
 def test_user_admin():
@@ -155,8 +219,17 @@ def test_user_admin():
   assert_true(len(response.context["users"]) > 1)
   assert_true("Hue Users" in response.content)
 
+  # Need to give access to the user for the rest of the test
+  group = Group.objects.create(name="test-group")
+  perm = HuePermission.objects.get(app='useradmin', action='access')
+  GroupPermission.objects.create(group=group, hue_permission=perm)
+  test_user = User.objects.get(username=FUNNY_NAME)
+  test_user.groups.add(Group.objects.get(name="test-group"))
+  test_user.save()
+
   # Check permissions by logging in as the new user
   c_reg = make_logged_in_client(username=FUNNY_NAME, password="test")
+
   # Regular user should be able to modify oneself
   response = c_reg.post('/useradmin/users/edit/%s' % (FUNNY_NAME_QUOTED,),
                         dict(username = FUNNY_NAME,

+ 13 - 2
apps/useradmin/src/useradmin/views.py

@@ -31,6 +31,8 @@ from django.contrib.auth.models import User, Group
 from desktop.lib.django_util import get_username_re_rule, get_groupname_re_rule, render, PopupException, format_preserving_redirect
 from django.core import urlresolvers
 
+from useradmin.models import GroupPermission, HuePermission
+
 LOG = logging.getLogger(__name__)
 
 __users_lock = threading.Lock()
@@ -215,7 +217,6 @@ def edit_group(request, name=None):
   return render('edit_group.mako', request,
     dict(form=form, action=request.path, name=name))
 
-
 def _check_remove_last_super(user_obj):
   """Raise an error if we're removing the last superuser"""
   if not user_obj.is_superuser:
@@ -311,13 +312,15 @@ class GroupEditForm(forms.ModelForm):
 
     if self.instance.id:
       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(initial_members, User.objects.order_by('username'))
+    self.fields["permissions"] = _make_model_field(initial_perms, HuePermission.objects.order_by('app','description'))
 
   def _compute_diff(self, field_name):
-    """Used for members, but not permissions."""
     current = set(self.fields[field_name].initial_objs)
     updated = set(self.cleaned_data[field_name])
     delete = current.difference(updated)
@@ -327,6 +330,7 @@ class GroupEditForm(forms.ModelForm):
   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")
@@ -337,6 +341,13 @@ class GroupEditForm(forms.ModelForm):
       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)
+
 def _make_model_field(initial, choices, multi=True):
   """ Creates multiple choice field with given query object as choices. """
   if multi:

+ 14 - 11
desktop/core/src/desktop/auth/backend.py

@@ -21,10 +21,9 @@ These classes should implement the interface described at:
   http://docs.djangoproject.com/en/1.0/topics/auth/#writing-an-authentication-backend
 
 In addition, the User classes they return must support:
- - get_primary_group() (returns a string)
  - get_groups() (returns a list of strings)
  - get_home_directory() (returns None or a string)
- - has_desktop_permission(action, app) -> boolean
+ - has_hue_permission(action, app) -> boolean
 Because Django's models are sometimes unfriendly, you'll want
 User to remain a django.contrib.auth.models.User object.
 
@@ -36,6 +35,7 @@ import logging
 import desktop.conf
 from django.utils.importlib import import_module
 from django.core.exceptions import ImproperlyConfigured
+from useradmin.models import get_profile
 
 import pam
 from django_auth_ldap.backend import LDAPBackend, ldap_settings
@@ -75,8 +75,7 @@ def rewrite_user(user):
   though this could be generalized.
   """
   augment = get_user_augmentation_class()(user)
-  for attr in ("get_groups", "get_primary_group",
-      "get_home_directory", "has_desktop_permission"):
+  for attr in ("get_groups", "get_home_directory", "has_hue_permission"):
     setattr(user, attr, getattr(augment, attr))
   return user
 
@@ -84,17 +83,17 @@ class DefaultUserAugmentor(object):
   def __init__(self, parent):
     self._parent = parent
 
-  def get_groups(self):
-    return [ self._parent.username ]
+  def _get_profile(self):
+    return get_profile(self._parent)
 
-  def get_primary_group(self):
-    return self._parent.username
+  def get_groups(self):
+    return self._get_profile().get_groups()
 
   def get_home_directory(self):
-    return "/user/%s" % self._parent.username
+    return self._get_profile().home_directory
 
-  def has_desktop_permission(self, action, app):
-    return True
+  def has_hue_permission(self, action, app):
+    return self._get_profile().has_hue_permission(action=action, app=app)
 
 def find_or_create_user(username, password=None):
   try:
@@ -203,6 +202,8 @@ class PamBackend(DesktopBackendBase):
       except User.DoesNotExist:
         user = find_or_create_user(username, None)
         if user is not None and user.is_active:
+          profile = get_profile(user)
+          profile.save()
           user.is_superuser = is_super
           user.save()
 
@@ -261,6 +262,8 @@ class LdapBackend(object):
       return None
 
     if user is not None and user.is_active:
+      profile = get_profile(user)
+      profile.save()
       user.is_superuser = is_super
       user = rewrite_user(user)
       return user

+ 2 - 2
desktop/core/src/desktop/decorators.py

@@ -25,7 +25,7 @@ except ImportError:
 
 LOG = logging.getLogger(__name__)
 
-def desktop_permission_required(action, app):
+def hue_permission_required(action, app):
   """
   Checks that the user has permissions to do
   action 'action' on app 'app'.
@@ -35,7 +35,7 @@ def desktop_permission_required(action, app):
   def decorator(view_func):
     @wraps(view_func)
     def decorated(request, *args, **kwargs):
-      if not request.user.has_desktop_permission(action, app):
+      if not request.user.has_hue_permission(action, app):
         raise PopupException("Permission denied (%s/%s)" % (action, app))
       return view_func(request, *args, **kwargs)
     return decorated

+ 27 - 0
desktop/core/src/desktop/lib/test_utils.py

@@ -0,0 +1,27 @@
+#!/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.
+
+from django.contrib.auth.models import Group, User
+from useradmin.models import HuePermission, GroupPermission
+
+def grant_access(username, groupname, appname):
+    grp = Group.objects.create(name=groupname)
+    perm = HuePermission.objects.get(app=appname,action='access')
+    GroupPermission.objects.create(group=grp, hue_permission=perm)
+    user = User.objects.get(username=username)
+    user.groups.add(grp)
+    user.save()

+ 1 - 1
desktop/core/src/desktop/middleware.py

@@ -272,7 +272,7 @@ class LoginAndPermissionMiddleware(object):
       AppSpecificMiddleware.augment_request_with_app(request, view_func)
       if request._desktop_app and \
           request._desktop_app != "desktop" and \
-          not request.user.has_desktop_permission(action="access", app=request._desktop_app):
+          not request.user.has_hue_permission(action="access", app=request._desktop_app):
         access_log(request, 'permission denied', level=access_log_level)
         return PopupException("You do not have permission to access the %s application." % (request._desktop_app.capitalize())).response(request)
       else:

+ 2 - 2
desktop/core/src/desktop/views.py

@@ -121,10 +121,10 @@ def prefs(request, key=None):
 
 
 def bootstrap(request):
-  """Concatenates bootstrap.js files from all installed desktop apps."""
+  """Concatenates bootstrap.js files from all installed Hue apps."""
 
   # Has some None's for apps that don't have bootsraps.
-  all_bootstraps = [ (app, app.get_bootstrap_file()) for app in appmanager.DESKTOP_APPS if request.user.has_desktop_permission(action="access", app=app.name) ]
+  all_bootstraps = [ (app, app.get_bootstrap_file()) for app in appmanager.DESKTOP_APPS if request.user.has_hue_permission(action="access", app=app.name) ]
 
   # Iterator over the streams.
   concatenated = [ "\n/* %s */\n%s" % (app.name, b.read()) for app, b in all_bootstraps if b is not None ]