From 255bb0e4aff04cae9351029d4cff7fa0b904a630 Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Tue, 1 Sep 2015 10:06:42 +0200 Subject: [PATCH 1/2] add a switch-user feature (fixes #8142) The new switch-user button in the manager allow any super-user to become another user for debugging purpose. --- src/authentic2/backends/models_backend.py | 6 +++ src/authentic2/constants.py | 2 + src/authentic2/context_processors.py | 4 +- src/authentic2/manager/user_views.py | 9 ++++ src/authentic2/profile_urls.py | 1 + src/authentic2/profile_views.py | 4 ++ src/authentic2/settings.py | 1 + src/authentic2/templates/authentic2/base.html | 2 +- .../templates/idp/account_management.html | 3 ++ src/authentic2/utils.py | 57 ++++++++++++++++++++-- 10 files changed, 83 insertions(+), 6 deletions(-) diff --git a/src/authentic2/backends/models_backend.py b/src/authentic2/backends/models_backend.py index 7e89a12..ab027da 100644 --- a/src/authentic2/backends/models_backend.py +++ b/src/authentic2/backends/models_backend.py @@ -61,3 +61,9 @@ class ModelBackend(ModelBackend): return user else: user_login_failure(user.get_username()) + + +class DummyModelBackend(ModelBackend): + def authenticate(self, user=None): + if user is not None: + return user diff --git a/src/authentic2/constants.py b/src/authentic2/constants.py index fb17c6f..4488b68 100644 --- a/src/authentic2/constants.py +++ b/src/authentic2/constants.py @@ -2,3 +2,5 @@ NONCE_FIELD_NAME = 'nonce' CANCEL_FIELD_NAME = 'cancel' AUTHENTICATION_EVENTS_SESSION_KEY = 'authentication-events' +SWITCH_USER_SESSION_KEY = '_switch_user' +LAST_LOGIN_SESSION_KEY = '_last_login' diff --git a/src/authentic2/context_processors.py b/src/authentic2/context_processors.py index 22fd4eb..6dd0c84 100644 --- a/src/authentic2/context_processors.py +++ b/src/authentic2/context_processors.py @@ -3,7 +3,7 @@ from collections import defaultdict from pkg_resources import get_distribution from django.conf import settings -from . import utils, app_settings +from . import utils, app_settings, constants class UserFederations(object): '''Provide access to all federations of the current user''' @@ -41,4 +41,6 @@ def a2_processor(request): __AUTHENTIC2_DISTRIBUTION = str(get_distribution('authentic2')) variables['AUTHENTIC2_VERSION'] = __AUTHENTIC2_DISTRIBUTION variables['add_to_blocks'] = defaultdict(lambda:[]) + variables['LAST_LOGIN'] = request.session.get(constants.LAST_LOGIN_SESSION_KEY) + variables['USER_SWITCHED'] = constants.SWITCH_USER_SESSION_KEY in request.session return variables diff --git a/src/authentic2/manager/user_views.py b/src/authentic2/manager/user_views.py index 7fb5980..f446e01 100644 --- a/src/authentic2/manager/user_views.py +++ b/src/authentic2/manager/user_views.py @@ -8,8 +8,12 @@ from django.core.urlresolvers import reverse from django.contrib.auth import get_user_model from django.contrib import messages from django.http import HttpResponseRedirect +from django.views.generic.detail import SingleObjectMixin +from django.views.generic import View +from authentic2.constants import SWITCH_USER_SESSION_KEY from authentic2.models import Attribute, PasswordReset +from authentic2.utils import switch_user from .views import BaseTableView, BaseAddView, PassRequestToFormMixin, \ BaseEditView, ActionMixin, OtherActionsMixin, Action, ExportMixin, \ @@ -104,6 +108,8 @@ class UserEditView(PassRequestToFormMixin, OtherActionsMixin, _('Delete'), _('Do you really want to delete "%s" ?') % self.object.username) + if self.request.user.is_superuser: + yield Action('switch_user', _('Impersonate this user')) def action_force_password_change(self, request, *args, **kwargs): PasswordReset.objects.get_or_create(user=self.object) @@ -156,6 +162,9 @@ class UserEditView(PassRequestToFormMixin, OtherActionsMixin, def action_delete_password_reset(self, request, *args, **kwargs): PasswordReset.objects.filter(user=self.object).delete() + def action_switch_user(self, request, *args, **kwargs): + return switch_user(request, self.object) + # Copied from PasswordResetForm implementation def send_mail(self, subject_template_name, email_template_name, context, to_email): diff --git a/src/authentic2/profile_urls.py b/src/authentic2/profile_urls.py index 67ac2b1..ae4d3ce 100644 --- a/src/authentic2/profile_urls.py +++ b/src/authentic2/profile_urls.py @@ -69,4 +69,5 @@ urlpatterns = patterns('authentic2.views', url(r'^password/reset/done/$', auth_views.password_reset_done, name='auth_password_reset_done'), + url(r'^switch-back/$', profile_views.switch_back, name='a2-switch-back'), ) diff --git a/src/authentic2/profile_views.py b/src/authentic2/profile_views.py index 82790e4..a2e38bf 100644 --- a/src/authentic2/profile_views.py +++ b/src/authentic2/profile_views.py @@ -114,3 +114,7 @@ class PasswordResetConfirmView(cbv.RedirectToNextURLViewMixin, FormView): return utils.login(self.request, self.user, 'email') password_reset_confirm = PasswordResetConfirmView.as_view() + + +def switch_back(request): + return utils.switch_back(request) diff --git a/src/authentic2/settings.py b/src/authentic2/settings.py index 819c4e2..f6e8ea0 100644 --- a/src/authentic2/settings.py +++ b/src/authentic2/settings.py @@ -134,6 +134,7 @@ AUTHENTICATION_BACKENDS = ( 'authentic2.backends.ldap_backend.LDAPBackend', 'authentic2.backends.ldap_backend.LDAPBackendPasswordLost', 'authentic2.backends.models_backend.ModelBackend', + 'authentic2.backends.models_backend.DummyModelBackend', 'django_rbac.backends.DjangoRBACBackend', ) AUTHENTICATION_BACKENDS = plugins.register_plugins_authentication_backends( diff --git a/src/authentic2/templates/authentic2/base.html b/src/authentic2/templates/authentic2/base.html index d579a0b..544b56d 100644 --- a/src/authentic2/templates/authentic2/base.html +++ b/src/authentic2/templates/authentic2/base.html @@ -26,7 +26,7 @@ {% if request.user.is_authenticated %}
{% block user %} -

+

{% blocktrans with request.user.get_full_name as username %}Hello {{ username }}.{% endblocktrans %} {% trans "Logout" %}

diff --git a/src/authentic2/templates/idp/account_management.html b/src/authentic2/templates/idp/account_management.html index a038a67..88a7914 100644 --- a/src/authentic2/templates/idp/account_management.html +++ b/src/authentic2/templates/idp/account_management.html @@ -34,6 +34,9 @@ {% if allow_account_deletion %}

{% trans "Delete profile" %}

{% endif %} +{% if USER_SWITCHED %} +

{% trans "Switch back" %}

+{% endif %}

{% trans "Credentials" %}

{% for html_block in frontends_block %} diff --git a/src/authentic2/utils.py b/src/authentic2/utils.py index 57665a5..8dba86e 100644 --- a/src/authentic2/utils.py +++ b/src/authentic2/utils.py @@ -13,10 +13,11 @@ from importlib import import_module import django from django.conf import settings from django.http import HttpResponseRedirect -from django.core.exceptions import ImproperlyConfigured +from django.core.exceptions import ImproperlyConfigured, PermissionDenied from django.core import urlresolvers from django.http.request import QueryDict -from django.contrib.auth import REDIRECT_FIELD_NAME, login as auth_login +from django.contrib.auth import (REDIRECT_FIELD_NAME, login as auth_login, + SESSION_KEY, HASH_SESSION_KEY, BACKEND_SESSION_KEY, authenticate) from django import forms from django.forms.util import ErrorList from django.forms.utils import to_current_timezone @@ -28,6 +29,8 @@ from django.core.mail import send_mail from django.core import signing from django.core.urlresolvers import reverse from django.utils.formats import localize +from django.contrib import messages +from django.utils.functional import empty try: from django.core.exceptions import FieldDoesNotExist @@ -303,8 +306,9 @@ def login(request, user, how, **kwargs): URL or settings.LOGIN_REDIRECT_URL.''' last_login = user.last_login auth_login(request, user) - if 'last_login' not in request.session: - request.session['last_login'] = localize(to_current_timezone(last_login), True) + if constants.LAST_LOGIN_SESSION_KEY not in request.session: + request.session[constants.LAST_LOGIN_SESSION_KEY] = \ + localize(to_current_timezone(last_login), True) record_authentication_event(request, how) return continue_to_next_url(request, **kwargs) @@ -567,3 +571,48 @@ def lower_keys(d): def to_dict_of_set(d): '''Convert a dictionary of sequence into a dictionary of sets''' return dict((k, set(v)) for k, v in d.iteritems()) + + +def switch_user(request, new_user): + '''Switch to another user and remember currently logged in user in the + session. Reserved to superusers.''' + logger = logging.getLogger(__name__) + if constants.SWITCH_USER_SESSION_KEY in request.session: + messages.error(request, _('Your user is already switched, go to your ' + 'account page and come back to your original ' + 'user to do it again.')) + else: + if not request.user.is_superuser: + raise PermissionDenied + switched = {} + for key in (SESSION_KEY, BACKEND_SESSION_KEY, HASH_SESSION_KEY, + constants.LAST_LOGIN_SESSION_KEY): + switched[key] = request.session[key] + user = authenticate(user=new_user) + auth_login(request, user) + request.session[constants.SWITCH_USER_SESSION_KEY] = switched + if constants.LAST_LOGIN_SESSION_KEY not in request.session: + request.session[constants.LAST_LOGIN_SESSION_KEY] = \ + localize(to_current_timezone(new_user.last_login), True) + messages.info(request, _('Successfully switched to user %s') % + new_user.get_full_name()) + logging.info('switched to user %s', new_user) + return continue_to_next_url(request) + + +def switch_back(request): + '''Switch back to original superuser after a user switch''' + logger = logging.getLogger(__name__) + if constants.SWITCH_USER_SESSION_KEY in request.session: + switched = request.session[constants.SWITCH_USER_SESSION_KEY] + for key in switched: + request.session[key] = switched[key] + del request.session[constants.SWITCH_USER_SESSION_KEY] + del request._cached_user + request.user._wrapped = empty + messages.info(request, _('Successfully switched back to user %s') % + request.user.get_full_name()) + logger.info('switched back to user %s', request.user) + else: + messages.warning(request, _('No user to switch back to')) + return continue_to_next_url(request) -- 2.1.4