Przeglądaj źródła

[useradmin] Add n users that match wild card expression in LDAP import

User and group import support wild card expressions.
Add all matched users and groups.
abec 13 lat temu
rodzic
commit
896fa2093d

+ 14 - 14
apps/useradmin/src/useradmin/forms.py

@@ -99,8 +99,8 @@ class SuperUserChangeForm(UserChangeForm):
         self.initial['groups'] = []
         self.initial['groups'] = []
 
 
 
 
-class AddLdapUserForm(forms.Form):
-  username = forms.RegexField(
+class AddLdapUsersForm(forms.Form):
+  username_pattern = forms.RegexField(
       label=_("Username"),
       label=_("Username"),
       max_length=64,
       max_length=64,
       regex='^%s$' % (get_username_re_rule(),),
       regex='^%s$' % (get_username_re_rule(),),
@@ -117,22 +117,22 @@ class AddLdapUserForm(forms.Form):
                                             required=False)
                                             required=False)
 
 
   def clean(self):
   def clean(self):
-    cleaned_data = super(AddLdapUserForm, self).clean()
-    username = cleaned_data.get("username")
+    cleaned_data = super(AddLdapUsersForm, self).clean()
+    username_pattern = cleaned_data.get("username_pattern")
     dn = cleaned_data.get("dn")
     dn = cleaned_data.get("dn")
 
 
     if not 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())
+      if username_pattern is not None and len(username_pattern) > 30:
+        msg = _('Too long: 30 characters or fewer and not %s') % username_pattern
+        errors = self._errors.setdefault('username_pattern', ErrorList())
         errors.append(msg)
         errors.append(msg)
         raise forms.ValidationError(msg)
         raise forms.ValidationError(msg)
 
 
     return cleaned_data
     return cleaned_data
 
 
 
 
-class AddLdapGroupForm(forms.Form):
-  name = forms.RegexField(
+class AddLdapGroupsForm(forms.Form):
+  groupname_pattern = forms.RegexField(
       label="Name",
       label="Name",
       max_length=64,
       max_length=64,
       regex='^%s$' % get_groupname_re_rule(),
       regex='^%s$' % get_groupname_re_rule(),
@@ -153,14 +153,14 @@ class AddLdapGroupForm(forms.Form):
                                                 required=False)
                                                 required=False)
 
 
   def clean(self):
   def clean(self):
-    cleaned_data = super(AddLdapGroupForm, self).clean()
-    name = cleaned_data.get("name")
+    cleaned_data = super(AddLdapGroupsForm, self).clean()
+    groupname_pattern = cleaned_data.get("groupname_pattern")
     dn = cleaned_data.get("dn")
     dn = cleaned_data.get("dn")
 
 
     if not 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())
+      if groupname_pattern is not None and len(groupname_pattern) > 30:
+        msg = _('Too long: 30 characters or fewer and not %s') % groupname_pattern
+        errors = self._errors.setdefault('groupname_pattern', ErrorList())
         errors.append(msg)
         errors.append(msg)
         raise forms.ValidationError(msg)
         raise forms.ValidationError(msg)
 
 

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

@@ -81,7 +81,7 @@ class LdapConnection(object):
       except:
       except:
         raise RuntimeError("Failed to bind to LDAP server anonymously")
         raise RuntimeError("Failed to bind to LDAP server anonymously")
 
 
-  def find_user(self, username, find_by_dn=False):
+  def find_users(self, username_pattern, find_by_dn=False):
     """
     """
     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.
@@ -95,7 +95,7 @@ class LdapConnection(object):
     user_name_attr = desktop.conf.LDAP.USERS.USER_NAME_ATTR.get()
     user_name_attr = desktop.conf.LDAP.USERS.USER_NAME_ATTR.get()
 
 
     # Allow wild cards on non distinguished names
     # Allow wild cards on non distinguished names
-    sanitized_name = ldap.filter.escape_filter_chars(username).replace(r'\2a', r'*')
+    sanitized_name = ldap.filter.escape_filter_chars(username_pattern).replace(r'\2a', r'*')
     if find_by_dn:
     if find_by_dn:
       user_name_filter = '(distinguishedName=' + sanitized_name + ')'
       user_name_filter = '(distinguishedName=' + sanitized_name + ')'
     else:
     else:
@@ -104,22 +104,24 @@ class LdapConnection(object):
 
 
     ldap_result_id = self.ldap_handle.search(base_dn, scope, ldap_filter)
     ldap_result_id = self.ldap_handle.search(base_dn, scope, ldap_filter)
     result_type, result_data = self.ldap_handle.result(ldap_result_id)
     result_type, result_data = self.ldap_handle.result(ldap_result_id)
+    user_info = []
     if result_data and result_type == ldap.RES_SEARCH_RESULT:
     if result_data and result_type == ldap.RES_SEARCH_RESULT:
-      data = result_data[0][1]
-      user_info = { 'username': data[user_name_attr][0] }
+      for result_data_item in result_data:
+        data = result_data_item[1]
+        ldap_info = { 'username': data[user_name_attr][0] }
 
 
-      if 'givenName' in data:
-        user_info['first'] = data['givenName'][0]
-      if 'sn' in data:
-        user_info['last'] = data['sn'][0]
-      if 'mail' in data:
-        user_info['email'] = data['mail'][0]
+        if 'givenName' in data:
+          ldap_info['first'] = data['givenName'][0]
+        if 'sn' in data:
+          ldap_info['last'] = data['sn'][0]
+        if 'mail' in data:
+          ldap_info['email'] = data['mail'][0]
 
 
-      return user_info
+        user_info.append(ldap_info)
 
 
-    return None
+    return user_info
 
 
-  def find_group(self, groupname, find_by_dn=False):
+  def find_groups(self, groupname_pattern, find_by_dn=False):
     """
     """
     LDAP search helper method for finding groups
     LDAP search helper method for finding groups
     """
     """
