Skip to content

Commit 29780a4

Browse files
committed
RBAC: force-fallback flag + env var + e2e fallback wiring (TRI-8715)
Adds a `forceFallback` option to the RBAC loader so tests (and any other consumer that sets RBAC_FORCE_FALLBACK=1) pin authentication to the OSS fallback regardless of whether the enterprise plugin is installed. - internal-packages/rbac: LazyController and RoleBaseAccess.create now accept RbacCreateOptions.forceFallback. When true, load() skips the dynamic import of @triggerdotdev/plugins/rbac and constructs RoleBaseAccessFallback directly. - apps/webapp env: new RBAC_FORCE_FALLBACK BoolEnv, threaded into app/services/rbac.server.ts. - testcontainers webapp: set RBAC_FORCE_FALLBACK=1 so e2e tests exercise the fallback deterministically. - api-auth.e2e.test.ts: covers the fallback wiring end-to-end via the existing /admin/concurrency route, which already uses rbac.authenticateSession + ability.canSuper().
1 parent 38c7b25 commit 29780a4

6 files changed

Lines changed: 55 additions & 6 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: improvement
4+
---
5+
6+
RBAC plugin: add RBAC_FORCE_FALLBACK env var so tests can pin the loader to the OSS fallback without depending on whether the enterprise plugin is installed.

apps/webapp/app/env.server.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1454,6 +1454,11 @@ const EnvironmentSchema = z
14541454
// Private connections
14551455
PRIVATE_CONNECTIONS_ENABLED: z.string().optional(),
14561456
PRIVATE_CONNECTIONS_AWS_ACCOUNT_IDS: z.string().optional(),
1457+
1458+
// Force the RBAC plugin loader to use the OSS fallback, bypassing the enterprise plugin.
1459+
// Set to "1"/"true" in tests so auth behavior is deterministic regardless of whether
1460+
// @triggerdotdev/plugins/rbac is installed in the environment.
1461+
RBAC_FORCE_FALLBACK: BoolEnv.default(false),
14571462
})
14581463
.and(GithubAppEnvSchema)
14591464
.and(S2EnvSchema);

apps/webapp/app/services/rbac.server.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { prisma } from "~/db.server";
22
import plugin from "@trigger.dev/rbac";
3+
import { env } from "~/env.server";
34
import { getUserId } from "./session.server";
45

56
async function getSessionUserId(request: Request): Promise<string | null> {
@@ -9,4 +10,8 @@ async function getSessionUserId(request: Request): Promise<string | null> {
910

1011
// plugin.create() is synchronous — returns a lazy controller that loads the enterprise plugin
1112
// on first call. Top-level await is not used because CJS output format does not support it.
12-
export const rbac = plugin.create(prisma, { getSessionUserId });
13+
export const rbac = plugin.create(
14+
prisma,
15+
{ getSessionUserId },
16+
{ forceFallback: env.RBAC_FORCE_FALLBACK }
17+
);

apps/webapp/test/api-auth.e2e.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,16 @@ describe("JWT bearer auth — baseline behavior", () => {
119119
expect(res.status).toBe(401);
120120
});
121121
});
122+
123+
// Exercises the RBAC plugin loader end-to-end. The test server boots with
124+
// RBAC_FORCE_FALLBACK=1 (see internal-packages/testcontainers/src/webapp.ts), which makes
125+
// rbac.server.ts select the OSS fallback over the enterprise plugin. /admin/concurrency uses
126+
// rbac.authenticateSession internally; an unauthenticated request must flow through
127+
// LazyController → RoleBaseAccessFallback → redirect("/login").
128+
describe("RBAC plugin — fallback wiring", () => {
129+
it("unauthenticated dashboard route redirects to /login via the OSS fallback", async () => {
130+
const res = await server.webapp.fetch("/admin/concurrency", { redirect: "manual" });
131+
expect(res.status).toBe(302);
132+
expect(res.headers.get("location")).toBe("/login");
133+
});
134+
});

internal-packages/rbac/src/index.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,29 @@ export type { RoleBaseAccessController };
1111

1212
type RbacHelpers = { getSessionUserId: (request: Request) => Promise<string | null> };
1313

14+
export type RbacCreateOptions = {
15+
// When true, skip loading the enterprise plugin and use the OSS fallback directly.
16+
// Useful for tests that need deterministic auth behavior without the enterprise plugin.
17+
forceFallback?: boolean;
18+
};
19+
1420
// Loads the enterprise plugin lazily; falls back to the OSS implementation if not installed.
1521
// Synchronous create() avoids top-level await (not supported in the webapp's CJS build).
1622
class LazyController implements RoleBaseAccessController {
1723
private readonly _init: Promise<RoleBaseAccessController>;
1824

19-
constructor(prisma: PrismaClient, helpers: RbacHelpers) {
20-
this._init = this.load(prisma, helpers);
25+
constructor(prisma: PrismaClient, helpers: RbacHelpers, options?: RbacCreateOptions) {
26+
this._init = this.load(prisma, helpers, options);
2127
}
2228

23-
private async load(prisma: PrismaClient, helpers: RbacHelpers): Promise<RoleBaseAccessController> {
29+
private async load(
30+
prisma: PrismaClient,
31+
helpers: RbacHelpers,
32+
options?: RbacCreateOptions
33+
): Promise<RoleBaseAccessController> {
34+
if (options?.forceFallback) {
35+
return new RoleBaseAccessFallback(prisma).create(helpers);
36+
}
2437
try {
2538
const moduleName = "@triggerdotdev/plugins/rbac";
2639
const module = await import(moduleName);
@@ -98,8 +111,12 @@ class LazyController implements RoleBaseAccessController {
98111

99112
class RoleBaseAccess {
100113
// Synchronous — returns a lazy controller that loads the enterprise plugin on first call.
101-
create(prisma: PrismaClient, helpers: RbacHelpers): RoleBaseAccessController {
102-
return new LazyController(prisma, helpers);
114+
create(
115+
prisma: PrismaClient,
116+
helpers: RbacHelpers,
117+
options?: RbacCreateOptions
118+
): RoleBaseAccessController {
119+
return new LazyController(prisma, helpers, options);
103120
}
104121
}
105122

internal-packages/testcontainers/src/webapp.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ export async function startWebapp(
8181
RUN_ENGINE_TTL_SYSTEM_DISABLED: "true", // disables TTL expiry system (BoolEnv)
8282
RUN_ENGINE_TTL_CONSUMERS_DISABLED: "true", // disables TTL consumers (BoolEnv)
8383
RUN_REPLICATION_ENABLED: "0",
84+
// Force the RBAC plugin to use the OSS fallback in e2e tests so auth behavior is
85+
// deterministic regardless of whether the enterprise plugin is installed.
86+
RBAC_FORCE_FALLBACK: "1",
8487
NODE_PATH: nodePath,
8588
},
8689
stdio: ["ignore", "pipe", "pipe"],

0 commit comments

Comments
 (0)