Эх сурвалжийг харах

HUE-908 [core] User/group membership should be delegated to the back-end

- New backend system for managing sync'ing.
- Changed import group functionality to import single entry when using dn.
Abraham Elmahrek 12 жил өмнө
parent
commit
5529a9a05a

+ 7 - 3
apps/useradmin/src/useradmin/ldap_access.py

@@ -117,13 +117,13 @@ class LdapConnection(object):
     sanitized_name = ldap.filter.escape_filter_chars(username_pattern).replace(r'\2a', r'*')
     search_dn, user_name_filter = self._get_search_params(sanitized_name, user_name_attr, find_by_dn)
     ldap_filter = '(&' + user_filter + user_name_filter + ')'
+    attrlist = ['isMemberOf', 'memberOf', 'givenName', 'sn', 'mail', 'dn', user_name_attr]
 
-    ldap_result_id = self.ldap_handle.search(search_dn, scope, ldap_filter)
+    ldap_result_id = self.ldap_handle.search(search_dn, scope, ldap_filter, attrlist)
     result_type, result_data = self.ldap_handle.result(ldap_result_id)
     user_info = []
     if result_data and result_type == ldap.RES_SEARCH_RESULT:
       for dn, data in result_data:
-
         # Skip Active Directory # refldap entries.
         if dn is not None:
 
@@ -143,6 +143,11 @@ class LdapConnection(object):
             ldap_info['last'] = data['sn'][0]
           if 'mail' in data:
             ldap_info['email'] = data['mail'][0]
+          # memberOf and isMemberOf should be the same if they both exist
+          if 'memberOf' in data:
+            ldap_info['groups'] = data['memberOf']
+          if 'isMemberOf' in data:
+            ldap_info['groups'] = data['isMemberOf']
 
           user_info.append(ldap_info)
 
@@ -153,7 +158,6 @@ class LdapConnection(object):
     LDAP search helper method for finding groups
     """
     base_dn = self._get_root_dn()
-    scope = ldap.SCOPE_SUBTREE
 
     group_filter = desktop.conf.LDAP.GROUPS.GROUP_FILTER.get()
     if not group_filter.startswith('('):

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

@@ -64,6 +64,7 @@ import useradmin.conf
 
 LOG = logging.getLogger(__name__)
 
+
 class UserProfile(models.Model):
   """
   WARNING: Some of the columns in the UserProfile object have been added

+ 102 - 37
apps/useradmin/src/useradmin/tests.py

@@ -28,7 +28,10 @@ import ldap
 from nose.plugins.attrib import attr
 from nose.tools import assert_true, assert_equal, assert_false
 
+import desktop.conf
+from desktop.lib.test_utils import grant_access
 from desktop.lib.django_test_util import make_logged_in_client
+from django.conf import settings
 from django.contrib.auth.models import User, Group
 from django.utils.encoding import smart_unicode
 from django.core.urlresolvers import reverse
@@ -60,48 +63,45 @@ class LdapTestConnection(object):
   It also includes functionality to fake modifications to an LDAP server.  It is designed
   as a singleton, to allow for changes to persist across discrete connections.
   """
-
-  _instance = None
   def __init__(self):
-    if LdapTestConnection._instance is None:
-      LdapTestConnection._instance = LdapTestConnection._Singleton()
+    self._instance = LdapTestConnection.Data()
 
   def add_user_group_for_test(self, user, group):
-    LdapTestConnection._instance.groups[group]['members'].append(user)
+    self._instance.groups[group]['members'].append(user)
 
   def remove_user_group_for_test(self, user, group):
-    LdapTestConnection._instance.groups[group]['members'].remove(user)
+    self._instance.groups[group]['members'].remove(user)
 
   def find_users(self, username_pattern, find_by_dn=False, scope=ldap.SCOPE_SUBTREE):
     """ Returns info for a particular user """
     if find_by_dn:
-      data = filter(lambda attrs: attrs['dn'] == username_pattern, LdapTestConnection._instance.users.values())
+      data = filter(lambda attrs: attrs['dn'] == username_pattern, self._instance.users.values())
     else:
       username_pattern = username_pattern.replace('.','\\.').replace('*', '.*')
-      usernames = filter(lambda username: re.match(username_pattern, username), LdapTestConnection._instance.users.keys())
-      data = [LdapTestConnection._instance.users.get(username) for username in usernames]
+      usernames = filter(lambda username: re.match(username_pattern, username), self._instance.users.keys())
+      data = [self._instance.users.get(username) for username in usernames]
     return data
 
   def find_groups(self, groupname_pattern, find_by_dn=False, scope=ldap.SCOPE_SUBTREE):
     """ Return all groups in the system with parents and children """
     if find_by_dn:
-      data = filter(lambda attrs: attrs['dn'] == groupname_pattern, LdapTestConnection._instance.groups.values())
+      data = filter(lambda attrs: attrs['dn'] == groupname_pattern, self._instance.groups.values())
       # SCOPE_SUBTREE means we return all sub-entries of the desired entry along with the desired entry.
       if data and scope == ldap.SCOPE_SUBTREE:
-        sub_data = filter(lambda attrs: attrs['dn'].endswith(data[0]['dn']), LdapTestConnection._instance.groups.values())
+        sub_data = filter(lambda attrs: attrs['dn'].endswith(data[0]['dn']), self._instance.groups.values())
         data.extend(sub_data)
     else:
       groupname_pattern = groupname_pattern.replace('.','\\.').replace('*', '.*')
-      groupnames = filter(lambda username: re.match(groupname_pattern, username), LdapTestConnection._instance.groups.keys())
-      data = [LdapTestConnection._instance.groups.get(groupname) for groupname in groupnames]
+      groupnames = filter(lambda username: re.match(groupname_pattern, username), self._instance.groups.keys())
+      data = [self._instance.groups.get(groupname) for groupname in groupnames]
     return data
 
-  class _Singleton:
+  class Data:
     def __init__(self):
-      self.users = {'moe': {'dn': 'uid=moe,ou=People,dc=example,dc=com', 'username':'moe', 'first':'Moe', 'email':'moe@stooges.com'},
-                    'lårry': {'dn': 'uid=lårry,ou=People,dc=example,dc=com', 'username':'lårry', 'first':'Larry', 'last':'Stooge', 'email':'larry@stooges.com'},
-                    'curly': {'dn': 'uid=curly,ou=People,dc=example,dc=com', 'username':'curly', 'first':'Curly', 'last':'Stooge', 'email':'curly@stooges.com'},
-                    'rock': {'dn': 'uid=rock,ou=People,dc=example,dc=com', 'username':'rock', 'first':'rock', 'last':'man', 'email':'rockman@stooges.com'},
+      self.users = {'moe': {'dn': 'uid=moe,ou=People,dc=example,dc=com', 'username':'moe', 'first':'Moe', 'email':'moe@stooges.com', 'groups': ['cn=TestUsers,ou=Groups,dc=example,dc=com']},
+                    'lårry': {'dn': 'uid=lårry,ou=People,dc=example,dc=com', 'username':'lårry', 'first':'Larry', 'last':'Stooge', 'email':'larry@stooges.com', 'groups': ['cn=TestUsers,ou=Groups,dc=example,dc=com', 'cn=Test Administrators,cn=TestUsers,ou=Groups,dc=example,dc=com']},
+                    'curly': {'dn': 'uid=curly,ou=People,dc=example,dc=com', 'username':'curly', 'first':'Curly', 'last':'Stooge', 'email':'curly@stooges.com', 'groups': ['cn=TestUsers,ou=Groups,dc=example,dc=com', 'cn=Test Administrators,cn=TestUsers,ou=Groups,dc=example,dc=com']},
+                    'rock': {'dn': 'uid=rock,ou=People,dc=example,dc=com', 'username':'rock', 'first':'rock', 'last':'man', 'email':'rockman@stooges.com', 'groups': ['cn=Test Administrators,cn=TestUsers,ou=Groups,dc=example,dc=com']},
                     'otherguy': {'dn': 'uid=otherguy,ou=People,dc=example,dc=com', 'username':'otherguy', 'first':'Other', 'last':'Guy', 'email':'other@guy.com'}}
 
       self.groups = {'TestUsers': {
@@ -435,6 +435,56 @@ def test_user_admin():
   assert_true("You must specify a password when creating a new user." in response.content)
 
 
+def test_useradmin_ldap_user_group_membership_sync():
+  reset = [desktop.conf.AUTH.USER_GROUP_MEMBERSHIP_SYNCHRONIZATION_BACKEND.set_for_testing('desktop.auth.backend.LdapSynchronizationBackend')]
+  settings.MIDDLEWARE_CLASSES.append('desktop.middleware.UserGroupSynchronizationMiddleware')
+
+  reset_all_users()
+  reset_all_groups()
+
+  # Set up LDAP tests to use a LdapTestConnection instead of an actual LDAP connection
+  ldap_access.CACHED_LDAP_CONN = LdapTestConnection()
+
+  try:
+    # Import curly who is part of TestUsers and Test Administrators
+    import_ldap_users('curly', sync_groups=False, import_by_dn=False)
+
+    # Set a password so that we can login
+    user = User.objects.get(username='curly')
+    user.set_password('test')
+    user.save()
+
+    # Should have 0 groups
+    assert_equal(0, user.groups.all().count())
+
+    # Make an authenticated request as curly so that we can see call middleware.
+    c = make_logged_in_client('curly', 'test', is_superuser=False)
+    grant_access("curly", "test", "useradmin")
+    response = c.get('/useradmin/users')
+
+    # Refresh user groups
+    user = User.objects.get(username='curly')
+
+    # Should have 3 groups now. 2 from LDAP and 1 from 'grant_access' call.
+    assert_equal(3, user.groups.all().count(), user.groups.all())
+
+    # Now remove a group and try again.
+    old_group = ldap_access.CACHED_LDAP_CONN._instance.users['curly']['groups'].pop()
+
+    # Make an authenticated request as curly so that we can see call middleware.
+    response = c.get('/useradmin/users')
+
+    # Refresh user groups
+    user = User.objects.get(username='curly')
+
+    # Should have 2 groups now. 1 from LDAP and 1 from 'grant_access' call.
+    assert_equal(3, user.groups.all().count(), user.groups.all())
+  finally:
+    for finish in reset:
+      finish()
+    settings.MIDDLEWARE_CLASSES.remove('desktop.middleware.UserGroupSynchronizationMiddleware')
+
+
 def test_useradmin_ldap_group_integration():
   reset_all_users()
   reset_all_groups()
@@ -442,45 +492,51 @@ def test_useradmin_ldap_group_integration():
   # Set up LDAP tests to use a LdapTestConnection instead of an actual LDAP connection
   ldap_access.CACHED_LDAP_CONN = LdapTestConnection()
 
+  # Import groups only
+  import_ldap_groups('TestUsers', import_members=False, import_members_recursive=False, sync_users=False, import_by_dn=False)
+  test_users = Group.objects.get(name='TestUsers')
+  assert_true(LdapGroup.objects.filter(group=test_users).exists())
+  assert_equal(test_users.user_set.all().count(), 0)
+
   # Import all members of TestUsers
-  import_ldap_groups('TestUsers', import_members=True, import_members_recursive=False, import_by_dn=False)
+  import_ldap_groups('TestUsers', import_members=True, import_members_recursive=False, sync_users=True, import_by_dn=False)
   test_users = Group.objects.get(name='TestUsers')
   assert_true(LdapGroup.objects.filter(group=test_users).exists())
-  assert_equal(len(test_users.user_set.all()), 3)
+  assert_equal(test_users.user_set.all().count(), 3)
 
   # Should import a group, but will only sync already-imported members
-  import_ldap_groups('Test Administrators', import_members=False, import_members_recursive=False, import_by_dn=False)
-  assert_equal(len(User.objects.all()), 3)
-  assert_equal(len(Group.objects.all()), 2)
+  import_ldap_groups('Test Administrators', import_members=False, import_members_recursive=False, sync_users=True, import_by_dn=False)
+  assert_equal(User.objects.all().count(), 3)
+  assert_equal(Group.objects.all().count(), 2)
   test_admins = Group.objects.get(name='Test Administrators')
-  assert_equal(len(test_admins.user_set.all()), 2)
+  assert_equal(test_admins.user_set.all().count(), 2)
   larry = User.objects.get(username='lårry')
   assert_equal(test_admins.user_set.all()[0].username, larry.username)
 
   # Only sync already imported
   ldap_access.CACHED_LDAP_CONN.remove_user_group_for_test('uid=moe,ou=People,dc=example,dc=com', 'TestUsers')
-  import_ldap_groups('TestUsers', import_members=False, import_members_recursive=False, import_by_dn=False)
-  assert_equal(len(test_users.user_set.all()), 2)
-  assert_equal(len(User.objects.get(username='moe').groups.all()), 0)
+  import_ldap_groups('TestUsers', import_members=False, import_members_recursive=False, sync_users=True, import_by_dn=False)
+  assert_equal(test_users.user_set.all().count(), 2)
+  assert_equal(User.objects.get(username='moe').groups.all().count(), 0)
 
   # Import missing user
   ldap_access.CACHED_LDAP_CONN.add_user_group_for_test('uid=moe,ou=People,dc=example,dc=com', 'TestUsers')
-  import_ldap_groups('TestUsers', import_members=True, import_members_recursive=False, import_by_dn=False)
-  assert_equal(len(test_users.user_set.all()), 3)
-  assert_equal(len(User.objects.get(username='moe').groups.all()), 1)
+  import_ldap_groups('TestUsers', import_members=True, import_members_recursive=False, sync_users=True, import_by_dn=False)
+  assert_equal(test_users.user_set.all().count(), 3)
+  assert_equal(User.objects.get(username='moe').groups.all().count(), 1)
 
   # Import all members of TestUsers and members of subgroups
-  import_ldap_groups('TestUsers', import_members=True, import_members_recursive=True, import_by_dn=False)
+  import_ldap_groups('TestUsers', import_members=True, import_members_recursive=True, sync_users=True, import_by_dn=False)
   test_users = Group.objects.get(name='TestUsers')
   assert_true(LdapGroup.objects.filter(group=test_users).exists())
-  assert_equal(len(test_users.user_set.all()), 4)
+  assert_equal(test_users.user_set.all().count(), 4)
 
   # Make sure Hue groups with naming collisions don't get marked as LDAP groups
   hue_user = User.objects.create(username='otherguy', first_name='Different', last_name='Guy')
   hue_group = Group.objects.create(name='OtherGroup')
   hue_group.user_set.add(hue_user)
   hue_group.save()
-  import_ldap_groups('OtherGroup', import_members=False, import_members_recursive=False, import_by_dn=False)
+  import_ldap_groups('OtherGroup', import_members=False, import_members_recursive=False, sync_users=True, import_by_dn=False)
   assert_false(LdapGroup.objects.filter(group=hue_group).exists())
   assert_true(hue_group.user_set.filter(username=hue_user.username).exists())
 
@@ -492,7 +548,7 @@ def test_useradmin_ldap_user_integration():
   ldap_access.CACHED_LDAP_CONN = LdapTestConnection()
 
   # Try importing a user
-  import_ldap_users('lårry', import_by_dn=False)
+  import_ldap_users('lårry', sync_groups=False, import_by_dn=False)
   larry = User.objects.get(username='lårry')
   assert_true(larry.first_name == 'Larry')
   assert_true(larry.last_name == 'Stooge')
@@ -502,17 +558,26 @@ def test_useradmin_ldap_user_integration():
   # Should be a noop
   sync_ldap_users()
   sync_ldap_groups()
-  assert_equal(len(User.objects.all()), 1)
-  assert_equal(len(Group.objects.all()), 0)
+  assert_equal(User.objects.all().count(), 1)
+  assert_equal(Group.objects.all().count(), 0)
 
   # Make sure that if a Hue user already exists with a naming collision, we
   # won't overwrite any of that user's information.
   hue_user = User.objects.create(username='otherguy', first_name='Different', last_name='Guy')
-  import_ldap_users('otherguy', import_by_dn=False)
+  import_ldap_users('otherguy', sync_groups=False, import_by_dn=False)
   hue_user = User.objects.get(username='otherguy')
   assert_equal(get_profile(hue_user).creation_method, str(UserProfile.CreationMethod.HUE))
   assert_equal(hue_user.first_name, 'Different')
 
+  # Try importing a user and sync groups
+  import_ldap_users('curly', sync_groups=True, import_by_dn=False)
+  curly = User.objects.get(username='curly')
+  assert_equal(curly.first_name, 'Curly')
+  assert_equal(curly.last_name, 'Stooge')
+  assert_equal(curly.email, 'curly@stooges.com')
+  assert_equal(get_profile(curly).creation_method, str(UserProfile.CreationMethod.EXTERNAL))
+  assert_equal(2, curly.groups.all().count(), curly.groups.all())
+
 
 def test_add_ldap_users():
   URL = reverse(add_ldap_users)

+ 41 - 15
apps/useradmin/src/useradmin/views.py

@@ -29,8 +29,8 @@ try:
 except ImportError:
   import simplejson as json
 
+import ldap
 import ldap_access
-from ldap import LDAPError
 
 from django.contrib.auth.models import User, Group
 from desktop.lib.django_util import render
@@ -276,8 +276,8 @@ def add_ldap_users(request):
       username_pattern = form.cleaned_data['username_pattern']
       import_by_dn = form.cleaned_data['dn']
       try:
-        users = import_ldap_users(username_pattern, import_by_dn)
-      except LDAPError, e:
+        users = import_ldap_users(username_pattern, False, import_by_dn)
+      except ldap.LDAPError, e:
         LOG.error("LDAP Exception: %s" % e)
         raise PopupException(_('There was an error when communicating with LDAP'), detail=str(e))
 
@@ -320,8 +320,8 @@ def add_ldap_groups(request):
       import_members = form.cleaned_data['import_members']
       import_members_recursive = form.cleaned_data['import_members_recursive']
       try:
-        groups = import_ldap_groups(groupname_pattern, import_members, import_members_recursive, import_by_dn)
-      except LDAPError, e:
+        groups = import_ldap_groups(groupname_pattern, import_members=import_members, import_members_recursive=import_members_recursive, sync_users=True, import_by_dn=import_by_dn)
+      except ldap.LDAPError, e:
         LOG.error(_("LDAP Exception: %s") % e)
         raise PopupException(_('There was an error when communicating with LDAP'), detail=str(e))
 
@@ -354,7 +354,7 @@ def sync_ldap_users_groups(request):
       try:
         users = sync_ldap_users()
         groups = sync_ldap_groups()
-      except LDAPError:
+      except ldap.LDAPError:
         LOG.error("LDAP Exception: %s" % e)
         raise PopupException(_('There was an error when communicating with LDAP'), detail=str(e))
 
@@ -457,7 +457,7 @@ def sync_unix_users_and_groups(min_uid, max_uid, min_gid, max_gid, check_shell):
   __groups_lock.release()
 
 
-def _import_ldap_users(username_pattern, import_by_dn=False):
+def _import_ldap_users(username_pattern, sync_groups=False, import_by_dn=False):
   """
   Import a user from LDAP. If import_by_dn is true, this will import the user by
   the distinguished name, rather than the configured username attribute.
