Technical Discovery Notes - AuthZ for Course Authoring
v0.5
This document lists the findings on the preliminary technical discovery being done for the AuthZ for Course Authoring feature set being defined here: https://openedx.atlassian.net/wiki/spaces/OEPM/pages/edit-v2/5364121605
Auditability
Desired functionalities:
Keeping the history of changes done to permissions, for example, each time a user is added to the team, who added it. If a library changed to allow public read, who did it.
Findings:
pycasbin has a method called โenforce_exโ which can be used instead of the โenforceโ method being used currently. The difference is that โenforce_exโ, in addition to specify if the permission is allowed, it also returns information on which policy caused the response.
Example:
enforcement test: contributor content_libraries.view_library lib:WGU:CSPROB
Result: โ ALLOWED: contributor content_libraries.view_library lib:WGU:CSPROB
Policy that caused the answer:['role^library_user', 'act^content_libraries.reuse_library_content', 'lib^*', 'allow']
This could be useful for debugging and explainability, however doesnโt help with the primary requirements.
openedx-authz doesnโt store change history at this point
In order to keep the change history, a new model needs to be created, and the api methods used to modify the model should be responsible of updating the table whenever a change is done.
Context on the user that requested the changes may need to be passed to the api calls if itโs not already available.
ย
Multi scope roles
Desired functionalities:
Simplify assignment of a policy to a user that applies to multiple resources
Findings:
Currently, the matcher in the model only supports either validating a permission to a specific scope item (lib:WGU:CSPROB), or everything in the scope that the permission supports (
*, if*matches the same namespace specified in the permission definition (lib, course, org, etc.)).This is because the โgโ (grouping) function in the Casbin model only accounts for exact matches.
This can be overcome by specifying a matching function that supports glob-like syntax, this is done by calling โenforcer.add_named_domain_matching_func("g", key_match_func)โ after instantiating the enforcer in the
_initialize_enforcermethod on theAuthzEnforcerclass in openedx-authz.key_match_funcis imported fromcasbin.util. It allows matching the end of a string with a โ*โ. See
Functions | Apache Casbin for details and other available matching functions.Further testing, research and implementation needs to be done to make sure this doesnโt break existing functionality, doesnโt introduce security concerns (may need to add extra validations to the api when modifying role assignments), and understand whatโs the performance impact of this.
ย
On adding permissions to existing course features on top of existing API endpoints
The main endpoints being used on the different pages and features on the Studio Course interfaces are:
Course list:
GET /api/contentstore/v1/home/: Global permissions and general toggles
GET /api/contentstore/v2/home/courses/: Courses list
Course Outline:
GET /course/(courseid)/search_reindex/: Reindex course
POST /xblock/: New section, New subsection, New Unit, Add Xblock
POST /xblock/(blockid)/: Update xblock
DELETE /xblock/(blockid)/: Delete xblock
POST /xblock/(blockid)/, body {"publish":"make_public"}: Publish
Course updates:
GET /course_info_update/(courseid)/: List course updates
POST /course_info_update/(courseid/: Crease course update
PUT /xblock/(bockid)@handouts: Edit course handouts
Pages & Resources:
GET /api/course_apps/v1/apps/(courseid)/: List Course apps
PATCH /api/course_apps/v1/apps/(courseid)/: Enable calculator app
PUT /xblock/: Add a new custom page
POST /xblock/(blockid)/: Update Page content
POST /textbooks/(courseid)/: Create Textbook
Files:
Note: Assets have their own namespace, example: 1. "/asset-v1:OpenedX+DemoX+DemoCourse+type@asset+block@Open_edX_Demo_Course___Textbooks.pdf"
GET /assets/(courseid)/: List Files
GET /(assetid): Download a file
GET /assets/(courseid)/(assetid)/usage: Get file info
PUT /assets/(courseid)/(assetid)/, payload: {"locked":true}: Lock file
DELETE /assets/(courseid)/(assetid)/: Delete file
Videos:
POST /videos/(courseid)/: Upload video
Schedule & Details:
GET /api/contentstore/v1/course_settings/(courseid)/
GET /api/contentstore/v1/course_details/(courseid)/
GET /api/courses/v1/courses/(courseid)/
PUT /api/contentstore/v1/course_details/(courseid)/: Save schedule and details changes
Grading:
GET /api/contentstore/v1/course_grading/(courseid)/: Get course grading data
POST /api/contentstore/v1/course_grading/(courseid)/: Update course grading
GET /api/contentstore/v1/course_settings/(courseid)/: Get other course settings
GET /api/courses/v1/courses/(courseid)/: Get general course information
Group configurations:
GET /api/contentstore/v1/group_configurations/(courseid)/: Get group configurations
POST /api/contentstore/v1/group_configurations/(courseid)/(configurationid)/: Edit group
POST /api/contentstore/v1/group_configurations/(courseid)/(configurationid)/: Create new group
DELETE /api/contentstore/v1/group_configurations/(courseid)/(configurationid)/(itemid)/: Delete group
Advanced Settings:
GET /api/contentstore/v0/advanced_settings/(courseid)/: Get advanced settings
PATCH /api/contentstore/v0/advanced_settings/(courseid)/: Update advanced settings
GET /api/contentstore/v1/proctoring_errors/(courseid)/: Get info on proctoring errors
Certificates:
GET /api/contentstore/v1/certificates/(courseid)/: Get certificates info
Checklists:
GET /api/courses/v1/courses/(courseid)/
GET /api/courses/v1/validation/(courseid)/
GET /api/courses/v1/quality/(courseid)/
ย
In resume, here is the list of the main endpoints used across studio for course features:
/api/contentstore/v1/home/
/api/contentstore/v2/home/courses/
/course/(courseid)/search_reindex/
/api/courses/v1/quality/(courseid)/
/api/courses/v1/validation/(courseid)/
/api/courses/v1/courses/(courseid)/
/api/contentstore/v1/certificates/(courseid)/
/api/contentstore/v0/advanced_settings/(courseid)/
/api/contentstore/v1/proctoring_errors/(courseid)/
/api/contentstore/v1/group_configurations/(courseid)/(group_configurations_id)/
/group_configurations/(courseid)/(group_configurations_id)/(item_id)/
/api/contentstore/v1/course_grading/(courseid)/
/api/contentstore/v1/course_settings/(courseid)/
/api/contentstore/v1/course_details/(courseid)/
/videos/(courseid)/
/assets/(courseid)/(fileid)/
/textbooks/(courseid)/
/xblock/
/xblock/(blockid)/
/api/course_apps/v1/apps/(courseid)/
/course_info_update/(courseid)/
Main courses permissions logic lives in the get_user_permissions function here:
openedx-platform/common/djangoapps/student/auth.py at 32b7f27c46b5e2c69b055cc0d3a41f9079c52e80 ยท openedx/openedx-platform
Most Endpoints use the has_studio_read_access or has_studio_write_access (also called has_course_author_access) functions to validate permissions, which in turn uses the get_user_permissions function.
This means that in most cases, we can replace the call to has_studio_write_access or has_studio_read_access to the desired openedx-authz validation function and permission.
For some features, this will be a simple replacement, for example for advanced settings, proctoring and grading which have specific endpoints.
However, some features, specially publish and general xblock updates, which share the same /xblock/ endpoint, they will require a more careful implementation to handle the different use cases with the desired new permissions.
In general, per-endpoint custom permissions are easy to implement, but if we want to implement more granular permissions on the same endpoint (like restricting specific properties), that requires more planning and careful implementation and testing.
ย
On implementing custom roles
The openedx-authz core, Casbin, and the DB representation of Casbin policies is prepared for supporting custom roles.
However, the higher level API layers require extra work to be able to support this feature.
Before supporting custom roles, the following items need to be taken care of:
Define a decentralized way of easily registering new permissions - this means, allowing external modules that depend on openedx-authz, to register their own permissions and metadata such as permission description and possibly icon, so that info can be used in the UI to display info on the permission on a user-friendly way.
Currently, permissions for libraries are hard-coded on openedx-authz, this would need to be externalized to be defined in edx-platform first.
REST API endpoints would need to be created to expose the list of permissions and their metadata so the frontend can consume it.
REST API endpoints need to be created to list, update, delete and create roles.
Permissions need to be defined for role management.
Existing mechanism for ensuring data consistency depends on database relationships with the model of the related item to be associated with permissions. This needs to be revised to more easily support extensibility to decentralize permission definition
An API to define default roles, or default policies to global roles, should be created so modules that depend on openedx-authz can define new defaults.
ย
Discovery for Course Team API retrocompatibility
From Studio > Settings > Course Team
List existing course team
Response:
{
"show_transfer_ownership_hint": false,
"users": [
{
"email": "contributor@example.com",
"id": 5,
"role": "instructor",
"username": "contributor"
},
{
"email": "admin@example.com",
"id": 4,
"role": "staff",
"username": "admin"
}
],
"allow_actions": true
}Add team member
POST http://studio.local.openedx.io:8001/course_team/course-v1:OpenedX+DemoX+DemoCourse/admin@example.com
Payload:
{"role":"staff"}Make team member an admin
PUT http://studio.local.openedx.io:8001/course_team/course-v1:OpenedX+DemoX+DemoCourse/admin@example.com
Payload:
{"role":"instructor"}Remove admin access
Payload:
{"role":"staff"}Remove team member
Notes:
It seems that "instructor" is admin, otherwise it's "staff".
From Instructor Dashboard > Instructor > Membership
Adding Staff user
POST http://local.openedx.io:8000/courses/course-v1:OpenedX+DemoX+DemoCourse/instructor/api/modify_access
Payload: Form data:
unique_student_identifier=contributor&
rolename=staff&
action=allow
Response:
{
"unique_student_identifier": "contributor",
"rolename": "staff",
"action": "allow",
"success": "yes"
}POST http://local.openedx.io:8000/courses/course-v1:OpenedX+DemoX+DemoCourse/instructor/api/modify_access
Paylod: Form data:
unique_student_identifier=contributor%40example.com&
rolename=staff&
action=revoke
Adding Limited Staff user
unique_student_identifier=contributor&
rolename=limited_staff&
action=allow
List team members
Payload: rolename=instructor
Response:
{
"course_id": "course-v1:OpenedX+DemoX+DemoCourse",
"instructor": [
{
"username": "admin",
"email": "admin@example.com",
"first_name": "",
"last_name": ""
}
]
}Adding Data Researcher
unique_student_identifier=contributor&rolename=data_researcher&action=allow
Adding Beta Tester
unique_student_identifier=contributor&rolename=beta&action=allow