Browse Source

HUE-2012 [core] AD multidomain support

ldap_servers takes precidence over normal ldap config.
backward compatible since original ldap config can be provided.
Add server selection logic on front end.
Split tests by configs: test_ldap_deprecated and test_ldap.
The former will test deprecated configs and the later will test new configs.
The Synchronization middleware will choose the first LDAP Server
config when deciding which server to synchronize with.
Abraham Elmahrek 11 years ago
parent
commit
b3a760de6e

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

@@ -24,6 +24,7 @@ from django.contrib.auth.models import User, Group
 from django.forms.util import ErrorList
 from django.utils.translation import ugettext as _, ugettext_lazy as _t
 
+from desktop import conf as desktop_conf
 from desktop.lib.django_util import get_username_re_rule, get_groupname_re_rule
 
 from useradmin.models import GroupPermission, HuePermission
@@ -34,6 +35,13 @@ from useradmin.models import get_default_user_group
 LOG = logging.getLogger(__name__)
 
 
+def get_server_choices():
+  if desktop_conf.LDAP.LDAP_SERVERS.get():
+    return [(ldap_server_record_key, ldap_server_record_key) for ldap_server_record_key in desktop_conf.LDAP.LDAP_SERVERS.get()]
+  else:
+    return [('LDAP', 'LDAP')]
+
+
 class UserChangeForm(django.contrib.auth.forms.UserChangeForm):
   """
   This is similar, but not quite the same as djagno.contrib.auth.forms.UserChangeForm
@@ -52,15 +60,15 @@ class UserChangeForm(django.contrib.auth.forms.UserChangeForm):
                                             initial=True,
                                             required=False)
 
+  class Meta(django.contrib.auth.forms.UserChangeForm.Meta):
+    fields = ["username", "first_name", "last_name", "email", "ensure_home_directory"]
+
   def __init__(self, *args, **kwargs):
     super(UserChangeForm, self).__init__(*args, **kwargs)
 
     if self.instance.id:
       self.fields['username'].widget.attrs['readonly'] = True
 
-  class Meta(django.contrib.auth.forms.UserChangeForm.Meta):
-    fields = ["username", "first_name", "last_name", "email", "ensure_home_directory"]
-
   def clean_password(self):
     return self.cleaned_data["password"]
 
@@ -124,6 +132,10 @@ class AddLdapUsersForm(forms.Form):
                                             initial=True,
                                             required=False)
 
+  def __init__(self, *args, **kwargs):
+    super(AddLdapUsersForm, self).__init__(*args, **kwargs)
+    self.fields['server'] = forms.ChoiceField(choices=get_server_choices(), required=False)
+
   def clean(self):
     cleaned_data = super(AddLdapUsersForm, self).clean()
     username_pattern = cleaned_data.get("username_pattern")
@@ -170,6 +182,10 @@ class AddLdapGroupsForm(forms.Form):
                                                 initial=False,
                                                 required=False)
 
+  def __init__(self, *args, **kwargs):
+    super(AddLdapGroupsForm, self).__init__(*args, **kwargs)
+    self.fields['server'] = forms.ChoiceField(choices=get_server_choices(), required=False)
+
   def clean(self):
     cleaned_data = super(AddLdapGroupsForm, self).clean()
     groupname_pattern = cleaned_data.get("groupname_pattern")
@@ -297,3 +313,6 @@ class SyncLdapUsersGroupsForm(forms.Form):
                                             help_text=_t("Create home directory for every user, if one doesn't already exist."),
                                             initial=True,
                                             required=False)
+  def __init__(self, *args, **kwargs):
+    super(SyncLdapUsersGroupsForm, self).__init__(*args, **kwargs)
+    self.fields['server'] = forms.ChoiceField(choices=get_server_choices(), required=False)

+ 45 - 32
apps/useradmin/src/useradmin/ldap_access.py

@@ -33,25 +33,30 @@ LOG = logging.getLogger(__name__)
 
 CACHED_LDAP_CONN = None
 
-def get_connection():
+
+def get_connection_from_server(server=None):
+  if server:
+    return get_connection(desktop.conf.LDAP.LDAP_SERVERS.get()[server], search_bind_authentication=desktop.conf.LDAP.SEARCH_BIND_AUTHENTICATION.get())
+  else:
+    return get_connection(desktop.conf.LDAP, search_bind_authentication=desktop.conf.LDAP.SEARCH_BIND_AUTHENTICATION.get())
+
+def get_connection(ldap_config, search_bind_authentication):
   global CACHED_LDAP_CONN
   if CACHED_LDAP_CONN is not None:
     return CACHED_LDAP_CONN
 
-  ldap_url = desktop.conf.LDAP.LDAP_URL.get()
-  nt_domain = desktop.conf.LDAP.NT_DOMAIN.get()
-  username = desktop.conf.LDAP.BIND_DN.get()
-  password = desktop.conf.LDAP.BIND_PASSWORD.get()
-  ldap_cert = desktop.conf.LDAP.LDAP_CERT.get()
-  search_bind_authentication = desktop.conf.LDAP.SEARCH_BIND_AUTHENTICATION.get()
+  ldap_url = ldap_config.LDAP_URL.get()
+  username = ldap_config.BIND_DN.get()
+  password = ldap_config.BIND_PASSWORD.get()
+  ldap_cert = ldap_config.LDAP_CERT.get()
 
   if ldap_url is None:
     raise Exception('No LDAP URL was specified')
 
   if search_bind_authentication:
-    return LdapConnection(ldap_url, username, password, ldap_cert)
+    return LdapConnection(ldap_config, ldap_url, username, password, ldap_cert)
   else:
-    return LdapConnection(ldap_url, get_ldap_username(username, nt_domain), password, ldap_cert)
+    return LdapConnection(ldap_config, ldap_url, get_ldap_username(username, ldap_config.NT_DOMAIN.get()), password, ldap_cert)
 
 def get_ldap_username(username, nt_domain):
   if nt_domain:
@@ -92,10 +97,12 @@ class LdapConnection(object):
   to easily query an LDAP server.
   """
 
-  def __init__(self, ldap_url, bind_user=None, bind_password=None, cert_file=None):
+  def __init__(self, ldap_config, ldap_url, bind_user=None, bind_password=None, cert_file=None):
     """
     Constructor initializes the LDAP connection
     """
+    self.ldap_config = ldap_config
+
     if cert_file is not None:
       ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_ALLOW)
       ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, cert_file)
@@ -109,7 +116,7 @@ class LdapConnection(object):
         self.ldap_handle.simple_bind_s(bind_user, bind_password)
       except:
         raise RuntimeError("Failed to bind to LDAP server as user %s" %
-            (bind_user,))
+            bind_user)
     else:
       try:
         # Do anonymous bind
@@ -185,7 +192,7 @@ class LdapConnection(object):
     return user_info
 
 
-  def _transform_find_group_results(self, result_data, group_name_attr):
+  def _transform_find_group_results(self, result_data, group_name_attr, group_member_attr):
     group_info = []
     if result_data:
       for dn, data in result_data:
@@ -204,9 +211,8 @@ class LdapConnection(object):
             'name': data[group_name_attr][0]
           }
 
-          member_attr = desktop.conf.LDAP.GROUPS.GROUP_MEMBER_ATTR.get()
-          if member_attr in data and 'posixGroup' not in data['objectClass']:
-            ldap_info['members'] = data[member_attr]
+          if group_member_attr in data and 'posixGroup' not in data['objectClass']:
+            ldap_info['members'] = data[group_member_attr]
           else:
             ldap_info['members'] = []
 
@@ -219,7 +225,7 @@ class LdapConnection(object):
 
     return group_info
 
-  def find_users(self, username_pattern, search_attr=None, user_name_attr=None, find_by_dn=False, scope=ldap.SCOPE_SUBTREE):
+  def find_users(self, username_pattern, search_attr=None, user_name_attr=None, user_filter=None, find_by_dn=False, scope=ldap.SCOPE_SUBTREE):
     """
     LDAP search helper method finding users. This supports searching for users
     by distinguished name, or the configured username attribute.
@@ -241,11 +247,14 @@ class LdapConnection(object):
     ``
     """
     if not search_attr:
-      search_attr = desktop.conf.LDAP.USERS.USER_NAME_ATTR.get()
+      search_attr = self.ldap_config.USERS.USER_NAME_ATTR.get()
+
     if not user_name_attr:
       user_name_attr = search_attr
 
-    user_filter = desktop.conf.LDAP.USERS.USER_FILTER.get()
+    if not user_filter:
+      user_filter = self.ldap_config.USERS.USER_FILTER.get()
+
     if not user_filter.startswith('('):
       user_filter = '(' + user_filter + ')'
 
@@ -266,7 +275,7 @@ class LdapConnection(object):
     else:
       return []
 
-  def find_groups(self, groupname_pattern, search_attr=None, group_name_attr=None, find_by_dn=False, scope=ldap.SCOPE_SUBTREE):
+  def find_groups(self, groupname_pattern, search_attr=None, group_name_attr=None, group_member_attr=None, group_filter=None, find_by_dn=False, scope=ldap.SCOPE_SUBTREE):
     """
     LDAP search helper method for finding groups
 
@@ -286,11 +295,17 @@ class LdapConnection(object):
     }
     """
     if not search_attr:
-      search_attr = desktop.conf.LDAP.GROUPS.GROUP_NAME_ATTR.get()
+      search_attr = self.ldap_config.GROUPS.GROUP_NAME_ATTR.get()
+
     if not group_name_attr:
       group_name_attr = search_attr
 
-    group_filter = desktop.conf.LDAP.GROUPS.GROUP_FILTER.get()
+    if not group_member_attr:
+      group_member_attr = self.ldap_config.GROUPS.GROUP_MEMBER_ATTR.get()
+
+    if not group_filter:
+      group_filter = self.ldap_config.GROUPS.GROUP_FILTER.get()
+
     if not group_filter.startswith('('):
       group_filter = '(' + group_filter + ')'
 
@@ -298,13 +313,13 @@ class LdapConnection(object):
     sanitized_name = ldap.filter.escape_filter_chars(groupname_pattern).replace(r'\2a', r'*')
     search_dn, group_name_filter = self._get_search_params(sanitized_name, search_attr, find_by_dn)
     ldap_filter = '(&' + group_filter + group_name_filter + ')'
-    attrlist = ['objectClass', 'dn', 'memberUid', desktop.conf.LDAP.GROUPS.GROUP_MEMBER_ATTR.get(), group_name_attr]
+    attrlist = ['objectClass', 'dn', 'memberUid', group_member_attr, group_name_attr]
 
     ldap_result_id = self.ldap_handle.search(search_dn, scope, ldap_filter, attrlist)
     result_type, result_data = self.ldap_handle.result(ldap_result_id)
 
     if result_type == ldap.RES_SEARCH_RESULT:
-      return self._transform_find_group_results(result_data, group_name_attr)
+      return self._transform_find_group_results(result_data, group_name_attr, group_member_attr)
     else:
       return []
 
@@ -330,19 +345,17 @@ class LdapConnection(object):
       return []
 
   def find_users_of_group(self, dn):
-    ldap_filter = desktop.conf.LDAP.USERS.USER_FILTER.get()
-    name_attr = desktop.conf.LDAP.USERS.USER_NAME_ATTR.get()
+    ldap_filter = self.ldap_config.USERS.USER_FILTER.get()
+    name_attr = self.ldap_config.USERS.USER_NAME_ATTR.get()
     result_data = self.find_members_of_group(dn, name_attr, ldap_filter)
     return self._transform_find_user_results(result_data, name_attr)
 
   def find_groups_of_group(self, dn):
-    ldap_filter = desktop.conf.LDAP.GROUPS.GROUP_FILTER.get()
-    name_attr = desktop.conf.LDAP.GROUPS.GROUP_NAME_ATTR.get()
+    ldap_filter = self.ldap_config.GROUPS.GROUP_FILTER.get()
+    name_attr = self.ldap_config.GROUPS.GROUP_NAME_ATTR.get()
+    member_attr = self.ldap_config.GROUPS.GROUP_MEMBER_ATTR.get()
     result_data = self.find_members_of_group(dn, name_attr, ldap_filter)
-    return self._transform_find_group_results(result_data, name_attr)
+    return self._transform_find_group_results(result_data, name_attr, member_attr)
 
   def _get_root_dn(self):
-    """
-    Returns the configured base DN (DC=desktop,DC=local).
-    """
-    return desktop.conf.LDAP.BASE_DN.get()
+    return self.ldap_config.BASE_DN.get()

+ 18 - 8
apps/useradmin/src/useradmin/management/commands/import_ldap_group.py

@@ -16,12 +16,15 @@
 # limitations under the License.
 from optparse import make_option
 
-from useradmin.views import import_ldap_groups
-
 from django.core.management.base import BaseCommand, CommandError
-
 from django.utils.translation import ugettext_lazy as _t, ugettext as _
 
+from desktop.conf import LDAP
+
+from useradmin import ldap_access
+from useradmin.views import import_ldap_groups
+
+
 class Command(BaseCommand):
   """
   Handler for importing LDAP groups into the Hue database.
@@ -40,11 +43,14 @@ class Command(BaseCommand):
                                       action="store_true",
                                       default=False),
       make_option("--import-members-recursive", help=_t("Import users from the group, but also do so recursively."),
-                                      action="store_true",
-                                      default=False),
+                                                action="store_true",
+                                                default=False),
       make_option("--sync-users", help=_t("Sync users in the group."),
-                                      action="store_true",
-                                      default=False),
+                                  action="store_true",
+                                  default=False),
+      make_option("--server", help=_t("Server to connect to."),
+                              action="store_true",
+                              default=None),
    )
 
   args = "group-name"
@@ -57,4 +63,8 @@ class Command(BaseCommand):
     import_by_dn = options['dn']
     import_members_recursive = options['import_members_recursive']
     sync_users = options['sync_users']
-    import_ldap_groups(group, import_members, import_members_recursive, sync_users, import_by_dn)
+    server = options['server']
+
+    connection = ldap_access.get_connection_from_server(server)
+
+    import_ldap_groups(connection, group, import_members, import_members_recursive, sync_users, import_by_dn)

+ 13 - 4
apps/useradmin/src/useradmin/management/commands/import_ldap_user.py

@@ -16,12 +16,14 @@
 # limitations under the License.
 from optparse import make_option
 
-from useradmin.views import import_ldap_users
-
 from django.core.management.base import BaseCommand, CommandError
-
 from django.utils.translation import ugettext_lazy as _t, ugettext as _
 
+from desktop.conf import LDAP
+
+from useradmin import ldap_access
+from useradmin.views import import_ldap_users
+
 class Command(BaseCommand):
   """
   Handler for importing LDAP users into the Hue database.
@@ -37,6 +39,9 @@ class Command(BaseCommand):
       make_option("--sync-groups", help=_t("Sync groups of the users."),
                                    action="store_true",
                                    default=False),
+      make_option("--server", help=_t("Server to connect to."),
+                              action="store_true",
+                              default=None),
   )
 
   args = "username"
@@ -47,4 +52,8 @@ class Command(BaseCommand):
 
     import_by_dn = options['dn']
     sync_groups = options['sync_groups']
-    import_ldap_users(user, sync_groups, import_by_dn)
+    server = options['server']
+
+    connection = ldap_access.get_connection_from_server(server)
+
+    import_ldap_users(connection, user, sync_groups, import_by_dn)

+ 21 - 5
apps/useradmin/src/useradmin/management/commands/sync_ldap_users_and_groups.py

@@ -14,11 +14,17 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-from useradmin.views import sync_ldap_users_and_groups
+from optparse import make_option
+
+from django.core.management.base import BaseCommand
+from django.utils.translation import ugettext_lazy as _t
 
-from django.core.management.base import NoArgsCommand
+from desktop.conf import LDAP
+
+from useradmin import ldap_access
+from useradmin.views import sync_ldap_users_and_groups
 
-class Command(NoArgsCommand):
+class Command(BaseCommand):
   """
   Handler for syncing the Hue database with LDAP users and groups.
 
@@ -27,5 +33,15 @@ class Command(NoArgsCommand):
   server's current state.
   """
 
-  def handle_noargs(self, **options):
-    sync_ldap_users_and_groups()
+  option_list = BaseCommand.option_list + (
+      make_option("--server", help=_t("Server to connect to."),
+                              action="store_true",
+                              default=None),
+   )
+
+  def handle(self, **options):
+    server = options['server']
+
+    connection = ldap_access.get_connection_from_server(server)
+
+    sync_ldap_users_and_groups(connection)

+ 9 - 1
apps/useradmin/src/useradmin/middleware.py

@@ -19,9 +19,13 @@ import logging
 
 from django.contrib.auth.models import User
 
+from desktop.conf import LDAP
+
 from models import UserProfile
 from views import import_ldap_users
 
+import ldap_access
+
 
 LOG = logging.getLogger(__name__)
 
@@ -44,5 +48,9 @@ class LdapSynchronizationMiddleware(object):
 
     # Cache should be cleared when user logs out.
     if self.USER_CACHE_NAME not in request.session:
-      request.session[self.USER_CACHE_NAME] = import_ldap_users(user.username, sync_groups=True, import_by_dn=False)
+      if LDAP.LDAP_SERVERS.get():
+        connection = ldap_access.get_connection_from_server(next(LDAP.LDAP_SERVERS.__iter__()))
+      else:
+        connection = ldap_access.get_connection_from_server()
+      request.session[self.USER_CACHE_NAME] = import_ldap_users(connection, user.username, sync_groups=True, import_by_dn=False)
       request.session.modified = True

+ 714 - 0
apps/useradmin/src/useradmin/test_ldap.py

