Skip to content

Commit 29b3955

Browse files
authored
fix: Respect permissions for indicator menus and version locking (#493)
* fix: Non superusers do not need unlock permission to edit unlocked or own drafts * fix: Indicator menu respects correct permissions * fix tests * fix premissions in test
1 parent 883d077 commit 29b3955

File tree

6 files changed

+174
-9
lines changed

6 files changed

+174
-9
lines changed

djangocms_versioning/admin.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import json
24
import warnings
35
from collections import OrderedDict
@@ -1365,7 +1367,7 @@ def unlock_view(self, request, object_id):
13651367
raise Http404
13661368

13671369
# Check that the user has unlock permission
1368-
if not request.user.has_perm("djangocms_versioning.delete_versionlock"):
1370+
if not request.user.has_perm(f"{self.model._meta.app_label}.delete_versionlock"):
13691371
return HttpResponseForbidden(force_str(_("You do not have permission to remove the version lock")))
13701372

13711373
# Unlock the version

djangocms_versioning/cms_toolbars.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ def _add_unlock_button(self):
136136
f"admin:{proxy_model._meta.app_label}_{proxy_model.__name__.lower()}_unlock",
137137
args=(version.pk,),
138138
)
139-
can_unlock = self.request.user.has_perm("djangocms_versioning.delete_versionlock")
139+
can_unlock = self.request.user.has_perm(f"{version._meta.app_label}.delete_versionlock")
140140
if can_unlock:
141141
extra_classes = [
142142
"cms-btn-action",

djangocms_versioning/conditions.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,12 @@ def inner(version, user):
8080

8181
def user_can_unlock(message: str) -> callable:
8282
def inner(version, user):
83-
if not user.has_perm("djangocms_versioning.delete_versionlock"):
83+
if conf.LOCK_VERSIONS:
84+
if user.has_perm(f"{version._meta.app_label}.delete_versionlock"):
85+
return
86+
draft_version = get_latest_draft_version(version)
87+
if draft_version and (draft_version.locked_by == user or draft_version.locked_by is None):
88+
return
8489
raise ConditionFailed(message)
8590
return inner
8691

djangocms_versioning/indicators.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from __future__ import annotations
22

33
from cms.utils.urlutils import admin_reverse
4-
from django.contrib.auth import get_permission_codename
54
from django.db import models
65
from django.utils.http import urlencode
76
from django.utils.translation import gettext_lazy as _
@@ -21,10 +20,13 @@ def _reverse_action(version, action, back=None):
2120
def content_indicator_menu(request, status, versions, back=""):
2221
from djangocms_versioning.helpers import version_list_url
2322

23+
has_change_perm =versions[0].has_change_permission(request.user)
24+
delete_versionlock_perm = f"{versions[0]._meta.app_label}.delete_versionlock"
25+
2426
menu = []
25-
if request.user.has_perm(f"cms.{get_permission_codename('change', versions[0]._meta)}"):
27+
if has_change_perm:
2628
if versions[0].check_unlock.as_bool(request.user):
27-
can_unlock = request.user.has_perm("djangocms_versioning.delete_versionlock")
29+
can_unlock = request.user.has_perm(delete_versionlock_perm)
2830
# disable if permissions are insufficient
2931
additional_class = "" if can_unlock else " cms-pagetree-dropdown-item-disabled"
3032
menu.append((

tests/test_indicators.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
from cms.models import GlobalPagePermission, Site
12
from cms.test_utils.testcases import CMSTestCase
23
from cms.utils.urlutils import admin_reverse
34

5+
from djangocms_versioning.constants import ARCHIVED, DRAFT, PUBLISHED
46
from djangocms_versioning.helpers import get_latest_admin_viewable_content
57
from djangocms_versioning.models import Version
68
from djangocms_versioning.test_utils.blogpost.admin import BlogContentAdmin
@@ -88,26 +90,50 @@ class TestVersionState(CMSTestCase):
8890
def test_page_indicators(self):
8991
"""The page content indicators render correctly"""
9092
page = PageFactory(node__depth=1) if TreeNode else PageFactory(depth=1)
93+
user = self._create_user(
94+
"albert",
95+
is_staff=True,
96+
is_superuser=False,
97+
permissions=["change_version", "change_page", "publish_page"]
98+
)
99+
gpp = GlobalPagePermission.objects.create(
100+
user=user,
101+
can_add=True,
102+
can_change=True,
103+
can_delete=True,
104+
can_publish=True,
105+
can_move_page=True,
106+
can_change_permissions=True,
107+
can_view=True,
108+
)
109+
gpp.sites.set([Site.objects.get_current()])
91110
version1 = PageVersionFactory(
92111
content__page=page,
93112
content__language="en",
113+
locked_by=None,
94114
)
95115
pk = version1.pk
96116

97117
page_tree = admin_reverse("cms_pagecontent_get_tree")
98-
with self.login_user_context(self.get_superuser()):
118+
with self.login_user_context(user):
99119
# New page has draft version, nothing else
100120
response = self.client.get(page_tree, {"language": "en"})
121+
# Check markers
101122
self.assertNotContains(response, "cms-pagetree-node-state-empty")
102123
self.assertContains(response, "cms-pagetree-node-state-draft")
103124
self.assertNotContains(response, "cms-pagetree-node-state-published")
104125
self.assertNotContains(response, "cms-pagetree-node-state-dirty")
105126
self.assertNotContains(response, "cms-pagetree-node-state-unpublished")
127+
# Check actions
128+
self.assertContains(response, "Manage Versions...")
129+
self.assertContains(response, "Publish")
106130

107131
# Now archive
108132
response = self.client.post(admin_reverse("djangocms_versioning_pagecontentversion_archive",
109133
args=(pk,)))
110134
self.assertEqual(response.status_code, 302) # Sends a redirect
135+
self.assertEqual(Version.objects.get(pk=pk).state, ARCHIVED)
136+
111137
# Is archived indicator? No draft indicator
112138
response = self.client.get(page_tree, {"language": "en"})
113139
self.assertContains(response, "cms-pagetree-node-state-archived")
@@ -117,21 +143,26 @@ def test_page_indicators(self):
117143
response = self.client.post(admin_reverse("djangocms_versioning_pagecontentversion_revert",
118144
args=(pk,)))
119145
self.assertEqual(response.status_code, 302) # Sends a redirect
146+
pk = Version.objects.filter_by_content_grouping_values(version1.content).order_by("-pk")[0].pk
147+
self.assertEqual(Version.objects.get(pk=pk).state, DRAFT)
148+
120149
# Is draft indicator? No archived indicator
121150
response = self.client.get(page_tree, {"language": "en"})
122151
self.assertContains(response, "cms-pagetree-node-state-draft")
123152
self.assertNotContains(response, "cms-pagetree-node-state-archived")
124153
# New draft was created, get new pk
125-
pk = Version.objects.filter_by_content_grouping_values(version1.content).order_by("-pk")[0].pk
126154

127155
# Now publish
128156
response = self.client.post(admin_reverse("djangocms_versioning_pagecontentversion_publish",
129157
args=(pk,)))
130158
self.assertEqual(response.status_code, 302) # Sends a redirect
159+
self.assertEqual(Version.objects.get(pk=pk).state, PUBLISHED)
131160
# Is published indicator? No draft indicator
132161
response = self.client.get(page_tree, {"language": "en"})
133162
self.assertContains(response, "cms-pagetree-node-state-published")
134163
self.assertNotContains(response, "cms-pagetree-node-state-draft")
164+
# Check actions
165+
self.assertContains(response, "Unpublish")
135166

136167
# Now unpublish
137168
response = self.client.post(admin_reverse("djangocms_versioning_pagecontentversion_unpublish",

tests/test_locking.py

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from cms.models import PlaceholderRelationField
44
from cms.test_utils.testcases import CMSTestCase
55
from cms.toolbar.items import TemplateItem
6-
from cms.toolbar.utils import get_object_preview_url
6+
from cms.toolbar.utils import get_object_edit_url, get_object_preview_url
77
from cms.utils import get_current_site
88
from django.contrib import admin
99
from django.contrib.auth.models import Permission
@@ -130,6 +130,131 @@ def test_user_does_not_have_change_permission(self):
130130
self.assertIsNotNone(version.locked_by) # Was locked
131131
self.assertEqual(response.status_code, 403)
132132

133+
def test_editor_without_delete_versionlock_permission_can_edit_unlocked_content(self):
134+
"""
135+
Non-superuser editors with delete_versionlock permission can access edit mode
136+
of unlocked content. This test verifies the fix for the issue where editors
137+
without delete_versionlock permission were unable to access edit mode even
138+
with appropriate change permissions.
139+
"""
140+
# Create an editor user with change permissions and no delete_versionlock permission
141+
editor = self._create_user(
142+
"editor_without_unlock",
143+
is_staff=True,
144+
is_superuser=False,
145+
permissions=["change_page"],
146+
)
147+
148+
# Create a version without a lock (unlocked)
149+
version = factories.PageVersionFactory(state=DRAFT, locked_by=None)
150+
151+
# Editor should be able to access the edit view
152+
url = get_object_edit_url(version.content)
153+
154+
with self.login_user_context(editor):
155+
response = self.client.get(url)
156+
157+
# Should succeed with 200 status
158+
self.assertEqual(response.status_code, 200)
159+
160+
def test_editor_without_delete_versionlock_permission_can_edit_their_locked_content(self):
161+
"""
162+
Non-superuser editors with delete_versionlock permission can access edit mode
163+
of unlocked content. This test verifies the fix for the issue where editors
164+
without delete_versionlock permission were unable to access edit mode even
165+
with appropriate change permissions.
166+
"""
167+
# Create an editor user with change permissions and no delete_versionlock permission
168+
editor = self._create_user(
169+
"editor_without_unlock",
170+
is_staff=True,
171+
is_superuser=False,
172+
permissions=["change_page"],
173+
)
174+
175+
# Create a version without a lock (unlocked)
176+
version = factories.PageVersionFactory(state=DRAFT, locked_by=editor)
177+
178+
# Editor should be able to access the edit view
179+
url = get_object_edit_url(version.content)
180+
181+
with self.login_user_context(editor):
182+
response = self.client.get(url)
183+
184+
# Should succeed with 200 status - this is the key test
185+
# Without delete_versionlock permission, this would return 403
186+
self.assertEqual(response.status_code, 200)
187+
188+
def test_editor_without_delete_versionlock_permission_cannot_edit_others_content(self):
189+
"""
190+
Non-superuser editors without delete_versionlock permission cannot access
191+
edit mode of content locked by another user. This is the expected behavior
192+
that was causing issues when users didn't have the delete_versionlock permission.
193+
"""
194+
# Create an editor user without delete_versionlock permission
195+
editor_without_unlock = self._create_user(
196+
"editor_without_unlock",
197+
is_staff=True,
198+
is_superuser=False,
199+
permissions=["change_page"],
200+
)
201+
202+
# Create a version locked by another user
203+
author = factories.UserFactory(is_staff=True)
204+
version = factories.PageVersionFactory(
205+
state=DRAFT,
206+
created_by=author,
207+
locked_by=author
208+
)
209+
210+
# Editor without unlock permission should not be able to access edit view
211+
url = get_object_edit_url(version.content)
212+
213+
with self.login_user_context(editor_without_unlock):
214+
response = self.client.get(url)
215+
216+
# Should be denied with 302 status -> redirect to preview
217+
self.assertEqual(response.status_code, 302)
218+
219+
# Version should still be locked by the original author
220+
updated_version = Version.objects.get(pk=version.pk)
221+
self.assertEqual(updated_version.locked_by, author)
222+
223+
def test_editor_with_delete_versionlock_permission_can_edit_others_content(self):
224+
"""
225+
Non-superuser editors without delete_versionlock permission cannot access
226+
edit mode of content locked by another user. This is the expected behavior
227+
that was causing issues when users didn't have the delete_versionlock permission.
228+
"""
229+
# Create an editor user without delete_versionlock permission
230+
editor_with_unlock = self._create_user(
231+
"editor_with_unlock",
232+
is_staff=True,
233+
is_superuser=False,
234+
permissions=["delete_versionlock", "change_page"],
235+
)
236+
237+
# Create a version locked by another user
238+
author = factories.UserFactory(is_staff=True)
239+
version = factories.PageVersionFactory(
240+
state=DRAFT,
241+
created_by=author,
242+
locked_by=author
243+
)
244+
245+
# Editor without unlock permission should not be able to access edit view
246+
url = get_object_edit_url(version.content)
247+
248+
with self.login_user_context(editor_with_unlock):
249+
response = self.client.get(url)
250+
251+
# Should be redirected with 302 status since user first must unlock
252+
self.assertEqual(response.status_code, 302)
253+
254+
# Version should still be locked by the original author
255+
updated_version = Version.objects.get(pk=version.pk)
256+
self.assertEqual(updated_version.locked_by, author)
257+
133258

134259
@override_settings(DJANGOCMS_VERSIONING_LOCK_VERSIONS=True)
135260
class VersionLockUnlockTestCase(CMSTestCase):

0 commit comments

Comments
 (0)