openedx_authz package#

Subpackages#

Submodules#

openedx_authz.admin module#

Admin configuration for openedx_authz.

class openedx_authz.admin.AuthzCourseAuthoringMigrationRunAdmin(model, admin_site)

Bases: ModelAdmin

Admin for AuthzCourseAuthoringMigrationRun to display additional metadata.

fields = ('scope_type', 'scope_key', 'migration_type', 'status', 'pretty_metadata', 'completed_at', 'created_at', 'updated_at')
list_display = ('id', 'scope_type', 'scope_key', 'migration_type', 'status', 'created_at', 'updated_at')
list_filter = ('scope_type', 'migration_type', 'status')
property media
pretty_metadata(obj)

Return formatted JSON for the metadata field.

readonly_fields = ('scope_type', 'scope_key', 'migration_type', 'status', 'pretty_metadata', 'completed_at', 'created_at', 'updated_at')
search_fields = ('scope_type', 'scope_key', 'migration_type', 'status')
class openedx_authz.admin.CasbinRuleAdmin(model, admin_site)

Bases: ModelAdmin

Admin for CasbinRule to display additional metadata.

form

alias of CasbinRuleForm

inlines = [<class 'openedx_authz.admin.ExtendedCasbinRuleInline'>]
list_display = ('id', 'ptype', 'v0', 'v1', 'v2', 'v3', 'v4', 'v5')
list_filter = ('ptype',)
property media
search_fields = ('ptype', 'v0', 'v1', 'v2', 'v3', 'v4', 'v5')
class openedx_authz.admin.CasbinRuleForm(*args, **kwargs)

Bases: ModelForm

Custom form for CasbinRule to make v3, v4, v5 fields optional.

class Meta

Bases: object

Meta class for CasbinRuleForm.

fields = '__all__'
model

alias of CasbinRule

base_fields = {'ptype': <django.forms.fields.CharField object>, 'v0': <django.forms.fields.CharField object>, 'v1': <django.forms.fields.CharField object>, 'v2': <django.forms.fields.CharField object>, 'v3': <django.forms.fields.CharField object>, 'v4': <django.forms.fields.CharField object>, 'v5': <django.forms.fields.CharField object>}
declared_fields = {}
property media

Return all media required to render the widgets on this form.

class openedx_authz.admin.ExtendedCasbinRuleInline(parent_model, admin_site)

Bases: StackedInline

Inline admin for ExtendedCasbinRule to display additional metadata.

can_delete = False
extra = 0
fields = ('casbin_rule_key', 'scope', 'subject', 'description', 'metadata', 'created_at', 'updated_at')
property media
model

alias of ExtendedCasbinRule

readonly_fields = ('casbin_rule_key', 'scope', 'subject', 'created_at', 'updated_at')
class openedx_authz.admin.RoleAssignmentAuditAdmin(model, admin_site)

Bases: ModelAdmin

Read-only admin for the role assignment audit log.

date_hierarchy = 'timestamp'
display_role(obj)

Role name without the namespace prefix.

display_scope(obj)

Scope key without the namespace prefix.

display_subject(obj)

Subject key without the namespace prefix.

has_add_permission(request)

Audit records are created by the system only.

has_change_permission(request, obj=None)

Audit records must not be modified after creation.

has_delete_permission(request, obj=None)

Audit records must not be deleted through the admin.

list_display = ('operation', 'display_subject', 'display_role', 'display_scope', 'actor_id', 'timestamp')
list_filter = ('operation', <class 'openedx_authz.admin.ScopeTypeFilter'>)
property media
readonly_fields = ('operation', 'subject', 'role', 'scope', 'actor_id', 'timestamp')
search_fields = ('subject', 'role', 'scope')
class openedx_authz.admin.ScopeTypeFilter(request, params, model, model_admin)

Bases: SimpleListFilter

Filter audit records by scope type (content library, course, etc.).

lookups(request, model_admin)

Return the available scope type choices.

Audit records are independent from live Casbin tables and scope objects: there are no FK references to filter on. The namespace prefix in the stored scope string (e.g. lib^, course-v1^) is the only available signal for categorizing records by scope type.

parameter_name = 'scope_type'
queryset(request, queryset)

