Skip to content

Conversation

@BryanttV
Copy link
Contributor

@BryanttV BryanttV commented Sep 15, 2025

Description

This PR adds the Casbin model. This model (model.conf) defines how requests and policies are structured and evaluated. The built model for Open edX takes into account the following points:

  • Resource Grouping
  • Namespace with Wildcard Support
  • System-Wide Roles
  • Granularity of Permissions
  • Exceptions / Negative Rules
  • Scoped Assignments

Supporting Information

How to Test

This command creates a Casbin enforcer using the model.conf configuration by default (or a custom model file) and a user-specified policy file, then provides an interactive mode for testing authorization enforcement requests.

python manage.py enforcement --policy-file-path /path/to/authz.policy
Enter enforcement test: user:alice act:manage lib:123
✓ ALLOWED: user:alice act:manage lib:123
Enter enforcement test: user:bob act:edit lib:456
✗ DENIED: user:bob act:edit lib:456 lib:456
image

@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 Sep 15, 2025
@openedx-webhooks
Copy link

openedx-webhooks commented Sep 15, 2025

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.

@BryanttV
Copy link
Contributor Author

BryanttV commented Sep 15, 2025

The full thread discussion is here.


Majo's comment: Thank you again, @BryanttV. Not sure what’s the difference between the editor and pycasbin. In any case, can you help me understand a,b,c proposals from the 2nd option? Not sure I follow.

  1. Not sure what’s the difference between the editor and pycasbin

    The actual difference lies in which engine is being used. In the example you shared in the editor, Node-Casbin is being used, and it works. However, when using PyCasbin (which, by the way, was broken in the editor last week), it doesn’t work (in certain scenarios). This has to do with how each engine implements its functions.

  2. Can you help me understand a,b,c proposals from the 2nd option? Not sure I follow

    Sure! I’ll explain my proposal again to make sure it’s clear.

    I did some tests with your model under Explicit decision containment, doing different enforcements. I specifically found two cases where it fails, and they are the following:

    Case 1

    Failing test

    The admin user has the platform_admin role, therefore they have permissions to perform any action on any resource. However, if we run the following enforce, it breaks:

    e.enforce("user:admin", "act:manage", "lib:math-basics", "org:OpenedX")

    And it fails because of this part of the matcher: regexMatch(r.obj, p.obj). When it evaluates regexMatch("lib:math-basics", "*") an error occurs, since * is not a valid regular expression. To fix this, there are 2 alternatives:

    • Use .* instead of * in the policy. That way the regex is valid. However, the enforcement result is False, when it should be True. It may require additional changes in the policy to work.
    • Use globMatch or keyMatch instead of regexMatch. Either of those functions accepts * in the policy. I lean more towards keyMatch since we’ll only use patterns like resource:*, and we won’t need other expressions like ?, **, [abc], etc. See the solution using keyMatch.

    Case 2

    Failing test

    g2, act:edit, act:read
    g2, act:edit, act:write
    
    g, user:alice, role:org_editor, org:OpenedX
    
    p, role:org_editor, ^act:(edit|delete)$, lib:*, allow

    The user alice has the org_editor role, therefore she can edit and delete (thanks to the regex) any library under the OpenedX organization. However, the last enforcements don’t work:

    e.enforce("user:alice", "act:edit", "lib:math-basics", "org:OpenedX")     # True
    e.enforce("user:alice", "act:delete", "lib:math-basics", "org:OpenedX")   # True
    e.enforce("user:alice", "act:read", "lib:math-basics", "org:OpenedX")     # False
    e.enforce("user:alice", "act:write", "lib:math-basics", "org:OpenedX")    # False

    And it fails because the regex doesn’t work with implicit permissions. So, when doing g2(p.act, r.act), it’s just a direct string evaluation, not regex evaluation. To fix this, there are 3 alternatives:

    • Don’t use implicit permissions inside regex. In that case, the policy should be ^act:(read|write|delete)$ instead of ^act:(edit|delete)$.
    • Assign permissions separately without using regex. For the previous example, we’d use two policies, one including act:edit, and another for act:delete. See the solution.
    • Use a function inside the g2 matching. Here it explains how it could be done. However, I tried some tests locally and couldn’t get it to work.

