Browse Source

HUE-1860 [core] sync ldap group memberships upon login

Add logic to the LdapBackend.
Add tests for LdapBackend.
Abraham Elmahrek 11 years ago
parent
commit
9674519342

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

@@ -129,6 +129,9 @@
     # Defaults to HTTP_REMOTE_USER
     # Defaults to HTTP_REMOTE_USER
     ## remote_user_header=HTTP_REMOTE_USER
     ## remote_user_header=HTTP_REMOTE_USER
 
 
+    # Synchronize a users groups when they login
+    ## sync_groups_on_login=false
+
     # Ignore the case of usernames when searching for existing users.
     # Ignore the case of usernames when searching for existing users.
     # Only supported in remoteUserDjangoBackend.
     # Only supported in remoteUserDjangoBackend.
     ## ignore_username_case=false
     ## ignore_username_case=false

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

@@ -183,6 +183,9 @@
     # For use when using LdapBackend for Hue authentication
     # For use when using LdapBackend for Hue authentication
     ## create_users_on_login = true
     ## create_users_on_login = true
 
 
+    # Synchronize a users groups when they login
+    ## sync_groups_on_login=false
+
     # Ignore the case of usernames when searching for existing users in Hue.
     # Ignore the case of usernames when searching for existing users in Hue.
     ## ignore_username_case=false
     ## ignore_username_case=false
 
 

+ 5 - 0
desktop/core/src/desktop/auth/backend.py

@@ -374,6 +374,8 @@ class LdapBackend(object):
         user.groups.add(default_group)
         user.groups.add(default_group)
         user.save()
         user.save()
 
 
+      if desktop.conf.LDAP.SYNC_GROUPS_ON_LOGIN.get():
+        self.import_groups(user)
       return user
       return user
 
 
     return None
     return None
@@ -383,6 +385,9 @@ class LdapBackend(object):
     user = rewrite_user(user)
     user = rewrite_user(user)
     return user
     return user
 
 
+  def import_groups(self, user):
+    import_ldap_users(user.username, sync_groups=True, import_by_dn=False)
+
   @classmethod
   @classmethod
   def manages_passwords_externally(cls):
   def manages_passwords_externally(cls):
     return True
     return True

+ 103 - 17
desktop/core/src/desktop/auth/views_test.py

@@ -22,11 +22,15 @@ from django.test.client import Client
 from django.conf import settings
 from django.conf import settings
 
 
 from desktop import conf, middleware
 from desktop import conf, middleware
-from desktop.auth.backend import rewrite_user
+from desktop.auth import backend
+from django_auth_ldap import backend as django_auth_ldap_backend
 from desktop.lib.django_test_util import make_logged_in_client
 from desktop.lib.django_test_util import make_logged_in_client
 from hadoop.test_base import PseudoHdfsTestBase
 from hadoop.test_base import PseudoHdfsTestBase
 from hadoop import pseudo_hdfs4
 from hadoop import pseudo_hdfs4
 
 
+from useradmin import ldap_access
+from useradmin.tests import LdapTestConnection
+
 
 
 class TestLoginWithHadoop(PseudoHdfsTestBase):
 class TestLoginWithHadoop(PseudoHdfsTestBase):
 
 
@@ -67,25 +71,33 @@ class TestLoginWithHadoop(PseudoHdfsTestBase):
 
 
 
 
 class TestLdapLogin(PseudoHdfsTestBase):
 class TestLdapLogin(PseudoHdfsTestBase):
+  reset = []
+
   @classmethod
   @classmethod
   def setup_class(cls):
   def setup_class(cls):
     PseudoHdfsTestBase.setup_class()
     PseudoHdfsTestBase.setup_class()
 
 
-    from desktop.auth import backend
-    cls.ldap_backend = backend.LdapBackend
-    backend.LdapBackend = MockLdapBackend
-    MockLdapBackend.__name__ = 'LdapBackend'
+    cls.backend = django_auth_ldap_backend.LDAPBackend
+    django_auth_ldap_backend.LDAPBackend = MockLdapBackend
+
+    # Need to recreate LdapBackend class with new monkey patched base class
+    reload(backend)
 
 
-    cls.old_backend = settings.AUTHENTICATION_BACKENDS
+    cls.old_backends = settings.AUTHENTICATION_BACKENDS
     settings.AUTHENTICATION_BACKENDS = ("desktop.auth.backend.LdapBackend",)
     settings.AUTHENTICATION_BACKENDS = ("desktop.auth.backend.LdapBackend",)
 
 
   @classmethod
   @classmethod
   def teardown_class(cls):
   def teardown_class(cls):
-    from desktop.auth import backend
-    backend.LdapBackend = cls.ldap_backend
+    django_auth_ldap_backend.LDAPBackend = cls.backend
 
 
-    settings.AUTHENTICATION_BACKENDS = cls.old_backend
-    MockLdapBackend.__name__ = 'MockLdapBackend'
+    # Need to recreate LdapBackend class with old base class
+    reload(backend)
+
+    settings.AUTHENTICATION_BACKENDS = cls.old_backends
+
+  def tearDown(self):
+    for finish in self.reset:
+      finish()
 
 
   def setUp(self):
   def setUp(self):
     # Simulate first login ever
     # Simulate first login ever
