Skip to content

Commit 78f034b

Browse files
committed
refactor: address review feedback
1 parent d8b47d8 commit 78f034b

4 files changed

Lines changed: 39 additions & 13 deletions

File tree

CHANGELOG.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@ Change Log
1414
Unreleased
1515
**********
1616

17+
1.20.0 - 2026-07-01
18+
*******************
19+
20+
Added
21+
=====
22+
23+
* Make ``scope`` optional when validating actions: the permission validation API and
24+
``/permissions/validate/me`` endpoint now allow checking whether a user holds a
25+
permission in any scope (via ``is_user_allowed_in_any_scope``) when no scope is provided.
26+
1727
1.19.0 - 2026-06-17
1828
*******************
1929

openedx_authz/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@
44

55
import os
66

7-
__version__ = "1.19.0"
7+
__version__ = "1.20.0"
88

99
ROOT_DIRECTORY = os.path.dirname(os.path.abspath(__file__))

openedx_authz/rest_api/v1/serializers.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,21 +70,14 @@ class PermissionValidationSerializer(ActionMixin, ScopeMixin): # pylint: disabl
7070
scope; when omitted, the permission is validated across any scope.
7171
"""
7272

73-
scope = serializers.CharField(max_length=255, required=False, allow_null=True)
73+
scope = serializers.CharField(max_length=255, required=False)
7474

7575

7676
class PermissionValidationResponseSerializer(PermissionValidationSerializer): # pylint: disable=abstract-method
7777
"""Serializer for permission validation response."""
7878

7979
allowed = serializers.BooleanField()
8080

81-
def to_representation(self, instance: dict) -> dict:
82-
"""Serialize the result, omitting ``scope`` when the request had no scope."""
83-
representation = super().to_representation(instance)
84-
if instance.get("scope") is None:
85-
representation.pop("scope", None)
86-
return representation
87-
8881

8982
class RoleScopeValidationMixin(serializers.Serializer): # pylint: disable=abstract-method
9083
"""Mixin providing role and scope validation logic."""

openedx_authz/tests/api/test_users.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -613,8 +613,21 @@ def test_is_user_allowed(self, username, action, scope_name, expected_result):
613613
# jane is library_user on lib:Org1:english_101, which never grants these permissions.
614614
("jane", permissions.DELETE_LIBRARY.identifier, False),
615615
("jane", permissions.MANAGE_LIBRARY_TEAM.identifier, False),
616+
# daniel is course_staff on course-v1:TestOrg+TestCourse+2024_T1, so he holds the
617+
# staff course permissions in at least one scope.
618+
("daniel", permissions.COURSES_VIEW_COURSE.identifier, True),
619+
("daniel", permissions.COURSES_EDIT_COURSE_CONTENT.identifier, True),
620+
("daniel", permissions.COURSES_PUBLISH_COURSE_CONTENT.identifier, True),
621+
# course_staff never grants team management, so daniel holds it in no scope.
622+
("daniel", permissions.COURSES_MANAGE_COURSE_TEAM.identifier, False),
623+
# carlos is course_staff on three different courses; he still holds these in some scope.
624+
("carlos", permissions.COURSES_VIEW_COURSE.identifier, True),
625+
("carlos", permissions.COURSES_MANAGE_ADVANCED_SETTINGS.identifier, True),
626+
# jane only holds a library role, so she has no course permission in any scope.
627+
("jane", permissions.COURSES_VIEW_COURSE.identifier, False),
616628
# A user without any assignment is not allowed in any scope.
617629
("nonexistent_user", permissions.MANAGE_LIBRARY_TEAM.identifier, False),
630+
("nonexistent_user", permissions.COURSES_VIEW_COURSE.identifier, False),
618631
)
619632
@unpack
620633
def test_is_user_allowed_in_any_scope(self, username, action, expected_result):
@@ -630,17 +643,27 @@ def test_is_user_allowed_in_any_scope(self, username, action, expected_result):
630643
)
631644
self.assertEqual(result, expected_result)
632645

633-
def test_is_user_allowed_in_any_scope_staff_always_allowed(self):
646+
@data(
647+
# Staff-only user
648+
("staff_member", {"is_staff": True}),
649+
# Superuser-only user
650+
("superuser_member", {"is_superuser": True}),
651+
# Both staff and superuser
652+
("staff_superuser_member", {"is_staff": True, "is_superuser": True}),
653+
)
654+
@unpack
655+
def test_is_user_allowed_in_any_scope_staff_always_allowed(self, username, flags):
634656
"""Staff/superusers are allowed for any action regardless of explicit assignments.
635657
636658
Expected result:
637-
- The function returns True for a staff user that has no role assignments.
659+
- The function returns True for a staff and/or superuser user that has no
660+
role assignments.
638661
"""
639662
User = get_user_model()
640-
User.objects.create_user(username="staff_member", email="staff_member@example.com", is_staff=True)
663+
User.objects.create_user(username=username, email=f"{username}@example.com", **flags)
641664

642665
result = is_user_allowed_in_any_scope(
643-
user_external_key="staff_member",
666+
user_external_key=username,
644667
action_external_key=permissions.MANAGE_LIBRARY_TEAM.identifier,
645668
)
646669
self.assertTrue(result)

0 commit comments

Comments
 (0)