@BryanttV
Copy link
Contributor Author

BryanttV commented Sep 16, 2025

Taking the above into account, this is the proposed model, which in fact is just some changes in the matcher from the original proposal:

[request_definition]
r = sub, act, obj, scope

[policy_definition]
p = sub, act, obj, eft

[role_definition]
g = _, _, _
g2 = _, _

[policy_effect]
e = some(where (p.eft == allow)) && !some(where (p.eft == deny))

[matchers]
m = (g(r.sub, p.sub, r.scope) || g(r.sub, p.sub, "*")) && keyMatch(r.obj, p.obj) && g2(p.act, r.act)
  • The first part that evaluates the Scope remains the same. ✅

  • The second part that evaluates the Object is modified. keyMatch would be used instead of regexMatch. Also, the first condition r.obj == p.obj is no longer needed, because it is already included within keyMatch:

    - && (r.obj == p.obj || regexMatch(r.obj, p.obj))
    + && keyMatch(r.obj, p.obj)
  • The third part that evaluates the Action is modified. Regex would no longer be used (TBD), therefore regexMatch(r.act, p.act) is removed. Also, g2(p.act, r.act) is enough to cover all scenarios:

    - && (r.act == p.act || regexMatch(r.act, p.act) || g2(p.act, r.act))
    + && g2(p.act, r.act)

@mphilbrick211 mphilbrick211 moved this from Needs Triage to Waiting on Author in Contributions Sep 16, 2025
@mariajgrimaldi mariajgrimaldi changed the title feat: add casbin model [FC-0099] feat: add casbin model Sep 17, 2025
@BryanttV BryanttV marked this pull request as ready for review September 17, 2025 13:22
Comment on lines 1 to 5
[request_definition]
r = sub, act, obj, scope

[policy_definition]
p = sub, act, obj, eft
Copy link
Member

@mariajgrimaldi mariajgrimaldi Sep 17, 2025

Choose a reason for hiding this comment

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

After thinking about it, I now lean toward not using obj at all. Here is why:

  • Every request and policy are explicitly scoped. Here are some examples: can I edit course 123? can I edit under org 123? or can I edit globally? This follows our note about not modeling containment ourselves here [FC-0099] docs: ADR proposing authorization foundation for openedx ecosystem #31 (comment)
  • If these are scoped, what value does obj add? If I can edit a course, there is a policy with my username, the role with the edit permission, and the course scope. If I can edit org-wide, the role has an org scope, and so on. If you think about it, we actually don't need the object.
  • obj seems useful only for exceptions. For example, org-wide edit rights except for one course. I still think we can model that without obj.

So at least for the MVP, I don't think we need to model the object. Maybe in the future when we need more granularity at the object level we might need to although I can't think of a use case at the moment. Do you have one in mind? Maybe I'm not seeing it.
One concern I do have about not modeling objects, although they seem unnecessary, is how difficult it would be to migrate to a model with objects if we get to need them in the future.

What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I see your point, and it makes sense that the scope already covers most of the use cases we care about in the MVP. Removing obj definitely simplifies the model and avoids redundancy.

However, I wonder how we could model exceptions (e.g., org-wide edit rights except for one course) without on obj?

Copy link
Member

Choose a reason for hiding this comment

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

