Skip to content

Commit 4009d0f

Browse files
matt-aitkenclaude
andcommitted
feat(webapp,rbac): REQUIRE_PLUGINS=1 fail-fast for required plugin loads
Adds a deployment-mode opt-in that hardens the plugin loader against silent degradation. Today, when the RBAC plugin module fails to load — whether because it's not installed or because a transitive dep is broken — the loader catches the error and returns the default fallback implementation. Correct for self-hosters; dangerous in deployments where the fallback's permissive auth is not an acceptable degraded state. - internal-packages/rbac/src/index.ts: in LazyController.load()'s catch block, after logging, throw an Error when `process.env.REQUIRE_PLUGINS === "1"`. The throw is captured into `this._init` (a rejected promise), so it surfaces on the first method call on the lazy controller. The forceFallback option (used by tests) still wins — it short-circuits before this code path, so tests aren't broken if a test runner happens to inherit REQUIRE_PLUGINS from its parent env. - apps/webapp/app/routes/healthcheck.tsx: call `rbac.isUsingPlugin()` after the DB ping. For self-hosters (and any deployment with the plugin loading cleanly) this is a noop. With REQUIRE_PLUGINS=1 and a failed plugin load, the throw surfaces here — healthcheck returns 500 and the rollout's readiness probe fails, so the new revision is rolled back before traffic shifts. REQUIRE_PLUGINS is intentionally plural and generic — future plugin contracts (audit logs, SSO) can read the same flag without renaming. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1015876 commit 4009d0f

3 files changed

Lines changed: 30 additions & 0 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
area: webapp
3+
type: feature
4+
---
5+
6+
Add `REQUIRE_PLUGINS=1` env var. When set, the RBAC plugin loader throws instead of silently falling back to the default implementation if the plugin module fails to load (missing, broken transitive dep, etc.). The webapp's `/healthcheck` route now resolves the lazy plugin controller so the throw surfaces during readiness probes — a deploy where the plugin didn't load fails the probe and is rolled back.
7+
8+
Self-hosters leave `REQUIRE_PLUGINS` unset and continue to use the fallback when no plugin is installed.

apps/webapp/app/routes/healthcheck.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { prisma } from "~/db.server";
22
import type { LoaderFunction } from "@remix-run/node";
33
import { env } from "~/env.server";
4+
import { rbac } from "~/services/rbac.server";
45

56
export const loader: LoaderFunction = async ({ request }) => {
67
try {
@@ -9,6 +10,13 @@ export const loader: LoaderFunction = async ({ request }) => {
910
}
1011

1112
await prisma.$queryRaw`SELECT 1`;
13+
14+
// Resolve the lazy plugin controller so plugin-load failures surface
15+
// during readiness probes. With REQUIRE_PLUGINS=1, a failed plugin
16+
// load throws here and the rollout's readiness probe fails. Without
17+
// REQUIRE_PLUGINS, the fallback resolves cleanly and this is a noop.
18+
await rbac.isUsingPlugin();
19+
1220
return new Response("OK");
1321
} catch (error: unknown) {
1422
console.log("healthcheck ❌", { error });

internal-packages/rbac/src/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,20 @@ class LazyController implements RoleBaseAccessController {
105105
"RBAC: no plugin installed (ERR_MODULE_NOT_FOUND); using fallback"
106106
);
107107
}
108+
109+
// Fail-fast for deployments that require plugins to be present. Set
110+
// REQUIRE_PLUGINS=1 in environments where the fallback is not an
111+
// acceptable degraded state — the throw surfaces on the first method
112+
// call on the lazy controller (e.g. via the webapp's /healthcheck
113+
// route), so the rollout's readiness probe fails and the deploy is
114+
// rolled back. Self-hosters leave REQUIRE_PLUGINS unset and continue
115+
// to use the fallback when no plugin is installed.
116+
if (process.env.REQUIRE_PLUGINS === "1") {
117+
throw new Error(
118+
`REQUIRE_PLUGINS=1 but plugin "${moduleName}" did not load: ${message}`
119+
);
120+
}
121+
108122
return new RoleBaseAccessFallback(prisma).create();
109123
}
110124
}

0 commit comments

Comments
 (0)