◐ Shell
clean mode source ↗

feat(webapp): enforce RBAC permissions on run, prompt, member, and billing routes by matt-aitken · Pull Request #3948 · triggerdotdev/trigger.dev

devin-ai-integration[bot]

devin-ai-integration[bot]

coderabbitai[bot]

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.
Migrate the GitHub settings resource-route action (connect-repo /
disconnect-repo / update-git-settings) to dashboardAction with a write:github
authorization block, and surface canManageGithub from the loader for UI
gating. Project membership checks unchanged; permissive in OSS.

devin-ai-integration[bot]

coderabbitai[bot]

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.
The team page's seat purchase button now disables itself with an
explanatory tooltip when the current role can't manage billing, matching
the server-side check the action already enforces.

devin-ai-integration[bot]

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.

devin-ai-integration[bot]

…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.

devin-ai-integration[bot]

@matt-aitken