Эх сурвалжийг харах

HUE-7807 [core] Add an ImpersonationBackend

Done the Django way for a maximum of re-use.
Remove unused login properties of DEMO_BACKEND at the same time.
Not documented for now as new.
Romain Rigaux 8 жил өмнө
parent
commit
501db18

+ 30 - 40
desktop/core/src/desktop/auth/backend.py

@@ -34,7 +34,7 @@ import pam
 
 
 import django.contrib.auth.backends
 import django.contrib.auth.backends
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
-from django.core.exceptions import ImproperlyConfigured
+from django.core.exceptions import ImproperlyConfigured, PermissionDenied
 from django.forms import ValidationError
 from django.forms import ValidationError
 from django.utils.importlib import import_module
 from django.utils.importlib import import_module
 
 
@@ -117,21 +117,21 @@ def find_user(username):
     user = None
     user = None
   return user
   return user
 
 
-def create_user(username, password):
+def create_user(username, password, is_superuser=True):
   LOG.info("Materializing user %s in the database" % username)
   LOG.info("Materializing user %s in the database" % username)
   user = User(username=username)
   user = User(username=username)
   if password is None:
   if password is None:
     user.set_unusable_password()
     user.set_unusable_password()
   else:
   else:
     user.set_password(password)
     user.set_password(password)
-  user.is_superuser = True
+  user.is_superuser = is_superuser
   user.save()
   user.save()
   return user
   return user
 
 
-def find_or_create_user(username, password=None):
+def find_or_create_user(username, password=None, is_superuser=True):
   user = find_user(username)
   user = find_user(username)
   if user is None:
   if user is None:
-    user = create_user(username, password)
+    user = create_user(username, password, is_superuser)
   return user
   return user
 
 
 def ensure_has_a_group(user):
 def ensure_has_a_group(user):
@@ -216,6 +216,31 @@ class AllowFirstUserDjangoBackend(django.contrib.auth.backends.ModelBackend):
     return User.objects.count() == 0
     return User.objects.count() == 0
 
 
 
 
+class ImpersonationBackend(django.contrib.auth.backends.ModelBackend):
+  """
+  Authenticate with a proxy user username/password but then login as another user.
+  Does not support a multiple backends setup.
+  """
+  def authenticate(self, username=None, password=None, login_as=None):
+    if not login_as:
+      return
+
+    authenticated = super(ImpersonationBackend, self).authenticate(username, password)
+
+    if not authenticated:
+      raise PermissionDenied()
+
+    user = find_or_create_user(login_as, password=None, is_superuser=False)
+    ensure_has_a_group(user)
+
+    return rewrite_user(user)
+
+  def get_user(self, user_id):
+    user = super(ImpersonationBackend, self).get_user(user_id)
+    user = rewrite_user(user)
+    return user
+
+
 class OAuthBackend(DesktopBackendBase):
 class OAuthBackend(DesktopBackendBase):
   """
   """
   Deprecated, use liboauth.backend.OAuthBackend instead
   Deprecated, use liboauth.backend.OAuthBackend instead
@@ -271,41 +296,6 @@ class AllowAllBackend(DesktopBackendBase):
     return True
     return True
 
 
 
 
-class DemoBackend(django.contrib.auth.backends.ModelBackend):
-  """
-  Log automatically users without a session with a new user account.
-  """
-  def authenticate(self, username, password):
-    username = force_username_case(username)
-    user = super(DemoBackend, self).authenticate(username, password)
-
-    if not user:
-      username = self._random_name()
-
-      user = find_or_create_user(username, 'HueRocks')
-
-      user.is_superuser = False
-      user.save()
-
-      ensure_has_a_group(user)
-
-    user = rewrite_user(user)
-
-    return user
-
-  def get_user(self, user_id):
-    user = super(DemoBackend, self).get_user(user_id)
-    user = rewrite_user(user)
-    return user
-
-  def _random_name(self):
-    import string
-    import random
-
-    N = 7
-    return ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(N))
-
-
 class PamBackend(DesktopBackendBase):
 class PamBackend(DesktopBackendBase):
   """
   """
   Authentication backend that uses PAM to authenticate logins. The first user to
   Authentication backend that uses PAM to authenticate logins. The first user to

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

