Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion openedx_authz/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from django import forms
from django.contrib import admin

from openedx_authz.models import ExtendedCasbinRule
from openedx_authz.models import AuthzCourseAuthoringMigrationRun, ExtendedCasbinRule


class CasbinRuleForm(forms.ModelForm):
Expand Down Expand Up @@ -48,3 +48,12 @@ class CasbinRuleAdmin(admin.ModelAdmin):
# TODO: In a future, possibly we should only show an inline for the rules that
# have an extended rule, and show the subject and scope information in detail.
inlines = [ExtendedCasbinRuleInline]


@admin.register(AuthzCourseAuthoringMigrationRun)
class AuthzCourseAuthoringMigrationRunAdmin(admin.ModelAdmin):
"""Admin for AuthzCourseAuthoringMigrationRun to display additional metadata."""

list_display = ("id", "scope_type", "scope_key", "migration_type", "status", "created_at", "updated_at")
search_fields = ("scope_type", "scope_key", "migration_type", "status")
list_filter = ("scope_type", "migration_type", "status")
18 changes: 18 additions & 0 deletions openedx_authz/api/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,24 @@ def get_admin_view_permission(cls) -> PermissionData:
"""
raise NotImplementedError("Subclasses must implement get_admin_view_permission method.")

@classmethod
def build_external_key(cls, org: str) -> str:
"""Build the external key for all resources within the given organization.

Args:
org (str): The organization identifier (e.g., ``DemoX``).

Returns:
str: The external key for the org-level glob (e.g., ``course-v1:DemoX+*``).

Examples:
>>> OrgCourseOverviewGlobData.build_external_key('DemoX')
'course-v1:DemoX+*'
>>> OrgContentLibraryGlobData.build_external_key('DemoX')
'lib:DemoX:*'
"""
return f"{cls.NAMESPACE}{EXTERNAL_KEY_SEPARATOR}{org}{cls.ID_SEPARATOR}{GLOBAL_SCOPE_WILDCARD}"

@classmethod
def get_org(cls, external_key: str) -> str | None:
"""Extract the organization identifier from the glob pattern.
Expand Down
21 changes: 21 additions & 0 deletions openedx_authz/api/roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"get_subject_role_assignments",
"get_subject_role_assignments_for_role_in_scope",
"get_subject_role_assignments_in_scope",
"get_all_role_assignments_per_scope_type",
"unassign_role_from_subject_in_scope",
"unassign_subject_from_all_roles",
]
Expand Down Expand Up @@ -553,3 +554,23 @@ def unassign_subject_from_all_roles(subject: SubjectData) -> bool:
"""
enforcer = AuthzEnforcer.get_enforcer()
return enforcer.remove_filtered_grouping_policy(GroupingPolicyIndex.SUBJECT.value, subject.namespaced_key)


def get_all_role_assignments_per_scope_type(scope_types: list[type[ScopeData]]) -> list[RoleAssignmentData]:
"""Get all role assignments matching any of the given scope types.

Loads all grouping policies from the enforcer and filters in Python. Casbin policies
store full scope keys (e.g., 'course-v1^course-v1:Org+Course+Run'), so there is no
way to query by scope type directly so the filtering must happen here.

Args:
scope_types: A list of ScopeData subclasses (not instances). Assignments matching
any of the given types are returned.

Returns:
list[RoleAssignmentData]: All assignments whose scope is an instance of any of the given scope types.
"""
return [
role_assignment for role_assignment in get_role_assignments()
if isinstance(role_assignment.scope, tuple(scope_types))
]
Loading
Loading