@@ -0,0 +1,714 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Licensed to Cloudera, Inc. under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  Cloudera, Inc. licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import ldap
+
+from nose.plugins.attrib import attr
+from nose.tools import assert_true, assert_equal, assert_false
+
+import desktop.conf
+from desktop.lib.test_utils import grant_access
+from desktop.lib.django_test_util import make_logged_in_client
+from django.conf import settings
+from django.contrib.auth.models import User, Group
+from django.core.urlresolvers import reverse
+
+from useradmin.models import LdapGroup, UserProfile
+from useradmin.models import get_profile
+
+from hadoop import pseudo_hdfs4
+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
+from tests import LdapTestConnection, reset_all_groups, reset_all_users
+
+
+def get_nonsense_config():
+  return {'nonsense': {
+    'users': {},
+    'groups': {}
+  }}
+
+
+def test_useradmin_ldap_user_group_membership_sync():
+  settings.MIDDLEWARE_CLASSES.append('useradmin.middleware.LdapSynchronizationMiddleware')
+
+  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()
+
+  reset = []
+
+  # Set to nonsensical value just to force new config usage.
+  # Should continue to use cached connection.
+  reset.append(desktop.conf.LDAP.LDAP_SERVERS.set_for_testing(get_nonsense_config()))
+
+  try:
+    # Import curly who is part of TestUsers and Test Administrators
+    import_ldap_users(ldap_access.CACHED_LDAP_CONN, 'curly', sync_groups=False, import_by_dn=False)
+
+    # Set a password so that we can login
+    user = User.objects.get(username='curly')
+    user.set_password('test')
+    user.save()
+
+    # Should have 0 groups
+    assert_equal(0, user.groups.all().count())
+
+    # Make an authenticated request as curly so that we can see call middleware.
+    c = make_logged_in_client('curly', 'test', is_superuser=False)
+    grant_access("curly", "test", "useradmin")
+    response = c.get('/useradmin/users')
+
+    # Refresh user groups
+    user = User.objects.get(username='curly')
+
+    # Should have 3 groups now. 2 from LDAP and 1 from 'grant_access' call.
+    assert_equal(3, user.groups.all().count(), user.groups.all())
+
+    # Now remove a group and try again.
+    old_group = ldap_access.CACHED_LDAP_CONN._instance.users['curly']['groups'].pop()
+
+    # Make an authenticated request as curly so that we can see call middleware.
+    response = c.get('/useradmin/users')
+
+    # Refresh user groups
+    user = User.objects.get(username='curly')
+
+    # Should have 2 groups now. 1 from LDAP and 1 from 'grant_access' call.
+    assert_equal(3, user.groups.all().count(), user.groups.all())
+  finally:
+    settings.MIDDLEWARE_CLASSES.remove('useradmin.middleware.LdapSynchronizationMiddleware')
+
+    for finish in reset:
+      finish()
+
+
+def test_useradmin_ldap_suboordinate_group_integration():
+  reset_all_users()
+  reset_all_groups()
+
+  reset = []
+
+  # Set up LDAP tests to use a LdapTestConnection instead of an actual LDAP connection
+  ldap_access.CACHED_LDAP_CONN = LdapTestConnection()
+
+  # Test old subgroups
+  reset.append(desktop.conf.LDAP.SUBGROUPS.set_for_testing("suboordinate"))
+
+  # Set to nonsensical value just to force new config usage.
+  # Should continue to use cached connection.
+  reset.append(desktop.conf.LDAP.LDAP_SERVERS.set_for_testing(get_nonsense_config()))
+
+  try:
+    # Import groups only
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'TestUsers', import_members=False, import_members_recursive=False, sync_users=False, import_by_dn=False)
+    test_users = Group.objects.get(name='TestUsers')
+    assert_true(LdapGroup.objects.filter(group=test_users).exists())
+    assert_equal(test_users.user_set.all().count(), 0)
+
+    # Import all members of TestUsers
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'TestUsers', import_members=True, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    test_users = Group.objects.get(name='TestUsers')
+    assert_true(LdapGroup.objects.filter(group=test_users).exists())
+    assert_equal(test_users.user_set.all().count(), 3)
+
+    # Should import a group, but will only sync already-imported members
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'Test Administrators', import_members=False, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    assert_equal(User.objects.all().count(), 3)
+    assert_equal(Group.objects.all().count(), 2)
+    test_admins = Group.objects.get(name='Test Administrators')
+    assert_equal(test_admins.user_set.all().count(), 2)
+    larry = User.objects.get(username='lårry')
+    assert_equal(test_admins.user_set.all()[0].username, larry.username)
+
+    # Only sync already imported
+    ldap_access.CACHED_LDAP_CONN.remove_user_group_for_test('uid=moe,ou=People,dc=example,dc=com', 'TestUsers')
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'TestUsers', import_members=False, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    assert_equal(test_users.user_set.all().count(), 2)
+    assert_equal(User.objects.get(username='moe').groups.all().count(), 0)
+
+    # Import missing user
+    ldap_access.CACHED_LDAP_CONN.add_user_group_for_test('uid=moe,ou=People,dc=example,dc=com', 'TestUsers')
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'TestUsers', import_members=True, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    assert_equal(test_users.user_set.all().count(), 3)
+    assert_equal(User.objects.get(username='moe').groups.all().count(), 1)
+
+    # Import all members of TestUsers and members of subgroups
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'TestUsers', import_members=True, import_members_recursive=True, sync_users=True, import_by_dn=False)
+    test_users = Group.objects.get(name='TestUsers')
+    assert_true(LdapGroup.objects.filter(group=test_users).exists())
+    assert_equal(test_users.user_set.all().count(), 4)
+
+    # Make sure Hue groups with naming collisions don't get marked as LDAP groups
+    hue_user = User.objects.create(username='otherguy', first_name='Different', last_name='Guy')
+    hue_group = Group.objects.create(name='OtherGroup')
+    hue_group.user_set.add(hue_user)
+    hue_group.save()
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'OtherGroup', import_members=False, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    assert_false(LdapGroup.objects.filter(group=hue_group).exists())
+    assert_true(hue_group.user_set.filter(username=hue_user.username).exists())
+  finally:
+    for finish in reset:
+      finish()
+
+
+def test_useradmin_ldap_nested_group_integration():
+  reset_all_users()
+  reset_all_groups()
+
+  reset = []
+
+  # Set up LDAP tests to use a LdapTestConnection instead of an actual LDAP connection
+  ldap_access.CACHED_LDAP_CONN = LdapTestConnection()
+
+  # Test old subgroups
+  reset.append(desktop.conf.LDAP.SUBGROUPS.set_for_testing("nested"))
+
+  # Set to nonsensical value just to force new config usage.
+  # Should continue to use cached connection.
+  reset.append(desktop.conf.LDAP.LDAP_SERVERS.set_for_testing(get_nonsense_config()))
+
+  try:
+    # Import groups only
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'TestUsers', import_members=False, import_members_recursive=False, sync_users=False, import_by_dn=False)
+    test_users = Group.objects.get(name='TestUsers')
+    assert_true(LdapGroup.objects.filter(group=test_users).exists())
+    assert_equal(test_users.user_set.all().count(), 0)
+
+    # Import all members of TestUsers
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'TestUsers', import_members=True, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    test_users = Group.objects.get(name='TestUsers')
+    assert_true(LdapGroup.objects.filter(group=test_users).exists())
+    assert_equal(test_users.user_set.all().count(), 3)
+
+    # Should import a group, but will only sync already-imported members
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'Test Administrators', import_members=False, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    assert_equal(User.objects.all().count(), 3)
+    assert_equal(Group.objects.all().count(), 2)
+    test_admins = Group.objects.get(name='Test Administrators')
+    assert_equal(test_admins.user_set.all().count(), 2)
+    larry = User.objects.get(username='lårry')
+    assert_equal(test_admins.user_set.all()[0].username, larry.username)
+
+    # Only sync already imported
+    assert_equal(test_users.user_set.all().count(), 3)
+    ldap_access.CACHED_LDAP_CONN.remove_user_group_for_test('uid=moe,ou=People,dc=example,dc=com', 'TestUsers')
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'TestUsers', import_members=False, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    assert_equal(test_users.user_set.all().count(), 2)
+    assert_equal(User.objects.get(username='moe').groups.all().count(), 0)
+
+    # Import missing user
+    ldap_access.CACHED_LDAP_CONN.add_user_group_for_test('uid=moe,ou=People,dc=example,dc=com', 'TestUsers')
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'TestUsers', import_members=True, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    assert_equal(test_users.user_set.all().count(), 3)
+    assert_equal(User.objects.get(username='moe').groups.all().count(), 1)
+
+    # Import all members of TestUsers and not members of suboordinate groups (even though specified)
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'TestUsers', import_members=True, import_members_recursive=True, sync_users=True, import_by_dn=False)
+    test_users = Group.objects.get(name='TestUsers')
+    assert_true(LdapGroup.objects.filter(group=test_users).exists())
+    assert_equal(test_users.user_set.all().count(), 3)
+
+    # Nested group import
+    # First without recursive import, then with.
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'NestedGroups', import_members=True, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    nested_groups = Group.objects.get(name='NestedGroups')
+    nested_group = Group.objects.get(name='NestedGroup')
+    assert_true(LdapGroup.objects.filter(group=nested_groups).exists())
+    assert_true(LdapGroup.objects.filter(group=nested_group).exists())
+    assert_equal(nested_groups.user_set.all().count(), 0, nested_groups.user_set.all())
+    assert_equal(nested_group.user_set.all().count(), 0, nested_group.user_set.all())
+
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'NestedGroups', import_members=True, import_members_recursive=True, sync_users=True, import_by_dn=False)
+    nested_groups = Group.objects.get(name='NestedGroups')
+    nested_group = Group.objects.get(name='NestedGroup')
+    assert_true(LdapGroup.objects.filter(group=nested_groups).exists())
+    assert_true(LdapGroup.objects.filter(group=nested_group).exists())
+    assert_equal(nested_groups.user_set.all().count(), 0, nested_groups.user_set.all())
+    assert_equal(nested_group.user_set.all().count(), 1, nested_group.user_set.all())
+
+    # 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(ldap_access.CACHED_LDAP_CONN, 'OtherGroup', import_members=False, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    assert_false(LdapGroup.objects.filter(group=hue_group).exists())
+    assert_true(hue_group.user_set.filter(username=hue_user.username).exists())
+  finally:
+    for finish in reset:
+      finish()
+
+
+def test_useradmin_ldap_suboordinate_posix_group_integration():
+  reset_all_users()
+  reset_all_groups()
+
+  reset = []
+
+  # Set up LDAP tests to use a LdapTestConnection instead of an actual LDAP connection
+  ldap_access.CACHED_LDAP_CONN = LdapTestConnection()
+
+  # Test old subgroups
+  reset.append(desktop.conf.LDAP.SUBGROUPS.set_for_testing("suboordinate"))
+
+  # Set to nonsensical value just to force new config usage.
+  # Should continue to use cached connection.
+  reset.append(desktop.conf.LDAP.LDAP_SERVERS.set_for_testing(get_nonsense_config()))
+
+  try:
+    # Import groups only
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'PosixGroup', import_members=False, import_members_recursive=False, sync_users=False, import_by_dn=False)
+    test_users = Group.objects.get(name='PosixGroup')
+    assert_true(LdapGroup.objects.filter(group=test_users).exists())
+    assert_equal(test_users.user_set.all().count(), 0)
+
+    # Import all members of TestUsers
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'PosixGroup', import_members=True, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    test_users = Group.objects.get(name='PosixGroup')
+    assert_true(LdapGroup.objects.filter(group=test_users).exists())
+    assert_equal(test_users.user_set.all().count(), 2)
+
+    # Should import a group, but will only sync already-imported members
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'Test Administrators', import_members=False, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    assert_equal(User.objects.all().count(), 2, User.objects.all())
+    assert_equal(Group.objects.all().count(), 2, Group.objects.all())
+    test_admins = Group.objects.get(name='Test Administrators')
+    assert_equal(test_admins.user_set.all().count(), 1)
+    larry = User.objects.get(username='lårry')
+    assert_equal(test_admins.user_set.all()[0].username, larry.username)
+
+    # Only sync already imported
+    ldap_access.CACHED_LDAP_CONN.remove_posix_user_group_for_test('posix_person', 'PosixGroup')
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'PosixGroup', import_members=False, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    assert_equal(test_users.user_set.all().count(), 1)
+    assert_equal(User.objects.get(username='posix_person').groups.all().count(), 0)
+
+    # Import missing user
+    ldap_access.CACHED_LDAP_CONN.add_posix_user_group_for_test('posix_person', 'PosixGroup')
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'PosixGroup', import_members=True, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    assert_equal(test_users.user_set.all().count(), 2)
+    assert_equal(User.objects.get(username='posix_person').groups.all().count(), 1)
+
+    # Import all members of PosixGroup and members of subgroups
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'PosixGroup', import_members=True, import_members_recursive=True, sync_users=True, import_by_dn=False)
+    test_users = Group.objects.get(name='PosixGroup')
+    assert_true(LdapGroup.objects.filter(group=test_users).exists())
+    assert_equal(test_users.user_set.all().count(), 3)
+
+    # 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(ldap_access.CACHED_LDAP_CONN, 'OtherGroup', import_members=False, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    assert_false(LdapGroup.objects.filter(group=hue_group).exists())
+    assert_true(hue_group.user_set.filter(username=hue_user.username).exists())
+  finally:
+    for finish in reset:
+      finish()
+
+
+def test_useradmin_ldap_nested_posix_group_integration():
+  reset_all_users()
+  reset_all_groups()
+
+  reset = []
+
+  # Set up LDAP tests to use a LdapTestConnection instead of an actual LDAP connection
+  ldap_access.CACHED_LDAP_CONN = LdapTestConnection()
+
+  # Test nested groups
+  reset.append(desktop.conf.LDAP.SUBGROUPS.set_for_testing("nested"))
+
+  # Set to nonsensical value just to force new config usage.
+  # Should continue to use cached connection.
+  reset.append(desktop.conf.LDAP.LDAP_SERVERS.set_for_testing(get_nonsense_config()))
+
+  try:
+    # Import groups only
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'PosixGroup', import_members=False, import_members_recursive=False, sync_users=False, import_by_dn=False)
+    test_users = Group.objects.get(name='PosixGroup')
+    assert_true(LdapGroup.objects.filter(group=test_users).exists())
+    assert_equal(test_users.user_set.all().count(), 0)
+
+    # Import all members of TestUsers
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'PosixGroup', import_members=True, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    test_users = Group.objects.get(name='PosixGroup')
+    assert_true(LdapGroup.objects.filter(group=test_users).exists())
+    assert_equal(test_users.user_set.all().count(), 2)
+
+    # Should import a group, but will only sync already-imported members
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'Test Administrators', import_members=False, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    assert_equal(User.objects.all().count(), 2, User.objects.all())
+    assert_equal(Group.objects.all().count(), 2, Group.objects.all())
+    test_admins = Group.objects.get(name='Test Administrators')
+    assert_equal(test_admins.user_set.all().count(), 1)
+    larry = User.objects.get(username='lårry')
+    assert_equal(test_admins.user_set.all()[0].username, larry.username)
+
+    # Only sync already imported
+    ldap_access.CACHED_LDAP_CONN.remove_posix_user_group_for_test('posix_person', 'PosixGroup')
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'PosixGroup', import_members=False, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    assert_equal(test_users.user_set.all().count(), 1)
+    assert_equal(User.objects.get(username='posix_person').groups.all().count(), 0)
+
+    # Import missing user
+    ldap_access.CACHED_LDAP_CONN.add_posix_user_group_for_test('posix_person', 'PosixGroup')
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'PosixGroup', import_members=True, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    assert_equal(test_users.user_set.all().count(), 2)
+    assert_equal(User.objects.get(username='posix_person').groups.all().count(), 1)
+
+    # Import all members of PosixGroup and members of subgroups (there should be no subgroups)
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'PosixGroup', import_members=True, import_members_recursive=True, sync_users=True, import_by_dn=False)
+    test_users = Group.objects.get(name='PosixGroup')
+    assert_true(LdapGroup.objects.filter(group=test_users).exists())
+    assert_equal(test_users.user_set.all().count(), 2)
+
+    # Import all members of NestedPosixGroups and members of subgroups
+    reset_all_users()
+    reset_all_groups()
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'NestedPosixGroups', import_members=True, import_members_recursive=True, sync_users=True, import_by_dn=False)
+    test_users = Group.objects.get(name='NestedPosixGroups')
+    assert_true(LdapGroup.objects.filter(group=test_users).exists())
+    assert_equal(test_users.user_set.all().count(), 0)
+    test_users = Group.objects.get(name='PosixGroup')
+    assert_true(LdapGroup.objects.filter(group=test_users).exists())
+    assert_equal(test_users.user_set.all().count(), 2)
+
+    # 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(ldap_access.CACHED_LDAP_CONN, 'OtherGroup', import_members=False, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    assert_false(LdapGroup.objects.filter(group=hue_group).exists())
+    assert_true(hue_group.user_set.filter(username=hue_user.username).exists())
+  finally:
+    for finish in reset:
+      finish()
+
+
+def test_useradmin_ldap_user_integration():
+  done = []
+
+  # Set to nonsensical value just to force new config usage.
+  # Should continue to use cached connection.
+  done.append(desktop.conf.LDAP.LDAP_SERVERS.set_for_testing(get_nonsense_config()))
+
+  try:
+    reset_all_users()
+    reset_all_groups()
+
+    # Set up LDAP tests to use a LdapTestConnection instead of an actual LDAP connection
+    ldap_access.CACHED_LDAP_CONN = LdapTestConnection()
+
+    # Try importing a user
+    import_ldap_users(ldap_access.CACHED_LDAP_CONN, 'lårry', sync_groups=False, import_by_dn=False)
+    larry = User.objects.get(username='lårry')
+    assert_true(larry.first_name == 'Larry')
+    assert_true(larry.last_name == 'Stooge')
+    assert_true(larry.email == 'larry@stooges.com')
+    assert_true(get_profile(larry).creation_method == str(UserProfile.CreationMethod.EXTERNAL))
+
+    # Should be a noop
+    sync_ldap_users(ldap_access.CACHED_LDAP_CONN)
+    sync_ldap_groups(ldap_access.CACHED_LDAP_CONN)
+    assert_equal(User.objects.all().count(), 1)
+    assert_equal(Group.objects.all().count(), 0)
+
+    # Make sure that if a Hue user already exists with a naming collision, we
+    # won't overwrite any of that user's information.
+    hue_user = User.objects.create(username='otherguy', first_name='Different', last_name='Guy')
+    import_ldap_users(ldap_access.CACHED_LDAP_CONN, 'otherguy', sync_groups=False, import_by_dn=False)
+    hue_user = User.objects.get(username='otherguy')
+    assert_equal(get_profile(hue_user).creation_method, str(UserProfile.CreationMethod.HUE))
+    assert_equal(hue_user.first_name, 'Different')
+
+    # Try importing a user and sync groups
+    import_ldap_users(ldap_access.CACHED_LDAP_CONN, 'curly', sync_groups=True, import_by_dn=False)
+    curly = User.objects.get(username='curly')
+    assert_equal(curly.first_name, 'Curly')
+    assert_equal(curly.last_name, 'Stooge')
+    assert_equal(curly.email, 'curly@stooges.com')
+    assert_equal(get_profile(curly).creation_method, str(UserProfile.CreationMethod.EXTERNAL))
+    assert_equal(2, curly.groups.all().count(), curly.groups.all())
+
+    reset_all_users()
+    reset_all_groups()
+
+    # Test import case sensitivity
+    done.append(desktop.conf.LDAP.IGNORE_USERNAME_CASE.set_for_testing(True))
+    import_ldap_users(ldap_access.CACHED_LDAP_CONN, 'Lårry', sync_groups=False, import_by_dn=False)
+    assert_false(User.objects.filter(username='Lårry').exists())
+    assert_true(User.objects.filter(username='lårry').exists())
+
+    # Test lower case
+    User.objects.filter(username__iexact='Rock').delete()
+    import_ldap_users(ldap_access.CACHED_LDAP_CONN, 'Rock', sync_groups=False, import_by_dn=False)
+    assert_true(User.objects.filter(username='Rock').exists())
+    assert_false(User.objects.filter(username='rock').exists())
+
+    done.append(desktop.conf.LDAP.FORCE_USERNAME_LOWERCASE.set_for_testing(True))
+
+    import_ldap_users(ldap_access.CACHED_LDAP_CONN, 'Rock', sync_groups=False, import_by_dn=False)
+    assert_true(User.objects.filter(username='Rock').exists())
+    assert_false(User.objects.filter(username='rock').exists())
+
+    User.objects.filter(username='Rock').delete()
+    import_ldap_users(ldap_access.CACHED_LDAP_CONN, 'Rock', sync_groups=False, import_by_dn=False)
+    assert_false(User.objects.filter(username='Rock').exists())
+    assert_true(User.objects.filter(username='rock').exists())
+  finally:
+    for finish in done:
+      finish()
+
+
+def test_add_ldap_users():
+  done = []
+
+  # Set to nonsensical value just to force new config usage.
+  # Should continue to use cached connection.
+  done.append(desktop.conf.LDAP.LDAP_SERVERS.set_for_testing(get_nonsense_config()))
+
+  try:
+    URL = reverse(add_ldap_users)
+
+    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()
+
+    c = make_logged_in_client('test', is_superuser=True)
+
+    assert_true(c.get(URL))
+
+    response = c.post(URL, dict(server='nonsense', 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(server='nonsense', username_pattern='bad_name', password1='test', password2='test'))
+    assert_true('Could not' in response.context['form'].errors['username_pattern'][0], response)
+
+    # Test wild card
+    response = c.post(URL, dict(server='nonsense', username_pattern='*r*', password1='test', password2='test'))
+    assert_true('/useradmin/users' in response['Location'], response)
+
+    # Test ignore case
+    done.append(desktop.conf.LDAP.IGNORE_USERNAME_CASE.set_for_testing(True))
+    User.objects.filter(username='moe').delete()
+    assert_false(User.objects.filter(username='Moe').exists())
+    assert_false(User.objects.filter(username='moe').exists())
+    response = c.post(URL, dict(server='nonsense', username_pattern='Moe', password1='test', password2='test'))
+    assert_true('Location' in response, response)
+    assert_true('/useradmin/users' in response['Location'], response)
+    assert_false(User.objects.filter(username='Moe').exists())
+    assert_true(User.objects.filter(username='moe').exists())
+
+    # Test lower case
+    done.append(desktop.conf.LDAP.FORCE_USERNAME_LOWERCASE.set_for_testing(True))
+    User.objects.filter(username__iexact='Rock').delete()
+    assert_false(User.objects.filter(username='Rock').exists())
+    assert_false(User.objects.filter(username='rock').exists())
+    response = c.post(URL, dict(server='nonsense', username_pattern='rock', password1='test', password2='test'))
+    assert_true('Location' in response, response)
+    assert_true('/useradmin/users' in response['Location'], response)
+    assert_false(User.objects.filter(username='Rock').exists())
+    assert_true(User.objects.filter(username='rock').exists())
+
+  finally:
+    for finish in done:
+      finish()
+
+
+def test_add_ldap_groups():
+  URL = reverse(add_ldap_groups)
+
+  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()
+
+  c = make_logged_in_client(username='test', is_superuser=True)
+
+  reset = []
+
+  # Set to nonsensical value just to force new config usage.
+  # Should continue to use cached connection.
+  reset.append(desktop.conf.LDAP.LDAP_SERVERS.set_for_testing(get_nonsense_config()))
+
+  try:
+
+    assert_true(c.get(URL))
+
+    response = c.post(URL, dict(server='nonsense', groupname_pattern='TestUsers'))
+    assert_true('Location' in response, response)
+    assert_true('/useradmin/groups' in response['Location'])
+
+    # Test with space
+    response = c.post(URL, dict(server='nonsense', groupname_pattern='Test Administrators'))
+    assert_true('Location' in response, response)
+    assert_true('/useradmin/groups' in response['Location'], response)
+
+    response = c.post(URL, dict(server='nonsense', 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
+    response = c.post(URL, dict(server='nonsense', groupname_pattern='*r*'))
+    assert_true('/useradmin/groups' in response['Location'], response)
+  finally:
+    for finish in reset:
+      finish()
+
+def test_sync_ldap_users_groups():
+  URL = reverse(sync_ldap_users_groups)
+
+  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()
+
+  c = make_logged_in_client('test', is_superuser=True)
+
+  reset = []
+
+  # Set to nonsensical value just to force new config usage.
+  # Should continue to use cached connection.
+  reset.append(desktop.conf.LDAP.LDAP_SERVERS.set_for_testing(get_nonsense_config()))
+
+  try:
+    assert_true(c.get(URL))
+    assert_true(c.post(URL))
+  finally:
+    for finish in reset:
+      finish()
+
+def test_ldap_exception_handling():
+  reset_all_users()
+  reset_all_groups()
+
+  # Set up LDAP tests to use a LdapTestConnection instead of an actual LDAP connection
+  class LdapTestConnectionError(LdapTestConnection):
+    def find_users(self, user, find_by_dn=False):
+      raise ldap.LDAPError('No such object')
+  ldap_access.CACHED_LDAP_CONN = LdapTestConnectionError()
+
+  c = make_logged_in_client('test', is_superuser=True)
+
+  reset = []
+
+  # Set to nonsensical value just to force new config usage.
+  # Should continue to use cached connection.
+  reset.append(desktop.conf.LDAP.LDAP_SERVERS.set_for_testing(get_nonsense_config()))
+
+  try:
+    response = c.post(reverse(add_ldap_users), dict(server='nonsense', username_pattern='moe', password1='test', password2='test'), follow=True)
+    assert_true('There was an error when communicating with LDAP' in response.content, response)
+  finally:
+    for finish in reset:
+      finish()
+
+@attr('requires_hadoop')
+def test_ensure_home_directory_add_ldap_users():
+  URL = reverse(add_ldap_users)
+
+  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()
+
+  cluster = pseudo_hdfs4.shared_cluster()
+  c = make_logged_in_client(cluster.superuser, is_superuser=True)
+  cluster.fs.setuser(cluster.superuser)
+
+  reset = []
+
+  # Set to nonsensical value just to force new config usage.
+  # Should continue to use cached connection.
+  reset.append(desktop.conf.LDAP.LDAP_SERVERS.set_for_testing(get_nonsense_config()))
+
+  try:
+    assert_true(c.get(URL))
+
+    response = c.post(URL, dict(server='nonsense', 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(server='nonsense', 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(server='nonsense', 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(server='nonsense', 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(u'/user/lårry'))
+    assert_true(cluster.fs.exists('/user/otherguy'))
+
+    # Clean up
+    cluster.fs.rmtree('/user/curly')
+    cluster.fs.rmtree(u'/user/lårry')
+    cluster.fs.rmtree('/user/otherguy')
+  finally:
+    for finish in reset:
+      finish()
+
+@attr('requires_hadoop')
+def test_ensure_home_directory_sync_ldap_users_groups():
+  URL = reverse(sync_ldap_users_groups)
+
+  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()
+
+  cluster = pseudo_hdfs4.shared_cluster()
+  c = make_logged_in_client(cluster.superuser, is_superuser=True)
+  cluster.fs.setuser(cluster.superuser)
+
+  reset = []
+
+  # Set to nonsensical value just to force new config usage.
+  # Should continue to use cached connection.
+  reset.append(desktop.conf.LDAP.LDAP_SERVERS.set_for_testing(get_nonsense_config()))
+
+  try:
+    c.post(reverse(add_ldap_users), dict(server='nonsense', username_pattern='curly', password1='test', password2='test'))
+    assert_false(cluster.fs.exists('/user/curly'))
+    assert_true(c.post(URL, dict(server='nonsense', ensure_home_directory=True)))
+    assert_true(cluster.fs.exists('/user/curly'))
+  finally:
+    for finish in reset:
+      finish()

+ 622 - 0
apps/useradmin/src/useradmin/test_ldap_deprecated.py

@@ -0,0 +1,622 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Licensed to Cloudera, Inc. under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  Cloudera, Inc. licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import ldap
+
+from nose.plugins.attrib import attr
+from nose.tools import assert_true, assert_equal, assert_false
+
+import desktop.conf
+from desktop.lib.test_utils import grant_access
+from desktop.lib.django_test_util import make_logged_in_client
+from django.conf import settings
+from django.contrib.auth.models import User, Group
+from django.core.urlresolvers import reverse
+
+from useradmin.models import LdapGroup, UserProfile
+from useradmin.models import get_profile
+
+from hadoop import pseudo_hdfs4
+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
+from tests import LdapTestConnection, reset_all_groups, reset_all_users
+
+
+def test_useradmin_ldap_user_group_membership_sync():
+  settings.MIDDLEWARE_CLASSES.append('useradmin.middleware.LdapSynchronizationMiddleware')
+
+  reset_all_users()
+  reset_all_groups()
+
+  # Set up LDAP tests to use a LdapTestConnection instead of an actual LDAP connection
+  ldap_access.CACHED_LDAP_CONN = LdapTestConnection()
+
+  try:
+    # Import curly who is part of TestUsers and Test Administrators
+    import_ldap_users(ldap_access.CACHED_LDAP_CONN, 'curly', sync_groups=False, import_by_dn=False)
+
+    # Set a password so that we can login
+    user = User.objects.get(username='curly')
+    user.set_password('test')
+    user.save()
+
+    # Should have 0 groups
+    assert_equal(0, user.groups.all().count())
+
+    # Make an authenticated request as curly so that we can see call middleware.
+    c = make_logged_in_client('curly', 'test', is_superuser=False)
+    grant_access("curly", "test", "useradmin")
+    response = c.get('/useradmin/users')
+
+    # Refresh user groups
+    user = User.objects.get(username='curly')
+
+    # Should have 3 groups now. 2 from LDAP and 1 from 'grant_access' call.
+    assert_equal(3, user.groups.all().count(), user.groups.all())
+
+    # Now remove a group and try again.
+    old_group = ldap_access.CACHED_LDAP_CONN._instance.users['curly']['groups'].pop()
+
+    # Make an authenticated request as curly so that we can see call middleware.
+    response = c.get('/useradmin/users')
+
+    # Refresh user groups
+    user = User.objects.get(username='curly')
+
+    # Should have 2 groups now. 1 from LDAP and 1 from 'grant_access' call.
+    assert_equal(3, user.groups.all().count(), user.groups.all())
+  finally:
+    settings.MIDDLEWARE_CLASSES.remove('useradmin.middleware.LdapSynchronizationMiddleware')
+
+
+def test_useradmin_ldap_suboordinate_group_integration():
+  reset_all_users()
+  reset_all_groups()
+
+  reset = []
+
+  # Set up LDAP tests to use a LdapTestConnection instead of an actual LDAP connection
+  ldap_access.CACHED_LDAP_CONN = LdapTestConnection()
+
+  # Test old subgroups
+  reset.append(desktop.conf.LDAP.SUBGROUPS.set_for_testing("suboordinate"))
+
+  try:
+    # Import groups only
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'TestUsers', import_members=False, import_members_recursive=False, sync_users=False, import_by_dn=False)
+    test_users = Group.objects.get(name='TestUsers')
+    assert_true(LdapGroup.objects.filter(group=test_users).exists())
+    assert_equal(test_users.user_set.all().count(), 0)
+
+    # Import all members of TestUsers
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'TestUsers', import_members=True, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    test_users = Group.objects.get(name='TestUsers')
+    assert_true(LdapGroup.objects.filter(group=test_users).exists())
+    assert_equal(test_users.user_set.all().count(), 3)
+
+    # Should import a group, but will only sync already-imported members
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'Test Administrators', import_members=False, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    assert_equal(User.objects.all().count(), 3)
+    assert_equal(Group.objects.all().count(), 2)
+    test_admins = Group.objects.get(name='Test Administrators')
+    assert_equal(test_admins.user_set.all().count(), 2)
+    larry = User.objects.get(username='lårry')
+    assert_equal(test_admins.user_set.all()[0].username, larry.username)
+
+    # Only sync already imported
+    ldap_access.CACHED_LDAP_CONN.remove_user_group_for_test('uid=moe,ou=People,dc=example,dc=com', 'TestUsers')
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'TestUsers', import_members=False, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    assert_equal(test_users.user_set.all().count(), 2)
+    assert_equal(User.objects.get(username='moe').groups.all().count(), 0)
+
+    # Import missing user
+    ldap_access.CACHED_LDAP_CONN.add_user_group_for_test('uid=moe,ou=People,dc=example,dc=com', 'TestUsers')
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'TestUsers', import_members=True, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    assert_equal(test_users.user_set.all().count(), 3)
+    assert_equal(User.objects.get(username='moe').groups.all().count(), 1)
+
+    # Import all members of TestUsers and members of subgroups
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'TestUsers', import_members=True, import_members_recursive=True, sync_users=True, import_by_dn=False)
+    test_users = Group.objects.get(name='TestUsers')
+    assert_true(LdapGroup.objects.filter(group=test_users).exists())
+    assert_equal(test_users.user_set.all().count(), 4)
+
+    # Make sure Hue groups with naming collisions don't get marked as LDAP groups
+    hue_user = User.objects.create(username='otherguy', first_name='Different', last_name='Guy')
+    hue_group = Group.objects.create(name='OtherGroup')
+    hue_group.user_set.add(hue_user)
+    hue_group.save()
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'OtherGroup', import_members=False, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    assert_false(LdapGroup.objects.filter(group=hue_group).exists())
+    assert_true(hue_group.user_set.filter(username=hue_user.username).exists())
+  finally:
+    for finish in reset:
+      finish()
+
+
+def test_useradmin_ldap_nested_group_integration():
+  reset_all_users()
+  reset_all_groups()
+
+  reset = []
+
+  # Set up LDAP tests to use a LdapTestConnection instead of an actual LDAP connection
+  ldap_access.CACHED_LDAP_CONN = LdapTestConnection()
+
+  # Test old subgroups
+  reset.append(desktop.conf.LDAP.SUBGROUPS.set_for_testing("nested"))
+
+  try:
+    # Import groups only
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'TestUsers', import_members=False, import_members_recursive=False, sync_users=False, import_by_dn=False)
+    test_users = Group.objects.get(name='TestUsers')
+    assert_true(LdapGroup.objects.filter(group=test_users).exists())
+    assert_equal(test_users.user_set.all().count(), 0)
+
+    # Import all members of TestUsers
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'TestUsers', import_members=True, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    test_users = Group.objects.get(name='TestUsers')
+    assert_true(LdapGroup.objects.filter(group=test_users).exists())
+    assert_equal(test_users.user_set.all().count(), 3)
+
+    # Should import a group, but will only sync already-imported members
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'Test Administrators', import_members=False, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    assert_equal(User.objects.all().count(), 3)
+    assert_equal(Group.objects.all().count(), 2)
+    test_admins = Group.objects.get(name='Test Administrators')
+    assert_equal(test_admins.user_set.all().count(), 2)
+    larry = User.objects.get(username='lårry')
+    assert_equal(test_admins.user_set.all()[0].username, larry.username)
+
+    # Only sync already imported
+    assert_equal(test_users.user_set.all().count(), 3)
+    ldap_access.CACHED_LDAP_CONN.remove_user_group_for_test('uid=moe,ou=People,dc=example,dc=com', 'TestUsers')
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'TestUsers', import_members=False, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    assert_equal(test_users.user_set.all().count(), 2)
+    assert_equal(User.objects.get(username='moe').groups.all().count(), 0)
+
+    # Import missing user
+    ldap_access.CACHED_LDAP_CONN.add_user_group_for_test('uid=moe,ou=People,dc=example,dc=com', 'TestUsers')
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'TestUsers', import_members=True, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    assert_equal(test_users.user_set.all().count(), 3)
+    assert_equal(User.objects.get(username='moe').groups.all().count(), 1)
+
+    # Import all members of TestUsers and not members of suboordinate groups (even though specified)
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'TestUsers', import_members=True, import_members_recursive=True, sync_users=True, import_by_dn=False)
+    test_users = Group.objects.get(name='TestUsers')
+    assert_true(LdapGroup.objects.filter(group=test_users).exists())
+    assert_equal(test_users.user_set.all().count(), 3)
+
+    # Nested group import
+    # First without recursive import, then with.
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'NestedGroups', import_members=True, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    nested_groups = Group.objects.get(name='NestedGroups')
+    nested_group = Group.objects.get(name='NestedGroup')
+    assert_true(LdapGroup.objects.filter(group=nested_groups).exists())
+    assert_true(LdapGroup.objects.filter(group=nested_group).exists())
+    assert_equal(nested_groups.user_set.all().count(), 0, nested_groups.user_set.all())
+    assert_equal(nested_group.user_set.all().count(), 0, nested_group.user_set.all())
+
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'NestedGroups', import_members=True, import_members_recursive=True, sync_users=True, import_by_dn=False)
+    nested_groups = Group.objects.get(name='NestedGroups')
+    nested_group = Group.objects.get(name='NestedGroup')
+    assert_true(LdapGroup.objects.filter(group=nested_groups).exists())
+    assert_true(LdapGroup.objects.filter(group=nested_group).exists())
+    assert_equal(nested_groups.user_set.all().count(), 0, nested_groups.user_set.all())
+    assert_equal(nested_group.user_set.all().count(), 1, nested_group.user_set.all())
+
+    # 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(ldap_access.CACHED_LDAP_CONN, 'OtherGroup', import_members=False, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    assert_false(LdapGroup.objects.filter(group=hue_group).exists())
+    assert_true(hue_group.user_set.filter(username=hue_user.username).exists())
+  finally:
+    for finish in reset:
+      finish()
+
+
+def test_useradmin_ldap_suboordinate_posix_group_integration():
+  reset_all_users()
+  reset_all_groups()
+
+  reset = []
+
+  # Set up LDAP tests to use a LdapTestConnection instead of an actual LDAP connection
+  ldap_access.CACHED_LDAP_CONN = LdapTestConnection()
+
+  # Test old subgroups
+  reset.append(desktop.conf.LDAP.SUBGROUPS.set_for_testing("suboordinate"))
+
+  try:
+    # Import groups only
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'PosixGroup', import_members=False, import_members_recursive=False, sync_users=False, import_by_dn=False)
+    test_users = Group.objects.get(name='PosixGroup')
+    assert_true(LdapGroup.objects.filter(group=test_users).exists())
+    assert_equal(test_users.user_set.all().count(), 0)
+
+    # Import all members of TestUsers
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'PosixGroup', import_members=True, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    test_users = Group.objects.get(name='PosixGroup')
+    assert_true(LdapGroup.objects.filter(group=test_users).exists())
+    assert_equal(test_users.user_set.all().count(), 2)
+
+    # Should import a group, but will only sync already-imported members
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'Test Administrators', import_members=False, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    assert_equal(User.objects.all().count(), 2, User.objects.all())
+    assert_equal(Group.objects.all().count(), 2, Group.objects.all())
+    test_admins = Group.objects.get(name='Test Administrators')
+    assert_equal(test_admins.user_set.all().count(), 1)
+    larry = User.objects.get(username='lårry')
+    assert_equal(test_admins.user_set.all()[0].username, larry.username)
+
+    # Only sync already imported
+    ldap_access.CACHED_LDAP_CONN.remove_posix_user_group_for_test('posix_person', 'PosixGroup')
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'PosixGroup', import_members=False, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    assert_equal(test_users.user_set.all().count(), 1)
+    assert_equal(User.objects.get(username='posix_person').groups.all().count(), 0)
+
+    # Import missing user
+    ldap_access.CACHED_LDAP_CONN.add_posix_user_group_for_test('posix_person', 'PosixGroup')
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'PosixGroup', import_members=True, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    assert_equal(test_users.user_set.all().count(), 2)
+    assert_equal(User.objects.get(username='posix_person').groups.all().count(), 1)
+
+    # Import all members of PosixGroup and members of subgroups
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'PosixGroup', import_members=True, import_members_recursive=True, sync_users=True, import_by_dn=False)
+    test_users = Group.objects.get(name='PosixGroup')
+    assert_true(LdapGroup.objects.filter(group=test_users).exists())
+    assert_equal(test_users.user_set.all().count(), 3)
+
+    # 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(ldap_access.CACHED_LDAP_CONN, 'OtherGroup', import_members=False, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    assert_false(LdapGroup.objects.filter(group=hue_group).exists())
+    assert_true(hue_group.user_set.filter(username=hue_user.username).exists())
+  finally:
+    for finish in reset:
+      finish()
+
+
+def test_useradmin_ldap_nested_posix_group_integration():
+  reset_all_users()
+  reset_all_groups()
+
+  reset = []
+
+  # Set up LDAP tests to use a LdapTestConnection instead of an actual LDAP connection
+  ldap_access.CACHED_LDAP_CONN = LdapTestConnection()
+
+  # Test nested groups
+  reset.append(desktop.conf.LDAP.SUBGROUPS.set_for_testing("nested"))
+
+  try:
+    # Import groups only
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'PosixGroup', import_members=False, import_members_recursive=False, sync_users=False, import_by_dn=False)
+    test_users = Group.objects.get(name='PosixGroup')
+    assert_true(LdapGroup.objects.filter(group=test_users).exists())
+    assert_equal(test_users.user_set.all().count(), 0)
+
+    # Import all members of TestUsers
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'PosixGroup', import_members=True, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    test_users = Group.objects.get(name='PosixGroup')
+    assert_true(LdapGroup.objects.filter(group=test_users).exists())
+    assert_equal(test_users.user_set.all().count(), 2)
+
+    # Should import a group, but will only sync already-imported members
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'Test Administrators', import_members=False, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    assert_equal(User.objects.all().count(), 2, User.objects.all())
+    assert_equal(Group.objects.all().count(), 2, Group.objects.all())
+    test_admins = Group.objects.get(name='Test Administrators')
+    assert_equal(test_admins.user_set.all().count(), 1)
+    larry = User.objects.get(username='lårry')
+    assert_equal(test_admins.user_set.all()[0].username, larry.username)
+
+    # Only sync already imported
+    ldap_access.CACHED_LDAP_CONN.remove_posix_user_group_for_test('posix_person', 'PosixGroup')
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'PosixGroup', import_members=False, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    assert_equal(test_users.user_set.all().count(), 1)
+    assert_equal(User.objects.get(username='posix_person').groups.all().count(), 0)
+
+    # Import missing user
+    ldap_access.CACHED_LDAP_CONN.add_posix_user_group_for_test('posix_person', 'PosixGroup')
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'PosixGroup', import_members=True, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    assert_equal(test_users.user_set.all().count(), 2)
+    assert_equal(User.objects.get(username='posix_person').groups.all().count(), 1)
+
+    # Import all members of PosixGroup and members of subgroups (there should be no subgroups)
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'PosixGroup', import_members=True, import_members_recursive=True, sync_users=True, import_by_dn=False)
+    test_users = Group.objects.get(name='PosixGroup')
+    assert_true(LdapGroup.objects.filter(group=test_users).exists())
+    assert_equal(test_users.user_set.all().count(), 2)
+
+    # Import all members of NestedPosixGroups and members of subgroups
+    reset_all_users()
+    reset_all_groups()
+    import_ldap_groups(ldap_access.CACHED_LDAP_CONN, 'NestedPosixGroups', import_members=True, import_members_recursive=True, sync_users=True, import_by_dn=False)
+    test_users = Group.objects.get(name='NestedPosixGroups')
+    assert_true(LdapGroup.objects.filter(group=test_users).exists())
+    assert_equal(test_users.user_set.all().count(), 0)
+    test_users = Group.objects.get(name='PosixGroup')
+    assert_true(LdapGroup.objects.filter(group=test_users).exists())
+    assert_equal(test_users.user_set.all().count(), 2)
+
+    # 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(ldap_access.CACHED_LDAP_CONN, 'OtherGroup', import_members=False, import_members_recursive=False, sync_users=True, import_by_dn=False)
+    assert_false(LdapGroup.objects.filter(group=hue_group).exists())
+    assert_true(hue_group.user_set.filter(username=hue_user.username).exists())
+  finally:
+    for finish in reset:
+      finish()
+
+
+def test_useradmin_ldap_user_integration():
+  done = []
+  try:
+    reset_all_users()
+    reset_all_groups()
+
+    # Set up LDAP tests to use a LdapTestConnection instead of an actual LDAP connection
+    ldap_access.CACHED_LDAP_CONN = LdapTestConnection()
+
+    # Try importing a user
+    import_ldap_users(ldap_access.CACHED_LDAP_CONN, 'lårry', sync_groups=False, import_by_dn=False)
+    larry = User.objects.get(username='lårry')
+    assert_true(larry.first_name == 'Larry')
+    assert_true(larry.last_name == 'Stooge')
+    assert_true(larry.email == 'larry@stooges.com')
+    assert_true(get_profile(larry).creation_method == str(UserProfile.CreationMethod.EXTERNAL))
+
+    # Should be a noop
+    sync_ldap_users(ldap_access.CACHED_LDAP_CONN)
+    sync_ldap_groups(ldap_access.CACHED_LDAP_CONN)
+    assert_equal(User.objects.all().count(), 1)
+    assert_equal(Group.objects.all().count(), 0)
+
+    # Make sure that if a Hue user already exists with a naming collision, we
+    # won't overwrite any of that user's information.
+    hue_user = User.objects.create(username='otherguy', first_name='Different', last_name='Guy')
+    import_ldap_users(ldap_access.CACHED_LDAP_CONN, 'otherguy', sync_groups=False, import_by_dn=False)
+    hue_user = User.objects.get(username='otherguy')
+    assert_equal(get_profile(hue_user).creation_method, str(UserProfile.CreationMethod.HUE))
+    assert_equal(hue_user.first_name, 'Different')
+
+    # Try importing a user and sync groups
+    import_ldap_users(ldap_access.CACHED_LDAP_CONN, 'curly', sync_groups=True, import_by_dn=False)
+    curly = User.objects.get(username='curly')
+    assert_equal(curly.first_name, 'Curly')
+    assert_equal(curly.last_name, 'Stooge')
+    assert_equal(curly.email, 'curly@stooges.com')
+    assert_equal(get_profile(curly).creation_method, str(UserProfile.CreationMethod.EXTERNAL))
+    assert_equal(2, curly.groups.all().count(), curly.groups.all())
+
+    reset_all_users()
+    reset_all_groups()
+
+    # Test import case sensitivity
+    done.append(desktop.conf.LDAP.IGNORE_USERNAME_CASE.set_for_testing(True))
+    import_ldap_users(ldap_access.CACHED_LDAP_CONN, 'Lårry', sync_groups=False, import_by_dn=False)
+    assert_false(User.objects.filter(username='Lårry').exists())
+    assert_true(User.objects.filter(username='lårry').exists())
+
+    # Test lower case
+    User.objects.filter(username__iexact='Rock').delete()
+    import_ldap_users(ldap_access.CACHED_LDAP_CONN, 'Rock', sync_groups=False, import_by_dn=False)
+    assert_true(User.objects.filter(username='Rock').exists())
+    assert_false(User.objects.filter(username='rock').exists())
+
+    done.append(desktop.conf.LDAP.FORCE_USERNAME_LOWERCASE.set_for_testing(True))
+
+    import_ldap_users(ldap_access.CACHED_LDAP_CONN, 'Rock', sync_groups=False, import_by_dn=False)
+    assert_true(User.objects.filter(username='Rock').exists())
+    assert_false(User.objects.filter(username='rock').exists())
+
+    User.objects.filter(username='Rock').delete()
+    import_ldap_users(ldap_access.CACHED_LDAP_CONN, 'Rock', sync_groups=False, import_by_dn=False)
+    assert_false(User.objects.filter(username='Rock').exists())
+    assert_true(User.objects.filter(username='rock').exists())
+  finally:
+    for finish in done:
+      finish()
+
+
+def test_add_ldap_users():
+  done = []
+  try:
+    URL = reverse(add_ldap_users)
+
+    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()
+
+    c = make_logged_in_client('test', is_superuser=True)
+
+    assert_true(c.get(URL))
+
+    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)
+
+    # Test wild card
+    response = c.post(URL, dict(username_pattern='*r*', password1='test', password2='test'))
+    assert_true('/useradmin/users' in response['Location'], response)
+
+    # Test ignore case
+    done.append(desktop.conf.LDAP.IGNORE_USERNAME_CASE.set_for_testing(True))
+    User.objects.filter(username='moe').delete()
+    assert_false(User.objects.filter(username='Moe').exists())
+    assert_false(User.objects.filter(username='moe').exists())
+    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)
+    assert_false(User.objects.filter(username='Moe').exists())
+    assert_true(User.objects.filter(username='moe').exists())
+
+    # Test lower case
+    done.append(desktop.conf.LDAP.FORCE_USERNAME_LOWERCASE.set_for_testing(True))
+    User.objects.filter(username__iexact='Rock').delete()
+    assert_false(User.objects.filter(username='Rock').exists())
+    assert_false(User.objects.filter(username='rock').exists())
+    response = c.post(URL, dict(username_pattern='rock', password1='test', password2='test'))
+    assert_true('Location' in response, response)
+    assert_true('/useradmin/users' in response['Location'], response)
+    assert_false(User.objects.filter(username='Rock').exists())
+    assert_true(User.objects.filter(username='rock').exists())
+
+  finally:
+    for finish in done:
+      finish()
+
+
+def test_add_ldap_groups():
+  URL = reverse(add_ldap_groups)
+
+  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()
+
+
+  c = make_logged_in_client(username='test', is_superuser=True)
+
+  assert_true(c.get(URL))
+
+  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(groupname_pattern='Test Administrators'))
+  assert_true('Location' in response, response)
+  assert_true('/useradmin/groups' in response['Location'], 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
+  response = c.post(URL, dict(groupname_pattern='*r*'))
+  assert_true('/useradmin/groups' in response['Location'], response)
+
+def test_sync_ldap_users_groups():
+  URL = reverse(sync_ldap_users_groups)
+
+  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()
+
+  c = make_logged_in_client('test', is_superuser=True)
+
+  assert_true(c.get(URL))
+  assert_true(c.post(URL))
+
+def test_ldap_exception_handling():
+  reset_all_users()
+  reset_all_groups()
+
+  # Set up LDAP tests to use a LdapTestConnection instead of an actual LDAP connection
+  class LdapTestConnectionError(LdapTestConnection):
+    def find_users(self, user, find_by_dn=False):
+      raise ldap.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_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_users():
+  URL = reverse(add_ldap_users)
+
+  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()
+
+  cluster = pseudo_hdfs4.shared_cluster()
+  c = make_logged_in_client(cluster.superuser, is_superuser=True)
+  cluster.fs.setuser(cluster.superuser)
+
+  assert_true(c.get(URL))
+
+  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_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_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(u'/user/lårry'))
+  assert_true(cluster.fs.exists('/user/otherguy'))
+
+  # Clean up
+  cluster.fs.rmtree('/user/curly')
+  cluster.fs.rmtree(u'/user/lårry')
+  cluster.fs.rmtree('/user/otherguy')
+
+@attr('requires_hadoop')
+def test_ensure_home_directory_sync_ldap_users_groups():
+  URL = reverse(sync_ldap_users_groups)
+
+  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()
+
+  cluster = pseudo_hdfs4.shared_cluster()
+  c = make_logged_in_client(cluster.superuser, is_superuser=True)
+  cluster.fs.setuser(cluster.superuser)
+
+  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'))

