Skip to content

Permissions

We currently have 2 models co-living currently:

  1. 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.
  2. 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): Renders children only when the user has the given permission. Uses useUserPermission(userId, permission) under the hood. Optional bypassIf prop can force render regardless.
  • useUserPermission(userId, permission) (utils/users.ts): Calls GET /alaners/:id/permissions/:permission and 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:

  1. Define a query-options helper that calls openApiPolicyCheckOptions with the resource path, path params, and query: { method: "get" | "post" | "put" | "patch" | "delete" }.
  2. In the component, call useQuery(yourQueryOptions(...)) and use data (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.