@@ -132,28 +134,31 @@ class LdapConnection(object):
     group_name_attr = desktop.conf.LDAP.GROUPS.GROUP_NAME_ATTR.get()
     group_name_attr = desktop.conf.LDAP.GROUPS.GROUP_NAME_ATTR.get()
 
 
     if find_by_dn:
     if find_by_dn:
-      sanitized_name = ldap.filter.escape_filter_chars(groupname)
+      sanitized_name = ldap.filter.escape_filter_chars(groupname_pattern).replace(r'\2a', r'*')
       group_name_filter = '(distinguishedName=' + sanitized_name + ')'
       group_name_filter = '(distinguishedName=' + sanitized_name + ')'
     else:
     else:
-      sanitized_name = ldap.filter.escape_filter_chars(groupname)
+      sanitized_name = ldap.filter.escape_filter_chars(groupname_pattern)
       group_name_filter = '(' + group_name_attr + '=' + sanitized_name + ')'
       group_name_filter = '(' + group_name_attr + '=' + sanitized_name + ')'
     ldap_filter = '(&' + group_filter + group_name_filter + ')'
     ldap_filter = '(&' + group_filter + group_name_filter + ')'
 
 
     ldap_result_id = self.ldap_handle.search(base_dn, scope, ldap_filter)
     ldap_result_id = self.ldap_handle.search(base_dn, scope, ldap_filter)
     result_type, result_data = self.ldap_handle.result(ldap_result_id)
     result_type, result_data = self.ldap_handle.result(ldap_result_id)
+
+    group_info = []
     if result_data and result_type == ldap.RES_SEARCH_RESULT:
     if result_data and result_type == ldap.RES_SEARCH_RESULT:
-      data = result_data[0][1]
-      group_info = { 'name': data[group_name_attr][0] }
+      for result_data_item in result_data:
+        data = result_data_item[1]
+        ldap_info = { 'name': data[group_name_attr][0] }
 
 
-      member_attr = desktop.conf.LDAP.GROUPS.GROUP_MEMBER_ATTR.get()
-      if member_attr in data:
-        group_info['members'] = data[member_attr]
-      else:
-        group_info['members'] = []
+        member_attr = desktop.conf.LDAP.GROUPS.GROUP_MEMBER_ATTR.get()
+        if member_attr in data:
+          ldap_info['members'] = data[member_attr]
+        else:
+          ldap_info['members'] = []
 
 
-      return group_info
+        group_info.append(ldap_info)
 
 
-    return None
+    return group_info
 
 
   def _get_root_dn(self):
   def _get_root_dn(self):
     """
     """

+ 1 - 1
apps/useradmin/src/useradmin/templates/add_ldap_user.mako → apps/useradmin/src/useradmin/templates/add_ldap_users.mako

@@ -47,7 +47,7 @@ ${layout.menubar(section='users', _=_)}
       % else:
       % else:
           <input type="submit" class="btn btn-primary" value="${_('Add/Sync user')}"/>
           <input type="submit" class="btn btn-primary" value="${_('Add/Sync user')}"/>
       % endif
       % endif
-      <a href="/useradmin/users" class="btn">${_('Cancel')}</a>
+      <a href="${url('useradmin.views.list_users')}" class="btn">${_('Cancel')}</a>
     </div>
     </div>
   </form>
   </form>
 </div>
 </div>

+ 1 - 1
apps/useradmin/src/useradmin/templates/list_groups.mako

@@ -37,7 +37,7 @@ ${layout.menubar(section='groups', _=_)}
         <%def name="creation()">
         <%def name="creation()">
             %if user.is_superuser:
             %if user.is_superuser:
                 <a id="addGroupBtn" href="${url('useradmin.views.edit_group')}" class="btn"><i class="icon-plus-sign"></i> ${_('Add group')}</a>
                 <a id="addGroupBtn" href="${url('useradmin.views.edit_group')}" class="btn"><i class="icon-plus-sign"></i> ${_('Add group')}</a>
-                <a id="addLdapGroupBtn" href="${url('useradmin.views.add_ldap_group')}" class="btn"><i class="icon-refresh"></i> ${_('Add/Sync LDAP group')}</a>
+                <a id="addLdapGroupBtn" href="${url('useradmin.views.add_ldap_groups')}" class="btn"><i class="icon-refresh"></i> ${_('Add/Sync LDAP group')}</a>
             %endif
             %endif
         </%def>
         </%def>
     </%actionbar:render>
     </%actionbar:render>

+ 1 - 1
apps/useradmin/src/useradmin/templates/list_users.mako

@@ -37,7 +37,7 @@ ${layout.menubar(section='users', _=_)}
         <%def name="creation()">
         <%def name="creation()">
             %if user.is_superuser:
             %if user.is_superuser:
                 <a href="${ url('useradmin.views.edit_user') }" class="btn"><i class="icon-user"></i> ${_('Add user')}</a>
                 <a href="${ url('useradmin.views.edit_user') }" class="btn"><i class="icon-user"></i> ${_('Add user')}</a>
