Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
c2a3168
Initial commit of RBAC split setup
matt-aitken Apr 19, 2026
cf73bae
Verdaccio publish
matt-aitken Apr 19, 2026
1f18764
Every change will republish when in dev
matt-aitken Apr 19, 2026
5c910d8
RBAC/plugin updates
matt-aitken Apr 21, 2026
e90a5a9
Update RBAC plugin interface: authenticateBearer/Session, drop Prisma…
matt-aitken Apr 22, 2026
048217e
JWT/realtime token integration: publicJWT subject, jwt metadata, allo…
matt-aitken Apr 22, 2026
5250410
Lazy loading of plugin
matt-aitken Apr 24, 2026
4a1f36d
RBAC: force-fallback flag + env var + e2e fallback wiring (TRI-8715)
matt-aitken Apr 24, 2026
5555496
RBAC: API auth e2e coverage — action + PAT + edge cases (TRI-8716)
matt-aitken Apr 24, 2026
d575f7a
RBAC: resource-scoped JWT e2e coverage (TRI-8716 follow-up)
matt-aitken Apr 24, 2026
46a4e3a
RBAC: pre-migration JWT behaviour tests for TRI-8719 risks (TRI-8716)
matt-aitken Apr 24, 2026
045ae6f
RBAC plugin: array resources + action alias wrapper (TRI-8719 Phase A)
matt-aitken Apr 24, 2026
e38028b
RBAC: migrate apiBuilder to rbac.authenticateBearer + ability.can (TR…
matt-aitken Apr 24, 2026
6288459
RBAC: dashboardLoader / dashboardAction + migrate admin pages (TRI-8717)
matt-aitken Apr 25, 2026
54c3934
RBAC plugin: authenticateAuthorize* accepts array resources
matt-aitken Apr 26, 2026
9d9b078
RBAC tests: shared-container test harness for the comprehensive auth …
matt-aitken Apr 26, 2026
b291856
RBAC plugin: Result types on mutation methods + OSS fallback (TRI-8747)
matt-aitken Apr 26, 2026
235316e
RBAC: split dashboardBuilder so client-bundle imports resolve
matt-aitken Apr 27, 2026
0353106
Code comments/formatting
matt-aitken Apr 27, 2026
a6fd008
Batch added resource
matt-aitken Apr 27, 2026
e58d9e3
Batch add resource
matt-aitken Apr 27, 2026
f713b17
RBAC: Teams page UI — role dropdowns, plan-aware disabling, manage ga…
matt-aitken Apr 27, 2026
63d9fc3
Delete API batches
matt-aitken Apr 27, 2026
70e56d8
RBAC: auto-assign system roles on org create + invite accept (TRI-8854)
matt-aitken Apr 28, 2026
280e955
RBAC: PAT creation flow with role selection (TRI-8749)
matt-aitken Apr 28, 2026
a172569
Use defaultValue instead of lots of useState
matt-aitken Apr 28, 2026
8003aab
RBAC tests: PAT auth comprehensive matrix (TRI-8741)
matt-aitken Apr 28, 2026
3c9722e
RBAC tests: dashboard session auth for admin pages (TRI-8742)
matt-aitken Apr 28, 2026
bb51ced
RBAC tests: cross-cutting auth edge cases (TRI-8743)
matt-aitken Apr 28, 2026
ad60d26
RBAC tests: waitpoint completions + input streams (TRI-8740)
matt-aitken Apr 28, 2026
4f515a8
RBAC tests: trigger task routes (TRI-8733)
matt-aitken Apr 28, 2026
bd584a5
RBAC tests: run lists (TRI-8736)
matt-aitken Apr 28, 2026
5a24e9c
RBAC tests: run mutations — cancel + idempotencyKeys.reset (TRI-8735)
matt-aitken Apr 28, 2026
c81dce2
RBAC tests: run resource routes — multi-key (TRI-8734)
matt-aitken Apr 28, 2026
93eb9d1
RBAC tests: batch retrieve + realtime (TRI-8737)
matt-aitken Apr 28, 2026
e396a04
RBAC tests: prompts (TRI-8738)
matt-aitken Apr 28, 2026
22ab2c1
RBAC tests: deployments + query (TRI-8739)
matt-aitken Apr 28, 2026
9199b49
RBAC tests: unblock e2e.full harness; all 162 tests pass (TRI-8731)
matt-aitken Apr 28, 2026
f755a49
RBAC tests: parameterise RBAC_FORCE_FALLBACK in testcontainers (TRI-8…
matt-aitken Apr 28, 2026
e45d201
RBAC tests: extract projectCreated to break platform.v3.server cycle …
matt-aitken Apr 28, 2026
85411e9
Latest lockfile… although it'll probably get conflicted again
matt-aitken Apr 28, 2026
1724119
RBAC: Roles page (TRI-8880)
matt-aitken Apr 28, 2026
9806adb
RBAC: drop upfront UserRole inserts from org-creation and invite flows
matt-aitken Apr 29, 2026
26e4a18
RBAC: scrub "enterprise" / "OSS" / cloud-side references from comments
matt-aitken Apr 29, 2026
9c33822
RBAC: scrub enterprise reference from rbac-force-fallback server-chan…
matt-aitken Apr 29, 2026
70741ee
RBAC: drop Wildcards group from Roles page client-side mapping
matt-aitken Apr 29, 2026
3281a70
RBAC: extend Permission + RbacResource for CASL conditional rules (TR…
matt-aitken Apr 29, 2026
6a26e13
RBAC: rework Roles page as a permission × role comparison Table (TRI-…
matt-aitken Apr 30, 2026
241ad84
RBAC: flex Roles page header + cell content horizontally with gap-1
matt-aitken Apr 30, 2026
66cfcb8
RBAC: left-align Roles page role columns (header + cells)
matt-aitken Apr 30, 2026
a4be47a
RBAC: shrink-to-content sizing for non-Description columns on Roles page
matt-aitken Apr 30, 2026
37f259d
RBAC: revert column shrink-to-content sizing on Roles page
matt-aitken Apr 30, 2026
152fe72
RBAC: render conditional cells as plain dimmed text (no tick + badge)
matt-aitken Apr 30, 2026
21d742e
RBAC: invite flow role picker via OrgMemberInvite.rbacRoleId (TRI-8892)
matt-aitken Apr 30, 2026
c61a748
Tightened up comments and log an error for failed role assignments
matt-aitken Apr 30, 2026
89079a5
RBAC: rbac.systemRoleIds() instead of duplicating role-id constants
matt-aitken May 1, 2026
6008f59
RBAC: give upgrade-link rows a value so Ariakit handles the click
matt-aitken May 1, 2026
a5684ed
RBAC: preserve render prop in SelectItem outside Combobox context
matt-aitken May 1, 2026
e1a1961
Fixed role link
matt-aitken May 1, 2026
2f89f90
RBAC: replace systemRoleIds() with systemRoles() catalogue
matt-aitken May 1, 2026
d511975
RBAC: address PR review — batch trigger AND, fallback resilience, pic…
matt-aitken May 1, 2026
081f644
Consolidate plugin changesets into a single patch entry
matt-aitken May 4, 2026
40f33d6
Tighten plugin changeset description
matt-aitken May 4, 2026
34872bb
Consolidate server-changes into a single entry
matt-aitken May 4, 2026
04a3cc4
Hide the Roles page for non-cloud
matt-aitken May 4, 2026
749fcd2
Hardcode legacy OrgMember.role=MEMBER on invite
matt-aitken May 4, 2026
74cbef2
Invite picker: allow at-or-below the inviter's own role
matt-aitken May 4, 2026
caade5d
Remove unneeded comment
matt-aitken May 4, 2026
f0d2c0d
Plugin: getUserRoles(userIds, orgId) batch lookup
matt-aitken May 4, 2026
fc445f8
Plugin owns permission display groups
matt-aitken May 4, 2026
cc4680b
Improved the team layout
matt-aitken May 4, 2026
8ea973c
Not enough permissions tooltip
matt-aitken May 4, 2026
70aa227
dashboardLoader/Action can access the context data
matt-aitken May 4, 2026
cd8f3d2
Sessions routes: post-RBAC apiBuilder shape + preserve superScopes se…
matt-aitken May 4, 2026
34be809
Plugin: authenticatePat for cap-and-floor PAT auth (TRI-9087)
matt-aitken May 4, 2026
048b87b
ci: align e2e-webapp-auth-full pnpm pin with root packageManager
matt-aitken May 4, 2026
878b91f
test(webapp): exclude *.e2e.full.test.ts from unit-test glob
matt-aitken May 4, 2026
bd40eb4
Address PR review: fallback grace window + multi-table query AND + UX
matt-aitken May 4, 2026
af527b2
test(webapp): every-table semantics for /api/v1/query
matt-aitken May 4, 2026
94b4c2c
apiBuilder: require explicit anyResource() / everyResource() at multi…
matt-aitken May 4, 2026
92b0494
Address Devin review: 4 RBAC auth path fixes
matt-aitken May 6, 2026
5897682
core: move sanitizeBranchName + isValidGitBranchName to @trigger.dev/…
matt-aitken May 6, 2026
876aa76
core/plugins/auth boundary: define slim AuthenticatedEnvironment, eli…
matt-aitken May 6, 2026
29e7633
lockfile updates for @trigger.dev/core dep on plugins
matt-aitken May 6, 2026
90616fc
lockfile (cont)
matt-aitken May 6, 2026
4d7689a
e2e-webapp-auth-full.yml: SHA-pin actions, drop persisted credentials
matt-aitken May 6, 2026
dd41a92
Address Devin review: Decimal coercion + replica plumbing + check-exp…
matt-aitken May 7, 2026
d7c0329
test(webapp): stop createInMemoryTracing from registering OTel globally
matt-aitken May 7, 2026
d42d00d
test(testcontainers): fix assertNonNullable — drop the require("vites…
matt-aitken May 7, 2026
c70d755
fix(rbac,webapp): pass userId through plugin context, drop session-co…
matt-aitken May 7, 2026
b9fcb1f
fix(test/utils/tracing): disable OTel globals before re-registering i…
matt-aitken May 9, 2026
2674417
fix(webapp): normalize PAT/OAT environments through the same toAuthen…
matt-aitken May 9, 2026
9f987ae
fix(webapp): backwards-compat for type-level scope on runs list + tig…
matt-aitken May 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/git-branch-utils.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@trigger.dev/core": patch
---

Add `sanitizeBranchName` and `isValidGitBranchName` exports under `@trigger.dev/core/v3/utils/gitBranch`. These were previously webapp-internal but are now shared with the RBAC fallback's branch-aware authentication path.
5 changes: 5 additions & 0 deletions .changeset/plugin-auth-path.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@trigger.dev/plugins": patch
---

The public interfaces for a plugin system. Initially consolidated authentication and authorization interfaces.
120 changes: 120 additions & 0 deletions .github/workflows/e2e-webapp-auth-full.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
name: "🛡️ E2E Tests: Webapp Auth (full)"

# Comprehensive RBAC auth test suite — see TRI-8731. Runs separately from
# the smoke e2e-webapp.yml because it covers every route family with a
# pass/fail matrix and would otherwise dominate per-PR CI time.
#
# Triggered:
# - Manually via workflow_dispatch.
# - Nightly via schedule.
# - On pull requests touching auth-relevant files only (paths filter).

permissions:
contents: read

on:
workflow_dispatch:
schedule:
- cron: "0 4 * * *" # 04:00 UTC daily
pull_request:
paths:
- "apps/webapp/app/services/routeBuilders/**"
- "apps/webapp/app/services/rbac.server.ts"
- "apps/webapp/app/services/apiAuth.server.ts"
- "apps/webapp/app/services/personalAccessToken.server.ts"
- "apps/webapp/app/services/sessionStorage.server.ts"
- "apps/webapp/app/routes/api.v*.**"
- "apps/webapp/app/routes/realtime.v*.**"
- "apps/webapp/test/**/*.e2e.full.test.ts"
- "apps/webapp/test/setup/global-e2e-full-setup.ts"
- "apps/webapp/test/helpers/sharedTestServer.ts"
- "apps/webapp/test/helpers/seedTestSession.ts"
- "apps/webapp/vitest.e2e.full.config.ts"
- "internal-packages/rbac/**"
- "packages/plugins/**"
- ".github/workflows/e2e-webapp-auth-full.yml"
Comment thread
matt-aitken marked this conversation as resolved.

jobs:
e2eAuthFull:
name: "🛡️ E2E Auth Tests (full)"
runs-on: ubuntu-latest
timeout-minutes: 30
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
steps:
- name: 🔧 Disable IPv6
run: |
sudo sysctl -w net.ipv6.conf.all.disable_ipv6=1
sudo sysctl -w net.ipv6.conf.default.disable_ipv6=1
sudo sysctl -w net.ipv6.conf.lo.disable_ipv6=1

- name: 🔧 Configure docker address pool
run: |
CONFIG='{
"default-address-pools" : [
{
"base" : "172.17.0.0/12",
"size" : 20
},
{
"base" : "192.168.0.0/16",
"size" : 24
}
]
}'
mkdir -p /etc/docker
echo "$CONFIG" | sudo tee /etc/docker/daemon.json

- name: 🔧 Restart docker daemon
run: sudo systemctl restart docker

- name: ⬇️ Checkout repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
# Don't leave the GITHUB_TOKEN in .git/config — this job
# doesn't need to push and the persisted creds would be
# readable from any subsequent step (zizmor/artipacked).
persist-credentials: false

- name: ⎔ Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
with:
version: 10.33.2

- name: ⎔ Setup node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 20.20.0
cache: "pnpm"

- name: 🐳 Login to DockerHub
if: ${{ env.DOCKERHUB_USERNAME }}
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: 🐳 Skipping DockerHub login (no secrets available)
if: ${{ !env.DOCKERHUB_USERNAME }}
run: echo "DockerHub login skipped because secrets are not available."

- name: 🐳 Pre-pull testcontainer images
if: ${{ env.DOCKERHUB_USERNAME }}
run: |
docker pull postgres:14
docker pull redis:7.2
docker pull testcontainers/ryuk:0.11.0

- name: 📥 Download deps
run: pnpm install --frozen-lockfile

- name: 📀 Generate Prisma Client
run: pnpm run generate

- name: 🏗️ Build Webapp
run: pnpm run build --filter webapp

- name: 🛡️ Run Webapp Full Auth E2E Tests
run: cd apps/webapp && pnpm exec vitest run --config vitest.e2e.full.config.ts --reporter=default
env:
WEBAPP_TEST_VERBOSE: "1"
6 changes: 6 additions & 0 deletions .server-changes/plugin-auth-path.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
area: webapp
type: improvement
---

Webapp now supports a plugin system. Initially consolidates authentication and authorization paths.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Cog8ToothIcon,
CreditCardIcon,
LockClosedIcon,
ShieldCheckIcon,
UserGroupIcon,
} from "@heroicons/react/20/solid";
import { ArrowLeftIcon } from "@heroicons/react/24/solid";
Expand All @@ -14,6 +15,7 @@ import { useFeatures } from "~/hooks/useFeatures";
import { type MatchedOrganization } from "~/hooks/useOrganizations";
import { cn } from "~/utils/cn";
import {
organizationRolesPath,
organizationSettingsPath,
organizationSlackIntegrationPath,
organizationTeamPath,
Expand Down Expand Up @@ -128,6 +130,18 @@ export function OrganizationSettingsSideMenu({
to={organizationTeamPath(organization)}
data-action="team"
/>
{isManagedCloud && (
<>
<SideMenuItem
name="Roles"
icon={ShieldCheckIcon}
activeIconColor="text-sky-500"
inactiveIconColor="text-sky-500"
to={organizationRolesPath(organization)}
data-action="roles"
/>
</>
)}
<SideMenuItem
name="Settings"
icon={Cog8ToothIcon}
Expand Down
10 changes: 9 additions & 1 deletion apps/webapp/app/components/primitives/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,15 @@ export function SelectItem({
...props
}: SelectItemProps) {
const combobox = Ariakit.useComboboxContext();
const render = combobox ? <Ariakit.ComboboxItem render={props.render} /> : undefined;
// In a Combobox context we wrap the caller's render in ComboboxItem
// so combobox keyboard nav still works. Outside a Combobox we pass
// the render through verbatim — without this, callers like
// SelectLinkItem (which uses render to swap in a <Link>) get their
// render prop silently dropped, which is why those rows looked
// clickable but didn't navigate.
const render = combobox
? <Ariakit.ComboboxItem render={props.render} />
: props.render;
const ref = React.useRef<HTMLDivElement>(null);
const select = Ariakit.useSelectContext();
const selectValue = select?.useState("value");
Expand Down
3 changes: 3 additions & 0 deletions apps/webapp/app/env.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1534,6 +1534,9 @@ const EnvironmentSchema = z
// Private connections
PRIVATE_CONNECTIONS_ENABLED: z.string().optional(),
PRIVATE_CONNECTIONS_AWS_ACCOUNT_IDS: z.string().optional(),

// Force RBAC to not use the plugin
RBAC_FORCE_FALLBACK: BoolEnv.default(false),
})
.and(GithubAppEnvSchema)
.and(S2EnvSchema)
Expand Down
40 changes: 38 additions & 2 deletions apps/webapp/app/models/member.server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { type Prisma, prisma } from "~/db.server";
import { createEnvironment } from "./organization.server";
import { customAlphabet } from "nanoid";
import { logger } from "~/services/logger.server";
import { rbac } from "~/services/rbac.server";

const tokenValueLength = 40;
const tokenGenerator = customAlphabet("123456789abcdefghijkmnopqrstuvwxyz", tokenValueLength);
Expand Down Expand Up @@ -86,10 +88,19 @@ export async function inviteMembers({
slug,
emails,
userId,
rbacRoleId,
}: {
slug: string;
emails: string[];
userId: string;
/**
* Optional RBAC role to attach to the invite. When set, accepted
* invites trigger `rbac.setUserRole(rbacRoleId)` after the OrgMember
* is created.
*
* `OrgMemberInvite.role` is still set if the plugin isn't installed.
*/
rbacRoleId?: string | null;
}) {
const org = await prisma.organization.findFirst({
where: { slug, members: { some: { userId } } },
Expand All @@ -107,6 +118,7 @@ export async function inviteMembers({
organizationId: org.id,
inviterId: userId,
role: "MEMBER",
rbacRoleId: rbacRoleId ?? null,
} satisfies Prisma.OrgMemberInviteCreateManyInput)
);

Expand Down Expand Up @@ -163,7 +175,7 @@ export async function acceptInvite({
user: { id: string; email: string };
inviteId: string;
}) {
return await prisma.$transaction(async (tx) => {
const result = await prisma.$transaction(async (tx) => {
// 1. Delete the invite and get the invite details
const invite = await tx.orgMemberInvite.delete({
where: {
Expand Down Expand Up @@ -207,8 +219,32 @@ export async function acceptInvite({
},
});

return { remainingInvites, organization: invite.organization };
return {
remainingInvites,
organization: invite.organization,
inviteRole: invite.role,
rbacRoleId: invite.rbacRoleId,
};
});

// If the invite carried an explicit RBAC role. Errors are logged, not fatal.
if (result.rbacRoleId) {
const roleResult = await rbac.setUserRole({
userId: user.id,
organizationId: result.organization.id,
roleId: result.rbacRoleId,
});
if (!roleResult.ok) {
logger.error("acceptInvite: skipped RBAC role assignment", {
organizationId: result.organization.id,
userId: user.id,
rbacRoleId: result.rbacRoleId,
reason: roleResult.error,
});
}
}
Comment thread
matt-aitken marked this conversation as resolved.

return { remainingInvites: result.remainingInvites, organization: result.organization };
}

export async function declineInvite({
Expand Down
2 changes: 1 addition & 1 deletion apps/webapp/app/models/project.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { $replica, prisma } from "~/db.server";
import type { Prisma, Project } from "@trigger.dev/database";
import { type Organization, createEnvironment } from "./organization.server";
import { env } from "~/env.server";
import { projectCreated } from "~/services/platform.v3.server";
import { projectCreated } from "~/services/projectCreated.server";
export type { Project } from "@trigger.dev/database";

const externalRefGenerator = customAlphabet("abcdefghijklmnopqrstuvwxyz", 20);
Expand Down
Loading
Loading