Skip to main content
Version: Next

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 Section 4.3.2OpenChoreoAuthModule on the backend, the OAuth2 factory and DynamicSignInPage on the frontend, plus the OpenChoreoPermissionApi ApiBlueprint 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