Added CUSTOM_READ_ONLY setting to customise when to restrict to read only access - Refs #70
M CHANGELOG +6 -0
@@ 1,6 1,12 @@ 
 Changes
 -------
 
+1.9.0 (unreleased)
+
+- Prevent redirect loop when MAX_DURATION is used. Refs ~petersanchez/django-impersonate#67
+- Allow OPTIONS requests when READ_ONLY is True. Refs ~petersanchez/django-impersonate#69
+- Added CUSTOM_READ_ONLY to customise when to restrict to read only access. Refs ~petersanchez/django-impersonate#70
+
 1.8.1 (2022-02-17)
 
 - Patch version bump for README updates. I know... I'm shameful.

          
M README.md +15 -2
@@ 241,11 241,24 @@ Value should be a string containing the 
     READ_ONLY
 
 A boolean that if set to `True` any requests that are not either `GET` or
-`HEAD` will result in a "Bad Request" response (status code 405). Use this if
-you want to limit your impersonating users to read only impersonation sessions.
+`HEAD` or `OPTIONS` will result in a "Bad Request" response (status code 405).
+Use this if you want to limit your impersonating users to read only
+impersonation sessions.
 
 Value should be a boolean, defaults to `False`
 
+If the `CUSTOM_READ_ONLY` is set, then that custom function is used, and this
+setting is ignored.
+
+    CUSTOM_READ_ONLY
+
+A string that represents a function (e.g. `module.submodule.mod.function_name`)
+that allows more fine grained control over who has read only access. It takes
+one argument, the request object, and should return True to restrict the user
+to only allow `GET`, `HEAD` and `OPTIONS` requests.
+
+It is optional, and if it is not present, `READ_ONLY` setting value applies.
+
     USE_HTTP_REFERER
 
 If this is set to `True`, then the app will attempt to be redirect you to

          
M impersonate/helpers.py +11 -0
@@ 128,3 128,14 @@ def check_allow_for_uri(uri):
         if re.search(exclusion, uri):
             return False
     return True
+
+
+def check_read_only(request):
+    ''' Returns True if can only do read only requests.
+        Uses the CUSTOM_READ_ONLY function if required, else
+        looks at the READ_ONLY setting.
+    '''
+    if settings.CUSTOM_READ_ONLY is not None:
+        custom_read_only_func = import_func_from_string(settings.CUSTOM_READ_ONLY)
+        return custom_read_only_func(request)
+    return settings.READ_ONLY

          
M impersonate/middleware.py +2 -2
@@ 7,7 7,7 @@ from django.utils import timezone
 from django.utils.deprecation import MiddlewareMixin
 from django.utils.functional import SimpleLazyObject
 
-from .helpers import User, check_allow_for_uri, check_allow_for_user
+from .helpers import User, check_allow_for_uri, check_allow_for_user, check_read_only
 from .settings import settings
 
 

          
@@ 50,7 50,7 @@ class ImpersonateMiddleware(MiddlewareMi
             except User.DoesNotExist:
                 return
 
-            if settings.READ_ONLY and request.method not in ['GET', 'HEAD', 'OPTIONS']:
+            if check_read_only(request) and request.method not in ['GET', 'HEAD', 'OPTIONS']:
                 return HttpResponseNotAllowed(['GET', 'HEAD', 'OPTIONS'])
 
             if check_allow_for_user(request, new_user) and check_allow_for_uri(

          
M impersonate/settings.py +1 -0
@@ 20,6 20,7 @@ username_field = getattr(User, 'USERNAME
     'SEARCH_FIELDS': [username_field, 'first_name', 'last_name', 'email'],
     'REDIRECT_URL': getattr(django_settings, 'LOGIN_REDIRECT_URL', u'/'),
     'READ_ONLY': False,
+    'CUSTOM_READ_ONLY': None,
     'ADMIN_DELETE_PERMISSION': False,
     'ADMIN_ADD_PERMISSION': False,
     'ADMIN_READ_ONLY': True,

          
M impersonate/tests.py +33 -0
@@ 89,6 89,13 @@ def test_qs(request):
     return User.objects.all().order_by('pk')
 
 
+def test_allow_read_only(request):
+    ''' Used via the IMPERSONATE['CUSTOM_READ_ONLY'] setting.
+        Simple check that the user is not a superuser.
+    '''
+    return not request.user.is_superuser
+
+
 class UserFactory(object):
     @staticmethod
     def create(**kwargs):

          
@@ 852,3 859,29 @@ class TestImpersonation(TestCase):
         self.assertEqual(resp.status_code, 200)
         resp = self.client.options(reverse('impersonate-test'))
         self.assertEqual(resp.status_code, 200)
+
+    @override_settings(IMPERSONATE={'CUSTOM_READ_ONLY': 'impersonate.tests.test_allow_read_only'})
+    def test_impersonate_custom_read_only(self):
+        # superuser is able to do all requests
+        self._impersonate_helper('user1', 'foobar', 4)
+        resp = self.client.post(reverse('impersonate-test'))
+        self.assertEqual(resp.status_code, 200)
+        resp = self.client.get(reverse('impersonate-test'))
+        self.assertEqual(resp.status_code, 200)
+        resp = self.client.head(reverse('impersonate-test'))
+        self.assertEqual(resp.status_code, 200)
+        resp = self.client.options(reverse('impersonate-test'))
+        self.assertEqual(resp.status_code, 200)
+        self.client.logout()
+
+        # staff user is only able to do read only requests
+        self._impersonate_helper('user3', 'foobar', 4)
+        resp = self.client.post(reverse('impersonate-test'))
+        self.assertEqual(resp.status_code, 405)
+        resp = self.client.get(reverse('impersonate-test'))
+        self.assertEqual(resp.status_code, 200)
+        resp = self.client.head(reverse('impersonate-test'))
+        self.assertEqual(resp.status_code, 200)
+        resp = self.client.options(reverse('impersonate-test'))
+        self.assertEqual(resp.status_code, 200)
+        self.client.logout()