Преглед изворни кода

[auth] Support multiple authentication backends in order of priority

Jenny Kim пре 10 година
родитељ
комит
98191e6

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

@@ -96,7 +96,7 @@ class UserChangeForm(django.contrib.auth.forms.UserChangeForm):
     if self.instance.id:
       self.fields['username'].widget.attrs['readonly'] = True
 
-    if desktop_conf.AUTH.BACKEND.get() == 'desktop.auth.backend.LdapBackend':
+    if 'desktop.auth.backend.LdapBackend' in desktop_conf.AUTH.BACKEND.get():
       self.fields['password1'].widget.attrs['readonly'] = True
       self.fields['password2'].widget.attrs['readonly'] = True
       self.fields['password_old'].widget.attrs['readonly'] = True

+ 2 - 1
apps/useradmin/src/useradmin/views.py

@@ -172,7 +172,8 @@ def delete_group(request):
 
 def require_change_password(self):
   """ Return true if user has never logged in before. """
-  if desktop.conf.AUTH.BACKEND.get() == 'desktop.auth.backend.AllowFirstUserDjangoBackend' and self.first_login and desktop.conf.AUTH.CHANGE_DEFAULT_PASSWORD.get():
+  if 'desktop.auth.backend.AllowFirstUserDjangoBackend' in desktop.conf.AUTH.BACKEND.get() and \
+     self.first_login and desktop.conf.AUTH.CHANGE_DEFAULT_PASSWORD.get():
     return True
 
 

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

@@ -214,6 +214,8 @@
     # - libopenid.backend.OpenIDBackend
     # - liboauth.backend.OAuthBackend
     #     (New oauth, support Twitter, Facebook, Google+ and Linkedin
+    # Multiple Authentication backends are supported by specifying a comma-separated list in order of priority.
+    # However, in order to enable OAuthBackend, it must be the ONLY backend configured.
     ## backend=desktop.auth.backend.AllowFirstUserDjangoBackend
 
     # The service to use when querying PAM.

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

@@ -26,8 +26,6 @@ In addition, the User classes they return must support:
  - has_hue_permission(action, app) -> boolean
 Because Django's models are sometimes unfriendly, you'll want
 User to remain a django.contrib.auth.models.User object.