-                <a href="${ url('useradmin.views.add_ldap_user') }" class="btn"><i class="icon-briefcase"></i> ${_('Add/Sync LDAP user')}</a>
+                <a href="${ url('useradmin.views.add_ldap_users') }" class="btn"><i class="icon-briefcase"></i> ${_('Add/Sync LDAP user')}</a>
                 <a href="javascript:void(0)" class="btn confirmationModal" data-confirmation-url="${ url('useradmin.views.sync_ldap_users_groups') }"><i class="icon-refresh"></i> ${_('Sync LDAP users/groups')}</a>
                 <a href="javascript:void(0)" class="btn confirmationModal" data-confirmation-url="${ url('useradmin.views.sync_ldap_users_groups') }"><i class="icon-refresh"></i> ${_('Sync LDAP users/groups')}</a>
             %endif
             %endif
         </%def>
         </%def>

+ 57 - 38
apps/useradmin/src/useradmin/tests.py

@@ -21,6 +21,7 @@
 Tests for "user admin"
 Tests for "user admin"
 """
 """
 
 
+import re
 import urllib
 import urllib
 from ldap import LDAPError
 from ldap import LDAPError
 
 
@@ -37,8 +38,8 @@ from useradmin.models import get_profile, get_default_user_group
 
 
 import useradmin.conf
 import useradmin.conf
 from hadoop import pseudo_hdfs4
 from hadoop import pseudo_hdfs4
-from views import sync_ldap_users, sync_ldap_groups, import_ldap_user, import_ldap_group, \
-                  add_ldap_user, add_ldap_group, sync_ldap_users_groups
+from views import sync_ldap_users, sync_ldap_groups, import_ldap_users, import_ldap_groups, \
+                  add_ldap_users, add_ldap_groups, sync_ldap_users_groups
 import ldap_access
 import ldap_access
 
 
 def reset_all_users():
 def reset_all_users():
@@ -71,13 +72,17 @@ 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_user(self, user, find_by_dn=False):
+  def find_users(self, username_pattern, find_by_dn=False):
     """ Returns info for a particular user """
     """ Returns info for a particular user """
-    return LdapTestConnection._instance.users.get(user, None)
+    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_group(self, groupname, find_by_dn=False):
+  def find_groups(self, groupname_pattern, find_by_dn=False):
     """ Return all groups in the system with parents and children """
     """ Return all groups in the system with parents and children """
-    return LdapTestConnection._instance.groups.get(groupname, None)
+    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]
 
 
   class _Singleton:
   class _Singleton:
     def __init__(self):
     def __init__(self):
@@ -239,8 +244,7 @@ def test_group_admin():
 
 
   group_count = len(Group.objects.all())
   group_count = len(Group.objects.all())
   response = c.post('/useradmin/groups/new', dict(name="with space"))
   response = c.post('/useradmin/groups/new', dict(name="with space"))
-  assert_true("Group name may only contain letters" in response.content)
-  assert_equal(len(Group.objects.all()), group_count)
+  assert_equal(len(Group.objects.all()), group_count + 1)
 
 
 def test_user_admin():
 def test_user_admin():
   FUNNY_NAME = '~`!@#$%^&*()_-+={}[]|\;"<>?/,.'
   FUNNY_NAME = '~`!@#$%^&*()_-+={}[]|\;"<>?/,.'
@@ -378,7 +382,7 @@ def test_useradmin_ldap_integration():
   ldap_access.CACHED_LDAP_CONN = LdapTestConnection()
   ldap_access.CACHED_LDAP_CONN = LdapTestConnection()
 
 
   # Try importing a user
   # Try importing a user
-  import_ldap_user('larry', import_by_dn=False)
+  import_ldap_users('larry', import_by_dn=False)
   larry = User.objects.get(username='larry')
   larry = User.objects.get(username='larry')
   assert_true(larry.first_name == 'Larry')
   assert_true(larry.first_name == 'Larry')
   assert_true(larry.last_name == 'Stooge')
   assert_true(larry.last_name == 'Stooge')
@@ -392,7 +396,7 @@ def test_useradmin_ldap_integration():
   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
   # Should import a group, but will only sync already-imported members
-  import_ldap_group('Test Administrators', import_members=False, import_by_dn=False)
+  import_ldap_groups('Test Administrators', import_members=False, import_by_dn=False)
   assert_equal(len(User.objects.all()), 1)
   assert_equal(len(User.objects.all()), 1)
   assert_equal(len(Group.objects.all()), 1)
   assert_equal(len(Group.objects.all()), 1)
   test_admins = Group.objects.get(name='Test Administrators')
   test_admins = Group.objects.get(name='Test Administrators')
@@ -400,25 +404,25 @@ def test_useradmin_ldap_integration():
   assert_equal(test_admins.user_set.all()[0].username, larry.username)
   assert_equal(test_admins.user_set.all()[0].username, larry.username)
 
 
   # Import all members of TestUsers
   # Import all members of TestUsers