@@ -128,6 +140,82 @@ class TestLdapLogin(PseudoHdfsTestBase):
     # Custom login process should not do 'http-equiv="refresh"' but call the correct view
     # Custom login process should not do 'http-equiv="refresh"' but call the correct view
     # 'Could not create home directory.' won't show up because the messages are consumed before
     # 'Could not create home directory.' won't show up because the messages are consumed before
 
 
+  def test_login_ignore_case(self):
+    self.reset.append(conf.LDAP.IGNORE_USERNAME_CASE.set_for_testing(True))
+
+    response = self.c.post('/accounts/login/', {
+        'username': "LDAP1",
+        'password': "ldap1"
+    })
+    assert_equal(302, response.status_code, "Expected ok redirect status.")
+    assert_equal(1, len(User.objects.all()))
+    assert_equal('LDAP1', User.objects.all()[0].username)
+
+    self.c.logout()
+
+    response = self.c.post('/accounts/login/', {
+        'username': "ldap1",
+        'password': "ldap1"
+    })
+    assert_equal(302, response.status_code, "Expected ok redirect status.")
+    assert_equal(1, len(User.objects.all()))
+    assert_equal('LDAP1', User.objects.all()[0].username)
+
+  def test_login_force_lower_case(self):
+    self.reset.append(conf.LDAP.FORCE_USERNAME_LOWERCASE.set_for_testing(True))
+
+    response = self.c.post('/accounts/login/', {
+        'username': "LDAP1",
+        'password': "ldap1"
+    })
+    assert_equal(302, response.status_code, "Expected ok redirect status.")
+    assert_equal(1, len(User.objects.all()))
+
+    self.c.logout()
+
+    response = self.c.post('/accounts/login/', {
+        'username': "ldap1",
+        'password': "ldap1"
+    })
+    assert_equal(302, response.status_code, "Expected ok redirect status.")
+    assert_equal(1, len(User.objects.all()))
+    assert_equal('ldap1', User.objects.all()[0].username)
+
+  def test_login_force_lower_case_and_ignore_case(self):
+    self.reset.append(conf.LDAP.IGNORE_USERNAME_CASE.set_for_testing(True))
+    self.reset.append(conf.LDAP.FORCE_USERNAME_LOWERCASE.set_for_testing(True))
+
+    response = self.c.post('/accounts/login/', {
+        'username': "LDAP1",
+        'password': "ldap1"
+    })
+    assert_equal(302, response.status_code, "Expected ok redirect status.")
+    assert_equal(1, len(User.objects.all()))
+    assert_equal('ldap1', User.objects.all()[0].username)
+
+    self.c.logout()
+
+    response = self.c.post('/accounts/login/', {
+        'username': "ldap1",
+        'password': "ldap1"
+    })
+    assert_equal(302, response.status_code, "Expected ok redirect status.")
+    assert_equal(1, len(User.objects.all()))
+    assert_equal('ldap1', User.objects.all()[0].username)
+
+  def test_import_groups_on_login(self):
+    self.reset.append(conf.LDAP.SYNC_GROUPS_ON_LOGIN.set_for_testing(True))
+    ldap_access.CACHED_LDAP_CONN = LdapTestConnection()
+
+    response = self.c.post('/accounts/login/', {
+      'username': "curly",
+      'password': "ldap1"
+    })
+    assert_equal(302, response.status_code, "Expected ok redirect status.")
+    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())
+
 
 
 class TestRemoteUserLogin(object):
 class TestRemoteUserLogin(object):
   reset = []
   reset = []
@@ -254,14 +342,12 @@ class TestLogin(object):
 
 
 
 
 class MockLdapBackend(object):
 class MockLdapBackend(object):
+  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):
-    user, created = User.objects.get_or_create(username=username)
+    user, created = self.get_or_create_user(username, None)
     return user
     return user
 
 
   def get_user(self, user_id):
   def get_user(self, user_id):
-    return rewrite_user(User.objects.get(id=user_id))
-
-  @classmethod
-  def manages_passwords_externally(cls):
-    return True
-LdapBackend = MockLdapBackend
+    return User.objects.get(id=user_id)

+ 4 - 0
desktop/core/src/desktop/conf.py

@@ -439,6 +439,10 @@ LDAP = ConfigSection(
       help=_("Create users when they login with their LDAP credentials."),
       help=_("Create users when they login with their LDAP credentials."),
       type=coerce_bool,
       type=coerce_bool,
       default=True),
       default=True),
+    SYNC_GROUPS_ON_LOGIN = Config("sync_groups_on_login",
+      help=_("Synchronize a users groups when they login."),
+      type=coerce_bool,
+      default=False),
     IGNORE_USERNAME_CASE = Config("ignore_username_case",
     IGNORE_USERNAME_CASE = Config("ignore_username_case",
       help=_("Ignore the case of usernames when searching for existing users in Hue."),
       help=_("Ignore the case of usernames when searching for existing users in Hue."),
       type=coerce_bool,
       type=coerce_bool,