Mmmm I'm not sure. If we add an exception for a single course, the system won't pick it up as part of the org policy cause those would be different scopes. I think we have two options to model this:

  1. We completely drop support for exceptions for the MVP and consider these policies as an allowlist -> I'm not sure about this, I'm concerned about the future re-work.
  2. We leave the door open for future support by allowing passing scopes as: containers (orgs can contain multiple courses) or object-level (single course or library). When using containers (like orgs), we must pass the object in the assignment - this would be easy to read. When using object-level, we only use the object and ignore the scope in the assignment - also easy to read. The object in the container won't be useful, but it works as documentation and future exceptions support via dynamic policies, for example. ([FC-0099] docs: ADR proposing authorization foundation for openedx ecosystem #31 (comment))

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mariajgrimaldi, I was testing a new model using your proposed approach without object in this branch.

What do you think about this? What I don’t like is having to specify the scope in both the role assignments (g) and the policies (p)., e.g.

# Bob is org admin for OpenedX (org scope)
g, user:bob, role:org_admin, org:OpenedX

# Org admin: can manage all resources within org OpenedX
p, role:org_admin, act:manage, org:OpenedX, allow

Copy link
Member

@mariajgrimaldi mariajgrimaldi Sep 18, 2025

Choose a reason for hiding this comment

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

I'd need to think about it more but I don't want to block this specific PR. Is this something we can refine as we go? Before the integration with the rest of the ecosystem, of course.

Copy link
Member

@mariajgrimaldi mariajgrimaldi Sep 22, 2025

Choose a reason for hiding this comment

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

Here's my quick analysis of the examples you shared. Maybe it helps explain why I'm still a bit confused while reviewing this again:
policy-review

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hi @mariajgrimaldi, thank you for your analysis. Yes, I really think the proposed model is confusing, and handling the scope both in the policy and in the role definitions might not be the best way to approach it. I’ve been working on another approach, based on your comments about not using an object, and I think it could be better structured.

Basically, the scope would be removed from the role definitions and used only in the policy. This branch has all the details, and I’d like you to review it. The main changes are:

  • There is no object (obj)
  • The scope exists only in the policy (p)
  • The role definition (g) no longer includes the scope

I’ll take a similar example to the previous one but using the new model:

# Organization-level permissions
p, role:org_admin, act:manage, org:OpenedX, allow
# All library edit permissions
p, role:library_author, act:edit, lib:*, allow

# Organization administrators
g, user:alice, role:org_admin
# Library authors
g, user:mary, role:library_author

An example request would be:

user:alice, act:manage, org:OpenedX    # True
user:alice, act:manage, org:MIT        # False

user:mary, act:edit, lib:math-basics   # True
user:mary, act:edit, lib:science-101   # True

I think the relationship is much clearer this way. Also, exceptions could be included. For example, Mary can edit all libraries except one specific library:

p, role:library_author, act:edit, lib:restricted, deny

So this would result in no permission:

user:mary, act:edit, lib:restricted    # False

What do you think of this proposal? Personally, I see it as an improvement, but I’d like to know if maybe I’m overlooking something important to consider.

Copy link
Member

@mariajgrimaldi mariajgrimaldi Sep 23, 2025

Choose a reason for hiding this comment

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

That’s definitely an improvement!

I made a few tweaks to address my main concern, which is that default roles shouldn't have access to specific scopes like org:OpenedX. Here's what I got:

[request_definition]
# Request format: subject (user), action, scope_id (specific resource being accessed)
r = sub, act, scope_id

[policy_definition]
# Policy format: subject (role), action, scope_type (pattern), effect (allow/deny)
p = sub, act, scope_type, eft

[role_definition]
# g: user-to-role assignments with scope (user, role, scope_id)
# g2: action hierarchies (parent_action, child_action)
g = _, _, _
g2 = _, _

[policy_effect]
# Deny overrides: if any policy says deny, result is deny; otherwise allow if any policy says allow
e = some(where (p.eft == allow)) && !some(where (p.eft == deny))

[matchers]
# Match if: user has role in scope AND scope matches pattern AND (action matches OR action inherits via g2)
m = g(r.sub, p.sub, r.scope_id) && keyMatch(r.scope_id, p.scope_type) && (r.act == p.act || g2(p.act, r.act))

