소스 검색

[useradmin] Import users from LDAP subgroup

Recursively import users from LDAP subgroup.

Configurable object class filters for groups and users:
'desktop.ldap.groups.group_filter', 'desktop.ldap.users.user_filter'.
Also, the attribute that is matched against is configured for
groups and users as well: 'desktop.ldap.groups.group_name_attr',
'desktop.ldap.users.user_name_attr'.

The convention is to have child entries be "subgroups"
and use the 'member' attribute to reference members in a group.
Abraham Elmahrek 12 년 전
부모
커밋
824bc64d3b

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

@@ -134,10 +134,10 @@ class AddLdapUsersForm(forms.Form):
 class AddLdapGroupsForm(forms.Form):
 class AddLdapGroupsForm(forms.Form):
   groupname_pattern = forms.RegexField(
   groupname_pattern = forms.RegexField(
       label=_t("Name"),
       label=_t("Name"),
-      max_length=64,
+      max_length=80,
       regex='^%s$' % get_groupname_re_rule(),
       regex='^%s$' % get_groupname_re_rule(),
-      help_text=_t("Required. 30 characters or fewer."),
-      error_messages={'invalid': _t("30 characters or fewer.") })
+      help_text=_t("Required. 80 characters or fewer."),
+      error_messages={'invalid': _t("80 characters or fewer.") })
   dn = forms.BooleanField(label=_t("Distinguished name"),
   dn = forms.BooleanField(label=_t("Distinguished name"),
                           help_text=_t("Whether or not the group should be imported by "
                           help_text=_t("Whether or not the group should be imported by "
                                     "distinguished name."),
                                     "distinguished name."),
@@ -147,6 +147,10 @@ class AddLdapGroupsForm(forms.Form):
                                       help_text=_t('Import unimported or new users from the group.'),
                                       help_text=_t('Import unimported or new users from the group.'),
                                       initial=False,
                                       initial=False,
                                       required=False)
                                       required=False)
