Skip to content

feat: add org glob support#225

Open
BryanttV wants to merge 8 commits intoopenedx:mainfrom
eduNEXT:bav/org-glob-support
Open

feat: add org glob support#225
BryanttV wants to merge 8 commits intoopenedx:mainfrom
eduNEXT:bav/org-glob-support

Conversation

@BryanttV
Copy link
Contributor

@BryanttV BryanttV commented Mar 12, 2026

Closes: #172

Description

This PR implements organization-level glob pattern matching for role assignments in the Casbin enforcer, enabling administrators to assign roles across all courses or libraries within a specific organization using a single policy rule.

Previously, the AuthZ system only supported:

  • Exact scope matching (e.g., lib:WGU:CSPROB or course-v1:OpenedX+DemoX+DemoCourse)

Now, administrators can assign roles using organization-level glob patterns:

  • Library scope: lib:DemoX* (matches all libraries in DemoX org)
  • Course scope: course-v1:OpenedX* (matches all courses in OpenedX org)

Changes

Core Functionality

  • Domain Matching Function: Added key_match_func to the Casbin enforcer to enable glob-like pattern matching at the end of scope strings

    • Modified AuthzEnforcer._initialize_enforcer() to register the matching function via enforcer.add_named_domain_matching_func("g", key_match_func)
    • This allows the "g" (grouping) function in the Casbin model to match patterns like course-v1:OpenedX*
  • Validation Rules:

    • Allowed: course-v1:OpenedX*, lib:DemoX* (org-level wildcards)
    • Blocked: course-v1* (wildcard before org)
    • Blocked: course-v1:* (wildcard without org prefix)
    • Blocked: course-v1:OpenedX+Course* (wildcard at course level)
    • Blocked: lib:DemoX:Slug* (wildcard at slug level)
    • Organization existence is verified to prevent overly broad permissions

Example Use Case

Before (required one role assignment per course):

assign_role_to_subject_in_scope(user, role, "course-v1:WGU+COURSE1+2026")
assign_role_to_subject_in_scope(user, role, "course-v1:WGU+COURSE2+2026")
assign_role_to_subject_in_scope(user, role, "course-v1:WGU+COURSE3+2026")
# ... hundreds of assignments for all WGU courses

After (single org-level assignment):

assign_role_to_subject_in_scope(user, role, "course-v1:WGU*")

Permission Check:

# All these now return True with a single org-level assignment
is_user_allowed("staff-user", "courses.manage_advanced_settings", "course-v1:WGU+COURSE1+2026")
is_user_allowed("staff-user", "courses.manage_advanced_settings", "course-v1:WGU+COURSE2+2026")
is_user_allowed("staff-user", "courses.manage_advanced_settings", "course-v1:WGU+ANYOTHER+2026")

Technical Details

Casbin Model Behavior

The key_match_func function from casbin.util enables pattern matching where:

  • course-v1:OpenedX* matches any string starting with course-v1:OpenedX
  • lib:DemoX* matches any string starting with lib:DemoX

This is applied to the domain (scope) parameter of the "g" grouping function in the Casbin model, allowing role assignments with glob patterns to match concrete resource scopes during permission checks.

Security Considerations

  • Organization existence validation prevents creation of overly broad permissions for non-existent organizations
  • Only org-level globs are permitted, preventing unintended permission escalation through course/run/slug wildcards
  • Validation occurs at role assignment time, failing fast with clear error messages

Test Plan

  • Unit tests for validation module (all scenarios covered)
  • Unit tests for enforcement with org-level globs
  • Verify valid glob patterns are accepted
  • Verify invalid glob patterns raise appropriate errors
  • Verify organization existence checks work correctly
  • Verify glob patterns match concrete scopes during permission checks
  • Verify existing functionality (exact matches, global wildcards) still works

Related Documentation

Merge checklist

Check off if complete or not applicable:

  • Version bumped
  • Changelog record added
  • Documentation updated (not only docstrings)
  • Fixup commits are squashed away
  • Unit tests added/updated
  • Manual testing instructions provided
  • Noted any: Concerns, dependencies, migration issues, deadlines, tickets

@openedx-webhooks openedx-webhooks added open-source-contribution PR author is not from Axim or 2U core contributor PR author is a Core Contributor (who may or may not have write access to this repo). labels Mar 12, 2026
@openedx-webhooks
Copy link

openedx-webhooks commented Mar 12, 2026

Thanks for the pull request, @BryanttV!

This repository is currently maintained by @openedx/committers-openedx-authz.

Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are ready for engineering review.

🔘 Get product approval

