feat(webapp): enforce RBAC permissions on run, prompt, member, and billing routes by matt-aitken · Pull Request #3948 · triggerdotdev/trigger.dev
Add checkPermissions(ability, checks) which maps a set of action/resource checks to a boolean record using the injected ability, so loaders can compute display-only permission flags server-side and pass them to the client. Add PermissionButton and PermissionLink wrappers that disable the underlying control and show an explanatory tooltip when a server-computed hasPermission flag is false. No permission logic ships to the client; the route builder authorization block remains the security boundary.
Wrap the dashboard cancel and replay resource-route actions in dashboardAction with an authorization block (write:runs), resolving the run's organization for the auth scope from Postgres with a mollifier buffer fallback. The existing org-membership queries are retained as the tenancy boundary; the RBAC check layers on top and only enforces under the enterprise plugin.
Migrate the bulk-action create/replay route and the bulk-action abort route to dashboardLoader/dashboardAction with a write:runs authorization block, resolving the org for the auth scope from the URL slug. Surface canCreateBulkAction and canAbort display flags via checkPermissions and gate the inspector's Cancel/Replay trigger and the Abort button. Tenancy queries (findProjectBySlug/findEnvironmentBySlug) are unchanged.
Surface write:runs as canReplayRun/canCancelRun from the run-detail loader (via the injected RBAC ability) and disable the Replay and Cancel controls with an explanatory tooltip when the role lacks it. Display only; the cancel/replay action routes are the enforcement boundary.
The setUserRole call in acceptInvite ran outside a try/catch, so a thrown
error from the RBAC plugin escaped and turned the whole invite-accept into
a 400 (the membership was already created in the transaction). Wrap it so
both a returned {ok:false} and a thrown error are logged, including the
stack, and never block joining the org.
…route + UI Migrate the prompt detail action to dashboardAction and check the right permission per intent: promote -> update:prompts, create/edit/remove/ reactivate override -> write:prompts. Surface canPromote / canWritePrompts display flags from the loader (via the injected ability) and gate the Promote, Reactivate, Create override, Edit, and Remove buttons. Tenancy queries unchanged; permissive in OSS, enforced under the enterprise plugin.
Migrate the invite, invite-resend, and invite-revoke routes to dashboardLoader/dashboardAction with a manage:members authorization block. The resend/revoke routes have no URL params, so the org for the auth scope is resolved from the form body (read via a cloned request) — from the invite's organization (resend) or the slug field (revoke). Gate the Resend/Revoke buttons on the team page with the existing canManageMembers flag. Existing tenancy/inviter checks in the model layer are unchanged.
Migrate the billing settings, standalone select-plan page, select-plan mutation, billing-alerts (loader + action), and Stripe customer-portal routes to dashboardLoader/dashboardAction with a manage:billing authorization block, resolving the org for the auth scope from the URL slug. The isManagedCloud guards and org-membership queries are unchanged; gating the page loaders means denied roles can't reach the billing UI at all. Permissive in OSS, enforced under the enterprise plugin.
…trols on write:runs Thread canCancelRuns/canReplayRuns (default true) through TaskRunsTable to RunActionsCell: disable + tooltip the Cancel/Replay popover items and hide the redundant hover icons when denied. The runs-index and errors loaders compute the flags from the injected ability; gate the index Bulk action button + r/c shortcuts and the errors Bulk replay link accordingly. Display only; the action routes enforce write:runs. Permissive in OSS.
…alerts routes These two routes reverted to raw loaders/actions when main's changes were taken during a merge conflict. Re-apply the dashboardLoader/dashboardAction migration with a manage:billing authorization block on top of main's current code (which added the showSelfServe branching), keeping the isManagedCloud guard and membership queries.
…tes + UI Migrate the three deployment resource-route actions to dashboardAction with a write:deployments authorization block, resolving the org for the auth scope from the project. Surface canWriteDeployments from the deployments loader and gate the Rollback/Promote/Cancel row-menu items (disable + tooltip when denied). Tenancy/membership queries unchanged; permissive in OSS.
Gate the GitHub settings panel controls (Install / Connect repo / Disconnect / Save) on the canManageGithub flag, and wrap the GitHub app install entry route in dashboardLoader with a write:github authorization block (org resolved from the org_slug query param). Membership queries unchanged; permissive in OSS.
Migrate the Vercel settings resource action, the Vercel app install entry, and the org-level uninstall action to dashboardLoader/dashboardAction with a write:vercel authorization block. Surface canManageVercel from the loaders and gate the Connect / Install / Reconnect / Disconnect / Save / Remove controls. Membership queries unchanged; permissive in OSS.
Add a reusable PermissionDenied panel and use it on the API keys page: when a role can't read a given environment's secret key (e.g. deployed environments for a restricted role), the secret is withheld server-side and the page renders the panel instead. Regenerating an API key is gated the same way, enforced on the POST so a disabled button isn't the only guard.
…pages The billing, billing alerts, and invite pages hard-redirected to the org home when the current role lacked access, which looked like a broken link. They now render the page shell with a PermissionDenied panel (and a link to view roles), and withhold their data server-side when access is denied. The matching mutations stay enforced independently.
…v chips The roles comparison table now fills the remaining height with a sticky header and scrolls internally. A line under the description states the viewer's own role, and env-tier permission conditions render as environment chips for the environments they apply to instead of raw text.
The environment variables list now withholds values for environments the current role can't read and shows a permission-denied state in their place. The create dialog disables the environment targets the role can't write (with a tooltip) and its action rejects those targets server-side. The permission-denied states use the no-entry icon.
The environment variable API routes now apply the caller's role to the targeted environment tier when authenticated with a personal access token, so a restricted role can't read or write deployed env vars via the API. Environment API keys are scoped to a single environment already, so they are unaffected.
…ints The endpoints that hand a personal access token an environment's secret key or a key-signed JWT now apply the caller's role for that environment tier. A restricted role can't pull deployed-environment credentials, which is what stops it deploying via the CLI (deploy authenticates with the environment secret key). Environment API keys are scoped to a single environment already, so they are unaffected.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters