diff --git a/backend/mixins.py b/backend/mixins.py index bdde115e45091116aca91926a22e38742370a399..95a48a3dffdfa56d16ca22fe0f463ebffc0d86c6 100644 --- a/backend/mixins.py +++ b/backend/mixins.py @@ -22,30 +22,18 @@ class OverrideView(Exception): class VisitorMixin(NM2LayoutMixin): """ - Add self.visitor and self.impersonator to the View for the person visiting - the site + Add self.visitor to the View for the person visiting the site """ # Define to "dd" "am" or "admin" to raise PermissionDenied if the # given test on the visitor fails require_visitor = None def set_visitor_info(self): - self.impersonator = None - if not self.request.user.is_authenticated: self.visitor = None else: self.visitor = self.request.user - # Implement impersonation if requested in session - if self.visitor.is_admin: - key = self.request.session.get("impersonate", None) - if key is not None: - p = bmodels.Person.lookup(key) - if p is not None: - self.impersonator = self.visitor - self.visitor = p - def load_objects(self): """ Hook to set self.* members from request parameters, so that they are @@ -79,7 +67,6 @@ class VisitorMixin(NM2LayoutMixin): def get_context_data(self, **kw): ctx = super(VisitorMixin, self).get_context_data(**kw) ctx["visitor"] = self.visitor - ctx["impersonator"] = self.impersonator return ctx @@ -121,11 +108,6 @@ class VisitPersonMixin(VisitorMixin): if "am_candidate" in self.person.perms: res.append(NavLink( reverse("admin:backend_am_add") + f"?person={self.person.id}", _("Make AM"))) - if self.visitor != self.person: - res.append(NavLink( - reverse("impersonate", kwargs={ - "key": self.person.lookup_key}) + f"?url={self.request.build_absolute_uri()}", - _("Impersonate"), "random")) if self.person.is_dd: res.append(NavLink( reverse("mia_wat_ping", kwargs={"key": self.person.lookup_key}), _("WAT ping"), "heartbeat")) diff --git a/backend/models.py b/backend/models.py index 0469bf088b88c5b070d1123f74d96195313ad0bf..02000576fae2736c66906d089d32386960fe176e 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 abc0b1b7c516621b89ed17100e7dd91096ee33f0..13a2f1f296faa6aaf311d19a72054635665bf78c 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/__init__.py b/impersonate/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/impersonate/admin.py b/impersonate/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..8c38f3f3dad51e4585f3984282c2a4bec5349c1e --- /dev/null +++ b/impersonate/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/impersonate/apps.py b/impersonate/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..813354fd2598c7da2a4e471b88a9654d404bfed9 --- /dev/null +++ b/impersonate/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ImpersonateConfig(AppConfig): + name = 'impersonate' diff --git a/impersonate/middleware.py b/impersonate/middleware.py new file mode 100644 index 0000000000000000000000000000000000000000..1d3fcc12bf3ff5cfc4b6320a99c12869e1676114 --- /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/migrations/__init__.py b/impersonate/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/impersonate/models.py b/impersonate/models.py new file mode 100644 index 0000000000000000000000000000000000000000..71a836239075aa6e6e4ecb700e9c42c95c022d91 --- /dev/null +++ b/impersonate/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/impersonate/tests.py b/impersonate/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..e655703b195ade8b09ff98216176c55d5a58dae2 --- /dev/null +++ b/impersonate/tests.py @@ -0,0 +1,63 @@ +from __future__ import annotations +from django.test import TestCase +from django.urls import reverse +from backend.unittest import TestBase +from django.contrib.auth import get_user_model + + +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) + + response = client.get(reverse("impersonate:whoami")) + self.assertJSONEqual(response.content, { + 'impersonator': None, + 'impersonator_desc': None, + 'user': visitor.pk, + 'user_desc': str(visitor), + }) + + response = client.post(reverse("impersonate:impersonate"), data={"pk": visited.pk, "next": "/"}) + self.assertRedirectMatches(response, "^/$") + + 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: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 new file mode 100644 index 0000000000000000000000000000000000000000..0dfa00731f56ffc010c86a2bb818460f93c71c0d --- /dev/null +++ b/impersonate/urls.py @@ -0,0 +1,10 @@ +from __future__ import annotations +from django.urls import path +from . import views + +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 new file mode 100644 index 0000000000000000000000000000000000000000..83e52f43dc153601bd698de65ae35853aec9ea56 --- /dev/null +++ b/impersonate/views.py @@ -0,0 +1,47 @@ +from __future__ import annotations +from django.utils.translation import ugettext as _ +from django.views.generic import View +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): + def post(self, request, *args, **kw): + User = get_user_model() + 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_staff: + raise PermissionDenied + pk = request.POST.get("pk") + if pk is None: + del request.session["impersonate"] + messages.add_message(request, messages.INFO, _("Impersonation canceled")) + user = effective_user + else: + try: + user = User.objects.get(pk=pk) + except User.DoesNotExist: + raise PermissionDenied + request.session["impersonate"] = user.pk + messages.info(request, _("Impersonating {}").format(user)) + + url = request.POST.get("next", None) + if url is None: + 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 20c9e470836530cd7d17c68e194db0adf39ac24c..a7aa49089a77b51836681b33b606511600ea4e60 100644 --- a/nm2/settings.py +++ b/nm2/settings.py @@ -74,6 +74,7 @@ INSTALLED_APPS = [ 'sitechecks', 'deploy', 'signon', + 'impersonate', ] MIDDLEWARE = [ @@ -87,6 +88,7 @@ MIDDLEWARE = [ 'signon.middleware.SignonMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'impersonate.middleware.ImpersonateMiddleware', ] AUTHENTICATION_BACKENDS = [ diff --git a/nm2/urls.py b/nm2/urls.py index 1bdc10281ffa776dbf21e02bf4e87320bc4a5119..4c27f48e963a3d420a3ac314bddca0f6de7eef44 100644 --- a/nm2/urls.py +++ b/nm2/urls.py @@ -44,10 +44,9 @@ urlpatterns = [ path('minechangelogs/', include("minechangelogs.urls")), path('sitechecks/', include("sitechecks.urls")), path('deploy/', include("deploy.urls")), - path('rest/api/', include(router.urls)), - path('signon/', include("signon.urls")), + path('impersonate/', include("impersonate.urls")), # Uncomment the admin/doc line below to enable admin documentation: path('admin/doc/', include('django.contrib.admindocs.urls')), diff --git a/nmlayout/templates/nm2-base.html b/nmlayout/templates/nm2-base.html index c9c1ffa652c0b4cc1fccf6105831be082383ec41..a6f10d24b85339d8c8b54ff52998dec8a1843d45 100644 --- a/nmlayout/templates/nm2-base.html +++ b/nmlayout/templates/nm2-base.html @@ -37,6 +37,13 @@ window.nm2.url_api_people = "{% url 'api_people' %}"; {% for link in navbar.person %} {% if link.icon %} {% endif %}{{link.label}} {% endfor %} + {% if visitor != person %} +
{% csrf_token %} + + + +
+ {% endif %} {% endif %} @@ -71,8 +78,11 @@ window.nm2.url_api_people = "{% url 'api_people' %}";