+ 1 - 591
apps/useradmin/src/useradmin/tests.py

@@ -24,23 +24,16 @@ import urllib
 from nose.plugins.attrib import attr
 from nose.tools import assert_true, assert_equal, assert_false
 
-import desktop.conf
-from desktop.lib.test_utils import grant_access
 from desktop.lib.django_test_util import make_logged_in_client
-from django.conf import settings
 from django.contrib.auth.models import User, Group
 from django.utils.encoding import smart_unicode
 from django.core.urlresolvers import reverse
 
-from useradmin.models import HuePermission, GroupPermission, LdapGroup, UserProfile
+from useradmin.models import HuePermission, GroupPermission, UserProfile
 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_users, import_ldap_groups, \
-                  add_ldap_users, add_ldap_groups, sync_ldap_users_groups
-
-import ldap_access
 
 
 def reset_all_users():
@@ -522,589 +515,6 @@ def test_user_admin():
   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)
 
-
-def test_useradmin_ldap_user_group_membership_sync():
-  settings.MIDDLEWARE_CLASSES.append('useradmin.middleware.LdapSynchronizationMiddleware')
-
-  reset_all_users()
-  reset_all_groups()
-
-  # Set up LDAP tests to use a LdapTestConnection instead of an actual LDAP connection
-  ldap_access.CACHED_LDAP_CONN = LdapTestConnection()
-
-  try:
-    # Import curly who is part of TestUsers and Test Administrators
-    import_ldap_users('curly', sync_groups=False, import_by_dn=False)
-
-    # Set a password so that we can login
-    user = User.objects.get(username='curly')
-    user.set_password('test')
-    user.save()
-
-    # Should have 0 groups
-    assert_equal(0, user.groups.all().count())
-
-    # Make an authenticated request as curly so that we can see call middleware.
-    c = make_logged_in_client('curly', 'test', is_superuser=False)
-    grant_access("curly", "test", "useradmin")
-    response = c.get('/useradmin/users')
-
-    # Refresh user groups
-    user = User.objects.get(username='curly')
-
-    # Should have 3 groups now. 2 from LDAP and 1 from 'grant_access' call.
-    assert_equal(3, user.groups.all().count(), user.groups.all())
-
-    # Now remove a group and try again.
-    old_group = ldap_access.CACHED_LDAP_CONN._instance.users['curly']['groups'].pop()
-
-    # Make an authenticated request as curly so that we can see call middleware.
-    response = c.get('/useradmin/users')
-
-    # Refresh user groups
-    user = User.objects.get(username='curly')
-
-    # Should have 2 groups now. 1 from LDAP and 1 from 'grant_access' call.
-    assert_equal(3, user.groups.all().count(), user.groups.all())
-  finally:
-    settings.MIDDLEWARE_CLASSES.remove('useradmin.middleware.LdapSynchronizationMiddleware')
-
-
-def test_useradmin_ldap_suboordinate_group_integration():
-  reset_all_users()
-  reset_all_groups()
-
-  reset = []
-
-  # Set up LDAP tests to use a LdapTestConnection instead of an actual LDAP connection
-  ldap_access.CACHED_LDAP_CONN = LdapTestConnection()
-
-  # Test old subgroups
-  reset.append(desktop.conf.LDAP.SUBGROUPS.set_for_testing("suboordinate"))
-
-  try:
-    # Import groups only
-    import_ldap_groups('TestUsers', import_members=False, import_members_recursive=False, sync_users=False, import_by_dn=False)
-    test_users = Group.objects.get(name='TestUsers')
-    assert_true(LdapGroup.objects.filter(group=test_users).exists())
-    assert_equal(test_users.user_set.all().count(), 0)
-
-    # Import all members of TestUsers
-    import_ldap_groups('TestUsers', import_members=True, import_members_recursive=False, sync_users=True, import_by_dn=False)
-    test_users = Group.objects.get(name='TestUsers')
-    assert_true(LdapGroup.objects.filter(group=test_users).exists())
-    assert_equal(test_users.user_set.all().count(), 3)
-
-    # Should import a group, but will only sync already-imported members
-    import_ldap_groups('Test Administrators', import_members=False, import_members_recursive=False, sync_users=True, import_by_dn=False)
-    assert_equal(User.objects.all().count(), 3)
-    assert_equal(Group.objects.all().count(), 2)
-    test_admins = Group.objects.get(name='Test Administrators')
-    assert_equal(test_admins.user_set.all().count(), 2)
-    larry = User.objects.get(username='lårry')
-    assert_equal(test_admins.user_set.all()[0].username, larry.username)
-
-    # Only sync already imported
-    ldap_access.CACHED_LDAP_CONN.remove_user_group_for_test('uid=moe,ou=People,dc=example,dc=com', 'TestUsers')
-    import_ldap_groups('TestUsers', import_members=False, import_members_recursive=False, sync_users=True, import_by_dn=False)
-    assert_equal(test_users.user_set.all().count(), 2)
-    assert_equal(User.objects.get(username='moe').groups.all().count(), 0)
-
-    # Import missing user
-    ldap_access.CACHED_LDAP_CONN.add_user_group_for_test('uid=moe,ou=People,dc=example,dc=com', 'TestUsers')
-    import_ldap_groups('TestUsers', import_members=True, import_members_recursive=False, sync_users=True, import_by_dn=False)
-    assert_equal(test_users.user_set.all().count(), 3)
-    assert_equal(User.objects.get(username='moe').groups.all().count(), 1)
-
-    # Import all members of TestUsers and members of subgroups
-    import_ldap_groups('TestUsers', import_members=True, import_members_recursive=True, sync_users=True, import_by_dn=False)
-    test_users = Group.objects.get(name='TestUsers')
-    assert_true(LdapGroup.objects.filter(group=test_users).exists())
-    assert_equal(test_users.user_set.all().count(), 4)
-
-    # Make sure Hue groups with naming collisions don't get marked as LDAP groups
-    hue_user = User.objects.create(username='otherguy', first_name='Different', last_name='Guy')
-    hue_group = Group.objects.create(name='OtherGroup')
-    hue_group.user_set.add(hue_user)
-    hue_group.save()
-    import_ldap_groups('OtherGroup', import_members=False, import_members_recursive=False, sync_users=True, import_by_dn=False)
-    assert_false(LdapGroup.objects.filter(group=hue_group).exists())
-    assert_true(hue_group.user_set.filter(username=hue_user.username).exists())
-  finally:
-    for finish in reset:
-      finish()
-
-
-def test_useradmin_ldap_nested_group_integration():
-  reset_all_users()
-  reset_all_groups()
-
-  reset = []
-
-  # Set up LDAP tests to use a LdapTestConnection instead of an actual LDAP connection
-  ldap_access.CACHED_LDAP_CONN = LdapTestConnection()
-
-  # Test old subgroups
-  reset.append(desktop.conf.LDAP.SUBGROUPS.set_for_testing("nested"))
-
-  try:
-    # Import groups only
-    import_ldap_groups('TestUsers', import_members=False, import_members_recursive=False, sync_users=False, import_by_dn=False)
-    test_users = Group.objects.get(name='TestUsers')
-    assert_true(LdapGroup.objects.filter(group=test_users).exists())
-    assert_equal(test_users.user_set.all().count(), 0)
-
-    # Import all members of TestUsers
-    import_ldap_groups('TestUsers', import_members=True, import_members_recursive=False, sync_users=True, import_by_dn=False)
-    test_users = Group.objects.get(name='TestUsers')
-    assert_true(LdapGroup.objects.filter(group=test_users).exists())
-    assert_equal(test_users.user_set.all().count(), 3)
-
-    # Should import a group, but will only sync already-imported members
-    import_ldap_groups('Test Administrators', import_members=False, import_members_recursive=False, sync_users=True, import_by_dn=False)
-    assert_equal(User.objects.all().count(), 3)
-    assert_equal(Group.objects.all().count(), 2)
-    test_admins = Group.objects.get(name='Test Administrators')
-    assert_equal(test_admins.user_set.all().count(), 2)
-    larry = User.objects.get(username='lårry')
-    assert_equal(test_admins.user_set.all()[0].username, larry.username)
-
-    # Only sync already imported
-    assert_equal(test_users.user_set.all().count(), 3)
-    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, sync_users=True, import_by_dn=False)
-    assert_equal(test_users.user_set.all().count(), 2)
-    assert_equal(User.objects.get(username='moe').groups.all().count(), 0)
-
-    # Import missing user
-    ldap_access.CACHED_LDAP_CONN.add_user_group_for_test('uid=moe,ou=People,dc=example,dc=com', 'TestUsers')
-    import_ldap_groups('TestUsers', import_members=True, import_members_recursive=False, sync_users=True, import_by_dn=False)
-    assert_equal(test_users.user_set.all().count(), 3)
-    assert_equal(User.objects.get(username='moe').groups.all().count(), 1)
-
-    # Import all members of TestUsers and not members of suboordinate groups (even though specified)
-    import_ldap_groups('TestUsers', import_members=True, import_members_recursive=True, sync_users=True, import_by_dn=False)
-    test_users = Group.objects.get(name='TestUsers')
-    assert_true(LdapGroup.objects.filter(group=test_users).exists())
-    assert_equal(test_users.user_set.all().count(), 3)
-
-    # Nested group import
-    # First without recursive import, then with.
-    import_ldap_groups('NestedGroups', import_members=True, import_members_recursive=False, sync_users=True, import_by_dn=False)
-    nested_groups = Group.objects.get(name='NestedGroups')
-    nested_group = Group.objects.get(name='NestedGroup')
-    assert_true(LdapGroup.objects.filter(group=nested_groups).exists())
-    assert_true(LdapGroup.objects.filter(group=nested_group).exists())
-    assert_equal(nested_groups.user_set.all().count(), 0, nested_groups.user_set.all())
-    assert_equal(nested_group.user_set.all().count(), 0, nested_group.user_set.all())
-
-    import_ldap_groups('NestedGroups', import_members=True, import_members_recursive=True, sync_users=True, import_by_dn=False)
-    nested_groups = Group.objects.get(name='NestedGroups')
-    nested_group = Group.objects.get(name='NestedGroup')
-    assert_true(LdapGroup.objects.filter(group=nested_groups).exists())
-    assert_true(LdapGroup.objects.filter(group=nested_group).exists())
-    assert_equal(nested_groups.user_set.all().count(), 0, nested_groups.user_set.all())
-    assert_equal(nested_group.user_set.all().count(), 1, nested_group.user_set.all())
-
-    # 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, sync_users=True, import_by_dn=False)
-    assert_false(LdapGroup.objects.filter(group=hue_group).exists())
-    assert_true(hue_group.user_set.filter(username=hue_user.username).exists())
-  finally:
-    for finish in reset:
-      finish()
-
-
-def test_useradmin_ldap_suboordinate_posix_group_integration():
-  reset_all_users()
-  reset_all_groups()
-
-  reset = []
-
-  # Set up LDAP tests to use a LdapTestConnection instead of an actual LDAP connection
-  ldap_access.CACHED_LDAP_CONN = LdapTestConnection()
-
-  # Test old subgroups
-  reset.append(desktop.conf.LDAP.SUBGROUPS.set_for_testing("suboordinate"))
-
-  try:
-    # Import groups only
-    import_ldap_groups('PosixGroup', import_members=False, import_members_recursive=False, sync_users=False, import_by_dn=False)
-    test_users = Group.objects.get(name='PosixGroup')
-    assert_true(LdapGroup.objects.filter(group=test_users).exists())
-    assert_equal(test_users.user_set.all().count(), 0)
-
-    # Import all members of TestUsers
-    import_ldap_groups('PosixGroup', import_members=True, import_members_recursive=False, sync_users=True, import_by_dn=False)
-    test_users = Group.objects.get(name='PosixGroup')
-    assert_true(LdapGroup.objects.filter(group=test_users).exists())
-    assert_equal(test_users.user_set.all().count(), 2)
-
-    # Should import a group, but will only sync already-imported members
-    import_ldap_groups('Test Administrators', import_members=False, import_members_recursive=False, sync_users=True, import_by_dn=False)
-    assert_equal(User.objects.all().count(), 2, User.objects.all())
-    assert_equal(Group.objects.all().count(), 2, Group.objects.all())
-    test_admins = Group.objects.get(name='Test Administrators')
-    assert_equal(test_admins.user_set.all().count(), 1)
-    larry = User.objects.get(username='lårry')
-    assert_equal(test_admins.user_set.all()[0].username, larry.username)
-
-    # Only sync already imported
-    ldap_access.CACHED_LDAP_CONN.remove_posix_user_group_for_test('posix_person', 'PosixGroup')
-    import_ldap_groups('PosixGroup', import_members=False, import_members_recursive=False, sync_users=True, import_by_dn=False)
-    assert_equal(test_users.user_set.all().count(), 1)
-    assert_equal(User.objects.get(username='posix_person').groups.all().count(), 0)
-
-    # Import missing user
-    ldap_access.CACHED_LDAP_CONN.add_posix_user_group_for_test('posix_person', 'PosixGroup')
-    import_ldap_groups('PosixGroup', import_members=True, import_members_recursive=False, sync_users=True, import_by_dn=False)
-    assert_equal(test_users.user_set.all().count(), 2)
-    assert_equal(User.objects.get(username='posix_person').groups.all().count(), 1)
-
-    # Import all members of PosixGroup and members of subgroups
-    import_ldap_groups('PosixGroup', import_members=True, import_members_recursive=True, sync_users=True, import_by_dn=False)
-    test_users = Group.objects.get(name='PosixGroup')
-    assert_true(LdapGroup.objects.filter(group=test_users).exists())
-    assert_equal(test_users.user_set.all().count(), 3)
-
-    # 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, sync_users=True, import_by_dn=False)
-    assert_false(LdapGroup.objects.filter(group=hue_group).exists())
-    assert_true(hue_group.user_set.filter(username=hue_user.username).exists())
-  finally:
-    for finish in reset:
-      finish()
-
-
-def test_useradmin_ldap_nested_posix_group_integration():
-  reset_all_users()
-  reset_all_groups()
-
-  reset = []
-
-  # Set up LDAP tests to use a LdapTestConnection instead of an actual LDAP connection
-  ldap_access.CACHED_LDAP_CONN = LdapTestConnection()
-
-  # Test nested groups
-  reset.append(desktop.conf.LDAP.SUBGROUPS.set_for_testing("nested"))
-
-  try:
-    # Import groups only
-    import_ldap_groups('PosixGroup', import_members=False, import_members_recursive=False, sync_users=False, import_by_dn=False)
-    test_users = Group.objects.get(name='PosixGroup')
-    assert_true(LdapGroup.objects.filter(group=test_users).exists())
-    assert_equal(test_users.user_set.all().count(), 0)
-
-    # Import all members of TestUsers
-    import_ldap_groups('PosixGroup', import_members=True, import_members_recursive=False, sync_users=True, import_by_dn=False)
-    test_users = Group.objects.get(name='PosixGroup')
-    assert_true(LdapGroup.objects.filter(group=test_users).exists())
-    assert_equal(test_users.user_set.all().count(), 2)
-
-    # Should import a group, but will only sync already-imported members
-    import_ldap_groups('Test Administrators', import_members=False, import_members_recursive=False, sync_users=True, import_by_dn=False)
-    assert_equal(User.objects.all().count(), 2, User.objects.all())
-    assert_equal(Group.objects.all().count(), 2, Group.objects.all())
-    test_admins = Group.objects.get(name='Test Administrators')
-    assert_equal(test_admins.user_set.all().count(), 1)
-    larry = User.objects.get(username='lårry')
-    assert_equal(test_admins.user_set.all()[0].username, larry.username)
-
-    # Only sync already imported
-    ldap_access.CACHED_LDAP_CONN.remove_posix_user_group_for_test('posix_person', 'PosixGroup')
-    import_ldap_groups('PosixGroup', import_members=False, import_members_recursive=False, sync_users=True, import_by_dn=False)
-    assert_equal(test_users.user_set.all().count(), 1)
-    assert_equal(User.objects.get(username='posix_person').groups.all().count(), 0)
-
-    # Import missing user
-    ldap_access.CACHED_LDAP_CONN.add_posix_user_group_for_test('posix_person', 'PosixGroup')
-    import_ldap_groups('PosixGroup', import_members=True, import_members_recursive=False, sync_users=True, import_by_dn=False)
-    assert_equal(test_users.user_set.all().count(), 2)
-    assert_equal(User.objects.get(username='posix_person').groups.all().count(), 1)
-
-    # Import all members of PosixGroup and members of subgroups (there should be no subgroups)
-    import_ldap_groups('PosixGroup', import_members=True, import_members_recursive=True, sync_users=True, import_by_dn=False)
-    test_users = Group.objects.get(name='PosixGroup')
-    assert_true(LdapGroup.objects.filter(group=test_users).exists())
-    assert_equal(test_users.user_set.all().count(), 2)
-
-    # Import all members of NestedPosixGroups and members of subgroups
-    reset_all_users()
-    reset_all_groups()
-    import_ldap_groups('NestedPosixGroups', import_members=True, import_members_recursive=True, sync_users=True, import_by_dn=False)
-    test_users = Group.objects.get(name='NestedPosixGroups')
-    assert_true(LdapGroup.objects.filter(group=test_users).exists())
-    assert_equal(test_users.user_set.all().count(), 0)
-    test_users = Group.objects.get(name='PosixGroup')
-    assert_true(LdapGroup.objects.filter(group=test_users).exists())
-    assert_equal(test_users.user_set.all().count(), 2)
-
-    # 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, sync_users=True, import_by_dn=False)
-    assert_false(LdapGroup.objects.filter(group=hue_group).exists())
-    assert_true(hue_group.user_set.filter(username=hue_user.username).exists())
-  finally:
-    for finish in reset:
-      finish()
-
-
-def test_useradmin_ldap_user_integration():
-  done = []
-  try:
-    reset_all_users()
-    reset_all_groups()
-
-    # Set up LDAP tests to use a LdapTestConnection instead of an actual LDAP connection
-    ldap_access.CACHED_LDAP_CONN = LdapTestConnection()
-
-    # Try importing a user
-    import_ldap_users('lårry', sync_groups=False, import_by_dn=False)
-    larry = User.objects.get(username='lårry')
-    assert_true(larry.first_name == 'Larry')
-    assert_true(larry.last_name == 'Stooge')
-    assert_true(larry.email == 'larry@stooges.com')
-    assert_true(get_profile(larry).creation_method == str(UserProfile.CreationMethod.EXTERNAL))
-
-    # Should be a noop
-    sync_ldap_users()
-    sync_ldap_groups()
-    assert_equal(User.objects.all().count(), 1)
-    assert_equal(Group.objects.all().count(), 0)
-
-    # Make sure that if a Hue user already exists with a naming collision, we
-    # won't overwrite any of that user's information.
-    hue_user = User.objects.create(username='otherguy', first_name='Different', last_name='Guy')
-    import_ldap_users('otherguy', sync_groups=False, import_by_dn=False)
-    hue_user = User.objects.get(username='otherguy')
-    assert_equal(get_profile(hue_user).creation_method, str(UserProfile.CreationMethod.HUE))
-    assert_equal(hue_user.first_name, 'Different')
-
-    # Try importing a user and sync groups
-    import_ldap_users('curly', sync_groups=True, import_by_dn=False)
-    curly = User.objects.get(username='curly')
-    assert_equal(curly.first_name, 'Curly')
-    assert_equal(curly.last_name, 'Stooge')
-    assert_equal(curly.email, 'curly@stooges.com')
-    assert_equal(get_profile(curly).creation_method, str(UserProfile.CreationMethod.EXTERNAL))
-    assert_equal(2, curly.groups.all().count(), curly.groups.all())
-
-    reset_all_users()
-    reset_all_groups()
-
-    # Test import case sensitivity
-    done.append(desktop.conf.LDAP.IGNORE_USERNAME_CASE.set_for_testing(True))
-    import_ldap_users('Lårry', sync_groups=False, import_by_dn=False)
-    assert_false(User.objects.filter(username='Lårry').exists())
-    assert_true(User.objects.filter(username='lårry').exists())
-
-    # Test lower case
-    User.objects.filter(username__iexact='Rock').delete()
-    import_ldap_users('Rock', sync_groups=False, import_by_dn=False)
-    assert_true(User.objects.filter(username='Rock').exists())
-    assert_false(User.objects.filter(username='rock').exists())
-
-    done.append(desktop.conf.LDAP.FORCE_USERNAME_LOWERCASE.set_for_testing(True))
-
-    import_ldap_users('Rock', sync_groups=False, import_by_dn=False)
-    assert_true(User.objects.filter(username='Rock').exists())
-    assert_false(User.objects.filter(username='rock').exists())
-
-    User.objects.filter(username='Rock').delete()
-    import_ldap_users('Rock', sync_groups=False, import_by_dn=False)
-    assert_false(User.objects.filter(username='Rock').exists())
-    assert_true(User.objects.filter(username='rock').exists())
-  finally:
-    for finish in done:
-      finish()
-
-
-def test_add_ldap_users():
-  done = []
-  try:
-    URL = reverse(add_ldap_users)
-
-    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()
-
-    c = make_logged_in_client('test', is_superuser=True)
-
-    assert_true(c.get(URL))
-
-    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)
-
-    # Test wild card
-    response = c.post(URL, dict(username_pattern='*r*', password1='test', password2='test'))
-    assert_true('/useradmin/users' in response['Location'], response)
-
-    # Test ignore case
-    done.append(desktop.conf.LDAP.IGNORE_USERNAME_CASE.set_for_testing(True))
-    User.objects.filter(username='moe').delete()
-    assert_false(User.objects.filter(username='Moe').exists())
-    assert_false(User.objects.filter(username='moe').exists())
-    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)
-    assert_false(User.objects.filter(username='Moe').exists())
-    assert_true(User.objects.filter(username='moe').exists())
-
-    # Test lower case
-    done.append(desktop.conf.LDAP.FORCE_USERNAME_LOWERCASE.set_for_testing(True))
-    User.objects.filter(username__iexact='Rock').delete()
-    assert_false(User.objects.filter(username='Rock').exists())
-    assert_false(User.objects.filter(username='rock').exists())
-    response = c.post(URL, dict(username_pattern='rock', password1='test', password2='test'))
-    assert_true('Location' in response, response)
-    assert_true('/useradmin/users' in response['Location'], response)
-    assert_false(User.objects.filter(username='Rock').exists())
-    assert_true(User.objects.filter(username='rock').exists())
-
-  finally:
-    for finish in done:
-      finish()
-
-
-def test_add_ldap_groups():
-  URL = reverse(add_ldap_groups)
-
-  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()
-
-
-  c = make_logged_in_client(username='test', is_superuser=True)
-
-  assert_true(c.get(URL))
-
-  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(groupname_pattern='Test Administrators'))
-  assert_true('Location' in response, response)
-  assert_true('/useradmin/groups' in response['Location'], 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
-  response = c.post(URL, dict(groupname_pattern='*r*'))
-  assert_true('/useradmin/groups' in response['Location'], response)
-
-def test_sync_ldap_users_groups():
-  URL = reverse(sync_ldap_users_groups)
-
-  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()
-
-  c = make_logged_in_client('test', is_superuser=True)
-
-  assert_true(c.get(URL))
-  assert_true(c.post(URL))
-
-def test_ldap_exception_handling():
-  reset_all_users()
-  reset_all_groups()
-
-  # Set up LDAP tests to use a LdapTestConnection instead of an actual LDAP connection
-  class LdapTestConnectionError(LdapTestConnection):
-    def find_users(self, user, find_by_dn=False):
-      raise ldap.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_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_users():
-  URL = reverse(add_ldap_users)
-
-  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()
-
-  cluster = pseudo_hdfs4.shared_cluster()
-  c = make_logged_in_client(cluster.superuser, is_superuser=True)
-  cluster.fs.setuser(cluster.superuser)
-
-  assert_true(c.get(URL))
-
-  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_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_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(u'/user/lårry'))
-  assert_true(cluster.fs.exists('/user/otherguy'))
-
-  # Clean up
-  cluster.fs.rmtree('/user/curly')
-  cluster.fs.rmtree(u'/user/lårry')
-  cluster.fs.rmtree('/user/otherguy')
-
-@attr('requires_hadoop')
-def test_ensure_home_directory_sync_ldap_users_groups():
-  URL = reverse(sync_ldap_users_groups)
-
-  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()
-
-  cluster = pseudo_hdfs4.shared_cluster()
-  c = make_logged_in_client(cluster.superuser, is_superuser=True)
-  cluster.fs.setuser(cluster.superuser)
-
-  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'))
-
 @attr('requires_hadoop')
 def test_ensure_home_directory():
   reset_all_users()