UPDATE: the problem here ^ would be that we're closing the door for resources that are not necessarily scopes. For example: an asset within a course (course scope, resource asset). A valid response to this could be we won't support it, though. We still haven't made this decision.

Another more flexible option could be:

[request_definition]
r = sub, act, object_id, scope_id # Specific resource being accessed under the scope. I have to have the permission on the scope with the namespaced-object

[policy_definition]
p = sub, act, object_type, scope_type, eft # Generic template for roles definitions: all courses within an org, all libraries within an org, anything within an org, etc  

[role_definition]
g = _, _, _ # Same as before
g2 = _, _

[policy_effect]
e = some(where (p.eft == allow)) && !some(where (p.eft == deny))

[matchers]
m = g(r.sub, p.sub, r.scope_id) && keyMatch(r.object_id, p.object_type) && keyMatch(r.scope_id, p.scope_type) && (r.act == p.act || g2(p.act, r.act))

Regarding this matcher, I think there might be something missing in the role definition cause we're not associating the object_id in any g type. This one is similar to our original proposal, but with a different naming approach.


I don’t want to hold this up any longer. What I like about option 2 is that we can move to option 1 pretty easily, and the other way around as well. If you agree, we could go with your proposal (option 1) now and switch to option 2 later if needed.

What do you think? Do you think this could work?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hi again, @mariajgrimaldi. Thank you very much for your comments and suggestions.

I definitely think that proposal N1 you mentioned is better structured, especially considering the concern about default roles. I’ve been testing this new model, and it works very well for our MVP needs. Based on the current use cases, it’s not necessary to include an object (obj), only the scope, which also makes the modeling simpler.

From your proposal, I added the following to the matcher:

- g(r.sub, p.sub, r.scope)
+ (g(r.sub, p.sub, r.scope) || g(r.sub, p.sub, '*'))

This covers cases where we need to assign permissions across all libraries:

p, role:library_admin, manage_library, lib:*, allow
g, user:steve, role:library_admin, *

This way, requests like the following are allowed without having to assign each specific library:

user:steve manage_library lib:123
user:steve manage_library lib:xyz

What do you think about this?

I’m currently updating the tests to reflect the changes in the model.

Copy link
Member

Choose a reason for hiding this comment

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

That's great, @BryanttV! Thanks for trying out my suggestions. Let's use this as our base model, and if we need more coverage later on, we can build on it.

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.

More detailed comments about the implementation. This is looking good! Thank you so much :)

Should we share this on the slack channel to get more reviews?

Comment on lines 19 to 43
help = (
"Test Casbin enforcement policies using model.conf, policy.csv, and request.txt. "
"Supports interactive mode for custom testing."
)
Copy link
Member

Choose a reason for hiding this comment

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

More than testing I see this as a simulation for a real environment to test whether our model is working properly. Can we change the wording to reflect that? Also I think we need to change requests.txt for requests.sample.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I updated the command. What do you think?

Comment on lines 1 to 5
[request_definition]
r = sub, act, obj, scope

[policy_definition]
p = sub, act, obj, eft
Copy link
Member

Choose a reason for hiding this comment

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

Mmmm I'm not sure. If we add an exception for a single course, the system won't pick it up as part of the org policy cause those would be different scopes. I think we have two options to model this:

  1. We completely drop support for exceptions for the MVP and consider these policies as an allowlist -> I'm not sure about this, I'm concerned about the future re-work.
  2. We leave the door open for future support by allowing passing scopes as: containers (orgs can contain multiple courses) or object-level (single course or library). When using containers (like orgs), we must pass the object in the assignment - this would be easy to read. When using object-level, we only use the object and ignore the scope in the assignment - also easy to read. The object in the container won't be useful, but it works as documentation and future exceptions support via dynamic policies, for example. ([FC-0099] docs: ADR proposing authorization foundation for openedx ecosystem #31 (comment))

