Skip to main content
Version: v1.1.x

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 stateSet in BackstagePolicy behaviour
authz off (local dev)authz.enabled: falsePolicy 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: truePolicy 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:

  1. If the permission name starts with openchoreo.:
    • If authz.enabled = false: ALLOW.
    • If authz.enabled = true: query /authz/profile, evaluate, return ALLOW or DENY.
  2. If the permission name is one of the OpenChoreo-managed catalog permissions (e.g. catalog.entity.read for an OpenChoreo-synced entity): same rules as above.
  3. 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