@@ -60,7 +60,7 @@ class AuthenticationForm(AuthAuthenticationForm):
     'inactive': _t("Account deactivated. Please contact an administrator."),
     'inactive': _t("Account deactivated. Please contact an administrator."),
   }
   }
 
 
-  username = CharField(label=_t("Username"), max_length=30, widget=TextInput(attrs={'maxlength': 30, 'placeholder': _t("Username"), 'autocomplete': 'off', 'autofocus': 'autofocus'}))
+  username = CharField(label=_t("Username"), widget=TextInput(attrs={'maxlength': 30, 'placeholder': _t("Username"), 'autocomplete': 'off', 'autofocus': 'autofocus'}))
   password = CharField(label=_t("Password"), widget=PasswordInput(attrs={'placeholder': _t("Password"), 'autocomplete': 'off'}))
   password = CharField(label=_t("Password"), widget=PasswordInput(attrs={'placeholder': _t("Password"), 'autocomplete': 'off'}))
 
 
   def authenticate(self):
   def authenticate(self):
@@ -97,11 +97,24 @@ class AuthenticationForm(AuthAuthenticationForm):
     return self.authenticate()
     return self.authenticate()
 
 
 
 
+class ImpersonationAuthenticationForm(AuthenticationForm):
+  login_as = CharField(label=_t("Login as"), max_length=30, widget=TextInput(attrs={'placeholder': _t("Login as username"), 'autocomplete': 'off'}))
+
+  def authenticate(self):
+    try:
+      super(AuthenticationForm, self).clean()
+    except:
+      # Expected to fail as login_as is nor provided by the parent Django AuthenticationForm, hence we redo it properly below.
+      pass
+    self.user_cache = authenticate(username=self.cleaned_data.get('username'), password=self.cleaned_data.get('password'), login_as=self.cleaned_data.get('login_as'))
+    return self.user_cache
+
+
 class LdapAuthenticationForm(AuthenticationForm):
 class LdapAuthenticationForm(AuthenticationForm):
   """
   """
   Adds NT_DOMAINS selector.
   Adds NT_DOMAINS selector.
   """
   """
-  
+
   def __init__(self, *args, **kwargs):
   def __init__(self, *args, **kwargs):
     super(LdapAuthenticationForm, self).__init__(*args, **kwargs)
     super(LdapAuthenticationForm, self).__init__(*args, **kwargs)
     self.fields['server'] = ChoiceField(choices=get_server_choices())
     self.fields['server'] = ChoiceField(choices=get_server_choices())

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

@@ -36,11 +36,12 @@ from django.http import HttpResponseRedirect
 from django.utils.translation import ugettext as _
 from django.utils.translation import ugettext as _
 
 
 from desktop.auth import forms as auth_forms
 from desktop.auth import forms as auth_forms
+from desktop.auth.forms import ImpersonationAuthenticationForm
 from desktop.lib.django_util import render
 from desktop.lib.django_util import render
 from desktop.lib.django_util import login_notrequired
 from desktop.lib.django_util import login_notrequired
 from desktop.lib.django_util import JsonResponse
 from desktop.lib.django_util import JsonResponse
 from desktop.log.access import access_warn, last_access_map
 from desktop.log.access import access_warn, last_access_map
-from desktop.conf import LDAP, OAUTH, DEMO_ENABLED
+from desktop.conf import OAUTH
 from desktop.settings import LOAD_BALANCER_COOKIE
 from desktop.settings import LOAD_BALANCER_COOKIE
 
 
 from hadoop.fs.exceptions import WebHdfsException
 from hadoop.fs.exceptions import WebHdfsException
@@ -95,7 +96,10 @@ def dt_login(request, from_modal=False):
     AuthenticationForm = auth_forms.LdapAuthenticationForm
     AuthenticationForm = auth_forms.LdapAuthenticationForm
   else:
   else:
     UserCreationForm = auth_forms.UserCreationForm
     UserCreationForm = auth_forms.UserCreationForm