-  import_ldap_group('TestUsers', import_members=True, import_by_dn=False)
+  import_ldap_groups('TestUsers', import_members=True, import_by_dn=False)
   test_users = Group.objects.get(name='TestUsers')
   test_users = Group.objects.get(name='TestUsers')
   assert_true(LdapGroup.objects.filter(group=test_users).exists())
   assert_true(LdapGroup.objects.filter(group=test_users).exists())
   assert_equal(len(test_users.user_set.all()), 3)
   assert_equal(len(test_users.user_set.all()), 3)
 
 
   ldap_access.CACHED_LDAP_CONN.remove_user_group_for_test('moe', 'TestUsers')
   ldap_access.CACHED_LDAP_CONN.remove_user_group_for_test('moe', 'TestUsers')
-  import_ldap_group('TestUsers', import_members=False, import_by_dn=False)
+  import_ldap_groups('TestUsers', import_members=False, import_by_dn=False)
   assert_equal(len(test_users.user_set.all()), 2)
   assert_equal(len(test_users.user_set.all()), 2)
   assert_equal(len(User.objects.get(username='moe').groups.all()), 0)
   assert_equal(len(User.objects.get(username='moe').groups.all()), 0)
 
 
   ldap_access.CACHED_LDAP_CONN.add_user_group_for_test('moe', 'TestUsers')
   ldap_access.CACHED_LDAP_CONN.add_user_group_for_test('moe', 'TestUsers')
-  import_ldap_group('TestUsers', import_members=False, import_by_dn=False)
+  import_ldap_groups('TestUsers', import_members=False, import_by_dn=False)
   assert_equal(len(test_users.user_set.all()), 3)
   assert_equal(len(test_users.user_set.all()), 3)
   assert_equal(len(User.objects.get(username='moe').groups.all()), 1)
   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')
-  import_ldap_user('otherguy', import_by_dn=False)
+  import_ldap_users('otherguy', import_by_dn=False)
   hue_user = User.objects.get(username='otherguy')
   hue_user = User.objects.get(username='otherguy')
   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')
@@ -427,12 +431,12 @@ def test_useradmin_ldap_integration():
   hue_group = Group.objects.create(name='OtherGroup')
   hue_group = Group.objects.create(name='OtherGroup')
   hue_group.user_set.add(hue_user)
   hue_group.user_set.add(hue_user)
   hue_group.save()
   hue_group.save()
-  import_ldap_group('OtherGroup', import_members=False, import_by_dn=False)
+  import_ldap_groups('OtherGroup', import_members=False, import_by_dn=False)
   assert_false(LdapGroup.objects.filter(group=hue_group).exists())
   assert_false(LdapGroup.objects.filter(group=hue_group).exists())
   assert_true(hue_group.user_set.filter(username=hue_user.username).exists())
   assert_true(hue_group.user_set.filter(username=hue_user.username).exists())
 
 
-def test_add_ldap_user():
-  URL = reverse(add_ldap_user)
+def test_add_ldap_users():
+  URL = reverse(add_ldap_users)
 
 
   reset_all_users()
   reset_all_users()
   reset_all_groups()
   reset_all_groups()
@@ -444,14 +448,19 @@ def test_add_ldap_user():
 
 
   assert_true(c.get(URL))
   assert_true(c.get(URL))
 
 
-  response = c.post(URL, dict(username='moe', password1='test', password2='test'))
-  assert_true('/useradmin/users' in response['Location'])
+  response = c.post(URL, dict(username_pattern='moe', password1='test', password2='test'))
+  assert_true('Location' in response, response)
+  assert_true('/useradmin/users' in response['Location'], response)
+
+  response = c.post(URL, dict(username_pattern='bad_name', password1='test', password2='test'))
+  assert_true('Could not' in response.context['form'].errors['username_pattern'][0], response)
 
 
-  response = c.post(URL, dict(username='bad_name', password1='test', password2='test'))
-  assert_true('Could not' in response.context['form'].errors['username'][0])
+  # Test wild card
+  response = c.post(URL, dict(username_pattern='*r*', password1='test', password2='test'))
+  assert_true('/useradmin/users' in response['Location'], response)
 
 
-def test_add_ldap_group():
-  URL = reverse(add_ldap_group)
+def test_add_ldap_groups():
+  URL = reverse(add_ldap_groups)
 
 
   reset_all_users()
   reset_all_users()
   reset_all_groups()
   reset_all_groups()
@@ -464,16 +473,17 @@ def test_add_ldap_group():
 
 
   assert_true(c.get(URL))
   assert_true(c.get(URL))
 
 
-  response = c.post(URL, dict(name='TestUsers', password1='test', password2='test'))
+  response = c.post(URL, dict(groupname_pattern='TestUsers'))
+  assert_true('Location' in response, response)
   assert_true('/useradmin/groups' in response['Location'])
   assert_true('/useradmin/groups' in response['Location'])
 
 
   # Test with space
   # Test with space
-  response = c.post(URL, dict(name='Test Administrators', password1='test', password2='test'))
-  assert_true('/useradmin/groups' in response['Location'])
+  response = c.post(URL, dict(groupname_pattern='Test Administrators'))
+  assert_true('Location' in response, response)
+  assert_true('/useradmin/groups' in response['Location'], response)
 
 
-  response = c.post(URL, dict(name='toolongnametoolongnametoolongname',
-                              password1='test', password2='test'))
-  assert_true('Too long' in response.context['form'].errors['name'][0])
+  response = c.post(URL, dict(groupname_pattern='toolongnametoolongnametoolongname'))
+  assert_true('30 characters or fewer' in response.context['form'].errors['groupname_pattern'][0], response)
 
 
 def test_sync_ldap_users_groups():
 def test_sync_ldap_users_groups():
   URL = reverse(sync_ldap_users_groups)
   URL = reverse(sync_ldap_users_groups)
