0024: API Contract for User-Grouped Role Assignments#
Status#
Draft
Context#
The new Figma design and product requirements for the Team Members tab in the Admin Console change how assignments are presented: instead of listing one row per assignment, the tab now groups assignments by user, showing one row per user with their assignments nested underneath. The existing endpoints for gathering user assignment data don’t return all the fields this user-grouped view needs. Before we can continue building it, we need to define a contract, either for a new endpoint or for a backwards-compatible change to an existing one, that provides those fields.
Following that design, the view displays a table of users. Each row shows the username, email, a single scope the related assigned role, along with a control to expand the row and reveal up to three assigned roles and the user’s total number of assigned roles.
The table can be searched by username, email, or full name; sorted by username, full name or email; filtered by organization, role, or scope; and is paginated.
Decision#
Extend the existing /api/authz/v1/users/ endpoint, defined in TeamMembersAPIView,
to include a list of assignments for each user.
This endpoint was originally created for an earlier version of the Team Members tab that was deprioritized and never implemented in a previous phase of the RBAC project. At that time, the view did not expose each user’s assigned roles, only a total count.
As part of this change, the existing assignation_count field is renamed to
assignment_count. The rest of the repository consistently uses “assignment”
(e.g. RoleAssignmentData, get_visible_role_assignments_for_user, the
/api/authz/v1/users/<username>/assignments/ endpoint), so assignation_count
is an inconsistent outlier. Renaming it now keeps the new user-grouped fields
(assignments and assignment_count) aligned with that convention. The field
is safe to rename because the GET /api/authz/v1/users/ endpoint is not called
at all by
frontend-app-admin-console,
the only client of the AuthZ API. Its current Team Members table is sourced from
the assignment-grouped GET /api/authz/v1/assignments/ endpoint, so neither the
endpoint nor the assignation_count field has any released consumer.
The embedded assignments are not paginated. Each user includes only the first n
assignments, where n defaults to 3 and can be overridden by the assignments_limit
query parameter.
The assignments_limit will be capped to a maximum of 10 items to avoid expensive
requests.
REST API for Team Members view#
The existing /api/authz/v1/users/ endpoint will be extended to return a list of
assignments per user. The number of assignments returned is capped by the
assignments_limit parameter, which defaults to 3, and is limited to a maxumum of 10.
API Definition#
GET /api/authz/v1/users/#
Retrieve all users that have at least one role assignment (team members). Results are filtered according to the calling user’s scope-level view permissions.
Query Parameters:#
roles(optional): Comma-separated list of roles to filter by (e.g.course_auditor,library_admin).scopes(optional): Comma-separated list of scopes to filter by (e.g.lib:Org1:LIB1).orgs(optional): Comma-separated list of orgs to filter by (e.g.Org1,Org2).search(optional): Search term to filter users by username, full name, or email.assignments_limit(optional): Maximum number of assignments to populate in theassignmentsarray for each user. Defaults to 3, maximum 10. The full total is always reported inassignment_count.sort_by(optional): Field to sort by. Options:username,full_name,email. Defaults tousername.order(optional): Sort order,ascordesc. Defaults toasc.page(optional): Page number for pagination.page_size(optional): Number of items per page.
Example:
GET /api/authz/v1/users/?roles=library_admin&orgs=Org1&search=john&assignments_limit=3&sort_by=username&order=asc&page=1&page_size=10
Response Body:#
Format:
{
count: number
next: string | null
previous: string | null
results: Array<{
username: string
full_name: string
email: string
assignment_count: number
assignments: Array<{
role: string
org: string
scope: string
scope_display_name: string
permission_count: number
}>
}>
}
The assignments array is populated with up to assignments_limit entries
(default 3, maximum 10), while assignment_count always reflects the user’s total number of
assignments regardless of the limit.
Each assignment includes a scope_display_name field alongside the existing
scope key. This is a new field relative to the current assignment-shaped
endpoints (GET /api/authz/v1/assignments/ and
GET /api/authz/v1/users/<username>/assignments/), which only return the
scope key. The UI needs the human-readable name to label each assignment, so
scope_display_name carries it while scope remains the stable machine
identifier.
The display name is not stored in the authorization policy store; it lives in the
platform models (CourseOverview.display_name for courses and the library’s
learning_package.title for content libraries). Because assignments are read
from the policy store, resolving names requires reading those models. Implementers
must fetch the names in bulk (a single batched lookup per scope type per page,
keyed by scope), following the batching pattern already used for user data
(get_user_map) and for scope display names in ScopesAPIView. Resolving the
name per assignment row would introduce N+1 queries and must be avoided.
For assignments that have no single concrete resource, such as superadmin entries
or glob scopes, scope_display_name has no meaningful value and is returned as
an empty string. If an assignment references a scope whose backing course or
library no longer exists, the name cannot be resolved and is likewise returned as
an empty string.
The example below shows the different scope kinds an assignment can reference at
the time of writing: a specific library, a specific course, an organization-level
glob (all libraries or all courses in an org), and a platform-level glob (all
libraries or all courses platform-wide). Specific scopes resolve to a
scope_display_name; glob scopes have no single backing resource, so
scope_display_name is an empty string.
{
"count": 2,
"next": null,
"previous": null,
"results": [
{
"username": "jane_doe",
"full_name": "Jane Doe",
"email": "jane_doe@example.com",
"assignment_count": 3,
"assignments": [
{
"role": "library_admin",
"org": "Org1",
"scope": "lib:Org1:LIB1",
"scope_display_name": "Intro to CS Library",
"permission_count": 11
},
{
"role": "course_staff",
"org": "Org1",
"scope": "course-v1:Org1+CS101+2024",
"scope_display_name": "Introduction to Computer Science",
"permission_count": 27
},
{
"role": "library_admin",
"org": "Org1",
"scope": "lib:Org1:*",
"scope_display_name": "",
"permission_count": 11
}
]
},
{
"username": "john_doe",
"full_name": "John Doe",
"email": "john_doe@example.com",
"assignment_count": 2,
"assignments": [
{
"role": "course_staff",
"org": "Org2",
"scope": "course-v1:Org2+*",
"scope_display_name": "",
"permission_count": 27
},
{
"role": "library_user",
"org": "*",
"scope": "lib:*",
"scope_display_name": "",
"permission_count": 4
}
]
}
]
}
Notes on the scope kinds shown above:
lib:Org1:LIB1andcourse-v1:Org1+CS101+2024are specific scopes, so theirscope_display_nameis resolved from the backing library/course.lib:Org1:*andcourse-v1:Org2+*are organization-level globs;orgreflects the org (Org1,Org2) andscope_display_nameis empty.lib:*is a platform-level glob;orgis"*"andscope_display_nameis empty.
Possible response codes:#
200: Ok, includes the Response Body defined above.
400: Bad Request, happens when the request parameters are invalid.
401: Unauthorized, happens when the user is not authenticated/logged in.
403: Forbidden, happens when the user does not have the required permissions.
Consequences#
The existing /api/authz/v1/users/ endpoint will be extended to return the additional data: a nested
assignmentsarray per user and the renamedassignment_countfield.Each nested assignment gains a
scope_display_namefield, which is an addition compared to the existing assignment-shaped endpoints that return only thescopekey. Since display names are not held in the policy store, this field requires reading the platform course/library models. Implementation must resolve these names with batched, per-page lookups to avoid N+1 query performance issues.The endpoint gains a new
rolesquery parameter that filters the returned users by a comma-separated list of roles, supporting the role filter in the Team Members view.Renaming
assignation_counttoassignment_countis technically a breaking change to the response body. It is low-risk here because frontend-app-admin-console does not call theGET /api/authz/v1/users/endpoint at all, so no released client depends on the field. Any internal tests or fixtures referencingassignation_countmust still be updated.Both
assignmentsandassignment_countreflect only the assignments the calling user is permitted to see, so values may differ between viewers for the same target user.The nested
assignmentsarray is truncated toassignments_limitand is not paginated. A follow-up is needed if the UI requires a defined order for the truncated entries, since no ordering is currently guaranteed within a user’s assignments.
Rejected Alternatives#
Creating a new endpoint: The /api/authz/v1/users/ endpoint already provides most of the required logic, and it was originally created for this use case. Extending it is the simplest and most direct solution.
Loading each user’s assignments dynamically from the frontend after the initial /api/authz/v1/users/ response: This is suboptimal because it creates N+1 requests for a single page, increasing load time and placing unnecessary strain on the server.
References#
Figma design for the Team Members tab in the Admin Console.