From 33905a7fe3c9a039305724a3b3f359a4fec2191d Mon Sep 17 00:00:00 2001 From: Enrico Zini Date: Sat, 25 Apr 2020 13:03:22 +0200 Subject: [PATCH] Impersonate through a middleware. refs: #12 --- backend/models.py | 1 + backend/unittest.py | 6 ++-- impersonate/middleware.py | 34 +++++++++++++++++++ impersonate/tests.py | 70 +++++++++++++++++++++++++++++---------- impersonate/urls.py | 1 + impersonate/views.py | 14 +++++++- nm2/settings.py | 1 + 7 files changed, 106 insertions(+), 21 deletions(-) create mode 100644 impersonate/middleware.py diff --git a/backend/models.py b/backend/models.py index 0469bf0..0200057 100644 --- a/backend/models.py +++ b/backend/models.py @@ -49,6 +49,7 @@ class PersonManager(BaseUserManager): def create_superuser(self, email, **other_fields): other_fields["is_superuser"] = True + other_fields["is_staff"] = True return self.create_user(email, **other_fields) def get_or_none(self, *args, **kw): diff --git a/backend/unittest.py b/backend/unittest.py index abc0b1b..13a2f1f 100644 --- a/backend/unittest.py +++ b/backend/unittest.py @@ -103,7 +103,8 @@ class TestBase(nm2.lib.unittest.TestBase): else: raise NotImplementedError(f"{identity.issuer} not supported as identity during testing") - person = self.persons[person] + if isinstance(person, str): + person = self.persons[person] if person is not None: client.force_login(person, backend=self.TEST_AUTH_BACKEND) client.visitor = person @@ -128,7 +129,8 @@ class TestBase(nm2.lib.unittest.TestBase): else: raise NotImplementedError(f"{identity.issuer} not supported as identity during testing") - person = self.persons[person] + if isinstance(person, str): + person = self.persons[person] if person is not None: client.force_login(person, backend=self.TEST_AUTH_BACKEND) client.visitor = person diff --git a/impersonate/middleware.py b/impersonate/middleware.py new file mode 100644 index 0000000..1d3fcc1 --- /dev/null +++ b/impersonate/middleware.py @@ -0,0 +1,34 @@ +from __future__ import annotations +from django.core.exceptions import ImproperlyConfigured +from django.contrib.auth import get_user_model + + +class ImpersonateMiddleware: + def __init__(self, get_response): + self.get_response = get_response + self.User = get_user_model() + + def __call__(self, request): + # AuthenticationMiddleware is required so that request.user exists. + if not hasattr(request, 'user'): + raise ImproperlyConfigured( + "The impersonator middleware requires the authentication middleware" + " to be installed. Edit your MIDDLEWARE setting to insert" + " 'django.contrib.auth.middleware.AuthenticationMiddleware'" + " before the ImpersonateMiddleware class.") + + if request.user.is_authenticated: + # Implement impersonation if requested in session + if request.user.is_staff: + pk = request.session.get("impersonate", None) + if pk is not None: + try: + user = self.User.objects.get(pk=pk) + except self.User.DoesNotExist: + user = None + + if user is not None: + request.impersonator = request.user + request.user = user + + return self.get_response(request) diff --git a/impersonate/tests.py b/impersonate/tests.py index 1c0f2db..e655703 100644 --- a/impersonate/tests.py +++ b/impersonate/tests.py @@ -1,29 +1,63 @@ from __future__ import annotations from django.test import TestCase from django.urls import reverse -from backend.unittest import PersonFixtureMixin +from backend.unittest import TestBase +from django.contrib.auth import get_user_model -class TestPermissions(PersonFixtureMixin, TestCase): - @classmethod - def __add_extra_tests__(cls): - non_fd = ["pending", "dc", "dc_ga", "dm", "dm_ga", "dd_nu", "dd_u", "dd_e", "dd_r", "activeam", "oldam"] - fd = ["fd", "dam"] - - for visitor in [None] + non_fd: - for visited in non_fd + fd: - cls._add_method(cls._test_impersonate_fail, visitor, visited) +class TestPermissions(TestBase, TestCase): + def test_impersonate_staff(self): + User = get_user_model() + visitor = User.objects.create_superuser(email="admin@example.org", fullname="Admin", audit_skip=True) + visited = User.objects.create_user(email="user@example.org", fullname="User", audit_skip=True) + client = self.make_test_client(visitor) - for visitor in fd: - for visited in non_fd + fd: - cls._add_method(cls._test_impersonate_success, visitor, visited) + response = client.get(reverse("impersonate:whoami")) + self.assertJSONEqual(response.content, { + 'impersonator': None, + 'impersonator_desc': None, + 'user': visitor.pk, + 'user_desc': str(visitor), + }) - def _test_impersonate_success(self, visitor, visited): - client = self.make_test_client(visitor) - response = client.post(reverse("impersonate"), data={"pk": self.persons[visited].pk, "next": "/"}) + response = client.post(reverse("impersonate:impersonate"), data={"pk": visited.pk, "next": "/"}) self.assertRedirectMatches(response, "^/$") - def _test_impersonate_fail(self, visitor, visited): + response = client.get(reverse("impersonate:whoami")) + self.assertJSONEqual(response.content, { + 'impersonator': visitor.pk, + 'impersonator_desc': str(visitor), + 'user': visited.pk, + 'user_desc': str(visited), + }) + + def test_impersonate_user(self): + User = get_user_model() + visitor = User.objects.create_user(email="user@example.org", fullname="User", audit_skip=True) + visited = User.objects.create_user(email="user1@example.org", fullname="User1", audit_skip=True) client = self.make_test_client(visitor) - response = client.post(reverse("impersonate"), data={"pk": self.persons[visited].pk}) + response = client.post(reverse("impersonate:impersonate"), data={"pk": visited.pk}) self.assertPermissionDenied(response) + + response = client.get(reverse("impersonate:whoami")) + self.assertJSONEqual(response.content, { + 'impersonator': None, + 'impersonator_desc': None, + 'user': visitor.pk, + 'user_desc': str(visitor), + }) + + def test_impersonate_anonymous(self): + User = get_user_model() + visited = User.objects.create_user(email="user@example.org", fullname="User", audit_skip=True) + client = self.make_test_client(None) + response = client.post(reverse("impersonate:impersonate"), data={"pk": visited.pk}) + self.assertPermissionDenied(response) + + response = client.get(reverse("impersonate:whoami")) + self.assertJSONEqual(response.content, { + 'impersonator': None, + 'impersonator_desc': None, + 'user': None, + 'user_desc': "AnonymousUser", + }) diff --git a/impersonate/urls.py b/impersonate/urls.py index dce4619..0dfa007 100644 --- a/impersonate/urls.py +++ b/impersonate/urls.py @@ -6,4 +6,5 @@ app_name = "impersonate" urlpatterns = [ # Impersonate a user path('impersonate/', views.Impersonate.as_view(), name="impersonate"), + path('whoami/', views.Whoami.as_view(), name="whoami"), ] diff --git a/impersonate/views.py b/impersonate/views.py index eab368c..83e52f4 100644 --- a/impersonate/views.py +++ b/impersonate/views.py @@ -5,6 +5,7 @@ from django.shortcuts import redirect from django.core.exceptions import PermissionDenied from django.contrib import messages from django.contrib.auth import get_user_model +from django import http class Impersonate(View): @@ -13,7 +14,7 @@ class Impersonate(View): effective_user = getattr(request, "impersonator", None) if effective_user is None: effective_user = request.user - if not effective_user.is_authenticated or not effective_user.is_admin: + if not effective_user.is_authenticated or not effective_user.is_staff: raise PermissionDenied pk = request.POST.get("pk") if pk is None: @@ -33,3 +34,14 @@ class Impersonate(View): return redirect(user.get_absolute_url()) else: return redirect(url) + + +class Whoami(View): + def get(self, request, *args, **kw): + impersonator = getattr(request, "impersonator", None) + return http.JsonResponse({ + "user": request.user.pk, + "user_desc": str(request.user), + "impersonator": None if impersonator is None else impersonator.pk, + "impersonator_desc": None if impersonator is None else str(impersonator), + }) diff --git a/nm2/settings.py b/nm2/settings.py index 868d1d3..a7aa490 100644 --- a/nm2/settings.py +++ b/nm2/settings.py @@ -88,6 +88,7 @@ MIDDLEWARE = [ 'signon.middleware.SignonMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'impersonate.middleware.ImpersonateMiddleware', ] AUTHENTICATION_BACKENDS = [ -- GitLab