+ 60 - 54
apps/useradmin/src/useradmin/views.py

@@ -52,18 +52,20 @@ __groups_lock = threading.Lock()
 
 
 def list_users(request):
+  is_ldap_setup = bool(LDAP.LDAP_SERVERS.get()) or LDAP.LDAP_URL.get() is not None
   return render("list_users.mako", request, {
       'users': User.objects.all(),
       'users_json': json.dumps(list(User.objects.values_list('id', flat=True))),
       'request': request,
-      'is_ldap_setup': LDAP.LDAP_URL.get() is not None
+      'is_ldap_setup': is_ldap_setup
   })
 
 
 def list_groups(request):
+  is_ldap_setup = bool(LDAP.LDAP_SERVERS.get()) or LDAP.LDAP_URL.get() is not None
   return render("list_groups.mako", request, {
       'groups': Group.objects.all(),
-      'is_ldap_setup': LDAP.LDAP_URL.get() is not None
+      'is_ldap_setup': is_ldap_setup
   })
 
 
@@ -311,8 +313,10 @@ def add_ldap_users(request):
     if form.is_valid():
       username_pattern = form.cleaned_data['username_pattern']
       import_by_dn = form.cleaned_data['dn']
+      server = form.cleaned_data['server']
       try:
-        users = import_ldap_users(username_pattern, False, import_by_dn)
+        connection = ldap_access.get_connection_from_server(server)
+        users = import_ldap_users(connection, username_pattern, False, import_by_dn)
       except ldap.LDAPError, e:
         LOG.error("LDAP Exception: %s" % e)
         raise PopupException(_('There was an error when communicating with LDAP'), detail=str(e))