@mariajgrimaldi mariajgrimaldi changed the title [FC-0099] feat: add casbin model [FC-0099] feat: add casbin model configuration (CONF) Sep 18, 2025
Copy link
Contributor

@bmtcril bmtcril left a comment

Choose a reason for hiding this comment

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

Just a couple of questions in the files currently. I think we will need to sanitize inputs somehow since we're taking in user strings for things like usernames, orgs, course keys, library keys, etc. that may have asterisks or other unexpected characters in them. I'd love to see some test cases around that and make sure those cases all work as expected.

# - Scope determines which role assignments are considered
# - "*" = global scope (system-wide roles)
# - "org:OpenedX" = organization-scoped roles
# - "course:course-v1:..." = course-scoped roles
Copy link
Contributor

Choose a reason for hiding this comment

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

If there is a lib based scope we should add it here too

Copy link
Contributor

@bmtcril bmtcril Sep 19, 2025

Choose a reason for hiding this comment

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

I assume that the course-v1 isn't explicitly needed or validated? For instance ccx courses have a different prefix, and I'm sure someday there will be a course-v2. 🙂 It might be good to add tests for those things to make sure they continue to work since people always forget about ccx.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  1. I updated the comments to mention the lib scope.
  2. Correct, the prefix in this case would be course-v1, but another scope could be course-v2 or any other type of prefix we might need.

Copy link
Contributor

@MaferMazu MaferMazu left a comment

Choose a reason for hiding this comment

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

@BryanttV Thanks for this. I was testing the model, and it appears to be very good.

A suggestion: for clearer testing, it may be better to test not for roles, but for features, and use generic wording.

What I tested

Policy

# Action inherence (action grouping)
g2, act:manage, act:delete
g2, act:delete, act:edit
g2, act:edit, act:read

# Role definition and wildcard for resource
p, role:manager, act:manage, *, allow
p, role:org_manager, act:manage, *, allow
p, role:lib_manager, act:manage, lib:*, allow

# Role assignment, wildcard for scope, resource grouping via scope
g, user:admin, role:manager, *
g, user:org_admin, role:org_manager, org:MIT
g, user:org_lib_admin, role:lib_manager, org:MIT

#Granular assignment
p, user:editor, act:edit, *, allow
p, user:specific_lib_manager, act:manage, lib:specific, allow

#Deny access
p, user:admin, act:manage, lib:restricted-content, deny

#Custom role (role inheritance)
g, role:my_custom, role:manager, org:openedx
p, role:my_custom, act:draw, *, allow

g, user:creative_manager_openedx, role:my_custom, org:openedx

Requests with comments

# The user:org_admin only has access to org:MIT
user:org_admin, act:manage, something, null #false
user:org_admin, act:manage, something, org:MIT #true

# The user:org_lib_admin only has managment over resources lib:* and org:MIT
user:org_lib_admin, act:manage, something, org:MIT #false
user:org_lib_admin, act:manage, lib:some_lib, org:MIT #true

# Testing the action inherance (grouping)
user:org_admin, act:delete, something, org:MIT #true
user:org_admin, act:read, something, org:MIT #true

# Granular permissions
# The user:editor can edit all
user:editor, act:edit, something, null #true

# The user:specific_lib_manager, can manage lib:specific
user:specific_lib_manager, act:edit, lib, null #true
user:specific_lib_manager, act:edit, lib:specific, null #false

# Exceptions and deny access
user:admin, act:edit, lib:restricted-content, org:x #false

# Role inheritance
# user:creative_manager_openedx has role:my_custom which inherits from role:manager
user:creative_manager_openedx, act:read, something, org:openedx
Requests ready to copy and paste
user:org_admin, act:manage, something, null
user:org_admin, act:manage, something, org:MIT
user:org_lib_admin, act:manage, something, org:MIT
user:org_lib_admin, act:manage, lib:some_lib, org:MIT
user:org_admin, act:delete, something, org:MIT
user:org_admin, act:read, something, org:MIT
user:editor, act:edit, something, null
user:specific_lib_manager, act:edit, lib, null
user:specific_lib_manager, act:edit, lib:specific, null
user:admin, act:edit, lib:restricted-content, org:x
user:creative_manager_openedx, act:read, something, org:openedx