@@ -494,19 +494,45 @@ def _import_ldap_users(username_pattern, import_by_dn=False):
     profile.creation_method = UserProfile.CreationMethod.EXTERNAL
     profile.save()
     user.save()
-
     imported_users.append(user)
 
+    # sync groups
+    if sync_groups and 'groups' in ldap_info:
+      old_groups = set(user.groups.all())
+      new_groups = set()
+      # Skip if 'memberOf' or 'isMemberOf' are not set
+      for group_dn in ldap_info['groups']:
+        group_ldap_info = conn.find_groups(group_dn, find_by_dn=True, scope=ldap.SCOPE_BASE)
+        for group_info in group_ldap_info:
+          # Add only if user isn't part of group.
+          if not user.groups.filter(name=group_info['name']).exists():
+            groups = import_ldap_groups(group_info['dn'], import_members=False, import_members_recursive=False, sync_users=False, import_by_dn=True)
+            if groups:
+              new_groups.update(groups)
+
+      # Remove out of date groups
+      remove_groups = old_groups - new_groups
+      remove_ldap_groups = LdapGroup.objects.filter(group__in=remove_groups)
+      remove_groups_filtered = [ldapgroup.group for ldapgroup in remove_ldap_groups]
+      user.groups.filter(group__in=remove_groups_filtered).delete()
+      user.groups.add(*new_groups)
+      Group.objects.filter(group__in=remove_groups_filtered).delete()
+      remove_ldap_groups.delete()
+
   return imported_users
 
 
