Technical Discovery Notes - AuthZ for Course Authoring

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_enforcer method on the AuthzEnforcer class in openedx-authz.

  • key_match_func is imported from casbin.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:

  1. 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.

  2. Currently, permissions for libraries are hard-coded on openedx-authz, this would need to be externalized to be defined in edx-platform first.

  3. REST API endpoints would need to be created to expose the list of permissions and their metadata so the frontend can consume it.

  4. REST API endpoints need to be created to list, update, delete and create roles.

  5. Permissions need to be defined for role management.

  6. 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

  7. 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

GET http://studio.local.openedx.io:8001/api/contentstore/v1/course_team/course-v1:OpenedX+DemoX+DemoCourse

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

PUT http://studio.local.openedx.io:8001/course_team/course-v1:OpenedX+DemoX+DemoCourse/contributor@example.com

Payload:

{"role":"staff"}

Remove team member

DELETE http://studio.local.openedx.io:8001/course_team/course-v1:OpenedX+DemoX+DemoCourse/contributor@example.com

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

POST http://local.openedx.io:8000/courses/course-v1:OpenedX+DemoX+DemoCourse/instructor/api/list_course_role_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