Filter the queryset by scope namespace prefix.

title = 'scope type'
openedx_authz.admin.pretty_json(value) → str

Return an indented JSON representation of a value.

openedx_authz.apps module#

openedx_authz Django application initialization.

class openedx_authz.apps.OpenedxAuthzConfig(app_name, app_module)

Bases: AppConfig

Configuration for the openedx_authz Django application.

default_auto_field = 'django.db.models.BigAutoField'
name = 'openedx_authz'
plugin_app = {'settings_config': {'cms.djangoapp': {'common': {'relative_path': 'settings.common'}, 'production': {'relative_path': 'settings.production'}, 'test': {'relative_path': 'settings.test'}}, 'lms.djangoapp': {'common': {'relative_path': 'settings.common'}, 'production': {'relative_path': 'settings.production'}, 'test': {'relative_path': 'settings.test'}}}, 'url_config': {'cms.djangoapp': {'namespace': 'openedx-authz', 'regex': '^api/', 'relative_path': 'urls'}, 'lms.djangoapp': {'namespace': 'openedx-authz', 'regex': '^api/', 'relative_path': 'urls'}}}
ready()

Import signal handlers when Django starts.

verbose_name = 'Open edX AuthZ'

openedx_authz.data module#

Top-level data classes for actions and permissions.

These are defined here (rather than in openedx_authz.api.data) to avoid a circular import between openedx_authz.api.data and openedx_authz.constants.permissions.

class openedx_authz.data.ActionData(external_key: str = '', namespaced_key: str = '')

Bases: AuthZData

An action represents an operation that can be performed in the authorization system.

NAMESPACE

‘act’ for actions.

Type:

ClassVar[str]

external_key

The action identifier (e.g., ‘content_libraries.view_library’).

Type:

str

namespaced_key

The action identifier with namespace (e.g., ‘act^content_libraries.view_library’).

Type:

str

Examples

>>> action = ActionData(external_key='content_libraries.delete_library')
>>> action.namespaced_key
'act^content_libraries.delete_library'
>>> action.name
'Content Libraries > Delete Library'
NAMESPACE: ClassVar[str] = 'act'
property name: str

The human-readable name of the action (e.g., ‘Content Libraries > Delete Library’).

class openedx_authz.data.AuthZData(external_key: str = '', namespaced_key: str = '')

Bases: AuthzBaseClass

Base class for all authz data classes.

external_key: str
namespaced_key: str
class openedx_authz.data.AuthzBaseClass

Bases: object

Base class for all authz classes.

NAMESPACE: ClassVar[str] = None
SEPARATOR: ClassVar[str] = '^'
class openedx_authz.data.GroupingPolicyIndex(*values)

Bases: Enum

Index positions for fields in a Casbin grouping policy (g).

Grouping policies represent role assignments that link subjects to roles within scopes. Format: [subject, role, scope, …]

This mirrors PolicyIndex — same strict parse() / caller-pads contract — and is the single source of truth for the g row field layout, so the engine renderer and any other consumer of a grouping row agree on both the mapping and the minimum shape without reaching into the api layer.

SUBJECT

Position 0 - The subject identifier (e.g., ‘user^john_doe’).

ROLE

Position 1 - The role identifier (e.g., ‘role^instructor’).

SCOPE

Position 2 - The scope identifier (e.g., ‘lib^lib:DemoX:CSPROB’).

Note

All three fields make up the parse width (parse() requires them). Casbin may still store a scope-less [subject, role] grouping row; a caller that may pass one pad()-s it first so the missing scope becomes an empty string rather than a parse error. Additional fields beyond position 2 are ignored.

ROLE = 1
SCOPE = 2
SUBJECT = 0
classmethod pad(values: list[str]) → list[str]

Pad values with empty strings up to required_width().

A stored grouping row may omit the scope segment ([subject, role]). Callers that accept partially populated rows pad first so the shared, strict parse() does not reject them — the same contract as PolicyIndex.pad().

classmethod parse(policy: list[str]) → tuple[str, str, str]

Return (subject, role, scope) from a Casbin g row.

The single place a g row is split into its fields, so every consumer agrees on both the layout and the minimum shape. Rows shorter than required_width() are rejected; a caller that wants to tolerate a partial row should pad() it first. This is the same strict contract as PolicyIndex.parse().