I tested using https://casbin.org/editor/ (and the PyCasbin option).

Notes about what we are supporting:

  • Resource Grouping ✅ (Note: we are checking the policy, but to know if a resource belongs to a group (for example, org), the request is the protagonist)
  • Role Inheritance Across the Scopes ⚠️ (It works, but I had problems when I wanted to inherit a role with a wildcard scope)
  • Namespace with Wildcard Support ✅
  • System-Wide Roles ✅
  • Granularity of Permissions ✅ (Note: for granularity assignments, the policy starts with p instead of g)
  • Exceptions / Negative Rules ✅
  • Scoped Assignments ✅
  • Action Grouping ✅

Details on what I encounter that fails:

In my test, I had:

#Custom role (role inheritance)
g, role:my_custom, role:manager, org:openedx
p, role:my_custom, act:draw, *, allow

g, user:creative_manager_openedx, role:my_custom, org:openedx

If I change the first line to a g, role:my_custom, role:manager, *, it doesn't work well (The request: user:creative_manager_openedx, act:manage, something, org:openedx returns false, and it should return true).

And we'll probably need those inheritances, not only for future custom roles, but for mapping that a library_admin inherits from library_author, and this inherits from library_collaborator, and so on.

# obj = object selector - supports multiple patterns via keyMatch:
# - Exact ID: "course:course-v1:OpenedX+Demo+2024"
# - Namespace wildcard: "course:*" (matches all courses)
# - Prefix patterns: "course:course-v1:OpenedX+*" (matches all OpenedX courses)
Copy link
Member

@mariajgrimaldi mariajgrimaldi Sep 22, 2025

Choose a reason for hiding this comment

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

[nit - nice to have] Does this look right?

course:course-v1:OpenedX+DemoX+DemoCourse
lib:lib:DemoX:CSPROB

Now that I see it in the conf, it looks off to me. Maybe we can consider something different:
course:<course-v1:OpenedX+DemoX+DemoCourse>
lib:<lib:DemoX:CSPROB>
...
course:<course_id...regex>

What do you think? I like it since it clearly separates what the namespace is and what the ID is.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, it might look a bit confusing. However, in the case of using <>, I suppose we would also have to change the definitions of role:<>, act:<>, user:<>, etc., to keep things consistent.

Another option would be to use something like this (I saw this suggestion somewhere else but can’t find it anymore):

If the scope doesn’t have a namespace, we include one:

org:OpenedX

If the scope already has a namespace, we leave it as is:

course-v1:OpenedX+DemoX+DemoCourse
lib:Openedx:CSPROB

Another option could be replacing the : with another distinguishing character, like @:

user@alice
org@OpenedX
course@course-v1:OpenedX+DemoX+DemoCourse
lib@lib:DemoX:CSPROB

For now, I think we could stick with the current format, and if needed, we can adopt a different notation later. What do you think?

Copy link
Member

@mariajgrimaldi mariajgrimaldi Sep 25, 2025

Choose a reason for hiding this comment

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

As @bmtcril suggested, let's use this for now, but please open an issue in the repo to review the points mentioned in his comment.

@mariajgrimaldi
Copy link
Member

Thank you so much for this! I'm still reviewing this, but looks good so far. I do still have some questions though:

  1. What do you mean by Role Inheritance Across the Scopes? In the model and ADRs we have proposed so far, we said that inheritance of scopes will be explicitly handled by whoever was writing the authorization checks. Do we mean something different by role inheritance across scopes?
  2. System-Wide Roles: are these instance-wide roles? Or system-wide, like a Django superuser would be? If these are instance level roles, can we document them as such?
  3. Can we explain in some docs how the checks for this model.conf work? Also some examples would be nice, no need for a sophisticated format, we can improve the docs later on. This would be useful for future reviewers.

