فهرست منبع

[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 سال پیش
والد
کامیت
896fa20

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

@@ -99,8 +99,8 @@ class SuperUserChangeForm(UserChangeForm):
         self.initial['groups'] = []
 
 
-class AddLdapUserForm(forms.Form):
-  username = forms.RegexField(
+class AddLdapUsersForm(forms.Form):
+  username_pattern = forms.RegexField(
       label=_("Username"),
       max_length=64,
       regex='^%s$' % (get_username_re_rule(),),
@@ -117,22 +117,22 @@ class AddLdapUserForm(forms.Form):
                                             required=False)
 
   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")
 
     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)
         raise forms.ValidationError(msg)
 
     return cleaned_data
 
 
-class AddLdapGroupForm(forms.Form):
-  name = forms.RegexField(
+class AddLdapGroupsForm(forms.Form):
+  groupname_pattern = forms.RegexField(
       label="Name",
       max_length=64,
       regex='^%s$' % get_groupname_re_rule(),
@@ -153,14 +153,14 @@ class AddLdapGroupForm(forms.Form):
                                                 required=False)
 
   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")
 
     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)
         raise forms.ValidationError(msg)
 

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

@@ -81,7 +81,7 @@ class LdapConnection(object):
       except:
         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
     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()
 
     # 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:
       user_name_filter = '(distinguishedName=' + sanitized_name + ')'
     else:
@@ -104,22 +104,24 @@ class LdapConnection(object):
 
     ldap_result_id = self.ldap_handle.search(base_dn, scope, ldap_filter)
     result_type, result_data = self.ldap_handle.result(ldap_result_id)
+    user_info = []
     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
     """
@@ -132,28 +134,31 @@ class LdapConnection(object):
     group_name_attr = desktop.conf.LDAP.GROUPS.GROUP_NAME_ATTR.get()
 
     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 + ')'
     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 + ')'
     ldap_filter = '(&' + group_filter + group_name_filter + ')'
 
     ldap_result_id = self.ldap_handle.search(base_dn, scope, ldap_filter)
     result_type, result_data = self.ldap_handle.result(ldap_result_id)
+
+    group_info = []
     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):
     """

+ 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:
           <input type="submit" class="btn btn-primary" value="${_('Add/Sync user')}"/>
       % endif
-      <a href="/useradmin/users" class="btn">${_('Cancel')}</a>
+      <a href="${url('useradmin.views.list_users')}" class="btn">${_('Cancel')}</a>
     </div>
   </form>
 </div>

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

@@ -37,7 +37,7 @@ ${layout.menubar(section='groups', _=_)}
         <%def name="creation()">
             %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="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
         </%def>
     </%actionbar:render>

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

@@ -37,7 +37,7 @@ ${layout.menubar(section='users', _=_)}
         <%def name="creation()">
             %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.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>
             %endif
         </%def>

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

@@ -21,6 +21,7 @@
 Tests for "user admin"
 """
 
+import re
 import urllib
 from ldap import LDAPError
 
@@ -37,8 +38,8 @@ from useradmin.models import get_profile, get_default_user_group
 
 import useradmin.conf
 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
 
 def reset_all_users():
@@ -71,13 +72,17 @@ class LdapTestConnection(object):
   def remove_user_group_for_test(self, user, group):
     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 """
-    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 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:
     def __init__(self):
@@ -239,8 +244,7 @@ def test_group_admin():
 
   group_count = len(Group.objects.all())
   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():
   FUNNY_NAME = '~`!@#$%^&*()_-+={}[]|\;"<>?/,.'
@@ -378,7 +382,7 @@ def test_useradmin_ldap_integration():
   ldap_access.CACHED_LDAP_CONN = LdapTestConnection()
 
   # 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')
   assert_true(larry.first_name == 'Larry')
   assert_true(larry.last_name == 'Stooge')
@@ -392,7 +396,7 @@ def test_useradmin_ldap_integration():
   assert_equal(len(Group.objects.all()), 0)
 
   # 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(Group.objects.all()), 1)
   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)
 
   # 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')
   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_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(User.objects.get(username='moe').groups.all()), 0)
 
   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(User.objects.get(username='moe').groups.all()), 1)
 
   # 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_user('otherguy', import_by_dn=False)
