Permissions¶
We currently have 2 models co-living currently:
- The legacy permission model. Each user is assigned a set of permission based on their roles. This is limited and we are moving away from this model towards ABAC.
- ABAC. The backend decides per resource and action whether the current user is allowed.
1. Legacy permissions¶
The legacy model assigns each user a set of permission strings (e.g. "reviews:approve") based on their roles. The backend exposes GET /alaners/:id/permissions/:permission, which returns whether that user has that permission. This model is limited and deprecated: it does not express resource-level or context-dependent rules. Do not use it for new features. Use ABAC instead (see below).
Checking permissions in the UI (legacy)¶
PermissionChecker(app/components/roles/PermissionChecker.tsx): Renderschildrenonly when the user has the given permission. UsesuseUserPermission(userId, permission)under the hood. OptionalbypassIfprop can force render regardless.useUserPermission(userId, permission)(utils/users.ts): CallsGET /alaners/:id/permissions/:permissionand returns whether the backend reports the permission as granted. Cached with a 5-minute stale time.
Permission names and semantics are defined by the backend; the frontend only shows or hides UI based on the API response.
2. ABAC¶
Attribute-Based Access Control (ABAC) is the preferred model. The backend decides, per resource and action (e.g. βcan the current user PUT this reviewβs release?β), whether the action is allowed. Policies are defined in backend code (see User Lifecycle β Role Management); the frontend does not know policy details, it only asks βcan I do X on Y?β.
Querying from the frontend¶
We use the openApiPolicyCheckOptions helper (from utils/backend.ts). It builds TanStack Query options that call a policy-check endpoint: typically a GET on the resource URL with a method query parameter indicating the action (e.g. put, post, delete). The API returns whether the current user is allowed to perform that action. You then use useQuery(...) and use the returned boolean to show/hide or enable/disable UI.
Pattern:
- Define a query-options helper that calls
openApiPolicyCheckOptionswith the resource path, path params, andquery: { method: "get" | "post" | "put" | "patch" | "delete" }. - In the component, call
useQuery(yourQueryOptions(...))and usedata(boolean) to gate the UI.
Example (reviews):
In utils/reviews.tsx:
export function canApproveReviewSubmissionQueryOptions(reviewId?: string, reviewSubmissionId?: string) {
return openApiPolicyCheckOptions({
path: "/reviews/{review_id}/review-submissions/{review_submission_id}/validation",
parameters: {
path: { review_id: reviewId!, review_submission_id: reviewSubmissionId! },
query: { method: "put" },
},
enabled: reviewId !== undefined && reviewSubmissionId !== undefined,
staleTime: 1000 * 60 * 5,
});
}
In the component (e.g. ViewReview/components/ReviewerSubmissionRow.tsx):
const { data: canApproveReviewSubmission } = useQuery(
canApproveReviewSubmissionQueryOptions(submission.review_id, submission.id),
);
// Then use canApproveReviewSubmission to show/hide the approve button or enable/disable it.
Other examples: canChangeReviewDatesQueryOptions, canReleaseReviewQueryOptions, canViewReviewSubmissionAnswersQueryOptions in utils/reviews.tsx; alanerCanChangeCoachQueryOptions in utils/alaners.ts.