@mariajgrimaldi mariajgrimaldi added the FC Relates to an Axim Funded Contribution project label Sep 22, 2025
@mariajgrimaldi
Copy link
Member

@MaferMazu: thanks for testing this out!

Could you please attach the requests you made so we can rerun them with our new model? Thanks!

- Display of loaded policies, role assignments, and action grouping rules

Example usage:
python manage.py lms enforcement --policy-file-path /path/to/authz.policy
Copy link
Member

@mariajgrimaldi mariajgrimaldi Sep 23, 2025

Choose a reason for hiding this comment

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

[nit] Can we run this outside the lms container? I think it'd be useful to be able to run this kind of command in an isolated environment without all that having an lms container running involves.

Copy link
Member

@mariajgrimaldi mariajgrimaldi Sep 24, 2025

Choose a reason for hiding this comment

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

I changed the test.py file to be this and can access the django shell without any issues - I removed the plugin_settings for convenience:

"""
Test settings for openedx_authz plugin.
"""

from django.conf import settings
import os
from openedx_authz import ROOT_DIRECTORY


# Add Casbin configuration
CASBIN_MODEL = os.path.join(ROOT_DIRECTORY, "engine", "config", "model.conf")
# Redis host and port are temporarily loaded here for the MVP
REDIS_HOST = "redis"
REDIS_PORT = 6379
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": "default.db",
        "USER": "",
        "PASSWORD": "",
        "HOST": "",
        "PORT": "",
    }
}

INSTALLED_APPS = (
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.messages",
    "django.contrib.sessions",
    "openedx_authz.apps.OpenedxAuthzConfig",
    "casbin_adapter.apps.CasbinAdapterConfig",
)

MIDDLEWARE = [
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
]

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]

SECRET_KEY = "test-secret-key"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I hadn’t tried it before, but with the current configuration, it’s also possible to run the command outside the container.

@MaferMazu
Copy link
Contributor

@mariajgrimaldi I tested using https://casbin.org/editor/ (and the PyCasbin option).
Policy

# Action inherence (action grouping)
g2, act:manage, act:delete
g2, act:delete, act:edit
g2, act:edit, act:read

# Role definition and wildcard for resource
p, role:manager, act:manage, *, allow
p, role:org_manager, act:manage, *, allow
p, role:lib_manager, act:manage, lib:*, allow

# Role assignment, wildcard for scope, resource grouping via scope
g, user:admin, role:manager, *
g, user:org_admin, role:org_manager, org:MIT
g, user:org_lib_admin, role:lib_manager, org:MIT

#Granular assignment
p, user:editor, act:edit, *, allow
p, user:specific_lib_manager, act:manage, lib:specific, allow

#Deny access
p, user:admin, act:manage, lib:restricted-content, deny

#Custom role (role inheritance)
g, role:my_custom, role:manager, org:openedx
p, role:my_custom, act:draw, *, allow

g, user:creative_manager_openedx, role:my_custom, org:openedx

Requests with comments

# The user:org_admin only has access to org:MIT
user:org_admin, act:manage, something, null #false
user:org_admin, act:manage, something, org:MIT #true

# The user:org_lib_admin only has managment over resources lib:* and org:MIT
user:org_lib_admin, act:manage, something, org:MIT #false
user:org_lib_admin, act:manage, lib:some_lib, org:MIT #true

# Testing the action inherance (grouping)
user:org_admin, act:delete, something, org:MIT #true
user:org_admin, act:read, something, org:MIT #true

# Granular permissions
# The user:editor can edit all
user:editor, act:edit, something, null #true

# The user:specific_lib_manager, can manage lib:specific
user:specific_lib_manager, act:edit, lib, null #true
user:specific_lib_manager, act:edit, lib:specific, null #false