If you haven't already, check this list to see if your contribution needs to go through the product review process.

  • If it does, you'll need to submit a product proposal for your contribution, and have it reviewed by the Product Working Group.
    • This process (including the steps you'll need to take) is documented here.
  • If it doesn't, simply proceed with the next step.
🔘 Provide context

To help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:

  • Dependencies

    This PR must be merged before / after / at the same time as ...

  • Blockers

    This PR is waiting for OEP-1234 to be accepted.

  • Timeline information

    This PR must be merged by XX date because ...

  • Partner information

    This is for a course on edx.org.

  • Supporting documentation
  • Relevant Open edX discussion forum threads
🔘 Get a green build

If one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green.

Details
Where can I find more information?

If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources:

When can I expect my changes to be merged?

Our goal is to get community contributions seen and reviewed as efficiently as possible.

However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:

  • The size and impact of the changes that it introduces
  • The need for product review
  • Maintenance status of the parent repository

💡 As a result it may take up to several weeks or months to complete a review and merge your PR.

@github-project-automation github-project-automation bot moved this to Needs Triage in Contributions Mar 12, 2026
@BryanttV BryanttV marked this pull request as ready for review March 13, 2026 16:48
Copy link
Member

@mariajgrimaldi mariajgrimaldi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you so much for this! I haven't tested this so this first review is only a static scan of the code. My main concern is how to maintain the extensibility we've built so far with multiple classes, and whether we could leverage that to include this (globs) as a new scope. Curious to know what you think!

Comment on lines +425 to +427
if GLOBAL_SCOPE_WILDCARD in external_key:
return external_key.startswith(cls.NAMESPACE) and external_key.endswith(GLOBAL_SCOPE_WILDCARD)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can glob be a new scope type so we can support it natively with the mechanisms we already have in place (inheritance, etc) like any other scope?

Raises:
ValueError: If the scope string contains invalid glob patterns.
"""
validate_scope_with_glob(scope)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be part of validating the key of the scope? As when validating whether a course id is valid.

Examples:
Valid scopes:
- CourseOverviewData(external_key="course-v1:OpenedX*") # org-level wildcard
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if I have OpenedXV2 as an organization and I assign a role in the scope OpenedX* then I'd also be assigning roles in OpenedXV2?

Copy link
Contributor

@rodmgwgu rodmgwgu Mar 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, perhaps we would need to include the separator to make sure this doesn't happen, like:

lib:OpenedX:*
course-v1:OpenedX+*

@@ -0,0 +1,149 @@
"""Validation utilities for OpenedX AuthZ API.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if all this validations should be part of our data structures instead of be independent here.

Comment on lines +79 to +81
parts = scope_prefix.split(EXTERNAL_KEY_SEPARATOR)

if len(parts) != 2 or parts[1] == "":
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add an inline comment here? What kind of examples will raise the value error?

Comment on lines +87 to +117
def _course_org_exists(org: str) -> bool:
"""Check if there is at least one course with the given org.
Args:
org (str): Organization identifier extracted from the course scope
Returns:
bool: True if there is at least one CourseOverview whose org field matches
the provided identifier in a case-sensitive way, False otherwise.
"""
from openedx_authz.models.scopes import CourseOverview # pylint: disable=import-outside-toplevel

course_obj = CourseOverview.objects.filter(org=org).only("org").last()
return course_obj is not None and course_obj.org == org


def _library_org_exists(org: str) -> bool:
"""Check if there is at least one content library with the given org.
Args:
org (str): Organization identifier extracted from the library scope
Returns:
bool: True if there is at least one ContentLibrary whose related
organization's short_name matches the provided identifier in a
case-sensitive way, False otherwise.
"""
from openedx_authz.models.scopes import ContentLibrary # pylint: disable=import-outside-toplevel

lib_obj = ContentLibrary.objects.filter(org__short_name=org).only("org").last()
return lib_obj is not None and lib_obj.org.short_name == org
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought we already had something similar in data.py? https://github.com/eduNEXT/openedx-authz/blob/200cf5ca6499c1e94414f21e33e3c3472f851659/openedx_authz/api/data.py#L459, what would be the difference between these two?

Comment on lines +96 to +100
# For glob scopes we don't create a Scope object since
# they don't represent a specific content library
if GLOBAL_SCOPE_WILDCARD in scope.external_key:
return None

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way, by even creating a glob scope data structure or using a mixing, of not having to include these lines each time we have to implement a new scope?

@BryanttV
Copy link
Contributor Author

Hi @mariajgrimaldi, thanks for the review!

My main concern is how to maintain the extensibility we've built so far with multiple classes, and whether we could leverage that to include this (globs) as a new scope. Curious to know what you think!

I definitely agree with your approach. I think we could handle new scopes specifically at the glob level, for example, in this case, something like OrgLevelLibraryGlob and OrgLevelCourseGlob. What do you think? Or would it be better to have a single GlobScope class that handles all validations? I’ll review that refactoring and let you know how it goes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core contributor PR author is a Core Contributor (who may or may not have write access to this repo). open-source-contribution PR author is not from Axim or 2U

Projects

Status: Needs Triage

Development

Successfully merging this pull request may close these issues.

Task - RBAC AuthZ - Multi scope roles: Implement support for glob permissions and validations

4 participants