Spike - RBAC AuthZ - Understand current course authoring roles and permissions logic and propose reusable solution

Spike - RBAC AuthZ - Understand current course authoring roles and permissions logic and propose reusable solution

Related ticket: https://github.com/openedx/openedx-authz/issues/205

Objectives:

  • Research how the user access permissions are handled on the legacy system

  • Understand how filtering works for getting bulk objects that a user has access to

  • Propose a reusable solution or pattern we can use to ease further development and support both authz mechanisms

  • Estimate effort needed for implementation

Findings

Research how the user access permissions are handled on the legacy system

Permissions for courses on the legacy system are defined by the CourseAccessRole model, which holds the User-Course-Role or User-Org-Role relationship in the student_courseaccessrole table in the MySQL database.

Example contents of the student_courseaccessrole table:

mysql> SELECT * FROM student_courseaccessrole; +----+---------+------------------------------------+-----------------+---------+ | id | org | course_id | role | user_id | +----+---------+------------------------------------+-----------------+---------+ | 5 | OpenedX | course-v1:OpenedX+DemoX+DemoCourse | instructor | 4 | | 13 | OpenedX | course-v1:OpenedX+DemoX+DemoCourse | beta_testers | 5 | | 14 | OpenedX | course-v1:OpenedX+DemoX+DemoCourse | data_researcher | 5 | | 12 | OpenedX | course-v1:OpenedX+DemoX+DemoCourse | instructor | 5 | | 11 | OpenedX | course-v1:OpenedX+DemoX+DemoCourse | limited_staff | 5 | | 15 | OpenedX | course-v1:OpenedX+DemoX+DemoCourse | staff | 5 | +----+---------+------------------------------------+-----------------+---------+

In addition, any user that is set as a “staff user“ in Django (not not necessarily in the student_courseaccessrole table) has access to everything by default.

The CourseAccessRole model is defined in common/djangoapps/student/models/user.py

Available roles

The role can be any string, however the following roles are clearly defined in code:

Name as shown on the UI

Internal name

Name as shown on the UI

Internal name

Admin

instructor

Staff

staff

Limited Staff

limited_staff

Beta Testers

beta_testers

Course Data Researchers

data_researcher

In addition, the following roles are also defined, but don’t appear currently in the Studio or Instructor Dasbhoard UI:

  • finance_admin

  • sales_admin

  • library_user

  • org_course_creator_group

  • course_creator_group

  • support

Roles are defined in code in common/djangoapps/student/roles.py

Querying for permissions

Most endpoints use the following functions to check for course permissions:

def has_studio_read_access(user, course_key): """ Return True if user is allowed to view this course/library in studio. Will also return True if user has write access in studio (has_course_author_access) There is currently no such thing as read-only course access in studio, but there is read-only access to content libraries. """
def has_studio_write_access(user, course_key, service_variant=None): """ Return True if user has studio write access to the given course. Note that the CMS permissions model is with respect to courses. There is a super-admin permissions if user.is_staff is set. Also, since we're unifying the user database between LMS and CAS, I'm presuming that the course instructor (formally known as admin) will not be in both INSTRUCTOR and STAFF groups, so we have to cascade our queries here as INSTRUCTOR has all the rights that STAFF do. :param user: :param course_key: a CourseKey :param service_variant: the variant of the service (lms or cms). Permissions may differ between the two, see the comment in get_user_permissions for more details. """

Which in turn call the get_user_permissions function to check against a bitmap representation of studio permissions:

def get_user_permissions(user, course_key, org=None, service_variant=None): """ Get the bitmask of permissions that this user has in the given course context. Can also set course_key=None and pass in an org to get the user's permissions for that organization as a whole. :param user: a user :param course_key: a CourseKey or None :param org: an organization name or None :param service_variant: the variant of the service (lms or cms). Permissions may differ between the two, see the HACK comment in the function for more details. """

The possible studio permissions used in the bitmap are:

STUDIO_EDIT_ROLES = 8 STUDIO_VIEW_USERS = 4 STUDIO_EDIT_CONTENT = 2 STUDIO_VIEW_CONTENT = 1 STUDIO_NO_PERMISSIONS = 0

The way these are derived is: the get_user_permissions function checks the contents of the CourseAccessRole model against the requested course or org, and assign the bitmap values accordingly.

Please note, there is a caching layer between get_user_permissions and CourseAccessRole implemented by the AccessRole metaclass and it’s role subclasses.

These functions live in common/djangoapps/student/auth.py

 

Understand how filtering works for getting bulk objects that a user has access to

The courses list at the Studio homepage are given by the GET /api/contentstore/v2/home/courses/ endpoint.

This endpoint returns a list of courses, which are filtered according to the calling user permissions.

The relevant code path for this is:

  1. Endpoint get handler at HomePageCoursesViewV2

  2. get_course_context_v2 function

  3. get_courses_accessible_to_user function

  4. Either _accessible_courses_summary_iter or _accessible_courses_list_from_groups functions

  5. Both functions end up checking against the CourseAccessRole model, but with different approaches for performance reasons

    1. _accessible_courses_summary_iter Just uses stadard Django ORM filter utilities and uses has_studio_read_access to check each course

    2. _accessible_courses_list_from_groups First list courses and orgs from the CourseAccessRole model, and then populates courses data from this information.

 

Propose a reusable solution or pattern we can use to ease further development and support both authz mechanisms

  1. Compatibility layer: For permissions related to code paths that will not be changed by this project, we need to implement a compatibility layer to keep functionality. Because permissions on the legacy system are not granular, we will have to define equivalences and check at the role level.

    1. The new system will have equivalent roles for instructor, staff, limited_staff, beta_testers and data_researcher

    2. We can create a compatibility layer at the CourseAccessRole model level, which based on a flag would either check on the actual model, or on a compatibility layer that will check for equivalent permissions on the new authz system.

    3. Alternatively, we could implement the compatibility layer in the has_studio_read_access and has_studio_write_access functions, however this may not cover all code paths, like the one followed by _accessible_courses_list_from_groups

  2. Endpoint-level permission checks: Based on a flag, we would implement the granular permission or the old permission check on a endpoint to endpoint level.

  3. A Deprecation path should be planned and documented to eventually remove the flag and old code.