5740955357
## Summary Several dashboard routes performed actions a restricted role should not be able to do (cancel or replay runs, manage prompt versions, invite and manage members, manage billing) without any permission check. This adds role-based permission enforcement to those routes, and disables the matching UI controls (with a tooltip) when the current role lacks permission. Covered actions: - Runs: cancel and replay (single, bulk create, bulk abort) - Prompts: create or edit override versions, and promote a version to current - Members: invite, resend invite, revoke invite - Billing: change plan, billing alerts, and the customer portal ## How Each affected route now goes through the `dashboardLoader` / `dashboardAction` route builders with an `authorization` block declaring the required permission (or a per-intent check where one route handles several intents). Existing tenancy and data-scoping queries are untouched; this only layers permission checks on top. The UI follows disable-don't-hide: controls stay visible but disabled with a "You don't have permission to ..." tooltip. Two reusable pieces support this: `checkPermissions(ability, checks)` turns a set of checks into a boolean map a loader returns to the client, and `PermissionButton` / `PermissionLink` disable the underlying control and show a tooltip when a permission flag is false. ## Behaviour No change in the default configuration: permissions are permissive, so every control stays enabled and every route behaves as before. The checks only take effect when an RBAC plugin is installed. This also makes role assignment on invite-accept non-fatal, so a failure there cannot block joining an org. Verified with `pnpm run typecheck --filter webapp`; `checkPermissions` has unit tests.
72 lines
2.4 KiB
TypeScript
72 lines
2.4 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import type { RbacAbility } from "@trigger.dev/rbac";
|
|
import { checkPermissions } from "~/services/routeBuilders/permissions.server";
|
|
|
|
const permissive: RbacAbility = { can: () => true, canSuper: () => false };
|
|
const denyAll: RbacAbility = { can: () => false, canSuper: () => false };
|
|
|
|
describe("checkPermissions", () => {
|
|
it("returns true for every check under a permissive ability (OSS path)", () => {
|
|
const result = checkPermissions(permissive, {
|
|
canCancelRun: { action: "write", resource: { type: "runs" } },
|
|
canManageMembers: { action: "manage", resource: { type: "members" } },
|
|
});
|
|
|
|
expect(result).toEqual({ canCancelRun: true, canManageMembers: true });
|
|
});
|
|
|
|
it("returns false for every check under a deny-all ability", () => {
|
|
const result = checkPermissions(denyAll, {
|
|
canCancelRun: { action: "write", resource: { type: "runs" } },
|
|
});
|
|
|
|
expect(result).toEqual({ canCancelRun: false });
|
|
});
|
|
|
|
it("evaluates each check independently against can()", () => {
|
|
const ability: RbacAbility = {
|
|
can: (action, resource) => {
|
|
const r = Array.isArray(resource) ? resource[0] : resource;
|
|
return action === "read" || r.type === "tasks";
|
|
},
|
|
canSuper: () => false,
|
|
};
|
|
|
|
const result = checkPermissions(ability, {
|
|
readRuns: { action: "read", resource: { type: "runs" } },
|
|
writeRuns: { action: "write", resource: { type: "runs" } },
|
|
writeTasks: { action: "write", resource: { type: "tasks" } },
|
|
});
|
|
|
|
expect(result).toEqual({ readRuns: true, writeRuns: false, writeTasks: true });
|
|
});
|
|
|
|
it("supports requireSuper checks via canSuper()", () => {
|
|
const admin: RbacAbility = { can: () => false, canSuper: () => true };
|
|
|
|
expect(checkPermissions(admin, { adminOnly: { requireSuper: true } })).toEqual({
|
|
adminOnly: true,
|
|
});
|
|
expect(checkPermissions(denyAll, { adminOnly: { requireSuper: true } })).toEqual({
|
|
adminOnly: false,
|
|
});
|
|
});
|
|
|
|
it("passes resource arrays straight through to can()", () => {
|
|
const seen: unknown[] = [];
|
|
const ability: RbacAbility = {
|
|
can: (_action, resource) => {
|
|
seen.push(resource);
|
|
return true;
|
|
},
|
|
canSuper: () => false,
|
|
};
|
|
|
|
checkPermissions(ability, {
|
|
x: { action: "read", resource: [{ type: "runs" }, { type: "tasks" }] },
|
|
});
|
|
|
|
expect(seen[0]).toEqual([{ type: "runs" }, { type: "tasks" }]);
|
|
});
|
|
});
|