Received: from mail.netlandish.com (mail.netlandish.com [174.136.98.166])
	by code.netlandish.com (Postfix) with ESMTP id BF0FF8317C
	for <~petersanchez/public-inbox@lists.code.netlandish.com>; Sun, 19 Feb 2023 20:59:39 +0000 (UTC)
Received-SPF: Pass (mailfrom) identity=mailfrom; client-ip=209.85.128.41; helo=mail-wm1-f41.google.com; envelope-from=sarahvboyce95@gmail.com; receiver=<UNKNOWN> 
Authentication-Results: mail.netlandish.com;
	dkim=pass (2048-bit key; unprotected) header.d=gmail.com header.i=@gmail.com header.b=MWpsKlac
Received: from mail-wm1-f41.google.com (mail-wm1-f41.google.com [209.85.128.41])
	by mail.netlandish.com (Postfix) with ESMTP id B3919152FB1
	for <~petersanchez/public-inbox@lists.code.netlandish.com>; Sun, 19 Feb 2023 20:59:38 +0000 (UTC)
Received: by mail-wm1-f41.google.com with SMTP id p18-20020a05600c359200b003dc57ea0dfeso931508wmq.0
        for <~petersanchez/public-inbox@lists.code.netlandish.com>; Sun, 19 Feb 2023 12:59:38 -0800 (PST)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
        d=gmail.com; s=20210112;
        h=to:from:date:user-agent:references:in-reply-to:message-id:subject
         :content-transfer-encoding:mime-version:from:to:cc:subject:date
         :message-id:reply-to;
        bh=7u+oRyppRIaYNmDDVrvJj0UZL2YogIME2WuLtNx+h2k=;
        b=MWpsKlacfH3NTQ46IdDhg6UaloptDtYIRYkkrQK98AiXakDDgIMaQ/Sm0/cwumzdLZ
         4gD5+MeZlzjF8PHTgrV+0pYUOQp60QzwqCYjMypeF5wTbQ4U/WBK1b2HC1u8uCVP9ILe
         IGal772oMg02McVdL2Uc2a/wz1pLJnLURcQLRoG+Eta4xm3EDIbaEdqAc/HnSxm8e596
         ZA4F0yU9/C4R7arO5VTfZhEQGnWIhA7JOyz8uhiOeH4nJSnXL+Vuh2IhLzwK9B0fFvhS
         OhvAfDizjO7eQ5Ps+ucffzmzO9rBnB7ekmHAoAFxxrsHZCEpRqP28Yf4GygjBMgJ97Hj
         osMw==
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
        d=1e100.net; s=20210112;
        h=to:from:date:user-agent:references:in-reply-to:message-id:subject
         :content-transfer-encoding:mime-version:x-gm-message-state:from:to
         :cc:subject:date:message-id:reply-to;
        bh=7u+oRyppRIaYNmDDVrvJj0UZL2YogIME2WuLtNx+h2k=;
        b=6ngQVQd5pD3CVyI7lc4XRBjisxN8pwagaFnDORAdh4DY8M/arG64v/Un7T9FlXB/Ob
         tlT8nK7Ge/4y4qipvyhApn9ph/LgVgA1xXptpT5HLY7JIOQPA+wMhuuych3jwHgQfFWX
         UUaFwEONzd2I3qplaa5nSKk2z9u9Q/34HatQ5L7WQGSftIsbR38NvglT5JY6gGdu5LhC
         cAKb0QMPp2iaD9UHtMGGJQLNPH4irwtEjed+9zjPcLW8RXIz8qKJdO5p4gIOrUYNc8pc
         YgCZlsmdU+XJV7EE/H+mJ85hSv5x6OS7kLyEwmfIR821gHcN1wrtPyMVvTeBq6+ulLYH
         FCzA==
X-Gm-Message-State: AO0yUKXQ36tR4AsSv+be4Ei+ZZxW7LlZP/ErpQAUwxqT/b/Zg+WiKxZC
	esjwvBpc3uuyJrASmKO237OCi3nnWuo=
X-Google-Smtp-Source: AK7set+q9BqKqxX+caGnFWgziZmjTvBJI8y6B3e9GS0Z7O6t3eWvVdSGqPG+Nc1jFpfQQ0oJDoSdAw==
X-Received: by 2002:a05:600c:3414:b0:3e2:153e:6940 with SMTP id y20-20020a05600c341400b003e2153e6940mr9226483wmp.3.1676840377267;
        Sun, 19 Feb 2023 12:59:37 -0800 (PST)
Received: from localhost.localdomain (2a0a-a541-b663-0-f165-7174-1322-624e.ipv6dyn.netcologne.de. [2a0a:a541:b663:0:f165:7174:1322:624e])
        by smtp.gmail.com with ESMTPSA id y25-20020a1c4b19000000b003dc4480df80sm8547100wma.34.2023.02.19.12.59.36
        for <~petersanchez/public-inbox@lists.code.netlandish.com>
        (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);
        Sun, 19 Feb 2023 12:59:36 -0800 (PST)
MIME-Version: 1.0
Content-Type: text/plain; charset="us-ascii"
Content-Transfer-Encoding: 7bit
Subject: [PATCH 2 of 2 django-impersonate] Added CUSTOM_READ_ONLY setting to
 customise
 when to restrict to read only access - Refs #70
X-Mercurial-Node: 49802f32cb83c01c77abf49bd0743055f8331306
X-Mercurial-Series-Index: 2
X-Mercurial-Series-Total: 2
Message-Id: <49802f32cb83c01c77ab.1676840364@localhost.localdomain>
X-Mercurial-Series-Id: <77c3932f8751d8457a92.1676840363@localhost.localdomain>
In-Reply-To: <77c3932f8751d8457a92.1676840363@localhost.localdomain>
References: <77c3932f8751d8457a92.1676840363@localhost.localdomain>
User-Agent: Mercurial-patchbomb/6.2.3
Date: Sun, 19 Feb 2023 21:59:24 +0100
From: =?iso-8859-1?q?Sarah_Boyce?= <sarahvboyce95@gmail.com>
To: ~petersanchez/public-inbox@lists.code.netlandish.com

# HG changeset patch
# User Sarah Boyce <sarahvboyce95@gmail.com>
# Date 1676839415 -3600
#      Sun Feb 19 21:43:35 2023 +0100
# Node ID 49802f32cb83c01c77abf49bd0743055f8331306
# 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()

