Skip to content

Commit 2149087

Browse files
committed
feat: Implement scopes endpoint for admin console
1 parent a18df56 commit 2149087

9 files changed

Lines changed: 1089 additions & 5 deletions

File tree

CHANGELOG.rst

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

17+
1.9.0 - 2026-04-15
18+
******************
19+
20+
Added
21+
=====
22+
23+
* Add ``scopes/`` endpoint to list all scopes (courses and libraries), sorted by org, with search and pagination support.
24+
1725
1.8.0 - 2026-04-14
1826
******************
1927

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.8.0"
7+
__version__ = "1.9.0"
88

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

openedx_authz/api/data.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@
1414
from opaque_keys.edx.locator import LibraryLocatorV2
1515
from organizations.models import Organization
1616

17-
from openedx_authz.constants.permissions import COURSES_VIEW_COURSE_TEAM, VIEW_LIBRARY_TEAM
17+
from openedx_authz.constants.permissions import (
18+
COURSES_MANAGE_COURSE_TEAM,
19+
COURSES_VIEW_COURSE_TEAM,
20+
MANAGE_LIBRARY_TEAM,
21+
VIEW_LIBRARY_TEAM,
22+
)
1823
from openedx_authz.data import AUTHZ_POLICY_ATTRIBUTES_SEPARATOR, ActionData, AuthzBaseClass, AuthZData, PermissionData
1924
from openedx_authz.models.scopes import get_content_library_model, get_course_overview_model
2025

@@ -356,6 +361,20 @@ def get_admin_view_permission(cls) -> PermissionData:
356361
"""
357362
raise NotImplementedError("Subclasses must implement get_admin_view_permission method.")
358363

364+
@classmethod
365+
@abstractmethod
366+
def get_admin_manage_permission(cls) -> PermissionData:
367+
"""Get the permission required to manage this scope
368+
369+
This method should be implemented on every ScopeData subclass to define
370+
which permission to check against when a user tries to manage assignations
371+
related to this scope in the Admin Console.
372+
373+
Returns:
374+
PermissionData: The permission required to manage this scope in the admin console.
375+
"""
376+
raise NotImplementedError("Subclasses must implement get_admin_manage_permission method.")
377+
359378
@abstractmethod
360379
def get_object(self) -> Any | None:
361380
"""Retrieve the underlying domain object that this scope represents.
@@ -462,6 +481,15 @@ def get_admin_view_permission(cls) -> PermissionData:
462481
"""
463482
return VIEW_LIBRARY_TEAM
464483

484+
@classmethod
485+
def get_admin_manage_permission(cls) -> PermissionData:
486+
"""Get the permission required to manage this scope
487+
488+
Returns:
489+
PermissionData: The permission required to manage this scope in the admin console.
490+
"""
491+
return MANAGE_LIBRARY_TEAM
492+
465493
def get_object(self) -> ContentLibrary | None:
466494
"""Retrieve the ContentLibrary instance associated with this scope.
467495
@@ -584,6 +612,15 @@ def get_admin_view_permission(cls) -> PermissionData:
584612
"""
585613
return COURSES_VIEW_COURSE_TEAM
586614

615+
@classmethod
616+
def get_admin_manage_permission(cls) -> PermissionData:
617+
"""Get the permission required to manage this scope
618+
619+
Returns:
620+
PermissionData: The permission required to manage this scope in the admin console.
621+
"""
622+
return COURSES_MANAGE_COURSE_TEAM
623+
587624
def get_object(self) -> CourseOverview | None:
588625
"""Retrieve the CourseOverview instance associated with this scope.
589626
@@ -714,6 +751,15 @@ def build_external_key(cls, org: str) -> str:
714751
"""
715752
return f"{cls.NAMESPACE}{EXTERNAL_KEY_SEPARATOR}{org}{cls.ID_SEPARATOR}{GLOBAL_SCOPE_WILDCARD}"
716753

754+
@classmethod
755+
def get_admin_manage_permission(cls) -> PermissionData:
756+
"""Get the permission required to manage this scope
757+
758+
Returns:
759+
PermissionData: The permission required to manage this scope in the admin console.
760+
"""
761+
raise NotImplementedError("Subclasses must implement get_admin_manage_permission method.")
762+
717763
@classmethod
718764
def get_org(cls, external_key: str) -> str | None:
719765
"""Extract the organization identifier from the glob pattern.
@@ -812,6 +858,15 @@ def get_admin_view_permission(cls) -> PermissionData:
812858
"""
813859
return VIEW_LIBRARY_TEAM
814860

861+
@classmethod
862+
def get_admin_manage_permission(cls) -> PermissionData:
863+
"""Get the permission required to manage this scope
864+
865+
Returns:
866+
PermissionData: The permission required to manage this scope in the admin console.
867+
"""
868+
return MANAGE_LIBRARY_TEAM
869+
815870

816871
@define
817872
class OrgCourseOverviewGlobData(OrgGlobData):
@@ -861,6 +916,15 @@ def get_admin_view_permission(cls) -> PermissionData:
861916
"""
862917
return COURSES_VIEW_COURSE_TEAM
863918