@@ -355,8 +359,10 @@ def add_ldap_groups(request):
       import_by_dn = form.cleaned_data['dn']
       import_members = form.cleaned_data['import_members']
       import_members_recursive = form.cleaned_data['import_members_recursive']
+      server = form.cleaned_data['server']
       try:
-        groups = import_ldap_groups(groupname_pattern, import_members=import_members, import_members_recursive=import_members_recursive, sync_users=True, import_by_dn=import_by_dn)
+        connection = ldap_access.get_connection_from_server(server)
+        groups = import_ldap_groups(connection, groupname_pattern, import_members=import_members, import_members_recursive=import_members_recursive, sync_users=True, import_by_dn=import_by_dn)
       except ldap.LDAPError, e:
         LOG.error(_("LDAP Exception: %s") % e)
         raise PopupException(_('There was an error when communicating with LDAP'), detail=str(e))
@@ -388,17 +394,19 @@ def sync_ldap_users_groups(request):
     form = SyncLdapUsersGroupsForm(request.POST)
     if form.is_valid():
       is_ensuring_home_directory = form.cleaned_data['ensure_home_directory']
-      sync_ldap_users_and_groups(is_ensuring_home_directory, request.fs)
+      server = form.cleaned_data['server']
+      connection = ldap_access.get_connection_from_server(server)
+      sync_ldap_users_and_groups(connection, is_ensuring_home_directory, request.fs)
       return redirect(reverse(list_users))
   else:
     form = SyncLdapUsersGroupsForm()
 
   return render("sync_ldap_users_groups.mako", request, dict(path=request.path, form=form))
 