@@ -495,18 +505,18 @@ 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_user(self, user, find_by_dn=False):
+    def find_users(self, user, find_by_dn=False):
       raise LDAPError('No such object')
       raise 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)
 
 
-  response = c.post(reverse(add_ldap_user), dict(username='moe', password1='test', password2='test'), follow=True)
+  response = c.post(reverse(add_ldap_users), dict(username_pattern='moe', password1='test', password2='test'), follow=True)
   assert_true('There was an error when communicating with LDAP' in response.content, response)
   assert_true('There was an error when communicating with LDAP' in response.content, response)
 
 
 @attr('requires_hadoop')
 @attr('requires_hadoop')
-def test_ensure_home_directory_add_ldap_user():
-  URL = reverse(add_ldap_user)
+def test_ensure_home_directory_add_ldap_users():
+  URL = reverse(add_ldap_users)
 
 
   reset_all_users()
   reset_all_users()
   reset_all_groups()
   reset_all_groups()
@@ -520,24 +530,33 @@ def test_ensure_home_directory_add_ldap_user():
 
 
   assert_true(c.get(URL))
   assert_true(c.get(URL))
 
 
-  response = c.post(URL, dict(username='moe', password1='test', password2='test'))
+  response = c.post(URL, dict(username_pattern='moe', password1='test', password2='test'))
   assert_true('/useradmin/users' in response['Location'])
   assert_true('/useradmin/users' in response['Location'])
   assert_false(cluster.fs.exists('/user/moe'))
   assert_false(cluster.fs.exists('/user/moe'))
 
 
   # Try same thing with home directory creation.
   # Try same thing with home directory creation.
-  response = c.post(URL, dict(username='curly', password1='test', password2='test', ensure_home_directory=True))
+  response = c.post(URL, dict(username_pattern='curly', password1='test', password2='test', ensure_home_directory=True))
   assert_true('/useradmin/users' in response['Location'])
   assert_true('/useradmin/users' in response['Location'])
   assert_true(cluster.fs.exists('/user/curly'))
   assert_true(cluster.fs.exists('/user/curly'))
 
 
-  response = c.post(URL, dict(username='bad_name', password1='test', password2='test'))
-  assert_true('Could not' in response.context['form'].errors['username'][0])
+  response = c.post(URL, dict(username_pattern='bad_name', password1='test', password2='test'))
+  assert_true('Could not' in response.context['form'].errors['username_pattern'][0])
   assert_false(cluster.fs.exists('/user/bad_name'))
   assert_false(cluster.fs.exists('/user/bad_name'))
 
 
   # See if moe, who did not ask for his home directory, has a home directory.
   # See if moe, who did not ask for his home directory, has a home directory.
   assert_false(cluster.fs.exists('/user/moe'))
   assert_false(cluster.fs.exists('/user/moe'))
 
 
+  # Try wild card now
+  response = c.post(URL, dict(username_pattern='*r*', password1='test', password2='test', ensure_home_directory=True))
+  assert_true('/useradmin/users' in response['Location'])
+  assert_true(cluster.fs.exists('/user/curly'))
+  assert_true(cluster.fs.exists('/user/larry'))
+  assert_true(cluster.fs.exists('/user/otherguy'))
+
   # Clean up
   # Clean up
   cluster.fs.rmtree('/user/curly')
   cluster.fs.rmtree('/user/curly')
+  cluster.fs.rmtree('/user/larry')
+  cluster.fs.rmtree('/user/otherguy')
 
 
 @attr('requires_hadoop')
 @attr('requires_hadoop')
 def test_ensure_home_directory_sync_ldap_users_groups():
 def test_ensure_home_directory_sync_ldap_users_groups():
@@ -553,7 +572,7 @@ def test_ensure_home_directory_sync_ldap_users_groups():
   c = make_logged_in_client(cluster.superuser, is_superuser=True)
   c = make_logged_in_client(cluster.superuser, is_superuser=True)
   cluster.fs.setuser(cluster.superuser)
   cluster.fs.setuser(cluster.superuser)
 
 
-  response = c.post(reverse(add_ldap_user), dict(username='curly', password1='test', password2='test'))
+  response = c.post(reverse(add_ldap_users), dict(username_pattern='curly', password1='test', password2='test'))
   assert_false(cluster.fs.exists('/user/curly'))
   assert_false(cluster.fs.exists('/user/curly'))
   assert_true(c.post(URL, dict(ensure_home_directory=True)))
   assert_true(c.post(URL, dict(ensure_home_directory=True)))
   assert_true(cluster.fs.exists('/user/curly'))
   assert_true(cluster.fs.exists('/user/curly'))

+ 2 - 2
apps/useradmin/src/useradmin/urls.py