Raises:

ValueError – If policy has fewer than required_width() elements.

classmethod required_width() → int

Return the number of named fields a complete g row has (3), which parse() requires.

class openedx_authz.data.PermissionData(action: ActionData = None, effect: Literal['allow', 'deny'] = 'allow')

Bases: object

A permission combines an action with an effect (allow or deny).

action

The action being permitted or denied (ActionData instance).

Type:

openedx_authz.data.ActionData

effect

The effect of the permission, either ‘allow’ or ‘deny’ (default: ‘allow’).

Type:

Literal[‘allow’, ‘deny’]

Examples

>>> read_action = ActionData(external_key='read')
>>> permission = PermissionData(action=read_action, effect='allow')
>>> str(permission)
'Read - allow'
action: ActionData
effect: Literal['allow', 'deny']
property identifier: str

Get the permission identifier.

class openedx_authz.data.PolicyIndex(*values)

Bases: Enum

Index positions for fields in a Casbin policy (p).

Policies define permissions by linking roles to actions within scopes with an effect. Format: [role, action, scope, effect, …]

This is the single source of truth for the p row field layout, shared by every producer and consumer of a Casbin row (the engine renderer and the api data classes) so the mapping stays in one place.

ROLE

Position 0 - The role identifier (e.g., ‘role^instructor’).

ACT

Position 1 - The action identifier (e.g., ‘act^read’).

SCOPE

Position 2 - The scope identifier (e.g., ‘lib^lib:DemoX:CSPROB’).

EFFECT

Position 3 - The effect, either ‘allow’ or ‘deny’.

Note

Additional fields beyond position 3 are optional and currently ignored.

ACT = 1
EFFECT = 3
ROLE = 0
SCOPE = 2
classmethod pad(values: list[str]) → list[str]

Pad values with empty strings up to required_width().

Callers that accept partially populated rows (e.g. the renderer round-tripping an in-memory row) pad first so the shared, strict parse() does not reject them.

classmethod parse(policy: list[str]) → tuple[str, str, str, str]

Return (role, action, scope, effect) from a Casbin p row.

The single place a p row is split into its fields, so every consumer agrees on both the layout and the minimum shape. Rows shorter than required_width() are rejected; a caller that wants to tolerate a partial row should pad() it first.

Raises:

ValueError – If policy has fewer than required_width() elements.

classmethod required_width() → int

Return the number of leading fields that make up a complete p row (4).

openedx_authz.filters module#

Open edX Filters exposed by openedx_authz’s REST API.

class openedx_authz.filters.AuthorizationDataRequested

Bases: OpenEdxPublicFilter, Generic[AuthorizationItems]

Shared pipeline plumbing for operation-specific REST authorization filters.

Subclasses declare their payload type and filter identifier. Pipeline steps own rejection rules and return data in the original shape, preserving earlier errors.

classmethod run_filter(items: AuthorizationItems) → tuple[AuthorizationItems, list[dict[str, Any]]]

Run the operation’s configured pipeline with an initially empty error list.

Parameters:

items (AuthorizationItems) – Computed permission results or validated role change data, using the payload type declared by the subclass.

Returns:

Items in their original

shape and accumulated pipeline errors. Without a configured pipeline, returns the original items and an empty error list.

Return type:

tuple[AuthorizationItems, list[dict[str, Any]]]

class openedx_authz.filters.PermissionValidationRequested

Bases: AuthorizationDataRequested[list[ValidationItem]]

Filter computed permission results before response serialization.

Each item contains action and allowed, with an optional scope.

Trigger:

PermissionValidationMeView.post, after authorization checks and before response serialization.

Filter Type:

org.openedx.authz.permission_validation.requested.v1

filter_type = 'org.openedx.authz.permission_validation.requested.v1'
class openedx_authz.filters.RoleAssignmentItems

Bases: TypedDict

Validated input for assigning a role to users in one or more scopes.

role: str
scopes: list[str]
users: list[str]
class openedx_authz.filters.RoleAssignmentRequested

Bases: AuthorizationDataRequested[RoleAssignmentItems]

Filter validated role, users, and scopes before assignment writes.