# Exceptions and deny access
user:admin, act:edit, lib:restricted-content, org:x #false

# Role inheritance
# user:creative_manager_openedx has role:my_custom which inherits from role:manager
user:creative_manager_openedx, act:read, something, org:openedx

Requests ready to copy and paste

user:org_admin, act:manage, something, null
user:org_admin, act:manage, something, org:MIT
user:org_lib_admin, act:manage, something, org:MIT
user:org_lib_admin, act:manage, lib:some_lib, org:MIT
user:org_admin, act:delete, something, org:MIT
user:org_admin, act:read, something, org:MIT
user:editor, act:edit, something, null
user:specific_lib_manager, act:edit, lib, null
user:specific_lib_manager, act:edit, lib:specific, null
user:admin, act:edit, lib:restricted-content, org:x
user:creative_manager_openedx, act:read, something, org:openedx

@gviedma-aulasneo
Copy link

Just a reminder, we need to model complex scopes per role.

We expect custom roles that grant permissions to different resources with different scopes. The system should support a single role assignment that allows, for example:
Create Courses in org A
Instructor in courses D and F
Staff actions in course C
Library Admin in all libraries

To be clear, this is illustrative language, a role groups permissions, not roles, so please do not read this as role nesting.
Happy to share more detail on the AuthZ direction if helpful.

@mariajgrimaldi
Copy link
Member

mariajgrimaldi commented Sep 24, 2025

Thanks for the annotation, @gviedma-aulasneo! Right now we're focusing on immediate needs for the MVP, so the model in this PR is minimal and doesn't cover the use cases you mentioned. Still, we believe Casbin is flexible enough to handle them later. Our plan is to start with this minimal base model and extend it for more complex cases, while keeping it simple and maintaining compatibility with the current rules.

We might test some things out based on this model, but I personally wouldn't like to block this effort to validate those use cases. Does that sound right to you? Let me know!

@gviedma-aulasneo
Copy link

@mariajgrimaldi thanks, that works for me. This clarification came from a sync with @MaferMazu yesterday, she was unsure whether we could accommodate those cases, so I wanted to raise it early. If you are confident that the MVP model and Casbin can extend to cover them, I share your opinion of not blocking this PR and validating those scenarios later, given the short timeline.

chore: add base and test requirements
feat: add model.conf of casbin
test: include test suite for enforcement
# This model supports:
# - Scoped role assignments (user roles tied to specific contexts)
# - Action grouping (manage → read/write/delete to reduce duplication)
# - Action grouping (manage → read/write/edit/delete to reduce duplication)
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the difference between write and edit here? Is write just create + edit?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually, this example can also be seen like this:

# manage implies edit and delete
["g2", "act:manage", "act:edit"],
["g2", "act:manage", "act:delete"],
# edit implies read and write
["g2", "act:edit", "act:read"],
["g2", "act:edit", "act:write"],

by inheritance manage would include all permissions

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah thank you, I think I missed that there could be multiple levels of inheritance

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 addressing all of my comments! I think this is good enough for our base model, we can keep building on it if needed.

Great job!

Copy link
Contributor

@bmtcril bmtcril left a comment

Choose a reason for hiding this comment

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

I agree that this is plenty for this PR and we should get it merged and iterate. I'd also like to get some clarity around namespacing and ownership of namespaces for existing cases and the extensions since there is some possibility of collisions and unintended issues there.

@BryanttV
Copy link
Contributor Author

@mariajgrimaldi @bmtcril, I just created an issue to clarify the namespace: #65

@mariajgrimaldi mariajgrimaldi merged commit c9ed193 into openedx:main Sep 25, 2025
14 checks passed
@github-project-automation github-project-automation bot moved this from Waiting on Author to Done in Contributions Sep 25, 2025
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). FC Relates to an Axim Funded Contribution project open-source-contribution PR author is not from Axim or 2U

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

Create a solid model.conf to test Casbin with a use case close to what we'll implement

6 participants