feat: add support for course permission in authz rest api#274
feat: add support for course permission in authz rest api#274
Conversation
|
Thanks for the pull request, @MaferMazu! This repository is currently maintained by 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 approvalIf you haven't already, check this list to see if your contribution needs to go through the product review process.
🔘 Provide contextTo 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:
🔘 Get a green buildIf one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green. DetailsWhere 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:
💡 As a result it may take up to several weeks or months to complete a review and merge your PR. |
9f9dd2d to
c8d3d1c
Compare
| if not scope: | ||
| scopes = request.data.get("scopes") | ||
| if scopes and isinstance(scopes, list): | ||
| scope = scopes[0] |
There was a problem hiding this comment.
Is there any edge case where the list of scopes include mixed scopes? Then this won't work, not sure if that case is only a strech or a real possibility.
There was a problem hiding this comment.
Currently there is no use case that would have mixed scopes, as the current flow requires the user to select either a course or library role to apply. But it could be certainly a possibility in the future, I think we should document this with a comment and perhaps validate and fail here if we get mixed scope types.
There was a problem hiding this comment.
What we do get possibly is specific scopes and glob scopes mixed, but they will always point to the same type either lib or course.
There was a problem hiding this comment.
I just added more doc about it and the mixed scope check here: 528ca50
Let me know if you have more thoughts about this.
| perm_instance = self._get_permission_instance(request) # namespace resolved from scopes[0] | ||
| if not isinstance(perm_instance, MethodPermissionMixin): | ||
| return False | ||
| required = perm_instance.get_required_permissions(request, view) | ||
| if not required: | ||
| return False |
There was a problem hiding this comment.
Are these cases only relevant when having a scopes_list or can they be generalized for all requests (moving it one level up the tree)?
There was a problem hiding this comment.
When we have one scope, it is already covered, because we call return self._get_permission_instance(request).has_permission(request, view), and inside that, it could be a CoursePermission/ContentLibraryPermission.
class ContentLibraryPermission(MethodPermissionMixin, BaseScopePermission):
"""Permission handler for content library scopes.
This class implements permission checks specific to content library operations.
It uses the authz API to verify whether a user has the necessary permissions
to perform actions on library team members.
"""
NAMESPACE: ClassVar[str] = "lib"
"""``lib`` for content library scopes."""
def has_permission(self, request, view) -> bool:
"""Check if the user has permission to perform the requested action.
First checks if the view method has @authz_permissions decorator.
If present, validates all required permissions. If not present,
allows access by default.
Returns:
bool: True if the user has the required permission, False otherwise.
Also returns False if no scope value is provided in the request.
"""
scope_value = self.get_scope_value(request)
if not scope_value:
return False
permissions = self.get_required_permissions(request, view)
if permissions:
return self.validate_permissions(request, permissions, scope_value)
return TrueThe one that was missing is when we have multiple scopes.
| if permissions: | ||
| return self.validate_permissions(request, permissions, scope_value) |
There was a problem hiding this comment.
Shouldn't if not permissions return False instead?
There was a problem hiding this comment.
No, because in this case, permissions = self.get_required_permissions(request, view). We are checking the permissions inside the decorator.
I think it's clearer with the documentation I just added (96a74e8).
|
Thanks a lot for this! A few questions:
What do we mean by bulk enrollment? Are we referring to role assignment? Can you help me understand why this is a special case? |
| if not isinstance(perm_instance, MethodPermissionMixin): | ||
| return False |
There was a problem hiding this comment.
Why do we need this validation?
There was a problem hiding this comment.
The MethodPermissionMixin check is needed because the decorator is our only source of truth for which permission to verify against the actor. In a single-scope request, each handler manages this on its own — and some handlers intentionally default to True when no decorator is present (e.g., "if no permission is declared, allow it"). That fallback is safe for a single scope because the handler makes a conscious decision. But in a bulk request, defaulting to True across all scopes would be unsafe — we need an explicit permission declaration to verify that the actor has the required (e.g., read) permissions for each requested scope.
This is a refined answer with Claude's help; my first try wasn't as clear as this version. 🙈
Note: I copied and pasted a piece of code of a single-scope handler, you can check it here #274 (comment)
There was a problem hiding this comment.
I've tested the following scenarios integrated with the admin console (all done via the UI) and it's looking all good, thanks! Just pending the outstanding comments:
Non staff user with Course Admin role to a specific course:
- User can access the admin console
- User can see the role assignments for that course
- User can successfully remove a role assignment for that course
- User can successfully add a role assignment for that course
Non staff user with Course Admin role to a whole Org (glob Org):
- User can access the admin console
- User can see the course role assignments for that org
- User can successfully remove a course role assignment for that org
- User can successfully add a course role assignment for that org
Also tested with bulk assigning to a specific course and an org, and worked fine.
Thanks!
|
@mariajgrimaldi @BryanttV this is ready for a re-review. @mariajgrimaldi, regarding your question:
Yes, I was referring to bulk assignments (requests with the same operation across multiple scopes). I hope that with the refactor I did here (4d637ac), it's clearer. |
Description
This PR adds support for course permissions to the authz REST API methods. For example, allowing someone with course team management permissions to call the API to create, remove, and list roles.
How to test
Authenticate as a course admin, and you should be able to perform team management actions like creating, removing, and listing roles over the course you are an admin on.
Here is a Postman collection for a quick test: Open edX AuthZ - Role management.postman_collection.json
Also, I added tests to the code to verify that users with manage team have the proper rights over the REST API methods, and tested bulk enrollment to ensure that a user with permissions over a specific scope can't make a bulk enrollment to a different scope.
Extra
This resolves #273
Implementation details
This PR
validate_permissionsfrom "all must pass" to any. We only need to comply with one permission.DynamicScopePermission, and with the_has_bulk_permission, we are checking that the user has all permissions needed to perform the action within those scopes.Merge checklist:
Check off if complete or not applicable:
Technical documentation and code structure assisted by Claude 4.6 Sonnet; all logic verified and tested manually.