-def sync_ldap_users_and_groups(is_ensuring_home_directory=False, fs=None):
+def sync_ldap_users_and_groups(connection, is_ensuring_home_directory=False, fs=None):
   try:
-    users = sync_ldap_users()
-    groups = sync_ldap_groups()
+    users = sync_ldap_users(connection)
+    groups = sync_ldap_groups(connection)
   except ldap.LDAPError, e:
     LOG.error("LDAP Exception: %s" % e)
     raise PopupException(_('There was an error when communicating with LDAP'), detail=str(e))
@@ -496,21 +504,20 @@ def sync_unix_users_and_groups(min_uid, max_uid, min_gid, max_gid, check_shell):
   __groups_lock.release()
 
 
-def _import_ldap_users(username_pattern, sync_groups=False, import_by_dn=False):
+def _import_ldap_users(connection, username_pattern, sync_groups=False, import_by_dn=False):
   """
   Import a user from LDAP. If import_by_dn is true, this will import the user by
   the distinguished name, rather than the configured username attribute.
   """
-  conn = ldap_access.get_connection()
-  user_info = conn.find_users(username_pattern, find_by_dn=import_by_dn)
+  user_info = connection.find_users(username_pattern, find_by_dn=import_by_dn)
   if not user_info:
     LOG.warn("Could not get LDAP details for users with pattern %s" % username_pattern)
     return None
 
-  return _import_ldap_users_info(user_info, sync_groups, import_by_dn)
+  return _import_ldap_users_info(connection, user_info, sync_groups, import_by_dn)
 
 
-def _import_ldap_users_info(user_info, sync_groups=False, import_by_dn=False):
+def _import_ldap_users_info(connection, user_info, sync_groups=False, import_by_dn=False):
   """
   Import user_info found through ldap_access.find_users.
   """
@@ -543,16 +550,15 @@ def _import_ldap_users_info(user_info, sync_groups=False, import_by_dn=False):
 
     # sync groups
     if sync_groups and 'groups' in ldap_info:
-      conn = ldap_access.get_connection()
       old_groups = set(user.groups.all())
       new_groups = set()
       # Skip if 'memberOf' or 'isMemberOf' are not set
       for group_dn in ldap_info['groups']:
-        group_ldap_info = conn.find_groups(group_dn, find_by_dn=True, scope=ldap.SCOPE_BASE)
+        group_ldap_info = connection.find_groups(group_dn, find_by_dn=True, scope=ldap.SCOPE_BASE)
         for group_info in group_ldap_info:
           # Add only if user isn't part of group.
           if not user.groups.filter(name=group_info['name']).exists():
-            groups = import_ldap_groups(group_info['dn'], import_members=False, import_members_recursive=False, sync_users=False, import_by_dn=True)
+            groups = import_ldap_groups(connection, group_info['dn'], import_members=False, import_members_recursive=False, sync_users=False, import_by_dn=True)
             if groups:
               new_groups.update(groups)
 
@@ -568,48 +574,48 @@ def _import_ldap_users_info(user_info, sync_groups=False, import_by_dn=False):
   return imported_users
 
 
-def _import_ldap_members(conn, group, ldap_info, count=0, max_count=1):
+def _import_ldap_members(connection, group, ldap_info, count=0, max_count=1):
   if count >= max_count:
     return None
 
   # Find all users and groups of group.
-  users_info = conn.find_users_of_group(ldap_info['dn'])
-  groups_info = conn.find_groups_of_group(ldap_info['dn'])
+  users_info = connection.find_users_of_group(ldap_info['dn'])
+  groups_info = connection.find_groups_of_group(ldap_info['dn'])
   posix_members = ldap_info['posix_members']
 
   for user_info in users_info:
     LOG.debug("Importing user %s into group %s" % (smart_str(user_info['dn']), smart_str(group.name)))
-    users = _import_ldap_users(smart_str(user_info['dn']), import_by_dn=True)
+    users = _import_ldap_users(connection, smart_str(user_info['dn']), import_by_dn=True)
     group.user_set.add(*users)
 
   for group_info in groups_info:
     LOG.debug("Importing group %s" % smart_str(group_info['dn']))
-    groups = _import_ldap_groups(smart_str(group_info['dn']), import_by_dn=True)
+    groups = _import_ldap_groups(connection, smart_str(group_info['dn']), import_by_dn=True)
 
     # Must find all members of subgroups
     if len(groups) > 1:
       LOG.warn('Found multiple groups for member %s.' % smart_str(group_info['dn']))
     else:
       for group in groups:
-        _import_ldap_members(conn, group, group_info, count+1, max_count)
+        _import_ldap_members(connection, group, group_info, count+1, max_count)
 
   for posix_member in posix_members:
     LOG.debug("Importing posix user %s into group %s" % (smart_str(posix_member), smart_str(group.name)))
-    user_info = conn.find_users(posix_member, search_attr='uid', user_name_attr=desktop.conf.LDAP.USERS.USER_NAME_ATTR.get(), find_by_dn=False)
-    users = _import_ldap_users_info(user_info)
+    user_info = connection.find_users(posix_member, search_attr='uid', user_name_attr=desktop.conf.LDAP.USERS.USER_NAME_ATTR.get(), find_by_dn=False)
+    users = _import_ldap_users_info(connection, user_info)
 
     if users:
       LOG.debug("Adding member %s represented as users (should be a single user) %s to group %s" % (str(posix_member), str(users), str(group.name)))
       group.user_set.add(*users)
 
 
-def _sync_ldap_members(conn, group, ldap_info, count=0, max_count=1):
+def _sync_ldap_members(connection, group, ldap_info, count=0, max_count=1):
   if count >= max_count:
     return None
 
   # Find all users and groups of group.
-  users_info = conn.find_users_of_group(ldap_info['dn'])
-  groups_info = conn.find_groups_of_group(ldap_info['dn'])
+  users_info = connection.find_users_of_group(ldap_info['dn'])
+  groups_info = connection.find_groups_of_group(ldap_info['dn'])
   posix_members = ldap_info['posix_members']
 
   for user_info in users_info:
@@ -625,13 +631,13 @@ def _sync_ldap_members(conn, group, ldap_info, count=0, max_count=1):
 
     try:
       group = Group.objects.get(name=group_info['name'])
-      _sync_ldap_members(conn, group, group_info, count+1, max_count)
+      _sync_ldap_members(connection, group, group_info, count+1, max_count)
     except Group.DoesNotExist:
       LOG.debug("Synchronizing group %s failed. Group does not exist." % smart_str(group.name))
 
   for posix_member in posix_members:
     LOG.debug("Synchronizing posix user %s with group %s" % (smart_str(posix_member), smart_str(group.name)))
-    users_info = conn.find_users(posix_member, search_attr='uid', user_name_attr=desktop.conf.LDAP.USERS.USER_NAME_ATTR.get(), find_by_dn=False)
+    users_info = connection.find_users(posix_member, search_attr='uid', user_name_attr=desktop.conf.LDAP.USERS.USER_NAME_ATTR.get(), find_by_dn=False)
     for user_info in users_info:
       try:
         user = ldap_access.get_ldap_user(username=user_info['username'])
@@ -640,7 +646,7 @@ def _sync_ldap_members(conn, group, ldap_info, count=0, max_count=1):
         LOG.debug("Synchronizing posix user %s with group %s failed. User does not exist." % (smart_str(posix_member), smart_str(group.name)))
 
 
-def _import_ldap_nested_groups(groupname_pattern, import_members=False, recursive_import_members=False, sync_users=True, import_by_dn=False):
+def _import_ldap_nested_groups(connection, groupname_pattern, import_members=False, recursive_import_members=False, sync_users=True, import_by_dn=False):
   """
   Import a group from LDAP. If import_members is true, this will also import any
   LDAP users that exist within the group. This will use nested groups logic.
@@ -648,12 +654,11 @@ def _import_ldap_nested_groups(groupname_pattern, import_members=False, recursiv
   e.g. CN=subtest,OU=groups,DC=exampe,DC=COM is a member of CN=test,OU=groups,DC=exampe,DC=COM
   and they both of the object class "groupOfNames" (or some other object class for groups).
   """
-  conn = ldap_access.get_connection()
   if import_by_dn:
     scope = ldap.SCOPE_BASE
   else:
     scope = ldap.SCOPE_SUBTREE
-  group_info = conn.find_groups(groupname_pattern, find_by_dn=import_by_dn, scope=scope)
+  group_info = connection.find_groups(groupname_pattern, find_by_dn=import_by_dn, scope=scope)
 
   if not group_info:
     LOG.warn("Could not get LDAP details for group pattern %s" % groupname_pattern)
