68c20a30a328 — Leonhard Kuboschek 8 months ago
add option for auto-expiring impersonate sesions, implements #45
M README.md +7 -0
@@ 395,6 395,13 @@ can alter logs within the Django admin a
 
 Default is `True`
 
+    MAX_DURATION
+
+A number specifying the maximum allowed duration of impersonation
+sessions in **seconds**.
+
+Default is `None`
+
 Admin
 =====
 

          
M README.rst +9 -0
@@ 464,6 464,15 @@ admin users can alter logs within the Dj
 
 Default is ``True``
 
+::
+
+    MAX_DURATION
+
+A number specifying the maximum allowed duration of impersonation
+sessions in **seconds**.
+
+Default is `None`
+
 Admin
 =====
 

          
M impersonate/middleware.py +19 -0
@@ 1,5 1,8 @@ 
 # -*- coding: utf-8 -*-
+from datetime import datetime, timedelta
+
 from django.http import HttpResponseNotAllowed
+from django.utils import timezone
 from django.utils.deprecation import MiddlewareMixin
 
 from .helpers import User, check_allow_for_uri, check_allow_for_user

          
@@ 12,6 15,22 @@ class ImpersonateMiddleware(MiddlewareMi
         request.impersonator = None
 
         if request.user.is_authenticated and '_impersonate' in request.session:
+            if settings.MAX_DURATION:
+                if '_impersonate_start' not in request.session:
+                    return
+
+                start_time = datetime.fromtimestamp(
+                    request.session['_impersonate_start'], timezone.utc
+                )
+
+                if datetime.now(timezone.utc) - start_time > timedelta(
+                    seconds=settings.MAX_DURATION
+                ):
+                    del request.session['_impersonate']
+                    del request.session['_impersonate_start']
+
+                    return
+
             new_user_id = request.session['_impersonate']
             if isinstance(new_user_id, User):
                 # Edge case for issue 15

          
M impersonate/settings.py +1 -0
@@ 23,6 23,7 @@ username_field = getattr(User, 'USERNAME
     'ADMIN_DELETE_PERMISSION': False,
     'ADMIN_ADD_PERMISSION': False,
     'ADMIN_READ_ONLY': True,
+    'MAX_DURATION': None,
 }
 
 

          
M impersonate/tests.py +46 -2
@@ 19,18 19,19 @@ 
         is_superuser = False
         is_staff = False
 '''
+from datetime import datetime, timedelta, timezone
 from distutils.version import LooseVersion
 from unittest.mock import PropertyMock, patch
 from urllib.parse import urlencode, urlsplit
 
 import django
-from django.urls import include, path
 from django.contrib.admin.sites import AdminSite
 from django.contrib.auth import get_user_model
 from django.http import HttpResponse
 from django.test import TestCase
 from django.test.client import Client, RequestFactory
 from django.test.utils import override_settings
+from django.urls import include, path
 from django.utils.duration import duration_string
 
 from .admin import (

          
@@ 108,13 109,15 @@ class TestMiddleware(TestCase):
 
         def dummy_get_response(request):
             return None
+
         self.middleware = ImpersonateMiddleware(dummy_get_response)
 
-    def _impersonated_request(self, use_id=True):
+    def _impersonated_request(self, use_id=True, _impersonate_start=None):
         request = self.factory.get('/')
         request.user = self.superuser
         request.session = {
             '_impersonate': self.user.pk if use_id else self.user,
+            '_impersonate_start': _impersonate_start,
         }
         self.middleware.process_request(request)
 

          
@@ 133,6 136,47 @@ class TestMiddleware(TestCase):
         '''
         self._impersonated_request(use_id=False)
 
+    @override_settings(IMPERSONATE={'MAX_DURATION': 3600})
+    def test_impersonated_request_with_max_duration(self):
+        self._impersonated_request(
+            _impersonate_start=datetime.now(timezone.utc).timestamp()
+        )
+
+    @override_settings(IMPERSONATE={'MAX_DURATION': 3600})
+    def test_reject_without_start_time(self):
+        ''' Test to ensure that requests without a start time
+            are rejected when MAX_DURATION is set
+        '''
+        request = self.factory.get('/')
+        request.user = self.superuser
+        request.session = {
+            '_impersonate': self.user.pk,
+        }
+        self.middleware.process_request(request)
+
+        self.assertEqual(request.user, self.superuser)
+        self.assertFalse(request.user.is_impersonate)
+
+    @override_settings(IMPERSONATE={'MAX_DURATION': 3600})
+    def test_reject_expired_impersonation(self):
+        ''' Test to ensure that requests with a start time before (now - MAX_DURATION)
+            are rejected
+        '''
+        request = self.factory.get('/')
+        request.user = self.superuser
+        request.session = {
+            '_impersonate': self.user.pk,
+            '_impersonate_start': (
+                datetime.now(timezone.utc) - timedelta(seconds=3601)
+            ).timestamp(),
+        }
+        self.middleware.process_request(request)
+
+        self.assertEqual(request.user, self.superuser)
+        self.assertFalse(request.user.is_impersonate)
+        self.assertNotIn('_impersonate', request.session)
+        self.assertNotIn('_impersonate_start', request.session)
+
     def test_not_impersonated_request(self, use_id=True):
         """Check the real_user request attr is set correctly when **not** impersonating."""
         request = self.factory.get('/')

          
M impersonate/views.py +5 -0
@@ 1,9 1,11 @@ 
 # -*- coding: utf-8 -*-
 import logging
+from datetime import datetime
 
 from django.db.models import Q
 from django.http import Http404
 from django.shortcuts import get_object_or_404, redirect, render
+from django.utils import timezone
 
 from .decorators import allowed_user_required
 from .helpers import (

          
@@ 36,6 38,9 @@ def impersonate(request, uid):
         raise Http404('Invalid value given.')
     if check_allow_for_user(request, new_user):
         request.session['_impersonate'] = new_user.pk
+        request.session['_impersonate_start'] = datetime.now(
+            tz=timezone.utc
+        ).timestamp()
         prev_path = request.META.get('HTTP_REFERER')
         if prev_path:
             request.session[