-def _import_ldap_groups(groupname_pattern, import_members=False, recursive_import_members=False, import_by_dn=False):
+def _import_ldap_groups(groupname_pattern, import_members=False, recursive_import_members=False, sync_users=True, import_by_dn=False):
   """
   Import a group from LDAP. If import_members is true, this will also import any
   LDAP users that exist within the group.
   """
   conn = ldap_access.get_connection()
-  group_info = conn.find_groups(groupname_pattern, import_by_dn)
+  if import_by_dn:
+    scope = ldap.SCOPE_BASE
+  else:
+    scope = ldap.SCOPE_SUBTREE
+  group_info = conn.find_groups(groupname_pattern, import_by_dn, scope)
 
   if not group_info:
     LOG.warn(_("Could not get LDAP details for group pattern %s") % groupname_pattern)
@@ -541,7 +567,7 @@ def _import_ldap_groups(groupname_pattern, import_members=False, recursive_impor
         LOG.debug("Importing user %s" % str(member))
         users = _import_ldap_users(member, import_by_dn=True)
 
-      else:
+      elif sync_users:
         user_info = conn.find_users(member, find_by_dn=True)
         if len(user_info) > 1:
           LOG.warn('Found multiple users for member %s.' % member)
@@ -568,12 +594,12 @@ def _import_ldap_groups(groupname_pattern, import_members=False, recursive_impor
   return groups
 
 
-def import_ldap_users(user_pattern, import_by_dn):
-  return _import_ldap_users(user_pattern, import_by_dn)
+def import_ldap_users(user_pattern, sync_groups, import_by_dn):
+  return _import_ldap_users(user_pattern, sync_groups=sync_groups, import_by_dn=import_by_dn)
 
 
-def import_ldap_groups(group_pattern, import_members, import_members_recursive, import_by_dn):
-  return _import_ldap_groups(group_pattern, import_members, import_members_recursive, import_by_dn)
+def import_ldap_groups(group_pattern, import_members, import_members_recursive, sync_users, import_by_dn):
+  return _import_ldap_groups(group_pattern, import_members, import_members_recursive, sync_users, import_by_dn)
 
 
 def sync_ldap_users():

+ 3 - 0
desktop/conf.dist/hue.ini

@@ -94,6 +94,9 @@
     # - desktop.auth.backend.OAuthBackend
     ## backend=desktop.auth.backend.AllowFirstUserDjangoBackend
 
+    # Backend to synchronize user-group membership with
+    ## user_group_membership_synchronization_backend=desktop.auth.backend.LdapSynchronizationBackend
+
     ## pam_service=login
 
     # When using the desktop.auth.backend.RemoteUserDjangoBackend, this sets

+ 3 - 0
desktop/conf/pseudo-distributed.ini.tmpl

@@ -103,6 +103,9 @@
     # - desktop.auth.backend.OAuthBackend
     ## backend=desktop.auth.backend.AllowFirstUserDjangoBackend
 
+    # Backend to synchronize user-group membership with
+    ## user_group_membership_synchronization_backend=desktop.auth.backend.LdapSynchronizationBackend
+
     ## pam_service=login
 
     # When using the desktop.auth.backend.RemoteUserDjangoBackend, this sets

+ 33 - 2
desktop/core/src/desktop/auth/backend.py

@@ -36,6 +36,7 @@ import desktop.conf
 from django.utils.importlib import import_module
 from django.core.exceptions import ImproperlyConfigured
 from useradmin.models import get_profile, get_default_user_group, UserProfile
+from useradmin.views import import_ldap_users
 
 import pam
 from django_auth_ldap.backend import LDAPBackend, ldap_settings
@@ -137,9 +138,18 @@ class DesktopBackendBase(object):
     Implementors should return a boolean value which determines
     whether the given username and password pair is valid.
     """
-    raise Exception("Abstract class - must implement check_auth")
+    raise NotImplemented("Abstract class - must implement check_auth")
 
 
+class DesktopSynchronizationBackendBase(object):
+  """
+  Abstract base class for providing user-group membership sync'ing.
+
+  Extend this class and implement sync.
+  """
+  def sync(self, user):
+    raise NotImplemented("Abstract class - must implement sync")
+
 
 class AllowFirstUserDjangoBackend(django.contrib.auth.backends.ModelBackend):
   """
@@ -265,7 +275,6 @@ class PamBackend(DesktopBackendBase):
     return True
 
 
-
 class LdapBackend(object):
   """
   Authentication backend that uses LDAP to authenticate logins.
@@ -348,6 +357,28 @@ class LdapBackend(object):
     return True
 
 
+class LdapSynchronizationBackend(DesktopSynchronizationBackendBase):
+  """
+  Synchronize against LDAP authority.
+  """
+  USER_CACHE_NAME = 'ldap_use_group_sync_cache'
+
+  def sync(self, request):
+    user = request.user
+
+    if not user or not user.is_authenticated():
+      return
+
+    if not User.objects.filter(username=user.username, userprofile__creation_method=str(UserProfile.CreationMethod.EXTERNAL)).exists():
+      LOG.warn("User %s is not an Ldap user" % user.username)
+      return
+
+    # Cache should be cleared when user logs out.
+    if self.USER_CACHE_NAME not in request.session:
+      request.session[self.USER_CACHE_NAME] = import_ldap_users(user.username, sync_groups=True, import_by_dn=False)
+      request.session.modified = True
+
+
 class SpnegoDjangoBackend(django.contrib.auth.backends.ModelBackend):
   """
   A note about configuration:

+ 6 - 1
desktop/core/src/desktop/conf.py

@@ -245,7 +245,6 @@ SERVER_GROUP = Config(
   type=str,
   default="hue")
 
-
 CUSTOM = ConfigSection(
   key="custom",
   help=_("Customizations to the UI."),
@@ -267,6 +266,12 @@ AUTH = ConfigSection(
                         "django.contrib.auth.backends.ModelBackend (fully Django backend), " +
                         "desktop.auth.backend.AllowAllBackend (allows everyone), " +
                         "desktop.auth.backend.AllowFirstUserDjangoBackend (relies on Django and user manager, after the first login). ")),
+    USER_GROUP_MEMBERSHIP_SYNCHRONIZATION_BACKEND = Config(
+      key="user_group_membership_synchronization_backend",
+      help=_("Backend to synchronize user-group membership with."),
+      type=str,
+      default='',
+    ),
     USER_AUGMENTOR=Config("user_augmentor",
                    default="desktop.auth.backend.DefaultUserAugmentor",
                    help=_("Class which defines extra accessor methods for User objects.")),

+ 25 - 0
desktop/core/src/desktop/middleware.py

@@ -29,6 +29,7 @@ from django.core import exceptions, urlresolvers
 import django.db
 from django.core.urlresolvers import resolve
 from django.http import HttpResponseRedirect, HttpResponse
+from django.utils.importlib import import_module
 from django.utils.translation import ugettext as _
 from django.utils.http import urlquote
 from django.utils.encoding import iri_to_uri
@@ -57,6 +58,30 @@ DJANGO_VIEW_AUTH_WHITELIST = [
   django.views.generic.simple.redirect_to,
 ]
 
+class UserGroupSynchronizationMiddleware(object):
+  """
+  Middleware that synchronizes users and groups
+  """
+  def load_backend(self, path):
+    i = path.rfind('.')
+    module, attr = path[:i], path[i+1:]
+    try:
+      mod = import_module(module)
+    except ImportError, e:
+      raise ImproperlyConfigured('Error importing synchronization backend %s: "%s"' % (module, e))
+    try:
+        cls = getattr(mod, attr)
+    except AttributeError:
+      raise ImproperlyConfigured('Module "%s" does not define a "%s" synchronization backend' % (module, attr))
+    return cls()
+
+  def get_backend(self):
+    return self.load_backend(desktop.conf.AUTH.USER_GROUP_MEMBERSHIP_SYNCHRONIZATION_BACKEND.get())
+
+  def process_request(self, request):
+    backend = self.get_backend()
+    backend.sync(request)
+
 class AjaxMiddleware(object):
   """
   Middleware that augments request to set request.ajax

+ 5 - 1
desktop/core/src/desktop/settings.py

@@ -115,7 +115,7 @@ MIDDLEWARE_CLASSES = [
     'desktop.middleware.ExceptionMiddleware',
     'desktop.middleware.ClusterMiddleware',
     'desktop.middleware.AppSpecificMiddleware',
-    'django.middleware.transaction.TransactionMiddleware',
+    'django.middleware.transaction.TransactionMiddleware'
     # 'debug_toolbar.middleware.DebugToolbarMiddleware'
 ]
 
@@ -283,3 +283,7 @@ if default_db['ENGINE'] == 'moxy':
 # Set up environment variable so Kerberos libraries look at our private
 # ticket cache
 os.environ['KRB5CCNAME'] = desktop.conf.KERBEROS.CCACHE_PATH.get()
+
+#######
+if desktop.conf.AUTH.USER_GROUP_MEMBERSHIP_SYNCHRONIZATION_BACKEND.get():
+  MIDDLEWARE_CLASSES.append('desktop.middleware.UserGroupSynchronizationMiddleware')