@@ -27,8 +27,8 @@ urlpatterns = patterns('useradmin.views',
   url(r'^groups$', 'list_groups'),
   url(r'^groups$', 'list_groups'),
   url(r'^permissions$', 'list_permissions'),
   url(r'^permissions$', 'list_permissions'),
   url(r'^users/edit/(?P<username>%s)$' % (username_re,), 'edit_user'),
   url(r'^users/edit/(?P<username>%s)$' % (username_re,), 'edit_user'),
-  url(r'^users/add_ldap_user$', 'add_ldap_user'),
-  url(r'^users/add_ldap_group$', 'add_ldap_group'),
+  url(r'^users/add_ldap_users$', 'add_ldap_users'),
+  url(r'^users/add_ldap_groups$', 'add_ldap_groups'),
   url(r'^users/sync_ldap_users_groups$', 'sync_ldap_users_groups'),
   url(r'^users/sync_ldap_users_groups$', 'sync_ldap_users_groups'),
   url(r'^groups/edit/(?P<name>%s)$' % (groupname_re,), 'edit_group'),
   url(r'^groups/edit/(?P<name>%s)$' % (groupname_re,), 'edit_group'),
   url(r'^permissions/edit/(?P<app>.*)/(?P<priv>.*)$', 'edit_permission'),
   url(r'^permissions/edit/(?P<app>.*)/(?P<priv>.*)$', 'edit_permission'),

+ 112 - 86
apps/useradmin/src/useradmin/views.py

@@ -39,8 +39,8 @@ from django.shortcuts import redirect
 from hadoop.fs.exceptions import WebHdfsException
 from hadoop.fs.exceptions import WebHdfsException
 from useradmin.models import HuePermission, UserProfile, LdapGroup
 from useradmin.models import HuePermission, UserProfile, LdapGroup
 from useradmin.models import get_profile, get_default_user_group
 from useradmin.models import get_profile, get_default_user_group
-from useradmin.forms import SyncLdapUsersGroupsForm, AddLdapGroupForm,\
-  AddLdapUserForm, PermissionsEditForm, GroupEditForm, SuperUserChangeForm,\
+from useradmin.forms import SyncLdapUsersGroupsForm, AddLdapGroupsForm,\
+  AddLdapUsersForm, PermissionsEditForm, GroupEditForm, SuperUserChangeForm,\
   UserChangeForm
   UserChangeForm
 
 
 
 
@@ -256,9 +256,9 @@ def edit_permission(request, app=None, priv=None):
   return render('edit_permissions.mako', request, dict(form=form, action=request.path, app=app, priv=priv))
   return render('edit_permissions.mako', request, dict(form=form, action=request.path, app=app, priv=priv))
 
 
 
 
-def add_ldap_user(request):
+def add_ldap_users(request):
   """
   """
-  add_ldap_user(request) -> reply
+  add_ldap_users(request) -> reply
 
 
   Handler for importing LDAP users into the Hue database.
   Handler for importing LDAP users into the Hue database.
 
 
@@ -269,36 +269,37 @@ def add_ldap_user(request):
     raise PopupException(_("You must be a superuser to add another user."), error_code=401)
     raise PopupException(_("You must be a superuser to add another user."), error_code=401)
 
 
   if request.method == 'POST':
   if request.method == 'POST':
-    form = AddLdapUserForm(request.POST)
+    form = AddLdapUsersForm(request.POST)
     if form.is_valid():
     if form.is_valid():
-      username = form.cleaned_data['username']
+      username_pattern = form.cleaned_data['username_pattern']
       import_by_dn = form.cleaned_data['dn']
       import_by_dn = form.cleaned_data['dn']
       try:
       try:
-        user = import_ldap_user(username, import_by_dn)
+        users = import_ldap_users(username_pattern, 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))
 
 
-      if user and form.cleaned_data['ensure_home_directory']:
-        try:
-          ensure_home_directory(request.fs, user.username)
-        except (IOError, WebHdfsException), e:
-          request.error(_("Cannot make home directory for user %s." % user.username))
+      if users and form.cleaned_data['ensure_home_directory']:
+        for user in users:
+          try:
+            ensure_home_directory(request.fs, user.username)
+          except (IOError, WebHdfsException), e:
+            request.error(_("Cannot make home directory for user %s." % user.username))
 
 
-      if user is None:
-        errors = form._errors.setdefault('username', ErrorList())
-        errors.append(_('Could not get LDAP details for user %(username)s.') % dict(username=(username,)))
+      if not users:
+        errors = form._errors.setdefault('username_pattern', ErrorList())
+        errors.append(_('Could not get LDAP details for users in pattern %s.') % username_pattern)
       else:
       else:
         return redirect(reverse(list_users))
         return redirect(reverse(list_users))
   else:
   else:
-    form = AddLdapUserForm()
+    form = AddLdapUsersForm()
 
 
-  return render('add_ldap_user.mako', request, dict(form=form))
+  return render('add_ldap_users.mako', request, dict(form=form))
 
 
 
 
-def add_ldap_group(request):
+def add_ldap_groups(request):
   """
   """
-  add_ldap_group(request) -> reply
+  add_ldap_groups(request) -> reply
 
 
   Handler for importing LDAP groups into the Hue database.
   Handler for importing LDAP groups into the Hue database.
 
 
@@ -310,24 +311,25 @@ def add_ldap_group(request):
     raise PopupException(_("You must be a superuser to add another group."), error_code=401)
     raise PopupException(_("You must be a superuser to add another group."), error_code=401)
 
 
   if request.method == 'POST':
   if request.method == 'POST':
-    form = AddLdapGroupForm(request.POST)
+    form = AddLdapGroupsForm(request.POST)
     if form.is_valid():
     if form.is_valid():
-      groupname = form.cleaned_data['name']
+      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']
       try:
       try:
-        group = import_ldap_group(groupname, import_members, import_by_dn)
+        groups = import_ldap_groups(groupname_pattern, import_members, 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))
 
 
-      if group is None:
-        errors = form._errors.setdefault('name', ErrorList())
-        errors.append(_('Could not get LDAP details for group %(groupname)s') % dict(groupname=(groupname,)))
-      else:
+      if groups:
         return redirect(reverse(list_groups))
         return redirect(reverse(list_groups))
+      else:
+        errors = form._errors.setdefault('groupname_pattern', ErrorList())
+        errors.append(_('Could not get LDAP details for groups in pattern %s') % groupname_pattern)
+
   else:
   else:
-    form = AddLdapGroupForm()
+    form = AddLdapGroupsForm()
 
 
   return render('edit_group.mako', request, dict(form=form, action=request.path, ldap=True))
   return render('edit_group.mako', request, dict(form=form, action=request.path, ldap=True))
 
 
@@ -446,96 +448,120 @@ def sync_unix_users_and_groups(min_uid, max_uid, min_gid, max_gid, check_shell):
       # Here's where that user to group map we built comes in handy
       # Here's where that user to group map we built comes in handy
       hue_user.groups = user_groups[username]
       hue_user.groups = user_groups[username]
     hue_user.save()
     hue_user.save()
-    LOG.info("Synced user %s from Unix" % (hue_user.username,))
+    LOG.info(_("Synced user %s from Unix") % hue_user.username)
 
 
   __users_lock.release()
   __users_lock.release()
   __groups_lock.release()
   __groups_lock.release()
 
 
 
 
-def _import_ldap_user(username, import_by_dn=False):
+def _import_ldap_users(username_pattern, import_by_dn=False):
   """
   """
   Import a user from LDAP. If import_by_dn is true, this will import the user by
   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.
   the distinguished name, rather than the configured username attribute.
   """
   """
   conn = ldap_access.get_connection()
   conn = ldap_access.get_connection()
-  user_info = conn.find_user(username, import_by_dn)
-  if user_info is None:
-    LOG.warn("Could not get LDAP details for user %s" % (username,))
+  user_info = conn.find_users(username_pattern, import_by_dn)
+  if not user_info:
+    LOG.warn(_("Could not get LDAP details for users with pattern %s") % username_pattern)
     return None
     return None
 
 