-
-In Desktop, only one authentication backend may be specified.
 """
 from django.contrib.auth.models import User
 import django.contrib.auth.backends

+ 6 - 7
desktop/core/src/desktop/auth/views.py

@@ -22,7 +22,6 @@ except:
   oauth = None
 
 import cgi
-import datetime
 import logging
 import urllib
 
@@ -77,16 +76,16 @@ def first_login_ever():
   return False
 
 
-def get_backend_name():
-  return get_backends() and get_backends()[0].__class__.__name__
+def get_backend_names():
+  return get_backends and [backend.__class__.__name__ for backend in get_backends()]
 
 
 @login_notrequired
 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()) )
+  backend_names = get_backend_names()
+  is_active_directory = 'LdapBackend' in backend_names and ( bool(LDAP.NT_DOMAIN.get()) or bool(LDAP.LDAP_SERVERS.get()) )
 
   if is_active_directory:
     UserCreationForm = auth_forms.LdapUserCreationForm
@@ -114,7 +113,7 @@ def dt_login(request):
         if request.session.test_cookie_worked():
           request.session.delete_test_cookie()
 
-        if is_first_login_ever or backend_name in ('AllowAllBackend', 'LdapBackend'):
+        if is_first_login_ever or 'AllowAllBackend' in backend_names or 'LdapBackend' in backend_names:
           # Create home directory for first user.
           try:
             ensure_home_directory(request.fs, user.username)
@@ -151,7 +150,7 @@ def dt_login(request):
     'next': redirect_to,
     'first_login_ever': is_first_login_ever,
     'login_errors': request.method == 'POST',
-    'backend_name': backend_name,
+    'backend_names': backend_names,
     'active_directory': is_active_directory
   })
 

+ 236 - 99
desktop/core/src/desktop/auth/views_test.py

@@ -17,9 +17,9 @@
 
 from nose.tools import assert_true, assert_false, assert_equal
 
+from django.conf import settings
 from django.contrib.auth.models import User
 from django.test.client import Client
-from django.conf import settings
 
 from desktop import conf, middleware
 from desktop.auth import backend
@@ -35,19 +35,44 @@ from useradmin.views import import_ldap_groups
 
 class TestLoginWithHadoop(PseudoHdfsTestBase):
 
-  def setUp(self):
+  reset = []
+  test_username = 'test_login_with_hadoop'
+
+  @classmethod
+  def setup_class(cls):
     # Simulate first login ever
     User.objects.all().delete()
+    PseudoHdfsTestBase.setup_class()
+
+    cls.auth_backends = settings.AUTHENTICATION_BACKENDS
+    settings.AUTHENTICATION_BACKENDS = ('desktop.auth.backend.AllowFirstUserDjangoBackend',)
+
+  @classmethod
+  def teardown_class(cls):
+    settings.AUTHENTICATION_BACKENDS = cls.auth_backends
+
+  def setUp(self):
     self.c = Client()
 
+    self.reset.append( conf.AUTH.BACKEND.set_for_testing(['desktop.auth.backend.AllowFirstUserDjangoBackend']) )
+
+  def tearDown(self):
+    User.objects.all().delete()
+
+    for finish in self.reset:
+      finish()
+
+    if self.fs.exists("/user/%s" % self.test_username):
+      self.cluster.fs.do_as_superuser(self.cluster.fs.rmtree, "/user/%s" % self.test_username)
+
   def test_login(self):
     response = self.c.get('/accounts/login/')
     assert_equal(200, response.status_code, "Expected ok status.")
     assert_true(response.context['first_login_ever'])
 
-    response = self.c.post('/accounts/login/', dict(username="foo", password="foo"))
+    response = self.c.post('/accounts/login/', dict(username=self.test_username, password="foo"))
     assert_equal(302, response.status_code, "Expected ok redirect status.")
-    assert_true(self.fs.exists("/user/foo"))
+    assert_true(self.fs.exists("/user/%s" % self.test_username))
 
     response = self.c.get('/accounts/login/')
     assert_equal(200, response.status_code, "Expected ok status.")
@@ -61,59 +86,66 @@ class TestLoginWithHadoop(PseudoHdfsTestBase):
     # Create home directory as a file in order to fail in the home creation later
     cluster = pseudo_hdfs4.shared_cluster()
     fs = cluster.fs
-    assert_false(cluster.fs.exists("/user/test-hue-foo2"))
+    assert_false(cluster.fs.exists("/user/%s" % self.test_username))
+    fs.do_as_superuser(fs.create, "/user/%s" % self.test_username)
 
-    try:
-      fs.do_as_superuser(fs.create, "/user/test-hue-foo2")
-
-      response = self.c.post('/accounts/login/', {
-        'username': "test-hue-foo2",
+    response = self.c.post('/accounts/login/', {
+        'username': self.test_username,
         'password': "test-hue-foo2",
       }, follow=True)
 
-      assert_equal(200, response.status_code, "Expected ok status.")
-      assert_true('/beeswax' in response.content, response.content)
-      # 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
-    finally:
-      fs.do_as_superuser(fs.remove, '/user/test-hue-foo2')
+    assert_equal(200, response.status_code, "Expected ok status.")
+    assert_true('/beeswax' in response.content, response.content)
+    # 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
 
 
 class TestLdapLogin(PseudoHdfsTestBase):
+
   reset = []
+  test_username = 'test_ldap_login'
 
   @classmethod
   def setup_class(cls):
+    # Simulate first login ever
+    User.objects.all().delete()
+
     PseudoHdfsTestBase.setup_class()
 
-    cls.backend = django_auth_ldap_backend.LDAPBackend
+    cls.ldap_backend = django_auth_ldap_backend.LDAPBackend
     django_auth_ldap_backend.LDAPBackend = MockLdapBackend
 
+    # Override auth backend, settings are only loaded from conf at initialization so we can't use set_for_testing
+    cls.auth_backends = settings.AUTHENTICATION_BACKENDS
+    settings.AUTHENTICATION_BACKENDS = ('desktop.auth.backend.LdapBackend',)
+
     # Need to recreate LdapBackend class with new monkey patched base class
     reload(backend)
 
-    cls.old_backends = settings.AUTHENTICATION_BACKENDS
-    settings.AUTHENTICATION_BACKENDS = ("desktop.auth.backend.LdapBackend",)
-
   @classmethod
   def teardown_class(cls):
-    django_auth_ldap_backend.LDAPBackend = cls.backend
+    django_auth_ldap_backend.LDAPBackend = cls.ldap_backend
+
+    settings.AUTHENTICATION_BACKENDS = cls.auth_backends
 
-    # Need to recreate LdapBackend class with old base class
     reload(backend)
 
-    settings.AUTHENTICATION_BACKENDS = cls.old_backends
+  def setUp(self):
+    self.c = Client()
+    self.reset.append( conf.AUTH.BACKEND.set_for_testing(['desktop.auth.backend.LdapBackend']) )
+    self.reset.append(conf.LDAP.LDAP_URL.set_for_testing('does not matter'))
 
   def tearDown(self):
+    User.objects.all().delete()
+
     for finish in self.reset:
       finish()
 
-  def setUp(self):
-    # Simulate first login ever
-    User.objects.all().delete()
-    self.c = Client()
+    if self.fs.exists("/user/%s" % self.test_username):
+      self.cluster.fs.do_as_superuser(self.cluster.fs.rmtree, "/user/%s" % self.test_username)
 
-    self.reset.append(conf.LDAP.LDAP_URL.set_for_testing('does not matter'))
+    if self.fs.exists("/user/curly"):
+      self.cluster.fs.do_as_superuser(self.cluster.fs.rmtree, "/user/curly")
 
   def test_login(self):
     response = self.c.get('/accounts/login/')
@@ -121,11 +153,11 @@ class TestLdapLogin(PseudoHdfsTestBase):
     assert_false(response.context['first_login_ever'])
 
     response = self.c.post('/accounts/login/', {
-        'username': "ldap1",
+        'username': self.test_username,
         'password': "ldap1"
     })
     assert_equal(302, response.status_code, "Expected ok redirect status.")
-    assert_true(self.fs.exists("/user/ldap1"))
+    assert_true(self.fs.exists("/user/%s" % self.test_username))
 
     response = self.c.get('/accounts/login/')
     assert_equal(200, response.status_code, "Expected ok status.")
@@ -139,48 +171,44 @@ class TestLdapLogin(PseudoHdfsTestBase):
     # Create home directory as a file in order to fail in the home creation later
     cluster = pseudo_hdfs4.shared_cluster()
     fs = cluster.fs
-    assert_false(cluster.fs.exists("/user/test-hue-ldap2"))
-
-    try:
-      fs.do_as_superuser(fs.create, "/user/test-hue-ldap2")
+    assert_false(cluster.fs.exists("/user/%s" % self.test_username))
+    fs.do_as_superuser(fs.create, "/user/%s" % self.test_username)
 
-      response = self.c.post('/accounts/login/', {
-          'username': "test-hue-ldap2",
-          'password': "test-hue-ldap2",
-      }, follow=True)
-      assert_equal(200, response.status_code, "Expected ok status.")
-      assert_true('/beeswax' in response.content, response.content)
-      # 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
-    finally:
-      fs.do_as_superuser(fs.remove, '/user/test-hue-ldap2')
+    response = self.c.post('/accounts/login/', {
+        'username': self.test_username,
+        'password': "test-hue-ldap2",
+    }, follow=True)
+    assert_equal(200, response.status_code, "Expected ok status.")
+    assert_true('/beeswax' in response.content, response.content)
+    # 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
 
   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",
+        'username': self.test_username.upper(),
         '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)
+    assert_equal(self.test_username, User.objects.all()[0].username)
 
     self.c.logout()
 
     response = self.c.post('/accounts/login/', {
-        'username': "ldap1",
+        'username': self.test_username,
         '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)
+    assert_equal(self.test_username, 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",
+        'username': self.test_username.upper(),
         'password': "ldap1"
     })
     assert_equal(302, response.status_code, "Expected ok redirect status.")
@@ -189,34 +217,34 @@ class TestLdapLogin(PseudoHdfsTestBase):
     self.c.logout()
 
     response = self.c.post('/accounts/login/', {
-        'username': "ldap1",
+        'username': self.test_username,
         '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)
+    assert_equal(self.test_username, 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",
+        'username': self.test_username.upper(),
         '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)
+    assert_equal(self.test_username, User.objects.all()[0].username)
 
     self.c.logout()
 
     response = self.c.post('/accounts/login/', {
-        'username': "ldap1",
+        'username': self.test_username,
         '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)
+    assert_equal(self.test_username, 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))
@@ -235,35 +263,56 @@ class TestLdapLogin(PseudoHdfsTestBase):
     assert_equal(3, User.objects.all()[0].groups.all().count(), User.objects.all()[0].groups.all())
 
 
-class TestRemoteUserLogin(object):
+class TestRemoteUserLogin(PseudoHdfsTestBase):
+
   reset = []
+  test_username = "test_remote_user_login"
 
-  def setUp(self):
+  @classmethod
+  def setup_class(cls):
+    # Simulate first login ever
     User.objects.all().delete()
-    self.reset.append( conf.AUTH.BACKEND.set_for_testing('desktop.auth.backend.RemoteUserDjangoBackend') )
-    self.reset.append( conf.AUTH.REMOTE_USER_HEADER.set_for_testing('REMOTE_USER') ) # Set for middlware
-    self.backends = settings.AUTHENTICATION_BACKENDS
+
+    PseudoHdfsTestBase.setup_class()
+
+    cls.auth_backends = settings.AUTHENTICATION_BACKENDS
     settings.AUTHENTICATION_BACKENDS = ('desktop.auth.backend.RemoteUserDjangoBackend',)
-    self.remote_user_middleware_header = middleware.HueRemoteUserMiddleware.header
-    middleware.HueRemoteUserMiddleware.header = conf.AUTH.REMOTE_USER_HEADER.get() # Set for middlware
+    cls.remote_user_middleware_header = middleware.HueRemoteUserMiddleware.header
+    middleware.HueRemoteUserMiddleware.header = conf.AUTH.REMOTE_USER_HEADER.get()
+
+  @classmethod
+  def teardown_class(cls):
+    middleware.HueRemoteUserMiddleware.header = cls.remote_user_middleware_header
+    settings.AUTHENTICATION_BACKENDS = cls.auth_backends
+
+  def setUp(self):
+    self.reset.append( conf.AUTH.BACKEND.set_for_testing(['desktop.auth.backend.RemoteUserDjangoBackend']) )
+    self.reset.append( conf.AUTH.REMOTE_USER_HEADER.set_for_testing('REMOTE_USER') )  # Set for middleware
+
     self.c = Client()
 
   def tearDown(self):
-    middleware.HueRemoteUserMiddleware.header = self.remote_user_middleware_header
-    settings.AUTHENTICATION_BACKENDS = self.backends
     for finish in self.reset:
       finish()
 
+    User.objects.all().delete()
+
+    if self.fs.exists("/user/%s" % self.test_username):
+      self.cluster.fs.do_as_superuser(self.cluster.fs.rmtree, "/user/%s" % self.test_username)
+
+    if self.fs.exists("/user/%s_%s" % (self.test_username, '2')):
+      self.cluster.fs.do_as_superuser(self.cluster.fs.rmtree, "/user/%s_%s" % (self.test_username, '2'))
+
   def test_normal(self):
     response = self.c.get('/accounts/login/')
     assert_equal(200, response.status_code, "Expected ok status.")
     assert_false(response.context['first_login_ever'])
 
     assert_equal(0, len(User.objects.all()))
-    response = self.c.post('/accounts/login/', {}, **{"REMOTE_USER": "foo3"})
+    response = self.c.post('/accounts/login/', {}, **{"REMOTE_USER": self.test_username})
     assert_equal(200, response.status_code, "Expected ok status.")
     assert_equal(1, len(User.objects.all()))
-    assert_equal('foo3', User.objects.all()[0].username)
+    assert_equal(self.test_username, User.objects.all()[0].username)
 
   def test_ignore_case(self):
     self.reset.append( conf.AUTH.IGNORE_USERNAME_CASE.set_for_testing(True) )
@@ -272,25 +321,25 @@ class TestRemoteUserLogin(object):
     assert_equal(200, response.status_code, "Expected ok status.")
     assert_false(response.context['first_login_ever'])
 
-    response = self.c.post('/accounts/login/', {}, **{"REMOTE_USER": "foo3"})
+    response = self.c.post('/accounts/login/', {}, **{"REMOTE_USER": self.test_username})
     assert_equal(200, response.status_code, "Expected ok status.")
     assert_equal(1, len(User.objects.all()))
-    assert_equal('foo3', User.objects.all()[0].username)
+    assert_equal(self.test_username, User.objects.all()[0].username)
 
-    response = self.c.post('/accounts/login/', {}, **{"REMOTE_USER": "FOO3"})
+    response = self.c.post('/accounts/login/', {}, **{"REMOTE_USER": self.test_username.upper()})
     assert_equal(200, response.status_code, "Expected ok status.")
     assert_equal(1, len(User.objects.all()))
-    assert_equal('foo3', User.objects.all()[0].username)
+    assert_equal(self.test_username, User.objects.all()[0].username)
 
-    response = self.c.post('/accounts/login/', {}, **{"REMOTE_USER": "FOO4"})
+    response = self.c.post('/accounts/login/', {}, **{"REMOTE_USER": "%s_%s" % (self.test_username.upper(), '2')})
     assert_equal(200, response.status_code, "Expected ok status.")
     assert_equal(2, len(User.objects.all().order_by('username')))
-    assert_equal('foo4', User.objects.all()[1].username)
+    assert_equal("%s_%s" % (self.test_username, '2'), User.objects.all()[1].username)
 
-    response = self.c.post('/accounts/login/', {}, **{"REMOTE_USER": "foo4"})
+    response = self.c.post('/accounts/login/', {}, **{"REMOTE_USER": "%s_%s" % (self.test_username, '2')})
     assert_equal(200, response.status_code, "Expected ok status.")
     assert_equal(2, len(User.objects.all()))
-    assert_equal('foo4', User.objects.all().order_by('username')[1].username)
+    assert_equal("%s_%s" % (self.test_username, '2'), User.objects.all().order_by('username')[1].username)
 
   def test_force_lower_case(self):
     self.reset.append( conf.AUTH.FORCE_USERNAME_LOWERCASE.set_for_testing(True) )
@@ -299,23 +348,24 @@ class TestRemoteUserLogin(object):
     assert_equal(200, response.status_code, "Expected ok status.")
     assert_false(response.context['first_login_ever'])
 
-    response = self.c.post('/accounts/login/', {}, **{"REMOTE_USER": "foo3"})
+    response = self.c.post('/accounts/login/', {}, **{"REMOTE_USER": self.test_username})
     assert_equal(200, response.status_code, "Expected ok status.")
     assert_equal(1, len(User.objects.all()))
-    assert_equal('foo3', User.objects.all()[0].username)
+    assert_equal(self.test_username, User.objects.all()[0].username)
 
-    response = self.c.post('/accounts/login/', {}, **{"REMOTE_USER": "FOO3"})
+    response = self.c.post('/accounts/login/', {}, **{"REMOTE_USER": self.test_username.upper()})
     assert_equal(200, response.status_code, "Expected ok status.")
     assert_equal(1, len(User.objects.all()))
-    assert_equal('foo3', User.objects.all()[0].username)
+    assert_equal(self.test_username, User.objects.all()[0].username)
+
 
   def test_ignore_case_and_force_lower_case(self):
     reset = conf.AUTH.FORCE_USERNAME_LOWERCASE.set_for_testing(False)
     try:
-      response = self.c.post('/accounts/login/', {}, **{"REMOTE_USER": "FOO3"})
+      response = self.c.post('/accounts/login/', {}, **{"REMOTE_USER": self.test_username.upper()})
       assert_equal(200, response.status_code, "Expected ok status.")
       assert_equal(1, len(User.objects.all()))
-      assert_equal('FOO3', User.objects.all()[0].username)
+      assert_equal(self.test_username.upper(), User.objects.all()[0].username)
     finally:
       reset()
 
@@ -323,32 +373,119 @@ class TestRemoteUserLogin(object):
     self.reset.append( conf.AUTH.IGNORE_USERNAME_CASE.set_for_testing(True) )
 
     # Previously existing users should not be forced to lower case.
-    response = self.c.post('/accounts/login/', {}, **{"REMOTE_USER": "FOO3"})
+    response = self.c.post('/accounts/login/', {}, **{"REMOTE_USER": self.test_username.upper()})
     assert_equal(200, response.status_code, "Expected ok status.")
     assert_equal(1, len(User.objects.all()))
-    assert_equal('FOO3', User.objects.all()[0].username)
+    assert_equal(self.test_username.upper(), User.objects.all()[0].username)
 
     # New users should be forced to lowercase.
-    response = self.c.post('/accounts/login/', {}, **{"REMOTE_USER": "FOO4"})
+    response = self.c.post('/accounts/login/', {}, **{"REMOTE_USER": "%s_%s" % (self.test_username.upper(), '2')})
     assert_equal(200, response.status_code, "Expected ok status.")
     assert_equal(2, len(User.objects.all()))
-    assert_equal('foo4', User.objects.all().order_by('username')[1].username)
+    assert_equal("%s_%s" % (self.test_username, '2'), User.objects.all().order_by('username')[1].username)
+
 
+class TestMultipleBackendLogin(PseudoHdfsTestBase):
 
-class TestLogin(object):
   reset = []
+  test_username = "test_multiple_login"
+
+  @classmethod
+  def setup_class(cls):
+    # Simulate first login ever
+    User.objects.all().delete()
+
+    PseudoHdfsTestBase.setup_class()
+
+    cls.ldap_backend = django_auth_ldap_backend.LDAPBackend
+    django_auth_ldap_backend.LDAPBackend = MockLdapBackend
+
+    # Override auth backend, settings are only loaded from conf at initialization so we can't use set_for_testing
+    cls.auth_backends = settings.AUTHENTICATION_BACKENDS
+    settings.AUTHENTICATION_BACKENDS = ('desktop.auth.backend.LdapBackend','desktop.auth.backend.AllowFirstUserDjangoBackend')
+
+    # Need to recreate LdapBackend class with new monkey patched base class
+    reload(backend)
+
+  @classmethod
+  def teardown_class(cls):
+    django_auth_ldap_backend.LDAPBackend = cls.ldap_backend
+
+    settings.AUTHENTICATION_BACKENDS = cls.auth_backends
+
+    reload(backend)
 
   def setUp(self):
+    self.c = Client()
+    self.reset.append( conf.AUTH.BACKEND.set_for_testing(['desktop.auth.backend.LdapBackend','desktop.auth.backend.AllowFirstUserDjangoBackend']))
+    self.reset.append(conf.LDAP.LDAP_URL.set_for_testing('does not matter'))
+
+  def tearDown(self):
+    User.objects.all().delete()
+
+    for finish in self.reset:
+      finish()
+
+    if self.fs.exists("/user/%s" % self.test_username):
+      self.cluster.fs.do_as_superuser(self.cluster.fs.rmtree, "/user/%s" % self.test_username)
+
+  def test_login_with_ldap(self):
+    ldap_access.CACHED_LDAP_CONN = LdapTestConnection()
+
+    response = self.c.post('/accounts/login/', {
+      'username': "curly",
+      'password': "ldap1"
+    })
+    assert_equal(302, response.status_code, response.status_code)
+    assert_equal(1, len(User.objects.all()))
+
+  def test_fallback_to_db(self):
+    ldap_access.CACHED_LDAP_CONN = LdapTestConnection()
+
+    client = make_logged_in_client(username=self.test_username, password="test")
+    client.get('/accounts/logout')
+    user = User.objects.get(username=self.test_username)
+
+    response = self.c.post('/accounts/login/', dict(username=self.test_username, password="foo"))
+    assert_equal(302, response.status_code, "Expected ok redirect status.")
+    assert_true(self.fs.exists("/user/%s" % self.test_username))
+
+
+class TestLogin(PseudoHdfsTestBase):
+
+  reset = []
+  test_username = "test_login"
+
+  @classmethod
+  def setup_class(cls):
     # Simulate first login ever
     User.objects.all().delete()
+
+    PseudoHdfsTestBase.setup_class()
+
+    cls.auth_backends = settings.AUTHENTICATION_BACKENDS
+    settings.AUTHENTICATION_BACKENDS = ('desktop.auth.backend.AllowFirstUserDjangoBackend',)
+
+  @classmethod
+  def teardown_class(cls):
+    settings.AUTHENTICATION_BACKENDS = cls.auth_backends
+
+  def setUp(self):
     self.c = Client()
 
+    self.reset.append( conf.AUTH.BACKEND.set_for_testing(['desktop.auth.backend.AllowFirstUserDjangoBackend']) )
+
   def tearDown(self):
     for finish in self.reset:
       finish()
 
+    User.objects.all().delete()
+
+    if self.fs.exists("/user/%s" % self.test_username):
+      self.cluster.fs.do_as_superuser(self.cluster.fs.rmtree, "/user/%s" % self.test_username)
+
   def test_bad_first_user(self):
-    self.reset.append( conf.AUTH.BACKEND.set_for_testing("desktop.auth.backend.AllowFirstUserDjangoBackend") )
+    self.reset.append( conf.AUTH.BACKEND.set_for_testing(["desktop.auth.backend.AllowFirstUserDjangoBackend"]) )
 
     response = self.c.get('/accounts/login/')
     assert_equal(200, response.status_code, "Expected ok status.")
@@ -359,44 +496,44 @@ class TestLogin(object):
     assert_true('This value may contain only letters, numbers and @/./+/-/_ characters.' in response.content, response)
 
   def test_non_jframe_login(self):
-    client = make_logged_in_client(username="test", password="test")
+    client = make_logged_in_client(username=self.test_username, password="test")
     # Logout first
     client.get('/accounts/logout')
     # Login
-    response = client.post('/accounts/login/', dict(username="test", password="test"), follow=True)
+    response = client.post('/accounts/login/', dict(username=self.test_username, password="test"), follow=True)
     assert_true(any(["admin_wizard.mako" in _template.filename for _template in response.templates]), response.content) # Go to superuser wizard
 
   def test_login_expiration(self):
     """ Expiration test without superusers """
     old_settings = settings.ADMINS
-    self.reset.append( conf.AUTH.BACKEND.set_for_testing("desktop.auth.backend.AllowFirstUserDjangoBackend") )
+    self.reset.append( conf.AUTH.BACKEND.set_for_testing(["desktop.auth.backend.AllowFirstUserDjangoBackend"]) )
     self.reset.append( conf.AUTH.EXPIRES_AFTER.set_for_testing(0) )
     self.reset.append( conf.AUTH.EXPIRE_SUPERUSERS.set_for_testing(False) )
 
-    client = make_logged_in_client(username="test", password="test")
+    client = make_logged_in_client(username=self.test_username, password="test")
     client.get('/accounts/logout')
-    user = User.objects.get(username="test")
+    user = User.objects.get(username=self.test_username)
 
     # Login successfully
     try:
       user.is_superuser = True
       user.save()
-      response = client.post('/accounts/login/', dict(username="test", password="test"), follow=True)
+      response = client.post('/accounts/login/', dict(username=self.test_username, password="test"), follow=True)
       assert_equal(200, response.status_code, "Expected ok status.")
 
       client.get('/accounts/logout')
 
       # Login fail
-      settings.ADMINS = [('test', 'test@test.com')]
+      settings.ADMINS = [(self.test_username, 'test@test.com')]
       user.is_superuser = False
       user.save()
-      response = client.post('/accounts/login/', dict(username="test", password="test"), follow=True)
+      response = client.post('/accounts/login/', dict(username=self.test_username, password="test"), follow=True)
       assert_equal(200, response.status_code, "Expected ok status.")
       assert_true('Account deactivated. Please contact an <a href="mailto:test@test.com">administrator</a>' in response.content, response.content)
 
       # Failure should report an inactive user without admin link
       settings.ADMINS = []
-      response = client.post('/accounts/login/', dict(username="test", password="test"), follow=True)
+      response = client.post('/accounts/login/', dict(username=self.test_username, password="test"), follow=True)
       assert_equal(200, response.status_code, "Expected ok status.")
       assert_true("Account deactivated. Please contact an administrator." in response.content, response.content)
     finally:
@@ -404,18 +541,18 @@ class TestLogin(object):
 
   def test_login_expiration_with_superusers(self):
     """ Expiration test with superusers """
-    self.reset.append( conf.AUTH.BACKEND.set_for_testing("desktop.auth.backend.AllowFirstUserDjangoBackend") )
+    self.reset.append( conf.AUTH.BACKEND.set_for_testing(["desktop.auth.backend.AllowFirstUserDjangoBackend"]) )
     self.reset.append( conf.AUTH.EXPIRES_AFTER.set_for_testing(0) )
     self.reset.append( conf.AUTH.EXPIRE_SUPERUSERS.set_for_testing(True) )
 
-    client = make_logged_in_client(username="test", password="test")
+    client = make_logged_in_client(username=self.test_username, password="test")
     client.get('/accounts/logout')
-    user = User.objects.get(username="test")
+    user = User.objects.get(username=self.test_username)
 
     # Login fail
     user.is_superuser = True
     user.save()
-    response = client.post('/accounts/login/', dict(username="test", password="test"), follow=True)
+    response = client.post('/accounts/login/', dict(username=self.test_username, password="test"), follow=True)
     assert_equal(200, response.status_code, "Expected unauthorized status.")
 
 

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

@@ -515,11 +515,13 @@ AUTH = ConfigSection(
   help=_("Configuration options for user authentication into the web application."),
   members=dict(
     BACKEND=Config("backend",
-                   default="desktop.auth.backend.AllowFirstUserDjangoBackend",
+                   default=["desktop.auth.backend.AllowFirstUserDjangoBackend"],
+                   type=coerce_csv,
                    help=_("Authentication backend.  Common settings are "
                         "django.contrib.auth.backends.ModelBackend (fully Django backend), " +
                         "desktop.auth.backend.AllowAllBackend (allows everyone), " +
-                        "desktop.auth.backend.AllowFirstUserDjangoBackend (relies on Django and user manager, after the first login). ")),
+                        "desktop.auth.backend.AllowFirstUserDjangoBackend (relies on Django and user manager, after the first login). " +
+                        "Multiple Authentication backends are supported by specifying a comma-separated list in order of priority.")),
     USER_AUGMENTOR=Config("user_augmentor",
                    default="desktop.auth.backend.DefaultUserAugmentor",
                    help=_("Class which defines extra accessor methods for User objects.")),

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

@@ -480,7 +480,7 @@ class SpnegoMiddleware(object):
   """
 
   def __init__(self):