+  import_members_recursive = forms.BooleanField(label=_t('Import new members from all subgroups'),
+                                                help_text=_t('Import unimported or new users from the all subgroups.'),
+                                                initial=False,
+                                                required=False)
   ensure_home_directories = forms.BooleanField(label=_t('Create home directories'),
   ensure_home_directories = forms.BooleanField(label=_t('Create home directories'),
                                                 help_text=_t('Create home directories for every member imported, if members are being imported.'),
                                                 help_text=_t('Create home directories for every member imported, if members are being imported.'),
                                                 initial=True,
                                                 initial=True,

+ 24 - 10
apps/useradmin/src/useradmin/ldap_access.py

@@ -103,13 +103,11 @@ class LdapConnection(object):
     else:
     else:
       return (base_dn, '(' + attr + '=' + name + ')')
       return (base_dn, '(' + attr + '=' + name + ')')
 
 
-  def find_users(self, username_pattern, find_by_dn=False):
+  def find_users(self, username_pattern, find_by_dn=False, scope=ldap.SCOPE_SUBTREE):
     """
     """
     LDAP search helper method finding users. This supports searching for users
     LDAP search helper method finding users. This supports searching for users
     by distinguished name, or the configured username attribute.
     by distinguished name, or the configured username attribute.
     """
     """
-    scope = ldap.SCOPE_SUBTREE
-
     user_filter = desktop.conf.LDAP.USERS.USER_FILTER.get()
     user_filter = desktop.conf.LDAP.USERS.USER_FILTER.get()
     if not user_filter.startswith('('):
     if not user_filter.startswith('('):
       user_filter = '(' + user_filter + ')'
       user_filter = '(' + user_filter + ')'
@@ -124,9 +122,17 @@ class LdapConnection(object):
     result_type, result_data = self.ldap_handle.result(ldap_result_id)
     result_type, result_data = self.ldap_handle.result(ldap_result_id)
     user_info = []
     user_info = []
     if result_data and result_type == ldap.RES_SEARCH_RESULT:
     if result_data and result_type == ldap.RES_SEARCH_RESULT:
-      for result_data_item in result_data:
-        data = result_data_item[1]
-        ldap_info = { 'username': data[user_name_attr][0] }
+      for dn, data in result_data:
+
+        # Skip unnamed entries.
+        if user_name_attr not in data:
+          LOG.warn('Could not find %s in ldap attributes' % user_name_attr)
+          continue
+
+        ldap_info = {
+          'dn': dn,
+          'name': data[user_name_attr][0]
+        }
 
 
         if 'givenName' in data:
         if 'givenName' in data:
           ldap_info['first'] = data['givenName'][0]
           ldap_info['first'] = data['givenName'][0]
@@ -139,7 +145,7 @@ class LdapConnection(object):
 
 
     return user_info
     return user_info
 
 
-  def find_groups(self, groupname_pattern, find_by_dn=False):
+  def find_groups(self, groupname_pattern, find_by_dn=False, scope=ldap.SCOPE_SUBTREE):
     """
     """
     LDAP search helper method for finding groups
     LDAP search helper method for finding groups
     """
     """
@@ -161,9 +167,17 @@ class LdapConnection(object):
 
 
     group_info = []
     group_info = []
     if result_data and result_type == ldap.RES_SEARCH_RESULT:
     if result_data and result_type == ldap.RES_SEARCH_RESULT:
-      for result_data_item in result_data:
-        data = result_data_item[1]
-        ldap_info = { 'name': data[group_name_attr][0] }
+      for dn, data in result_data:
+
+        # Skip unnamed entries.
+        if group_name_attr not in data:
+          LOG.warn('Could not find %s in ldap attributes' % group_name_attr)
+          continue
+
+        ldap_info = {
+          'dn': dn,
+          'name': data[group_name_attr][0]
+        }
 
 
         member_attr = desktop.conf.LDAP.GROUPS.GROUP_MEMBER_ATTR.get()
         member_attr = desktop.conf.LDAP.GROUPS.GROUP_MEMBER_ATTR.get()
         if member_attr in data:
         if member_attr in data:

+ 94 - 53
apps/useradmin/src/useradmin/tests.py

@@ -23,7 +23,7 @@ Tests for "user admin"
 
 
 import re
 import re
 import urllib
 import urllib
-from ldap import LDAPError
+import ldap
 
 
 from nose.plugins.attrib import attr
 from nose.plugins.attrib import attr
 from nose.tools import assert_true, assert_equal, assert_false
 from nose.tools import assert_true, assert_equal, assert_false
@@ -72,28 +72,50 @@ class LdapTestConnection(object):
   def remove_user_group_for_test(self, user, group):
   def remove_user_group_for_test(self, user, group):
     LdapTestConnection._instance.groups[group]['members'].remove(user)
     LdapTestConnection._instance.groups[group]['members'].remove(user)
 
 
-  def find_users(self, username_pattern, find_by_dn=False):
+  def find_users(self, username_pattern, find_by_dn=False, scope=ldap.SCOPE_SUBTREE):
     """ Returns info for a particular user """
     """ Returns info for a particular user """
-    username_pattern = username_pattern.replace('.','\\.').replace('*', '.*')
-    usernames = filter(lambda username: re.match(username_pattern, username), LdapTestConnection._instance.users.keys())
-    return [LdapTestConnection._instance.users.get(username) for username in usernames]
-
-  def find_groups(self, groupname_pattern, find_by_dn=False):
+    if find_by_dn:
+      data = filter(lambda attrs: attrs['dn'] == username_pattern, LdapTestConnection._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]
+    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 """
     """ Return all groups in the system with parents and children """
-    groupname_pattern = groupname_pattern.replace('.','\\.').replace('*', '.*')
-    groupnames = filter(lambda username: re.match(groupname_pattern, username), LdapTestConnection._instance.groups.keys())
-    return [LdapTestConnection._instance.groups.get(groupname) for groupname in groupnames]
+    if find_by_dn:
+      data = filter(lambda attrs: attrs['dn'] == groupname_pattern, LdapTestConnection._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())
+        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]
+    return data
 
 
   class _Singleton:
   class _Singleton:
     def __init__(self):
     def __init__(self):
-      self.users = {'moe': {'username':'moe', 'first':'Moe', 'email':'moe@stooges.com'},
-                    'larry': {'username':'larry', 'first':'Larry', 'last':'Stooge', 'email':'larry@stooges.com'},
-                    'curly': {'username':'curly', 'first':'Curly', 'last':'Stooge', 'email':'curly@stooges.com'},
-                    'otherguy': {'username':'otherguy', 'first':'Other', 'last':'Guy', 'email':'other@guy.com'}}
-
-      self.groups = {'TestUsers': {'name':'TestUsers', 'members':['moe','larry','curly']},
-                     'Test Administrators': {'name':'Test Administrators', 'members':['curly','larry']},
-                     'OtherGroup': {'name':'OtherGroup', 'members':[]}}
+      self.users = {'moe': {'dn': 'uid=moe,ou=People,dc=example,dc=com', 'username':'moe', 'first':'Moe', 'email':'moe@stooges.com'},
+                    'larry': {'dn': 'uid=larry,ou=People,dc=example,dc=com', 'username':'larry', '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'},
+                    'otherguy': {'dn': 'uid=otherguy,ou=People,dc=example,dc=com', 'username':'otherguy', 'first':'Other', 'last':'Guy', 'email':'other@guy.com'}}
+
+      self.groups = {'TestUsers': {
+                        'dn': 'cn=TestUsers,ou=Groups,dc=example,dc=com',
+                        'name':'TestUsers',
+                        'members':['uid=moe,ou=People,dc=example,dc=com','uid=larry,ou=People,dc=example,dc=com','uid=curly,ou=People,dc=example,dc=com']},
+                     'Test Administrators': {
+                        'dn': 'cn=Test Administrators,cn=TestUsers,ou=Groups,dc=example,dc=com',
+                        'name':'Test Administrators',
+                        'members':['uid=rock,ou=People,dc=example,dc=com','uid=larry,ou=People,dc=example,dc=com','uid=curly,ou=People,dc=example,dc=com']},
+                     'OtherGroup': {
+                        'dn': 'cn=OtherGroup,cn=TestUsers,ou=Groups,dc=example,dc=com',
+                        'name':'OtherGroup',
+                        'members':[]}}
 
 
 
 
 
 
@@ -403,7 +425,57 @@ def test_user_admin():
   response = c_su.post('/useradmin/users/new', dict(username="test"))
   response = c_su.post('/useradmin/users/new', dict(username="test"))
   assert_true("You must specify a password when creating a new user." in response.content)
   assert_true("You must specify a password when creating a new user." in response.content)
 
 
-def test_useradmin_ldap_integration():
+
+def test_useradmin_ldap_group_integration():
+  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()
+
+  # Import all members of TestUsers
+  import_ldap_groups('TestUsers', import_members=True, import_members_recursive=False, 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)
+
+  # 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)
+  test_admins = Group.objects.get(name='Test Administrators')
+  assert_equal(len(test_admins.user_set.all()), 2)
+  larry = User.objects.get(username='larry')
+  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 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 all members of TestUsers and members of subgroups
+  import_ldap_groups('TestUsers', import_members=True, import_members_recursive=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)
+
+  # 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)
+  assert_false(LdapGroup.objects.filter(group=hue_group).exists())
+  assert_true(hue_group.user_set.filter(username=hue_user.username).exists())
+
+def test_useradmin_ldap_user_integration():
   reset_all_users()
   reset_all_users()
   reset_all_groups()
   reset_all_groups()
 
 
@@ -424,30 +496,6 @@ def test_useradmin_ldap_integration():
   assert_equal(len(User.objects.all()), 1)
   assert_equal(len(User.objects.all()), 1)
   assert_equal(len(Group.objects.all()), 0)
   assert_equal(len(Group.objects.all()), 0)
 
 
-  # Should import a group, but will only sync already-imported members
-  import_ldap_groups('Test Administrators', import_members=False, import_by_dn=False)
-  assert_equal(len(User.objects.all()), 1)
-  assert_equal(len(Group.objects.all()), 1)
-  test_admins = Group.objects.get(name='Test Administrators')
-  assert_equal(len(test_admins.user_set.all()), 1)
-  assert_equal(test_admins.user_set.all()[0].username, larry.username)
-
-  # Import all members of TestUsers
-  import_ldap_groups('TestUsers', import_members=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)
-
-  ldap_access.CACHED_LDAP_CONN.remove_user_group_for_test('moe', 'TestUsers')
-  import_ldap_groups('TestUsers', import_members=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)
-
-  ldap_access.CACHED_LDAP_CONN.add_user_group_for_test('moe', 'TestUsers')
-  import_ldap_groups('TestUsers', import_members=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)
-
   # Make sure that if a Hue user already exists with a naming collision, we
   # Make sure that if a Hue user already exists with a naming collision, we
   # won't overwrite any of that user's information.
   # won't overwrite any of that user's information.
   hue_user = User.objects.create(username='otherguy', first_name='Different', last_name='Guy')
   hue_user = User.objects.create(username='otherguy', first_name='Different', last_name='Guy')
@@ -456,13 +504,6 @@ def test_useradmin_ldap_integration():
   assert_equal(get_profile(hue_user).creation_method, str(UserProfile.CreationMethod.HUE))
   assert_equal(get_profile(hue_user).creation_method, str(UserProfile.CreationMethod.HUE))
   assert_equal(hue_user.first_name, 'Different')
   assert_equal(hue_user.first_name, 'Different')
 
 
-  # Make sure Hue groups with naming collisions don't get marked as LDAP groups
-  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_by_dn=False)
-  assert_false(LdapGroup.objects.filter(group=hue_group).exists())
-  assert_true(hue_group.user_set.filter(username=hue_user.username).exists())
 
 
 def test_add_ldap_users():
 def test_add_ldap_users():
   URL = reverse(add_ldap_users)
   URL = reverse(add_ldap_users)
@@ -511,8 +552,8 @@ def test_add_ldap_groups():
   assert_true('Location' in response, response)
   assert_true('Location' in response, response)
   assert_true('/useradmin/groups' in response['Location'], response)
   assert_true('/useradmin/groups' in response['Location'], response)
 
 
-  response = c.post(URL, dict(groupname_pattern='toolongnametoolongnametoolongname'))
-  assert_true('30 characters or fewer' in response.context['form'].errors['groupname_pattern'][0], response)
+  response = c.post(URL, dict(groupname_pattern='toolongnametoolongnametoolongnametoolongnametoolongnametoolongnametoolongnametoolongname'))
+  assert_true('Ensure this value has at most 80 characters' in response.context['form'].errors['groupname_pattern'][0], response)
 
 
   # Test wild card
   # Test wild card
   response = c.post(URL, dict(groupname_pattern='*r*'))
   response = c.post(URL, dict(groupname_pattern='*r*'))
@@ -539,7 +580,7 @@ def test_ldap_exception_handling():
   # Set up LDAP tests to use a LdapTestConnection instead of an actual LDAP connection
   # Set up LDAP tests to use a LdapTestConnection instead of an actual LDAP connection
   class LdapTestConnectionError(LdapTestConnection):
   class LdapTestConnectionError(LdapTestConnection):
     def find_users(self, user, find_by_dn=False):
     def find_users(self, user, find_by_dn=False):
-      raise LDAPError('No such object')
+      raise ldap.LDAPError('No such object')
   ldap_access.CACHED_LDAP_CONN = LdapTestConnectionError()
   ldap_access.CACHED_LDAP_CONN = LdapTestConnectionError()
 
 
   c = make_logged_in_client('test', is_superuser=True)
   c = make_logged_in_client('test', is_superuser=True)

+ 16 - 6
apps/useradmin/src/useradmin/views.py

@@ -323,8 +323,9 @@ def add_ldap_groups(request):
       groupname_pattern = form.cleaned_data['groupname_pattern']
       groupname_pattern = form.cleaned_data['groupname_pattern']
       import_by_dn = form.cleaned_data['dn']
       import_by_dn = form.cleaned_data['dn']
       import_members = form.cleaned_data['import_members']
       import_members = form.cleaned_data['import_members']
+      import_members_recursive = form.cleaned_data['import_members_recursive']
       try:
       try:
-        groups = import_ldap_groups(groupname_pattern, import_members, import_by_dn)
+        groups = import_ldap_groups(groupname_pattern, import_members, import_members_recursive, import_by_dn)
       except LDAPError, e:
       except LDAPError, e:
         LOG.error(_("LDAP Exception: %s") % e)
         LOG.error(_("LDAP Exception: %s") % e)
         raise PopupException(_('There was an error when communicating with LDAP'), detail=str(e))
         raise PopupException(_('There was an error when communicating with LDAP'), detail=str(e))
@@ -504,13 +505,14 @@ def _import_ldap_users(username_pattern, import_by_dn=False):
   return imported_users
   return imported_users
 
 
 
 
-def _import_ldap_groups(groupname_pattern, import_members=False, import_by_dn=False):
+def _import_ldap_groups(groupname_pattern, import_members=False, recursive_import_members=False, import_by_dn=False):
   """
   """
   Import a group from LDAP. If import_members is true, this will also import any
   Import a group from LDAP. If import_members is true, this will also import any
   LDAP users that exist within the group.
   LDAP users that exist within the group.
   """
   """
   conn = ldap_access.get_connection()
   conn = ldap_access.get_connection()
   group_info = conn.find_groups(groupname_pattern, import_by_dn)
   group_info = conn.find_groups(groupname_pattern, import_by_dn)
+
   if not group_info:
   if not group_info:
     LOG.warn(_("Could not get LDAP details for group pattern %s") % groupname_pattern)
     LOG.warn(_("Could not get LDAP details for group pattern %s") % groupname_pattern)
     return None
     return None
@@ -527,9 +529,17 @@ def _import_ldap_groups(groupname_pattern, import_members=False, import_by_dn=Fa
       return None
       return None
 
 
     LdapGroup.objects.get_or_create(group=group)
     LdapGroup.objects.get_or_create(group=group)
-
     group.user_set.clear()
     group.user_set.clear()
-    for member in ldap_info['members']:
+
+    # Find members for group and subgoups
+    members = ldap_info['members']
+    if recursive_import_members:
+      sub_group_info = conn.find_groups(ldap_info['dn'], True)
+      for sub_ldap_info in sub_group_info:
+        members.extend(sub_ldap_info['members'])
+
+    # Import/fetch users
+    for member in members:
       users = []
       users = []
 
 
       if import_members:
       if import_members:
@@ -567,8 +577,8 @@ def import_ldap_users(user_pattern, import_by_dn):
   return _import_ldap_users(user_pattern, import_by_dn)
   return _import_ldap_users(user_pattern, import_by_dn)
 
 
 
 
-def import_ldap_groups(group_pattern, import_members, import_by_dn):
-  return _import_ldap_groups(group_pattern, import_members, 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 sync_ldap_users():
 def sync_ldap_users():

+ 1 - 1
desktop/core/src/desktop/lib/django_util.py

@@ -44,7 +44,7 @@ MAKO = 'mako'
 
 
 # This is what Debian allows. See chkname.c in shadow.
 # This is what Debian allows. See chkname.c in shadow.
 USERNAME_RE_RULE = "[^-:\s][^:\s]*"
 USERNAME_RE_RULE = "[^-:\s][^:\s]*"
-GROUPNAME_RE_RULE = ".{,30}"
+GROUPNAME_RE_RULE = ".{,80}"
 
 
 
 
 # For backward compatibility for upgrades to Hue 2.2
 # For backward compatibility for upgrades to Hue 2.2