@@ -678,11 +683,11 @@ def _import_ldap_nested_groups(groupname_pattern, import_members=False, recursiv
     max_count = recursive_import_members and desktop.conf.LDAP.NESTED_MEMBERS_SEARCH_DEPTH.get() or 1
 
     if import_members:
-      _import_ldap_members(conn, group, ldap_info, max_count=max_count)
+      _import_ldap_members(connection, group, ldap_info, max_count=max_count)
 
     # Sync users
     if sync_users:
-      _sync_ldap_members(conn, group, ldap_info, max_count=max_count)
+      _sync_ldap_members(connection, group, ldap_info, max_count=max_count)
 
     group.save()
     groups.append(group)
@@ -690,7 +695,7 @@ def _import_ldap_nested_groups(groupname_pattern, import_members=False, recursiv
   return groups
 
 
-def _import_ldap_suboordinate_groups(groupname_pattern, import_members=False, recursive_import_members=False, sync_users=True, import_by_dn=False):
+def _import_ldap_suboordinate_groups(connection, groupname_pattern, import_members=False, recursive_import_members=False, sync_users=True, import_by_dn=False):
   """
   Import a group from LDAP. If import_members is true, this will also import any
   LDAP users that exist within the group. This will use suboordinate group logic.
@@ -698,12 +703,11 @@ def _import_ldap_suboordinate_groups(groupname_pattern, import_members=False, re
   e.g. CN=subtest,CN=test,OU=groups,DC=exampe,DC=COM is a suboordinate group of
   CN=test,OU=groups,DC=exampe,DC=COM
   """
-  conn = ldap_access.get_connection()
   if import_by_dn:
     scope = ldap.SCOPE_BASE
   else:
     scope = ldap.SCOPE_SUBTREE
-  group_info = conn.find_groups(groupname_pattern, find_by_dn=import_by_dn, scope=scope)
+  group_info = connection.find_groups(groupname_pattern, find_by_dn=import_by_dn, scope=scope)
 
   if not group_info:
     LOG.warn("Could not get LDAP details for group pattern %s" % groupname_pattern)
@@ -730,18 +734,18 @@ def _import_ldap_suboordinate_groups(groupname_pattern, import_members=False, re
     # @TODO: Deprecate recursive_import_members as it may not be useful.
     if import_members:
       if recursive_import_members:
-        for sub_ldap_info in conn.find_groups(ldap_info['dn'], find_by_dn=True):
+        for sub_ldap_info in connection.find_groups(ldap_info['dn'], find_by_dn=True):
           members += sub_ldap_info['members']
           posix_members += sub_ldap_info['posix_members']
 
       for member in members:
         LOG.debug("Importing user %s" % smart_str(member))
-        group.user_set.add( *( _import_ldap_users(member, import_by_dn=True) or [] ) )
+        group.user_set.add( *( _import_ldap_users(connection, member, import_by_dn=True) or [] ) )
 
     # Sync users
     if sync_users:
       for member in members:
-        user_info = conn.find_users(member, find_by_dn=True)
+        user_info = connection.find_users(member, find_by_dn=True)
         if len(user_info) > 1:
           LOG.warn('Found multiple users for member %s.' % member)
         else:
@@ -760,8 +764,8 @@ def _import_ldap_suboordinate_groups(groupname_pattern, import_members=False, re
           LOG.debug("Importing user %s" % str(posix_member))
           # posixGroup class defines 'memberUid' to be login names,
           # which are defined by 'uid'.
-          user_info = conn.find_users(posix_member, search_attr='uid', user_name_attr=desktop.conf.LDAP.USERS.USER_NAME_ATTR.get(), find_by_dn=False)
-          users = _import_ldap_users_info(user_info, import_by_dn=False)
+          user_info = connection.find_users(posix_member, search_attr='uid', user_name_attr=desktop.conf.LDAP.USERS.USER_NAME_ATTR.get(), find_by_dn=False)
+          users = _import_ldap_users_info(connection, user_info, import_by_dn=False)
 
           if users:
             LOG.debug("Adding member %s represented as users (should be a single user) %s to group %s" % (str(posix_member), str(users), str(group.name)))
@@ -769,7 +773,7 @@ def _import_ldap_suboordinate_groups(groupname_pattern, import_members=False, re
 
       if sync_users:
         for posix_member in posix_members:
-          user_info = conn.find_users(posix_member, search_attr='uid', user_name_attr=desktop.conf.LDAP.USERS.USER_NAME_ATTR.get(), find_by_dn=False)
+          user_info = connection.find_users(posix_member, search_attr='uid', user_name_attr=desktop.conf.LDAP.USERS.USER_NAME_ATTR.get(), find_by_dn=False)
           if len(user_info) > 1:
             LOG.warn('Found multiple users for member %s.' % posix_member)
           else:
@@ -786,34 +790,36 @@ def _import_ldap_suboordinate_groups(groupname_pattern, import_members=False, re
   return groups
 
 
-def _import_ldap_groups(groupname_pattern, import_members=False, recursive_import_members=False, sync_users=True, import_by_dn=False):
+def _import_ldap_groups(connection, groupname_pattern, import_members=False, recursive_import_members=False, sync_users=True, import_by_dn=False):
   """
   Import a group from LDAP. If import_members is true, this will also import any
   LDAP users that exist within the group.
   """
   if desktop.conf.LDAP.SUBGROUPS.get().lower() == 'suboordinate':
-    return _import_ldap_suboordinate_groups(groupname_pattern=groupname_pattern,
+    return _import_ldap_suboordinate_groups(connection=connection,
+                                            groupname_pattern=groupname_pattern,
                                             import_members=import_members,
                                             recursive_import_members=recursive_import_members,
                                             sync_users=sync_users,
                                             import_by_dn=import_by_dn)
   else:
-    return _import_ldap_nested_groups(groupname_pattern=groupname_pattern,
+    return _import_ldap_nested_groups(connection=connection,
+                                      groupname_pattern=groupname_pattern,
                                       import_members=import_members,
                                       recursive_import_members=recursive_import_members,
                                       sync_users=sync_users,
                                       import_by_dn=import_by_dn)
 
 
-def import_ldap_users(user_pattern, sync_groups, import_by_dn):
-  return _import_ldap_users(user_pattern, sync_groups=sync_groups, import_by_dn=import_by_dn)
+def import_ldap_users(connection, user_pattern, sync_groups, import_by_dn):
+  return _import_ldap_users(connection, user_pattern, sync_groups=sync_groups, import_by_dn=import_by_dn)
 
 
-def import_ldap_groups(group_pattern, import_members, import_members_recursive, sync_users, import_by_dn):
-  return _import_ldap_groups(group_pattern, import_members, import_members_recursive, sync_users, import_by_dn)
+def import_ldap_groups(connection, group_pattern, import_members, import_members_recursive, sync_users, import_by_dn):
+  return _import_ldap_groups(connection, group_pattern, import_members, import_members_recursive, sync_users, import_by_dn)
 
 
-def sync_ldap_users():
+def sync_ldap_users(connection):
   """
   Syncs LDAP user information. This will not import new
   users from LDAP. It is also not possible to import both a user and a
@@ -822,11 +828,11 @@ def sync_ldap_users():
   """
   users = User.objects.filter(userprofile__creation_method=str(UserProfile.CreationMethod.EXTERNAL)).all()
   for user in users:
-    _import_ldap_users(user.username)
+    _import_ldap_users(connection, user.username)
   return users
 
 
-def sync_ldap_groups():
+def sync_ldap_groups(connection):
   """
   Syncs LDAP group memberships. This will not import new
   groups from LDAP. It is also not possible to import both a user and a
@@ -835,5 +841,5 @@ def sync_ldap_groups():
   """
   groups = Group.objects.filter(group__in=LdapGroup.objects.all())
   for group in groups:
-    _import_ldap_groups(group.name)
+    _import_ldap_groups(connection, group.name)
   return groups

+ 47 - 4
desktop/conf.dist/hue.ini

@@ -162,9 +162,6 @@
     # The search base for finding users and groups
     ## base_dn="DC=mycompany,DC=com"
 
-    # The NT domain to connect to (only for use with Active Directory)
-    ## nt_domain=mycompany.com
-
     # URL of the LDAP server
     ## ldap_url=ldap://auth.mycompany.com
 
@@ -215,7 +212,7 @@
       # The username attribute in the LDAP schema
       ## user_name_attr=sAMAccountName
 
-      [[[groups]]]
+    [[[groups]]]
 
       # Base filter for searching for groups
       ## group_filter="objectclass=*"
@@ -225,6 +222,52 @@
 
       # The attribute of the group object which identifies the members of the group
       ## group_member_attr=members
+    
+    [[[ldap_servers]]]
+
+      ## [[[[mycompany]]]]
+
+        # The search base for finding users and groups
+        ## base_dn="DC=mycompany,DC=com"
+
+        # URL of the LDAP server
+        ## ldap_url=ldap://auth.mycompany.com
+
+        # A PEM-format file containing certificates for the CA's that
+        # Hue will trust for authentication over TLS.
+        # The certificate for the CA that signed the
+        # LDAP server certificate must be included among these certificates.
+        # See more here http://www.openldap.org/doc/admin24/tls.html.
+        ## ldap_cert=
+        ## use_start_tls=true
+
+        # Distinguished name of the user to bind as -- not necessary if the LDAP server
+        # supports anonymous searches
+        ## bind_dn="CN=ServiceAccount,DC=mycompany,DC=com"
+
+        # Password of the bind user -- not necessary if the LDAP server supports
+        # anonymous searches
+        ## bind_password=
+
+        # Pattern for searching for usernames -- Use <username> for the parameter
+        # For use when using LdapBackend for Hue authentication
+        ## ldap_username_pattern="uid=<username>,ou=People,dc=mycompany,dc=com"
+
+        ## [[[[[users]]]]]
+
+          # Base filter for searching for users
+          ## user_filter="objectclass=Person"
+
+          # The username attribute in the LDAP schema
+          ## user_name_attr=sAMAccountName
+
+        ## [[[[[groups]]]]]
+
+          # Base filter for searching for groups
+          ## group_filter="objectclass=groupOfNames"
+
+          # The username attribute in the LDAP schema
+          ## group_name_attr=cn
 
   # Configuration options for specifying the Desktop Database.  For more info,
   # see http://docs.djangoproject.com/en/1.1/ref/settings/#database-engine

+ 47 - 4
desktop/conf/pseudo-distributed.ini.tmpl

@@ -167,9 +167,6 @@
     # The search base for finding users and groups
     ## base_dn="DC=mycompany,DC=com"
 
-    # The NT domain to connect to (only for use with Active Directory)
-    ## nt_domain=mycompany.com
-
     # URL of the LDAP server
     ## ldap_url=ldap://auth.mycompany.com
 
@@ -223,7 +220,7 @@
       # The username attribute in the LDAP schema
       ## user_name_attr=sAMAccountName
 
-      [[[groups]]]
+    [[[groups]]]
 
       # Base filter for searching for groups
       ## group_filter="objectclass=*"
@@ -231,6 +228,52 @@
       # The username attribute in the LDAP schema
       ## group_name_attr=cn
 
+    [[[ldap_servers]]]
+
+      ## [[[[mycompany]]]]
+
+        # The search base for finding users and groups
+        ## base_dn="DC=mycompany,DC=com"
+
+        # URL of the LDAP server
+        ## ldap_url=ldap://auth.mycompany.com
+
+        # A PEM-format file containing certificates for the CA's that
+        # Hue will trust for authentication over TLS.
+        # The certificate for the CA that signed the
+        # LDAP server certificate must be included among these certificates.
+        # See more here http://www.openldap.org/doc/admin24/tls.html.
+        ## ldap_cert=
+        ## use_start_tls=true
+
+        # Distinguished name of the user to bind as -- not necessary if the LDAP server
+        # supports anonymous searches
+        ## bind_dn="CN=ServiceAccount,DC=mycompany,DC=com"
+
+        # Password of the bind user -- not necessary if the LDAP server supports
+        # anonymous searches
+        ## bind_password=
+
+        # Pattern for searching for usernames -- Use <username> for the parameter
+        # For use when using LdapBackend for Hue authentication
+        ## ldap_username_pattern="uid=<username>,ou=People,dc=mycompany,dc=com"
+
+        ## [[[[[users]]]]]
+
+          # Base filter for searching for users
+          ## user_filter="objectclass=Person"
+
+          # The username attribute in the LDAP schema
+          ## user_name_attr=sAMAccountName
+
+        ## [[[[[groups]]]]]
+
+          # Base filter for searching for groups
+          ## group_filter="objectclass=groupOfNames"
+
+          # The username attribute in the LDAP schema
+          ## group_name_attr=cn
+
   # Configuration options for specifying the Desktop Database.  For more info,
   # see http://docs.djangoproject.com/en/1.1/ref/settings/#database-engine
   # ------------------------------------------------------------------------

+ 37 - 25
desktop/core/src/desktop/auth/backend.py

@@ -40,7 +40,7 @@ from useradmin.views import import_ldap_users
 from useradmin import ldap_access
 
 import pam
-from django_auth_ldap.backend import LDAPBackend, ldap_settings
+from django_auth_ldap.backend import LDAPBackend
 import ldap
 from django_auth_ldap.config import LDAPSearch
 
@@ -323,24 +323,26 @@ class LdapBackend(object):
 
     self._backend = _LDAPBackend()
 
-    ldap_settings.AUTH_LDAP_SERVER_URI = desktop.conf.LDAP.LDAP_URL.get()
-    if ldap_settings.AUTH_LDAP_SERVER_URI is None:
+  def add_ldap_config(self, ldap_config):
+    if ldap_config.LDAP_URL.get() is None:
       LOG.warn("Could not find LDAP URL required for authentication.")
       return None
+    else:
+      setattr(self._backend.settings, 'SERVER_URI', ldap_config.LDAP_URL.get())
 
     if desktop.conf.LDAP.SEARCH_BIND_AUTHENTICATION.get():
       # New Search/Bind Auth
-      base_dn = desktop.conf.LDAP.BASE_DN.get()
-      user_name_attr = desktop.conf.LDAP.USERS.USER_NAME_ATTR.get()
-      user_filter = desktop.conf.LDAP.USERS.USER_FILTER.get()
+      base_dn = ldap_config.BASE_DN.get()
+      user_name_attr = ldap_config.USERS.USER_NAME_ATTR.get()
+      user_filter = ldap_config.USERS.USER_FILTER.get()
       if not user_filter.startswith('('):
         user_filter = '(' + user_filter + ')'
 
-      if desktop.conf.LDAP.BIND_DN.get():
-        bind_dn = desktop.conf.LDAP.BIND_DN.get()
-        ldap_settings.AUTH_LDAP_BIND_DN = bind_dn
-        bind_password = desktop.conf.LDAP.BIND_PASSWORD.get()
-        ldap_settings.AUTH_LDAP_BIND_PASSWORD = bind_password
+      if ldap_config.BIND_DN.get():
+        bind_dn = ldap_config.BIND_DN.get()
+        setattr(self._backend.settings, 'BIND_DN', bind_dn)
+        bind_password = ldap_config.BIND_PASSWORD.get()
+        setattr(self._backend.settings, 'BIND_PASSWORD', bind_password)
 
       if user_filter is None:
         search_bind_results = LDAPSearch(base_dn,
@@ -350,27 +352,37 @@ class LdapBackend(object):
         search_bind_results = LDAPSearch(base_dn,
             ldap.SCOPE_SUBTREE, "(&(" + user_name_attr + "=%(user)s)" + user_filter + ")")
 
-      ldap_settings.AUTH_LDAP_USER_SEARCH = search_bind_results
+      setattr(self._backend.settings, 'USER_SEARCH', search_bind_results)
     else:
-      nt_domain = desktop.conf.LDAP.NT_DOMAIN.get()
+      nt_domain = ldap_config.NT_DOMAIN.get()
       if nt_domain is None:
-        pattern = desktop.conf.LDAP.LDAP_USERNAME_PATTERN.get()
+        pattern = ldap_config.LDAP_USERNAME_PATTERN.get()
         pattern = pattern.replace('<username>', '%(user)s')
-        ldap_settings.AUTH_LDAP_USER_DN_TEMPLATE = pattern
+        setattr(self._backend.settings, 'USER_DN_TEMPLATE', pattern)
       else:
         # %(user)s is a special string that will get replaced during the authentication process
-        ldap_settings.AUTH_LDAP_USER_DN_TEMPLATE = "%(user)s@" + nt_domain
+        setattr(self._backend.settings, 'USER_DN_TEMPLATE', "%(user)s@" + nt_domain)
 
     # Certificate-related config settings
-    if desktop.conf.LDAP.LDAP_CERT.get():
-      ldap_settings.AUTH_LDAP_START_TLS = desktop.conf.LDAP.USE_START_TLS.get()
+    if ldap_config.LDAP_CERT.get():
+      setattr(self._backend.settings, 'START_TLS', ldap_config.USE_START_TLS.get())
       ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_ALLOW)
-      ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, desktop.conf.LDAP.LDAP_CERT.get())
+      ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, ldap_config.LDAP_CERT.get())
     else:
-      ldap_settings.AUTH_LDAP_START_TLS = False
+      setattr(self._backend.settings, 'START_TLS', False)
       ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
 
-  def authenticate(self, username=None, password=None):
+  def add_ldap_config_for_server(self, server):
+    if desktop.conf.LDAP.LDAP_SERVERS.get():
+      # Choose from multiple server configs
+      if server in desktop.conf.LDAP.LDAP_SERVERS.get():
+        self.add_ldap_config(desktop.conf.LDAP.LDAP_SERVERS.get()[server])
+    else:
+      self.add_ldap_config(desktop.conf.LDAP)
+
+  def authenticate(self, username=None, password=None, server=None):
+    self.add_ldap_config_for_server(server)
+
     username_filter_kwargs = ldap_access.get_ldap_user_kwargs(username)
 
     # Do this check up here, because the auth call creates a django user upon first login per user
@@ -409,8 +421,7 @@ class LdapBackend(object):
         user.save()
 
       if desktop.conf.LDAP.SYNC_GROUPS_ON_LOGIN.get():
-        self.import_groups(user)
-      return user
+        self.import_groups(server, user)
 
     return user
 
@@ -419,8 +430,9 @@ class LdapBackend(object):
     user = rewrite_user(user)
     return user
 
-  def import_groups(self, user):
-    import_ldap_users(user.username, sync_groups=True, import_by_dn=False)
+  def import_groups(self, server, user):
+    connection = ldap_access.get_connection_from_server(server)
+    import_ldap_users(connection, user.username, sync_groups=True, import_by_dn=False)
 
   @classmethod
   def manages_passwords_externally(cls):

+ 46 - 2
desktop/core/src/desktop/auth/forms.py

@@ -18,15 +18,23 @@
 import datetime
 
 from django.conf import settings
+from django.contrib.auth import authenticate
 from django.contrib.auth.models import User
 from django.contrib.auth.forms import AuthenticationForm as AuthAuthenticationForm, UserCreationForm as AuthUserCreationForm
-from django.forms import CharField, TextInput, PasswordInput, ValidationError
+from django.forms import CharField, TextInput, PasswordInput, ChoiceField, ValidationError
 from django.utils.safestring import mark_safe
 from django.utils.translation import ugettext_lazy as _t, ugettext as _
 
 from desktop import conf
 
 
+def get_server_choices():
+  if conf.LDAP.LDAP_SERVERS.get():
+    return [(ldap_server_record_key, ldap_server_record_key) for ldap_server_record_key in conf.LDAP.LDAP_SERVERS.get()]
+  else:
+    return [('LDAP', 'LDAP')]
+
+
 class AuthenticationForm(AuthAuthenticationForm):
   """
   Adds appropriate classes to authentication form
@@ -39,6 +47,9 @@ class AuthenticationForm(AuthAuthenticationForm):
   username = CharField(label=_t("Username"), max_length=30, widget=TextInput(attrs={'maxlength': 30, 'placeholder': _t("Username"), "autofocus": "autofocus"}))
   password = CharField(label=_t("Password"), widget=PasswordInput(attrs={'placeholder': _t("Password")}))
 
+  def authenticate(self):
+    return super(AuthenticationForm, self).clean()
+
   def clean(self):
     if conf.AUTH.EXPIRES_AFTER.get() > -1:
       try:
@@ -64,7 +75,34 @@ class AuthenticationForm(AuthAuthenticationForm):
         # This means the user managed to get their username wrong.
         pass
 
-    return super(AuthenticationForm, self).clean()
+    return self.authenticate()
+
+
+class LdapAuthenticationForm(AuthenticationForm):
+  """
+  Adds NT_DOMAINS selector.
+  """
+  
+  def __init__(self, *args, **kwargs):
+    super(LdapAuthenticationForm, self).__init__(*args, **kwargs)
+    self.fields['server'] = ChoiceField(choices=get_server_choices())
+
+  def authenticate(self):
+    username = self.cleaned_data.get('username')
+    password = self.cleaned_data.get('password')
+    server = self.cleaned_data.get('server')
+
+    if username and password:
+      self.user_cache = authenticate(username=username,
+                                     password=password,
+                                     server=server)
+      if self.user_cache is None:
+        raise ValidationError(
+          self.error_messages['invalid_login'])
+      elif not self.user_cache.is_active:
+        raise ValidationError(self.error_messages['inactive'])
+    self.check_for_test_cookie()
+    return self.cleaned_data
 
 
 class UserCreationForm(AuthUserCreationForm):
@@ -81,3 +119,9 @@ class UserCreationForm(AuthUserCreationForm):
       data['password1'] = data['password']
       data['password2'] = data['password']
     super(UserCreationForm, self).__init__(data=data, *args, **kwargs)
+
+
+class LdapUserCreationForm(UserCreationForm):
+  def __init__(self, *args, **kwargs):
+    super(LdapUserCreationForm, self).__init__(*args, **kwargs)
+    self.fields['server'] = ChoiceField(choices=get_server_choices())

+ 12 - 3
desktop/core/src/desktop/auth/views.py

@@ -37,11 +37,11 @@ from django.utils.translation import ugettext as _
 from hadoop.fs.exceptions import WebHdfsException
 from useradmin.views import ensure_home_directory
 
-from desktop.auth.forms import UserCreationForm, AuthenticationForm
+from desktop.auth import forms as auth_forms
 from desktop.lib.django_util import render
 from desktop.lib.django_util import login_notrequired
 from desktop.log.access import access_warn, last_access_map
-from desktop.conf import AUTH, OAUTH, DEMO_ENABLED
+from desktop.conf import AUTH, LDAP, OAUTH, DEMO_ENABLED
 
 
 LOG = logging.getLogger(__name__)
@@ -85,6 +85,14 @@ def dt_login(request):
   redirect_to = request.REQUEST.get('next', '/')
   is_first_login_ever = first_login_ever()
   backend_name = get_backend_name()
+  is_active_directory = backend_name == 'LdapBackend' and ( bool(LDAP.NT_DOMAIN.get()) or bool(LDAP.LDAP_SERVERS.get()) )
+
+  if is_active_directory:
+    UserCreationForm = auth_forms.LdapUserCreationForm
+    AuthenticationForm = auth_forms.LdapAuthenticationForm
+  else:
+    UserCreationForm = auth_forms.UserCreationForm
+    AuthenticationForm = auth_forms.AuthenticationForm
 
   if request.method == 'POST':
     # For first login, need to validate user info!
@@ -133,7 +141,8 @@ def dt_login(request):
     'next': redirect_to,
     'first_login_ever': is_first_login_ever,
     'login_errors': request.method == 'POST',
-    'backend_name': backend_name
+    'backend_name': backend_name,
+    'active_directory': is_active_directory
   })
 
 

+ 6 - 2
desktop/core/src/desktop/auth/views_test.py

@@ -104,6 +104,8 @@ class TestLdapLogin(PseudoHdfsTestBase):
     User.objects.all().delete()
     self.c = Client()
 
+    self.reset.append(conf.LDAP.LDAP_URL.set_for_testing('does not matter'))
+
   def test_login(self):
     response = self.c.get('/accounts/login/')
     assert_equal(200, response.status_code, "Expected ok status.")
@@ -211,7 +213,7 @@ class TestLdapLogin(PseudoHdfsTestBase):
       'username': "curly",
       'password': "ldap1"
     })
-    assert_equal(302, response.status_code, "Expected ok redirect status.")
+    assert_equal(302, response.status_code, response.status_code)
     assert_equal(1, len(User.objects.all()))
     # The two curly are a part of in LDAP and the default group.
     assert_equal(3, User.objects.all()[0].groups.all().count(), User.objects.all()[0].groups.all())
@@ -398,10 +400,12 @@ class TestLogin(object):
 
 
 class MockLdapBackend(object):
+  settings = django_auth_ldap_backend.LDAPSettings()
+
   def get_or_create_user(self, username, ldap_user):
     return User.objects.get_or_create(username)
 
-  def authenticate(self, username=None, password=None):
+  def authenticate(self, username=None, password=None, server=None):
     user, created = self.get_or_create_user(username, None)
     return user
 

+ 135 - 59
desktop/core/src/desktop/conf.py

@@ -421,36 +421,6 @@ LDAP = ConfigSection(
   key="ldap",
   help=_("Configuration options for LDAP connectivity."),
   members=dict(
-    BASE_DN=Config("base_dn",
-                   default=None,
-                   help=_("The base LDAP distinguished name to use for LDAP search.")),
-    NT_DOMAIN=Config("nt_domain",
-                     default=None,
-                     help=_("The NT domain used for LDAP authentication.")),
-    LDAP_URL=Config("ldap_url",
-                     default=None,
-                     help=_("The LDAP URL to connect to.")),
-    USE_START_TLS=Config("use_start_tls",
-                         default=True,
-                         type=coerce_bool,
-                         help=_("Use StartTLS when communicating with LDAP server.")),
-    LDAP_CERT=Config("ldap_cert",
-                     default=None,
-                     help=_("A PEM-format file containing certificates for the CA's that Hue will trust for authentication over TLS. The certificate for the CA that signed the LDAP server certificate must be included among these certificates. See more here http://www.openldap.org/doc/admin24/tls.html.")),
-    LDAP_USERNAME_PATTERN=Config("ldap_username_pattern",
-                                 default=None,
-                                 help=_("A pattern to use for constructing LDAP usernames.")),
-    BIND_DN=Config("bind_dn",
-                   default=None,
-                   help=_("The distinguished name to bind as, when importing from LDAP.")),
-    BIND_PASSWORD=Config("bind_password",
-                   default=None,
-                   private=True,
-                   help=_("The password for the bind user.")),
-    SEARCH_BIND_AUTHENTICATION=Config("search_bind_authentication",
-                   default=True,
-                   type=coerce_bool,
-                   help=_("Use search bind authentication.")),
     CREATE_USERS_ON_LOGIN = Config("create_users_on_login",
       help=_("Create users when they login with their LDAP credentials."),
       type=coerce_bool,
@@ -468,7 +438,6 @@ LDAP = ConfigSection(
       type=coerce_bool,
       private=True,
       default=False),
-
     SUBGROUPS = Config("subgroups",
       help=_("Choose which kind of subgrouping to use: nested or suboordinate (deprecated)."),
       type=coerce_str_lowercase,
@@ -510,7 +479,102 @@ LDAP = ConfigSection(
                                       "members of a group.")),
       )
     ),
-))
+
+    LDAP_SERVERS = UnspecifiedConfigSection(
+      key="ldap_servers",
+      help=_("LDAP server record."),
+      each=ConfigSection(
+        members=dict(
+          BASE_DN=Config("base_dn",
+                         default=None,
+                         help=_("The base LDAP distinguished name to use for LDAP search.")),
+          NT_DOMAIN=Config("nt_domain",
+                           default=None,
+                           help=_("The NT domain used for LDAP authentication.")),
+          LDAP_URL=Config("ldap_url",
+                           default=None,
+                           help=_("The LDAP URL to connect to.")),
+          USE_START_TLS=Config("use_start_tls",
+                               default=True,
+                               type=coerce_bool,
+                               help=_("Use StartTLS when communicating with LDAP server.")),
+          LDAP_CERT=Config("ldap_cert",
+                           default=None,
+                           help=_("A PEM-format file containing certificates for the CA's that Hue will trust for authentication over TLS. The certificate for the CA that signed the LDAP server certificate must be included among these certificates. See more here http://www.openldap.org/doc/admin24/tls.html.")),
+          LDAP_USERNAME_PATTERN=Config("ldap_username_pattern",
+                                       default=None,
+                                       help=_("A pattern to use for constructing LDAP usernames.")),
+          BIND_DN=Config("bind_dn",
+                         default=None,
+                         help=_("The distinguished name to bind as, when importing from LDAP.")),
+          BIND_PASSWORD=Config("bind_password",
+                         default=None,
+                         private=True,
+                         help=_("The password for the bind user.")),
+
+          USERS = ConfigSection(
+            key="users",
+            help=_("Configuration for LDAP user schema and search."),
+            members=dict(
+              USER_FILTER=Config("user_filter",
+                                 default="objectclass=*",
+                                 help=_("A base filter for use when searching for users.")),
+              USER_NAME_ATTR=Config("user_name_attr",
+                                    default="sAMAccountName",
+                                    help=_("The username attribute in the LDAP schema. "
+                                         "Typically, this is 'sAMAccountName' for AD and 'uid' "
+                                         "for other LDAP systems.")),
+            )
+          ),
+
+          GROUPS = ConfigSection(
+            key="groups",
+            help=_("Configuration for LDAP group schema and search."),
+            members=dict(
+              GROUP_FILTER=Config("group_filter",
+                                 default="objectclass=*",
+                                 help=_("A base filter for use when searching for groups.")),
+              GROUP_NAME_ATTR=Config("group_name_attr",
+                                    default="cn",
+                                    help=_("The group name attribute in the LDAP schema. "
+                                        "Typically, this is 'cn'.")),
+              GROUP_MEMBER_ATTR=Config("group_member_attr",
+                                       default="member",
+                                       help=_("The LDAP attribute which specifies the "
+                                            "members of a group.")),
+            ))))),
+
+    # Every thing below here is deprecated and should be removed in an upcoming major release.
+    BASE_DN=Config("base_dn",
+                   default=None,
+                   help=_("The base LDAP distinguished name to use for LDAP search.")),
+    NT_DOMAIN=Config("nt_domain",
+                     default=None,
+                     help=_("The NT domain used for LDAP authentication.")),
+    LDAP_URL=Config("ldap_url",
+                     default=None,
+                     help=_("The LDAP URL to connect to.")),
+    USE_START_TLS=Config("use_start_tls",
+                         default=True,
+                         type=coerce_bool,
+                         help=_("Use StartTLS when communicating with LDAP server.")),
+    LDAP_CERT=Config("ldap_cert",
+                     default=None,
+                     help=_("A PEM-format file containing certificates for the CA's that Hue will trust for authentication over TLS. The certificate for the CA that signed the LDAP server certificate must be included among these certificates. See more here http://www.openldap.org/doc/admin24/tls.html.")),
+    LDAP_USERNAME_PATTERN=Config("ldap_username_pattern",
+                                 default=None,
+                                 help=_("A pattern to use for constructing LDAP usernames.")),
+    BIND_DN=Config("bind_dn",
+                   default=None,
+                   help=_("The distinguished name to bind as, when importing from LDAP.")),
+    BIND_PASSWORD=Config("bind_password",
+                   default=None,
+                   private=True,
+                   help=_("The password for the bind user.")),
+    SEARCH_BIND_AUTHENTICATION=Config("search_bind_authentication",
+                   default=True,
+                   type=coerce_bool,
+                   help=_("Use search bind authentication."))))
 
 
 OAUTH = ConfigSection(
@@ -645,6 +709,40 @@ DJANGO_EMAIL_BACKEND = Config(
 )
 
 
+def validate_ldap(user, config):
+  res = []
+
+  if config.SEARCH_BIND_AUTHENTICATION.get():
+    if config.LDAP_URL.get() is not None and bool(config.BIND_DN.get()) != bool(config.BIND_PASSWORD.get()):
+      if config.BIND_DN.get() == None:
+        res.append((LDAP.BIND_DN,
+                  unicode(_("If you set bind_password, then you must set bind_dn."))))
+      else:
+        res.append((LDAP.BIND_PASSWORD,
+                    unicode(_("If you set bind_dn, then you must set bind_password."))))
+  else:
+    if config.NT_DOMAIN.get() is not None or \
+        config.LDAP_USERNAME_PATTERN.get() is not None:
+      if config.LDAP_URL.get() is None:
+        res.append((config.LDAP_URL,
+                    unicode(_("LDAP is only partially configured. An LDAP URL must be provided."))))
+
+    if config.LDAP_URL.get() is not None:
+      if config.NT_DOMAIN.get() is None and \
+          config.LDAP_USERNAME_PATTERN.get() is None:
+        res.append((config.LDAP_URL,
+                    unicode(_("LDAP is only partially configured. An NT Domain or username "
+                    "search pattern must be provided."))))
+
+    if config.LDAP_USERNAME_PATTERN.get() is not None and \
+        '<username>' not in config.LDAP_USERNAME_PATTERN.get():
+        res.append((config.LDAP_USERNAME_PATTERN,
+                   unicode(_("The LDAP username pattern should contain the special"
+                   "<username> replacement string for authentication."))))
+
+  return res
+
+
 def config_validator(user):
   """
   config_validator() -> [ (config_variable, error_message) ]
@@ -653,7 +751,7 @@ def config_validator(user):
   """
   from desktop.lib import i18n
 
-  res = [ ]
+  res = []
   if not SECRET_KEY.get():
     res.append((SECRET_KEY, unicode(_("Secret key should be configured as a random string."))))
 
@@ -682,32 +780,10 @@ def config_validator(user):
     res.extend(validate_path(KERBEROS.KINIT_PATH, is_dir=False))
     res.extend(validate_path(KERBEROS.CCACHE_PATH, is_dir=False))
 
-  if LDAP.SEARCH_BIND_AUTHENTICATION.get():
-    if LDAP.LDAP_URL.get() is not None and bool(LDAP.BIND_DN.get()) != bool(LDAP.BIND_PASSWORD.get()):
-      if LDAP.BIND_DN.get() == None:
-        res.append((LDAP.BIND_DN,
-                  unicode(_("If you set bind_password, then you must set bind_dn."))))
-      else:
-        res.append((LDAP.BIND_PASSWORD,
-                    unicode(_("If you set bind_dn, then you must set bind_password."))))
+  if LDAP.LDAP_SERVERS.get():
+    for ldap_record_key in LDAP.LDAP_SERVERS.get():
+      res.update(validate_ldap(user, LDAP.LDAP_SERVERS.get()[ldap_record_key]))
   else:
-    if LDAP.NT_DOMAIN.get() is not None or \
-        LDAP.LDAP_USERNAME_PATTERN.get() is not None:
-      if LDAP.LDAP_URL.get() is None:
-        res.append((LDAP.LDAP_URL,
-                    unicode(_("LDAP is only partially configured. An LDAP URL must be provided."))))
-
-    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,
-                    unicode(_("LDAP is only partially configured. An NT Domain or username "
-                    "search pattern must be provided."))))
-
-    if LDAP.LDAP_USERNAME_PATTERN.get() is not None and \
-        '<username>' not in LDAP.LDAP_USERNAME_PATTERN.get():
-        res.append((LDAP.LDAP_USERNAME_PATTERN,
-                   unicode(_("The LDAP username pattern should contain the special"
-                   "<username> replacement string for authentication."))))
+    res.update(validate_ldap(user, LDAP))
 
   return res

+ 38 - 0
desktop/core/src/desktop/templates/login.mako

@@ -22,6 +22,7 @@
 
 ${ commonheader("Welcome to Hue", "login", user, "50px") | n,unicode }
 
+<link rel="stylesheet" href="/static/ext/chosen/chosen.min.css">
 <style type="text/css">
   body {
     background-color: #FFF;
@@ -144,6 +145,29 @@ ${ commonheader("Welcome to Hue", "login", user, "50px") | n,unicode }
     font-weight: 400;
     margin-bottom: 20px;
   }
+
+  .chosen-single {
+    min-height: 38px;
+    text-align: left;
+    font-size: 18px;
+  }
+
+  .chosen-single span {
+    display: inline;
+    line-height: 38px;
+    vertical-align: middle;
+  }
+
+  .chosen-container-active.chosen-with-drop .chosen-single div b,
+  .chosen-container-single .chosen-single div b {
+    background-position-x: 1px;
+    background-position-y: 10px;
+  }
+
+  .chosen-container-active.chosen-with-drop .chosen-single div b {
+    background-position-x: -17px;
+    background-position-y: 10px;
+  }
 </style>
 
 
@@ -190,6 +214,13 @@ ${ commonheader("Welcome to Hue", "login", user, "50px") | n,unicode }
         </div>
         ${ form['password'].errors | n,unicode }
 
+        %if active_directory:
+        <div class="input-prepend">
+          <span class="add-on"><i class="fa fa-globe"></i></span>
+          ${ form['server'] | n,unicode }
+        </div>
+        %endif
+
         %if login_errors:
           <div class="alert alert-error" style="text-align: center">
             <strong><i class="fa fa-exclamation-triangle"></i> ${_('Error!')}</strong>
@@ -216,8 +247,15 @@ ${ commonheader("Welcome to Hue", "login", user, "50px") | n,unicode }
   </div>
 </div>
 
+<script src="/static/ext/chosen/chosen.jquery.min.js" type="text/javascript" charset="utf-8"></script>
 <script>
   $(document).ready(function () {
+    $("#id_server").chosen({
+      disable_search_threshold: 5,
+      width: "90%",
+      no_results_text: "${_('Oops, no database found!')}"
+    });
+
     $("form").on("submit", function () {
       window.setTimeout(function () {
         $("#logo").addClass("waiting");