-    if not 'SpnegoDjangoBackend' in desktop.conf.AUTH.BACKEND.get():
+    if not 'desktop.auth.backend.SpnegoMiddleware' in desktop.conf.AUTH.BACKEND.get():
       LOG.info('Unloading SpnegoMiddleware')
       raise exceptions.MiddlewareNotUsed
 
@@ -588,7 +588,7 @@ class HueRemoteUserMiddleware(RemoteUserMiddleware):
   in use.
   """
   def __init__(self):
-    if not 'RemoteUserDjangoBackend' in desktop.conf.AUTH.BACKEND.get():
+    if not 'desktop.auth.backend.RemoteUserDjangoBackend' in desktop.conf.AUTH.BACKEND.get():
       LOG.info('Unloading HueRemoteUserMiddleware')
       raise exceptions.MiddlewareNotUsed
     self.header = desktop.conf.AUTH.REMOTE_USER_HEADER.get()

+ 3 - 2
desktop/core/src/desktop/settings.py

@@ -330,10 +330,11 @@ if 'test' in sys.argv:
   CACHE_MIDDLEWARE_SECONDS = 0
 
 TIME_ZONE = desktop.conf.TIME_ZONE.get()
-# Desktop supports only one authentication backend.
-AUTHENTICATION_BACKENDS = (desktop.conf.AUTH.BACKEND.get(),)
+
 if desktop.conf.DEMO_ENABLED.get():
   AUTHENTICATION_BACKENDS = ('desktop.auth.backend.DemoBackend',)
+else:
+  AUTHENTICATION_BACKENDS = tuple(desktop.conf.AUTH.BACKEND.get())
 
 EMAIL_HOST = desktop.conf.SMTP.HOST.get()
 EMAIL_PORT = desktop.conf.SMTP.PORT.get()

+ 4 - 4
desktop/core/src/desktop/templates/login.mako

@@ -209,7 +209,7 @@ ${ commonheader("Welcome to Hue", "login", user, "50px") | n,unicode }
         %endif
 
         <div class="input-prepend
-          % if backend_name == 'OAuthBackend':
+          % if backend_names == ['OAuthBackend']:
             hide
           % endif
         ">
@@ -220,7 +220,7 @@ ${ commonheader("Welcome to Hue", "login", user, "50px") | n,unicode }
         ${ form['username'].errors | n,unicode }
 
         <div class="input-prepend
-          % if backend_name in ('AllowAllBackend', 'OAuthBackend'):
+          % if 'AllowAllBackend' in backend_names or backend_names == ['OAuthBackend']:
             hide
           % endif
         ">
@@ -273,11 +273,11 @@ ${ commonheader("Welcome to Hue", "login", user, "50px") | n,unicode }
       }, 1000);
     });
 
-    % if backend_name == 'AllowAllBackend':
+    % if 'AllowAllBackend' in backend_names:
       $('#id_password').val('password');
     % endif
 
-    % if backend_name == 'OAuthBackend':
+    % if backend_names == ['OAuthBackend']:
       $("input").css({"display": "block", "margin-left": "auto", "margin-right": "auto"});
       $("input").bind('click', function () {
         window.location.replace('/login/oauth/');