919+
@classmethod
920+
def get_admin_manage_permission(cls) -> PermissionData:
921+
"""Get the permission required to manage this scope
922+
923+
Returns:
924+
PermissionData: The permission required to manage this scope in the admin console.
925+
"""
926+
return COURSES_MANAGE_COURSE_TEAM
927+
864928

865929
class CCXCourseOverviewData(CourseOverviewData):
866930
"""CCX course scope for authorization in the Open edX platform.

openedx_authz/rest_api/v1/serializers.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""Serializers for the Open edX AuthZ REST API."""
22

33
from django.contrib.auth import get_user_model
4+
from opaque_keys.edx.locator import LibraryLocatorV2
5+
from organizations.serializers import OrganizationSerializer
46
from rest_framework import serializers
57

68
from openedx_authz import api
@@ -49,6 +51,12 @@ class OrderMixin(serializers.Serializer): # pylint: disable=abstract-method
4951
)
5052

5153

54+
class OrgMixin(serializers.Serializer): # pylint: disable=abstract-method
55+
"""Mixin providing org field functionality."""
56+
57+
org = serializers.CharField(required=False, max_length=255)
58+
59+
5260
class PermissionValidationSerializer(ActionMixin, ScopeMixin): # pylint: disable=abstract-method
5361
"""Serializer for permission validation request."""
5462

@@ -215,6 +223,14 @@ def get_roles(self, obj: api.RoleAssignmentData) -> list[str]:
215223
return [role.external_key for role in obj.roles]
216224

217225

226+
class ListScopesQuerySerializer(OrgMixin): # pylint: disable=abstract-method
227+
"""Serializer for validating query parameters in ScopesAPIView."""
228+
229+
management_permission_only = serializers.BooleanField(required=False, default=False)
230+
scope_type = serializers.ChoiceField(choices=["course", "library"], required=False, default=None, allow_null=True)
231+
search = serializers.CharField(required=False, default="", allow_blank=True)
232+
233+
218234
class ListTeamMembersSerializer(OrderMixin): # pylint: disable=abstract-method
219235
"""
220236
Serializer for listing team members.
@@ -346,3 +362,28 @@ def get_permission_count(self, obj: api.RoleAssignmentData | api.SuperAdminAssig
346362
return None
347363
case api.RoleAssignmentData():
348364
return len(obj.roles[0].permissions) if obj.roles else 0
365+
366+
367+
class ScopeSerializer(serializers.Serializer): # pylint: disable=abstract-method
368+
"""
369+
Serializer for scope.
370+
"""
371+
372+
external_key = serializers.SerializerMethodField()
373+
display_name = serializers.SerializerMethodField()
374+
org = serializers.SerializerMethodField()
375+
376+
def get_external_key(self, obj: dict) -> str:
377+
"""Get the external key for the given scope."""
378+
if obj["scope_type"] == "library":
379+
return str(LibraryLocatorV2(org=obj["org_name"], slug=obj["scope_id"]))
380+
return obj["scope_id"]
381+
382+
def get_display_name(self, obj: dict) -> str:
383+
"""Get the display name for the given scope."""
384+
return str(obj.get("display_name_col") or "")
385+
386+
def get_org(self, obj: dict) -> dict | None:
387+
"""Get the org for the given scope."""
388+
org = self.context.get("org_map", {}).get(obj["org_name"])
389+
return OrganizationSerializer(org).data if org else None

openedx_authz/rest_api/v1/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@
1818
path(
1919
"users/<str:username>/assignments/", views.TeamMemberAssignmentsAPIView.as_view(), name="user-assignment-list"
2020
),
21+
path("scopes/", views.ScopesAPIView.as_view(), name="scope-list"),
2122
]

0 commit comments

Comments
 (0)