# HG changeset patch # User Sarah Boyce # Date 1676839415 -3600 # Sun Feb 19 21:43:35 2023 +0100 # Node ID a70be42b2e05d6a6a67cc93eae3f3b1b812122ff # Parent 77c3932f8751d8457a92596acb8b6a8ba2f73dbb Added CUSTOM_READ_ONLY setting to customise when to restrict to read only access - Refs #70 diff --git a/CHANGELOG b/CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -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. diff --git a/README.md b/README.md --- a/README.md +++ b/README.md @@ -241,11 +241,24 @@ 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 diff --git a/impersonate/helpers.py b/impersonate/helpers.py --- a/impersonate/helpers.py +++ b/impersonate/helpers.py @@ -128,3 +128,14 @@ 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 diff --git a/impersonate/middleware.py b/impersonate/middleware.py --- a/impersonate/middleware.py +++ b/impersonate/middleware.py @@ -7,7 +7,7 @@ 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 @@ 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( diff --git a/impersonate/settings.py b/impersonate/settings.py --- a/impersonate/settings.py +++ b/impersonate/settings.py @@ -20,6 +20,7 @@ '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, diff --git a/impersonate/tests.py b/impersonate/tests.py --- a/impersonate/tests.py +++ b/impersonate/tests.py @@ -89,6 +89,13 @@ 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 @@ 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()