-    AuthenticationForm = auth_forms.AuthenticationForm
+    if 'ImpersonationBackend' in backend_names:
+      AuthenticationForm = ImpersonationAuthenticationForm
+    else:
+      AuthenticationForm = auth_forms.AuthenticationForm
 
 
   if request.method == 'POST':
   if request.method == 'POST':
     request.audit = {
     request.audit = {
@@ -111,8 +115,7 @@ def dt_login(request, from_modal=False):
       auth_form = AuthenticationForm(data=request.POST)
       auth_form = AuthenticationForm(data=request.POST)
 
 
       if auth_form.is_valid():
       if auth_form.is_valid():
-        # Must login by using the AuthenticationForm.
-        # It provides 'backends' on the User object.
+        # Must login by using the AuthenticationForm. It provides 'backends' on the User object.
         user = auth_form.get_user()
         user = auth_form.get_user()
         userprofile = get_profile(user)
         userprofile = get_profile(user)
 
 
@@ -158,12 +161,6 @@ def dt_login(request, from_modal=False):
       except (IOError, WebHdfsException), e:
       except (IOError, WebHdfsException), e:
         LOG.error('Could not create home directory for SAML user %s.' % request.user)
         LOG.error('Could not create home directory for SAML user %s.' % request.user)
 
 
-  if DEMO_ENABLED.get() and not 'admin' in request.REQUEST and request.user.username != 'hdfs':
-    user = authenticate(username=request.user.username, password='HueRocks')
-    login(request, user)
-    ensure_home_directory(request.fs, user.username)
-    return HttpResponseRedirect(redirect_to)
-
   if not from_modal:
   if not from_modal:
     request.session.set_test_cookie()
     request.session.set_test_cookie()
 
 

+ 32 - 0
desktop/core/src/desktop/auth/views_test.py

@@ -769,6 +769,38 @@ class TestLoginNoHadoop(object):
     assert_equal(200, response.status_code, "Expected ok status.")
     assert_equal(200, response.status_code, "Expected ok status.")
 
 
 
 
+class TestImpersonationBackend(object):
+  test_username = "test_login_impersonation"
+  test_login_as_username = "test_login_as_impersonation"
+
+  @classmethod
+  def setup_class(cls):
+    cls.client = make_logged_in_client(username=cls.test_username, password="test")
+    cls.auth_backends = settings.AUTHENTICATION_BACKENDS
+    settings.AUTHENTICATION_BACKENDS = ('desktop.auth.backend.ImpersonationBackend',)
+
+  @classmethod
+  def teardown_class(cls):
+    settings.AUTHENTICATION_BACKENDS = cls.auth_backends
+
+  def setUp(self):
+    self.reset = [conf.AUTH.BACKEND.set_for_testing(['desktop.auth.backend.ImpersonationBackend'])]
+
+  def tearDown(self):
+    for finish in self.reset:
+      finish()
+
+  def test_login_does_not_reset_groups(self):
+    self.client.get('/accounts/logout')
+
+    user = User.objects.get(username=self.test_username)
+    group, created = Group.objects.get_or_create(name=self.test_username)
+
+    response = self.client.post('/hue/accounts/login/', dict(username=self.test_username, password="test", login_as=self.test_login_as_username), follow=True)
+    assert_equal(200, response.status_code)
+    assert_equal(self.test_login_as_username, response.context['user'].username)
+
+
 class MockLdapBackend(object):
 class MockLdapBackend(object):
   settings = django_auth_ldap_backend.LDAPSettings()
   settings = django_auth_ldap_backend.LDAPSettings()
 
 

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

@@ -127,6 +127,12 @@ ${ commonheader(_("Welcome to Hue"), "login", user, request, "50px", True, True)
     </div>
     </div>
     %endif
     %endif
 
 
+    %if 'ImpersonationBackend' in backend_names:
+    <div class="text-input">
+      ${ form['login_as'] | n,unicode }
+    </div>
+    %endif
+
     %if login_errors and not form['username'].errors and not form['password'].errors:
     %if login_errors and not form['username'].errors and not form['password'].errors:
       %if form.errors:
       %if form.errors:
         % for error in form.errors:
         % for error in form.errors: