Skip to content

Commit 2abde7d

Browse files
committed
evaluate history model permissions explicitly, settings.SIMPLE_HISTORY_ENFORCE_HISTORY_MODEL_PERMISSIONS
1 parent 3070deb commit 2abde7d

File tree

6 files changed

+370
-31
lines changed

6 files changed

+370
-31
lines changed

CHANGES.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ Unreleased
77
- Fixed typos in the docs
88
- Removed n+1 query from ``bulk_create_with_history`` utility (gh-975)
99
- Started using ``exists`` query instead of ``count`` in ``populate_history`` command (gh-982)
10+
- add feature to evaluate ``history`` model permisions explicitly when
11+
``SIMPLE_HISTORY_ENFORCE_HISTORY_MODEL_PERMISSIONS`` is set to ``True``
12+
in ``settings``.
1013

1114
3.1.1 (2022-04-23)
1215
------------------

docs/admin.rst

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,86 @@ admin class
7272
Disabling the option to revert an object
7373
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
7474

75-
By default, an object can be reverted to its previous version. To disable this option update your settings with the following:
75+
By default, an object can be reverted to its previous version. To disable this option globally update your settings with the following:
7676

7777
.. code-block:: python
7878
79-
SIMPLE_HISTORY_REVERT_DISABLED=True
79+
SIMPLE_HISTORY_REVERT_DISABLED = True
8080
8181
When ``SIMPLE_HISTORY_REVERT_DISABLED`` is set to ``True``, the revert button is removed from the form.
8282

8383
.. image:: screens/10_revert_disabled.png
84+
85+
Enabling history model permissions in Admin
86+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
87+
88+
To have admin evaluate history model permissions explicitly, updating your settings
89+
with the following:
90+
91+
.. code-block:: python
92+
93+
SIMPLE_HISTORY_ENFORCE_HISTORY_MODEL_PERMISSIONS = True
94+
95+
By default ``SIMPLE_HISTORY_ENFORCE_HISTORY_MODEL_PERMISSIONS`` is set to ``False``.
96+
When set to ``False``, permissions applied to the ``Poll`` model also apply to the
97+
history model. That is, granting view and change permissions to the ``Poll`` model
98+
implicitly grants view and change permissions to the ``Poll`` history model.
99+
100+
The user below has view and change permissions to the ``Poll`` model and the ``Poll``
101+
history model in admin.
102+
103+
.. code-block:: python
104+
105+
user.user_permissions.clear()
106+
user.user_permissions.add(
107+
Permission.objects.get(codename="view_planet"),
108+
Permission.objects.get(codename="change_planet"),
109+
)
110+
111+
The user below has view permission to the ``Poll`` model and the ``Poll`` history model
112+
in admin.
113+
114+
.. code-block:: python
115+
116+
user.user_permissions.clear()
117+
user.user_permissions.add(
118+
Permission.objects.get(codename="view_planet"),
119+
)
120+
121+
When ``SIMPLE_HISTORY_ENFORCE_HISTORY_MODEL_PERMISSIONS`` is set to ``True``, permissions to
122+
history models are assigned and evaluated explicitly.
123+
124+
The user below *does not have* permission to the ``Poll`` history model in admin even
125+
though they *have* view permission to the ``Poll`` model.
126+
127+
.. code-block:: python
128+
129+
# SIMPLE_HISTORY_ENFORCE_HISTORY_MODEL_PERMISSIONS = True in settings
130+
user.user_permissions.clear()
131+
user.user_permissions.add(
132+
Permission.objects.get(codename="view_planet"),
133+
)
134+
135+
The user below has view permission to the ``Poll`` model and the ``Poll``
136+
history model.
137+
138+
.. code-block:: python
139+
140+
# SIMPLE_HISTORY_ENFORCE_HISTORY_MODEL_PERMISSIONS = True in settings
141+
user.user_permissions.clear()
142+
user.user_permissions.add(
143+
Permission.objects.get(codename="view_planet"),
144+
Permission.objects.get(codename="view_historicalplanet"),
145+
)
146+
147+
The user below has view permission to the ``Poll`` history model but will need to
148+
access the instance with a direct url since the ``Poll`` model will not be listed in
149+
the admin application index page nor the ``Poll`` changelist.
150+
151+
.. code-block:: python
152+
153+
# SIMPLE_HISTORY_ENFORCE_HISTORY_MODEL_PERMISSIONS = True in settings
154+
user.user_permissions.clear()
155+
user.user_permissions.add(
156+
Permission.objects.get(codename="view_historicalplanet"),
157+
)

simple_history/admin.py

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from django.contrib import admin
55
from django.contrib.admin import helpers
66
from django.contrib.admin.utils import unquote
7+
from django.contrib.auth import get_permission_codename
78
from django.core.exceptions import PermissionDenied
89
from django.shortcuts import get_object_or_404, render
910
from django.urls import re_path, reverse
@@ -12,7 +13,7 @@
1213
from django.utils.text import capfirst
1314
from django.utils.translation import gettext as _
1415

15-
from . import utils
16+
from .utils import get_history_manager_for_model
1617

1718
USER_NATURAL_KEY = tuple(key.lower() for key in settings.AUTH_USER_MODEL.split(".", 1))
1819

@@ -61,7 +62,7 @@ def history_view(self, request, object_id, extra_context=None):
6162
except action_list.model.DoesNotExist:
6263
raise http.Http404
6364

64-
if not self.has_change_permission(request, obj):
65+
if not self.has_view_history_or_change_history_permission(request, obj):
6566
raise PermissionDenied
6667

6768
# Set attribute on each action_list entry from admin methods
@@ -79,7 +80,7 @@ def history_view(self, request, object_id, extra_context=None):
7980
content_type.model,
8081
)
8182
context = {
82-
"title": self.history_view_title(obj),
83+
"title": self.history_view_title(request, obj),
8384
"action_list": action_list,
8485
"module_name": capfirst(force_str(opts.verbose_name_plural)),
8586
"object": obj,
@@ -88,7 +89,7 @@ def history_view(self, request, object_id, extra_context=None):
8889
"opts": opts,
8990
"admin_user_view": admin_user_view,
9091
"history_list_display": history_list_display,
91-
"revert_disabled": self.revert_disabled,
92+
"revert_disabled": self.revert_disabled(request, obj),
9293
}
9394
context.update(self.admin_site.each_context(request))
9495
context.update(extra_context or {})
@@ -97,8 +98,8 @@ def history_view(self, request, object_id, extra_context=None):
9798
request, self.object_history_template, context, **extra_kwargs
9899
)
99100

100-
def history_view_title(self, obj):
101-
if self.revert_disabled and not SIMPLE_HISTORY_EDIT:
101+
def history_view_title(self, request, obj):
102+
if self.revert_disabled(request, obj) and not SIMPLE_HISTORY_EDIT:
102103
return _("View history: %s") % force_str(obj)
103104
else:
104105
return _("Change history: %s") % force_str(obj)
@@ -131,7 +132,7 @@ def history_form_view(self, request, object_id, version_id, extra_context=None):
131132
).instance
132133
obj._state.adding = False
133134

134-
if not self.has_change_permission(request, obj):
135+
if not self.has_view_history_or_change_history_permission(request, obj):
135136
raise PermissionDenied
136137

137138
if SIMPLE_HISTORY_EDIT:
@@ -140,7 +141,7 @@ def history_form_view(self, request, object_id, version_id, extra_context=None):
140141
change_history = False
141142

142143
if "_change_history" in request.POST and SIMPLE_HISTORY_EDIT:
143-
history = utils.get_history_manager_for_model(obj)
144+
history = get_history_manager_for_model(obj)
144145
obj = history.get(pk=version_id).instance
145146

146147
formsets = []
@@ -173,7 +174,7 @@ def history_form_view(self, request, object_id, version_id, extra_context=None):
173174
model_name = original_opts.model_name
174175
url_triplet = self.admin_site.name, original_opts.app_label, model_name
175176
context = {
176-
"title": self.history_form_view_title(obj),
177+
"title": self.history_form_view_title(request, obj),
177178
"adminform": admin_form,
178179
"object_id": object_id,
179180
"original": obj,
@@ -186,12 +187,13 @@ def history_form_view(self, request, object_id, version_id, extra_context=None):
186187
"change_url": reverse("%s:%s_%s_change" % url_triplet, args=(obj.pk,)),
187188
"history_url": reverse("%s:%s_%s_history" % url_triplet, args=(obj.pk,)),
188189
"change_history": change_history,
189-
"revert_disabled": self.revert_disabled,
190+
"revert_disabled": self.revert_disabled(request, obj),
190191
# Context variables copied from render_change_form
191192
"add": False,
192193
"change": True,
193194
"has_add_permission": self.has_add_permission(request),
194-
"has_change_permission": self.has_change_permission(request, obj),
195+
"has_view_permission": self.has_view_history_permission(request, obj),
196+
"has_change_permission": self.has_change_history_permission(request, obj),
195197
"has_delete_permission": self.has_delete_permission(request, obj),
196198
"has_file_field": True,
197199
"has_absolute_url": False,
@@ -211,8 +213,8 @@ def history_form_view(self, request, object_id, version_id, extra_context=None):
211213
request, self.object_history_form_template, context, **extra_kwargs
212214
)
213215

214-
def history_form_view_title(self, obj):
215-
if self.revert_disabled:
216+
def history_form_view_title(self, request, obj):
217+
if self.revert_disabled(request, obj):
216218
return _("View %s") % force_str(obj)
217219
else:
218220
return _("Revert %s") % force_str(obj)
@@ -231,6 +233,54 @@ def content_type_model_cls(self):
231233
"""Returns the ContentType model class."""
232234
return django_apps.get_model("contenttypes.contenttype")
233235

236+
def revert_disabled(self, request, obj=None):
237+
"""If true, hides the `Revert` button in the submit_line template."""
238+
if getattr(settings, "SIMPLE_HISTORY_REVERT_DISABLED", False):
239+
return True
240+
elif self.has_view_history_permission(
241+
request, obj
242+
) and not self.has_change_history_permission(request, obj):
243+
return True
244+
return False
245+
246+
def has_view_permission(self, request, obj=None):
247+
return super().has_view_permission(request, obj)
248+
249+
def has_change_permission(self, request, obj=None):
250+
return super().has_change_permission(request, obj)
251+
252+
def has_view_or_change_permission(self, request, obj=None):
253+
return self.has_view_permission(request, obj) or self.has_change_permission(
254+
request, obj
255+
)
256+
257+
def has_view_history_or_change_history_permission(self, request, obj=None):
258+
if self.enforce_history_permissions:
259+
return self.has_view_history_permission(
260+
request, obj
261+
) or self.has_change_history_permission(request, obj)
262+
return self.has_view_or_change_permission(request, obj)
263+
264+
def has_view_history_permission(self, request, obj=None):
265+
if self.enforce_history_permissions:
266+
opts_history = self.model.history.model._meta
267+
codename_view_history = get_permission_codename("view", opts_history)
268+
return request.user.has_perm(
269+
"%s.%s" % (opts_history.app_label, codename_view_history)
270+
)
271+
return self.has_view_permission(request, obj)
272+
273+
def has_change_history_permission(self, request, obj=None):
274+
if self.enforce_history_permissions:
275+
opts_history = self.model.history.model._meta
276+
codename_change_history = get_permission_codename("change", opts_history)
277+
return request.user.has_perm(
278+
"%s.%s" % (opts_history.app_label, codename_change_history)
279+
)
280+
return self.has_change_permission(request, obj)
281+
234282
@property
235-
def revert_disabled(self):
236-
return getattr(settings, "SIMPLE_HISTORY_REVERT_DISABLED", False)
283+
def enforce_history_permissions(self):
284+
return getattr(
285+
settings, "SIMPLE_HISTORY_ENFORCE_HISTORY_MODEL_PERMISSIONS", False
286+
)

simple_history/tests/admin.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ class PersonAdmin(SimpleHistoryAdmin):
2121
def has_change_permission(self, request, obj=None):
2222
return False
2323

24+
def has_view_permission(self, request, obj=None):
25+
return False
26+
2427

2528
class ChoiceAdmin(SimpleHistoryAdmin):
2629
history_list_display = ["votes"]

simple_history/tests/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,7 @@ def __str__(self):
458458

459459
class Meta:
460460
verbose_name = "Planet"
461+
verbose_name_plural = "Planets"
461462

462463

463464
class Contact(models.Model):

0 commit comments

Comments
 (0)