Trigger:

RoleUserAPIView.put, after request validation and before assigning roles.

Filter Type:

org.openedx.authz.role_assignment.requested.v1

filter_type = 'org.openedx.authz.role_assignment.requested.v1'
class openedx_authz.filters.RoleRemovalItems

Bases: TypedDict

Validated input for removing a role from users in one scope.

role: str
scope: str
users: list[str]
class openedx_authz.filters.RoleRemovalRequested

Bases: AuthorizationDataRequested[RoleRemovalItems]

Filter validated role, users, and scope before removal writes.

Trigger:

RoleUserAPIView.delete, after request validation and before removing roles.

Filter Type:

org.openedx.authz.role_removal.requested.v1

filter_type = 'org.openedx.authz.role_removal.requested.v1'
class openedx_authz.filters.ScopedItem

Bases: TypedDict

Optional scope on a permission result; omission represents an any-scope check.

scope: str | None
class openedx_authz.filters.ValidationItem

Bases: ScopedItem

A permission result, including the action and its authorization outcome.

action: str
allowed: bool
scope: str | None

openedx_authz.handlers module#

Signal handlers for the authorization framework.

These handlers ensure proper cleanup and consistency when models are deleted.

openedx_authz.handlers.backfill_content_library_scope(sender, instance, **kwargs)

Link a pending ContentLibraryScope to the ContentLibrary that was just created for it.

See backfill_course_scope() above; same idea for content libraries, including the broad exception handling and the isolating savepoint.

openedx_authz.handlers.backfill_course_scope(sender, instance, **kwargs)

Link a pending CourseScope to the CourseOverview that was just created for it.

get_or_create_for_external_key() (openedx_authz/models/scopes.py) allows assigning a role for a course key before its CourseOverview exists, leaving course_overview null. This backfills that FK once the course shows up, e.g. after a course rerun finishes cloning. See CourseScope.link_pending_scope() for the actual lookup/update.

This runs on every CourseOverview save, so errors are logged rather than raised: a problem backfilling a pending scope must not break the caller’s save(). The query runs in its own savepoint so a failure doesn’t poison the caller’s transaction.

openedx_authz.handlers.create_audit_record_on_role_assignment_change(sender, role_assignment, **kwargs)

Create an audit record when a role assignment is created or deleted.

This handler listens for both creation and deletion of role assignments and logs the changes for auditing purposes.

Parameters:
  • sender – The signal class (ROLE_ASSIGNMENT_CREATED or ROLE_ASSIGNMENT_DELETED).

  • role_assignment – RoleAssignmentEventData carrying the operation, subject, role, scope, and actor.

  • **kwargs – Additional keyword arguments from the signal.

openedx_authz.handlers.delete_casbin_rule_on_extended_rule_deletion(sender, instance, **kwargs)

Delete the companion CasbinRule after its ExtendedCasbinRule disappears.

The handler keeps authorization data symmetric with three common flows:

  • Direct ExtendedCasbinRule deletes (API/UI) trigger removal of the linked CasbinRule.

  • Cascades from Scope or Subject deletions clear their ExtendedCasbinRule rows and, via this handler, the matching CasbinRule entries.

  • Cascades initiated from the CasbinRule side (enforcer cleanups) leave the query as a no-op because the row is already gone.

Running on post_delete ensures database cascades complete before the cleanup runs, so enforcer-driven deletions no longer raise false errors.

Parameters:
  • sender – The model class (ExtendedCasbinRule).

  • instance – The ExtendedCasbinRule instance being deleted.

  • **kwargs – Additional keyword arguments from the signal.

openedx_authz.handlers.get_effective_state(record: None, global_flag_enabled: bool) → bool

Return whether the feature is effectively active for the override and global flag.

An enabled override forces on or off, otherwise the result follows the global flag.

Parameters:
  • record (WaffleOverrideRecord | None) – The waffle flag record to evaluate.

  • global_flag_enabled (bool) – The state of the global flag.

Returns:

True if the feature is active, False otherwise.

Return type:

bool

openedx_authz.handlers.get_excluded_course_ids_for_org_migration(org_id: str, override_choice: str) → frozenset[str]

Collect course-level authoring flag overrides for an org that oppose the new org-level state.

