Permission policy
The @openchoreo/backstage-plugin-permission-backend-module-openchoreo-policy module installs a Backstage permission policy that consults the OpenChoreo authorization service for openchoreo.* permissions. For everything else (catalog read, scaffolder execute, โฆ) it returns ALLOW, so it is composable with your existing host policy as long as it is registered as the only top-level policy.
Wiringโ
The Backstage backend can only have one permission policy active. If your existing packages/backend/src/index.ts registers the default allow-all policy:
backend.add(
import("@backstage/plugin-permission-backend-module-allow-all-policy"),
);
Replace it with the OpenChoreo policy:
backend.add(import("@backstage/plugin-permission-backend"));
backend.add(
import("@openchoreo/backstage-plugin-permission-backend-module-openchoreo-policy"),
);
If you have a custom policy already, you can call into the OpenChoreo policy from yours โ see the policy's exported helpers in plugins/permission-backend-module-openchoreo-policy/src/index.ts.
Two operating modesโ
Behaviour switches on openchoreo.features.authz.enabled. This flag must mirror the OpenChoreo cluster's deployment state โ it is not an independent installer default:
| Cluster state | Set in Backstage | Policy behaviour |
|---|---|---|
| authz off (local dev) | authz.enabled: false | Policy short-circuits to ALLOW for every permission. Logs OpenChoreo permission policy disabled via openchoreo.features.authz.enabled=false. Using allow-all policy. at startup. |
| authz on (production) | authz.enabled: true | Policy queries ${openchoreo.baseUrl}/authz/profile for the user's capabilities, evaluates openchoreo.* permissions against the result, and falls back to ALLOW for non-OpenChoreo permissions. |
The in-tree OpenChoreo portal hides this knob because the Helm chart configures both halves in lockstep. External Backstage hosts must align by hand.
Enabling authz: extra wiring requiredโ
When the cluster runs with authz on, the policy needs the user's IDP token to reach /authz/profile. Install the IDP token middleware on the root HTTP router. Edit packages/backend/src/index.ts:
import { rootHttpRouterServiceFactory } from "@backstage/backend-defaults/rootHttpRouter";
import { createIdpTokenHeaderMiddleware } from "@openchoreo/openchoreo-auth";
backend.add(
rootHttpRouterServiceFactory({
configure: ({ app, applyDefaults, middleware }) => {
app.use(middleware.helmet());
app.use(middleware.cors());
app.use(middleware.compression());
app.use(middleware.logging());
// Reads the IDP token from the request and exposes it via
// AsyncLocalStorage so the policy can use it for /authz/profile calls.
app.use(createIdpTokenHeaderMiddleware());
applyDefaults();
},
}),
);
You also need the frontend OIDC sign-in provider so the policy receives the user's IDP token. That wiring is part of Core install ยง 4.3.2 โ OpenChoreoAuthModule on the backend, the OAuth2 factory and DynamicSignInPage on the frontend. Plus the OpenChoreoPermissionApi override, without which the IDP token doesn't reach the authorize call at all.
Composability semanticsโ
The policy decision tree:
- If the permission name starts with
openchoreo.:- If
authz.enabled = false: ALLOW. - If
authz.enabled = true: query/authz/profile, evaluate, return ALLOW or DENY.
- If
- If the permission name is one of the OpenChoreo-managed catalog permissions (e.g.
catalog.entity.readfor an OpenChoreo-synced entity): same rules as above. - Otherwise (e.g.
scaffolder.task.execute,catalog.entity.create, your-org's custom permissions): ALLOW.
This means your existing Backstage permissions UX (scaffolder gating, catalog write rules, โฆ) is unchanged.
Verificationโ
The simplest end-to-end smoke test runs the policy in AllowAll mode (matching a cluster deployed with authz: false). With the backend running:
# Acquire a guest token (development only).
TOKEN=$(curl -sS -X POST http://localhost:7007/api/auth/guest/refresh \
| jq -r '.backstageIdentity.token')
# Authorize a no-op OpenChoreo permission. Expect "ALLOW".
curl -sS -X POST http://localhost:7007/api/permission/authorize \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"items":[{"id":"x","permission":{"type":"basic","name":"openchoreo.foo.read"}}]}' | jq