+  import_ldap_users('otherguy', 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')
@@ -427,12 +431,12 @@ def test_useradmin_ldap_integration():
   hue_group = Group.objects.create(name='OtherGroup')
   hue_group.user_set.add(hue_user)
   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_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_groups()
@@ -444,14 +448,19 @@ def test_add_ldap_user():
 
   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_groups()
@@ -464,16 +473,17 @@ def test_add_ldap_group():
 
   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'])
 
   # 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():
   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
   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')
   ldap_access.CACHED_LDAP_CONN = LdapTestConnectionError()
 
   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)
 
 @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_groups()
@@ -520,24 +530,33 @@ def test_ensure_home_directory_add_ldap_user():
 
   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_false(cluster.fs.exists('/user/moe'))
 
   # 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(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'))
 
   # See if moe, who did not ask for his home directory, has a home directory.
   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
   cluster.fs.rmtree('/user/curly')
+  cluster.fs.rmtree('/user/larry')
+  cluster.fs.rmtree('/user/otherguy')
 
 @attr('requires_hadoop')
 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)
   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_true(c.post(URL, dict(ensure_home_directory=True)))
   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'^permissions$', 'list_permissions'),
   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'^groups/edit/(?P<name>%s)$' % (groupname_re,), 'edit_group'),
   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 useradmin.models import HuePermission, UserProfile, LdapGroup
 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
 
 
@@ -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))
 
 
-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.
 
@@ -269,36 +269,37 @@ def add_ldap_user(request):
     raise PopupException(_("You must be a superuser to add another user."), error_code=401)
 
   if request.method == 'POST':
-    form = AddLdapUserForm(request.POST)
+    form = AddLdapUsersForm(request.POST)
     if form.is_valid():
-      username = form.cleaned_data['username']
+      username_pattern = form.cleaned_data['username_pattern']
       import_by_dn = form.cleaned_data['dn']
       try:
-        user = import_ldap_user(username, import_by_dn)
+        users = import_ldap_users(username_pattern, import_by_dn)
       except LDAPError, e:
         LOG.error("LDAP Exception: %s" % 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:
         return redirect(reverse(list_users))
   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.
 
@@ -310,24 +311,25 @@ def add_ldap_group(request):
     raise PopupException(_("You must be a superuser to add another group."), error_code=401)
 
   if request.method == 'POST':
-    form = AddLdapGroupForm(request.POST)
+    form = AddLdapGroupsForm(request.POST)
     if form.is_valid():
-      groupname = form.cleaned_data['name']
+      groupname_pattern = form.cleaned_data['groupname_pattern']
       import_by_dn = form.cleaned_data['dn']
       import_members = form.cleaned_data['import_members']
       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:
-        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))
 
-      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))
+      else:
+        errors = form._errors.setdefault('groupname_pattern', ErrorList())
+        errors.append(_('Could not get LDAP details for groups in pattern %s') % groupname_pattern)
+
   else:
-    form = AddLdapGroupForm()
+    form = AddLdapGroupsForm()
 
   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
       hue_user.groups = user_groups[username]
     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()
   __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
   the distinguished name, rather than the configured username attribute.
   """
   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
 
-  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
   LDAP users that exist within the group.
   """
   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
 
-  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
 
-    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():
@@ -547,7 +573,7 @@ def sync_ldap_users():
   """
   users = User.objects.filter(userprofile__creation_method=str(UserProfile.CreationMethod.EXTERNAL)).all()
   for user in users:
-    _import_ldap_user(user.username)
+    _import_ldap_users(user.username)
   return users
 
 
@@ -560,5 +586,5 @@ def sync_ldap_groups():
   """
   groups = Group.objects.filter(group__in=LdapGroup.objects.all())
   for group in groups:
-    _import_ldap_group(group.name)
+    _import_ldap_groups(group.name)
   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.NT_DOMAIN.get() is None and \
         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 "
-                  "search pattern must be provided.")))
+                  "search pattern must be provided."))))
 
   if LDAP.LDAP_USERNAME_PATTERN.get() is not None and \
       '<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.
 USERNAME_RE_RULE = "[^-:\s][^:\s]*"
-GROUPNAME_RE_RULE = "^.{,30}$"
+GROUPNAME_RE_RULE = ".{,30}"
 
 class Encoder(simplejson.JSONEncoder):
   """

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

@@ -390,12 +390,12 @@ def test_config_check():
   try:
     cli = make_logged_in_client()
     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
     resp = cli.get('/status_bar/')