When the org flag changes, we need to exclude course ids that have a course-level authoring flag override that opposes the new org-level state.

Parameters:
  • org_id (str) – Organization short name.

  • override_choice (str) – The override choice of the org waffle flag.

Returns:

course ids excluded from org migration

Return type:

frozenset[str]

openedx_authz.handlers.get_migration_type(current_record: None, previous_record: None, global_flag_enabled: bool) → MigrationType | None

Determine the migration type by comparing the effective state before and after the transaction.

This accounts for the global flag state, meaning a transition could be triggered by removing a FORCE_OFF override when the global flag is ON.

Parameters:
  • current_record (WaffleOverrideRecord) – The state of the record in the current transaction.

  • previous_record (WaffleOverrideRecord | None) – The state of the record prior to the current transaction.

  • global_flag_enabled (bool) – The state of the global flag.

Returns:

If the flag is newly forced on. MigrationType.ROLLBACK: If the forced-on state is removed. None: If there is no effective change in the flag’s behavior.

Return type:

MigrationType.FORWARD

openedx_authz.handlers.handle_course_waffle_flag_change(sender, instance, **kwargs) → None

Handle changes to course-level waffle flags.

When the authz.enable_course_authoring flag is changed for a course, trigger the appropriate migration run. Only trigger if automatic migration is enabled in the settings.

Parameters:
  • sender – The model class (WaffleFlagCourseOverrideModel)

  • instance – The flag override instance being saved

  • **kwargs – Additional keyword arguments from the signal

openedx_authz.handlers.handle_org_waffle_flag_change(sender, instance, **kwargs) → None

Handle changes to organization-level waffle flags.

When the authz.enable_course_authoring flag is changed for an organization, trigger the appropriate migration run. Only trigger if automatic migration is enabled in the settings.

Parameters:
  • sender – The model class (WaffleFlagOrgOverrideModel)

  • instance – The flag override instance being saved

  • **kwargs – Additional keyword arguments from the signal

openedx_authz.handlers.trigger_course_authoring_migration(sender: type[None], instance: None, scope_key: str) → None

Trigger a migration run in response to a waffle flag change.

Determines the migration direction from the flag state, guards against no-op saves, and delegates execution to run_course_authoring_migration which handles tracking and concurrent-run protection.

Parameters:
  • sender – The model class (WaffleOverrideRecord).

  • instance – The waffle flag instance that triggered the migration.

  • scope_key (str) – Course ID or organization name.

openedx_authz.handlers.unassign_roles_on_user_retirement(sender, user, **kwargs)

Unassign roles from a user when they are retired.

This handler is triggered when a user is retired in the LMS. It ensures that any roles assigned to the user are removed, maintaining the integrity of the authorization system.

Parameters:
  • sender – The model class (User).

  • user – The user instance being retired.

  • **kwargs – Additional keyword arguments from the signal.

openedx_authz.urls module#

Open edX AuthZ API URLs.

openedx_authz.utils module#

General utility functions for Open edX AuthZ.

openedx_authz.utils.get_user_by_username_or_email(username_or_email: str) → User

Retrieve a user by their username or email address.

Parameters:

username_or_email (str) – The username or email address to search for.

Returns:

The User object if found and not retired.

Return type:

User

Raises:

User.DoesNotExist – If no user matches the provided username or email, or if the user has an associated retirement request.

openedx_authz.utils.get_waffle_flag_states() → dict

Retrieve the enablement state of the course-authoring waffle flag across different scopes.

Returns:

A dictionary mapping scopes to their activation status:
  • ’global’ (bool): True if the global waffle flag is enabled for everyone.

  • ’org_overrides’ (dict): Orgs with an organization-level override, as ‘on’ (forces the flag on) and ‘off’ (forces the flag off) lists.

  • ’course_overrides’ (dict): Courses with a course-level override, split the same way.

Return type:

dict

openedx_authz.utils.is_user_staff_or_superuser(username: str) → bool

Return True if the user with the given username is staff or superuser.

Uses RequestCache to avoid repeated DB lookups within the same request. Returns False if the user does not exist or has a retirement request.

Module contents#

Open edX AuthZ provides the architecture and foundations of the authorization framework.