-  user, created = User.objects.get_or_create(username=user_info['username'])
-  profile = get_profile(user)
-  if not created and profile.creation_method == str(UserProfile.CreationMethod.HUE):
-    # This is a Hue user, and shouldn't be overwritten
-    LOG.warn('There was a naming conflict while importing user %s' % (username,))
-    return None
+  imported_users = []
+  for ldap_info in user_info:
+    user, created = User.objects.get_or_create(username=ldap_info['username'])
+    profile = get_profile(user)
+    if not created and profile.creation_method == str(UserProfile.CreationMethod.HUE):
+      # This is a Hue user, and shouldn't be overwritten
+      LOG.warn(_('There was a naming conflict while importing user %(username)s in pattern %(username_pattern)s') % {
+        'username': ldap_info['username'],
+        'username_pattern': username_pattern
+      })
+      return None
+
+    default_group = get_default_user_group()
+    if created and default_group is not None:
+      user.groups.add(default_group)
 
 
-  default_group = get_default_user_group()
-  if created and default_group is not None:
-    user.groups.add(default_group)
+    if 'first' in ldap_info:
+      user.first_name = ldap_info['first']
+    if 'last' in ldap_info:
+      user.last_name = ldap_info['last']
+    if 'email' in ldap_info:
+      user.email = ldap_info['email']
 
 
-  if 'first' in user_info:
-    user.first_name = user_info['first']
-  if 'last' in user_info:
-    user.last_name = user_info['last']
-  if 'email' in user_info:
-    user.email = user_info['email']
+    profile.creation_method = UserProfile.CreationMethod.EXTERNAL
+    profile.save()
+    user.save()
 
 
-  profile.creation_method = UserProfile.CreationMethod.EXTERNAL
-  profile.save()
-  user.save()
+    imported_users.append(user)
 
 
-  return user
+  return imported_users
 
 
 
 
-def _import_ldap_group(groupname, import_members=False, import_by_dn=False):
+def _import_ldap_groups(groupname_pattern, 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_group(groupname, import_by_dn)
-  if group_info is None:
-    LOG.warn("Could not get LDAP details for group %s" % (groupname,))
+  group_info = conn.find_groups(groupname_pattern, import_by_dn)
+  if not group_info:
+    LOG.warn(_("Could not get LDAP details for group pattern %s") % groupname_pattern)
     return None
     return None
 
 
-  group, created = Group.objects.get_or_create(name=group_info['name'])
-  if not created and not LdapGroup.objects.filter(group=group).exists():
-    # This is a Hue group, and shouldn't be overwritten
-    LOG.warn('There was a naming conflict while importing group %s' % (groupname,))
-    return None
+  groups = []
+  for ldap_info in group_info:
+    group, created = Group.objects.get_or_create(name=ldap_info['name'])
+    if not created and not LdapGroup.objects.filter(group=group).exists():
+      # This is a Hue group, and shouldn't be overwritten
+      LOG.warn(_('There was a naming conflict while importing group %(groupname)s in pattern %(groupname_pattern)s') % {
+        'groupname': ldap_info['name'],
+        'groupname_pattern': groupname_pattern
+      })
+      return None
 
 
-  LdapGroup.objects.get_or_create(group=group)
+    LdapGroup.objects.get_or_create(group=group)
 
 
-  group.user_set.clear()
-  for member in group_info['members']:
-    if import_members:
-      LOG.debug("Importing user %s" % (member,))
-      user = _import_ldap_user(member, import_by_dn=True)
-    else:
-      user_info = conn.find_user(member, find_by_dn=True)
-      try:
-        user = User.objects.get(username=user_info['username'])
-      except User.DoesNotExist:
+    group.user_set.clear()
+    for member in ldap_info['members']:
+      users = []
+
+      if import_members:
+        LOG.debug(_("Importing user %s") % member)
+        users = _import_ldap_users(member, import_by_dn=True)
+
+      else:
+        user_info = conn.find_users(member, find_by_dn=True)
+        if len(user_info) > 1:
+          LOG.warn(_('Found multiple users for member %s.') % member)
+        else:
+          for ldap_info in user_info:
+            try:
+              user = User.objects.get(username=ldap_info['username'])
+            except User.DoesNotExist:
+              continue
+            users.append(user)
+
+      if not users:
+        # There was a naming conflict, or for some other reason, we couldn't get
+        # at the user
         continue
         continue
 
 
-    if user is None:
-      # There was a naming conflict, or for some other reason, we couldn't get
-      # at the user
-      continue
-    LOG.debug("Adding user %s to group %s" % (member, group.name))
-    group.user_set.add(user)
+      LOG.debug(_("Adding member %s represented as users (should be a single user) %s to group %s") % (member, str(users), group.name))
+      for user in users:
+        group.user_set.add(user)
 
 
-  group.save()
-  return group
+    group.save()
+    groups.append(group)
+
+  return groups
 
 
 
 
-def import_ldap_user(user, import_by_dn):
-  return _import_ldap_user(user, import_by_dn)
+def import_ldap_users(user_pattern, import_by_dn):
+  return _import_ldap_users(user_pattern, import_by_dn)
 
 
 
 
-def import_ldap_group(group, import_members, import_by_dn):
-  return _import_ldap_group(group, import_members, 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 sync_ldap_users():
 def sync_ldap_users():
@@ -547,7 +573,7 @@ def sync_ldap_users():
   """
   """
   users = User.objects.filter(userprofile__creation_method=str(UserProfile.CreationMethod.EXTERNAL)).all()
   users = User.objects.filter(userprofile__creation_method=str(UserProfile.CreationMethod.EXTERNAL)).all()
   for user in users:
   for user in users:
-    _import_ldap_user(user.username)
+    _import_ldap_users(user.username)
   return users
   return users
 
 
 
 
@@ -560,5 +586,5 @@ def sync_ldap_groups():
   """
   """
   groups = Group.objects.filter(group__in=LdapGroup.objects.all())
   groups = Group.objects.filter(group__in=LdapGroup.objects.all())
   for group in groups:
   for group in groups:
-    _import_ldap_group(group.name)
+    _import_ldap_groups(group.name)
   return groups
   return groups

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

@@ -467,9 +467,9 @@ def config_validator():
   if LDAP.LDAP_URL.get() is not None:
   if LDAP.LDAP_URL.get() is not None:
     if LDAP.NT_DOMAIN.get() is None and \
     if LDAP.NT_DOMAIN.get() is None and \
         LDAP.LDAP_USERNAME_PATTERN.get() is None:
         LDAP.LDAP_USERNAME_PATTERN.get() is None:
-      res.append(LDAP.LDAP_URL,
+      res.append((LDAP.LDAP_URL,
                   unicode(_("LDAP is only partially configured. An NT Domain or username "
                   unicode(_("LDAP is only partially configured. An NT Domain or username "
-                  "search pattern must be provided.")))
+                  "search pattern must be provided."))))
 
 
   if LDAP.LDAP_USERNAME_PATTERN.get() is not None and \
   if LDAP.LDAP_USERNAME_PATTERN.get() is not None and \
       '<username>' not in LDAP.LDAP_USERNAME_PATTERN.get():
       '<username>' not in LDAP.LDAP_USERNAME_PATTERN.get():

+ 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 = ".{,30}"
 
 
 class Encoder(simplejson.JSONEncoder):
 class Encoder(simplejson.JSONEncoder):
   """
   """

+ 6 - 6
desktop/core/src/desktop/tests.py

@@ -390,12 +390,12 @@ def test_config_check():
   try:
   try:
     cli = make_logged_in_client()
     cli = make_logged_in_client()
     resp = cli.get('/debug/check_config')
     resp = cli.get('/debug/check_config')
-    assert_true('Secret key should be configured' in resp.content)
-    assert_true('desktop.ssl_certificate' in resp.content)
-    assert_true('Path does not exist' in resp.content)
-    assert_true('SSL private key file should be set' in resp.content)
-    assert_true('klingon' in resp.content)
-    assert_true('Encoding not supported' in resp.content)
+    assert_true('Secret key should be configured' in resp.content, resp)
+    assert_true('desktop.ssl_certificate' in resp.content, resp)
+    assert_true('Path does not exist' in resp.content, resp)
+    assert_true('SSL private key file should be set' in resp.content, resp)
+    assert_true('klingon' in resp.content, resp)
+    assert_true('Encoding not supported' in resp.content, resp)
 
 
     # Alert present in the status bar
     # Alert present in the status bar
     resp = cli.get('/status_bar/')
     resp = cli.get('/status_bar/')