OPA Permissions Wrapper lets Backstage use Open Policy Agent for permission checks. You write policies in Rego outside your Backstage code. When a user performs an action, the wrapper sends the permission and identity to OPA. OPA evaluates your policy and returns a decision. The Backstage permission framework applies that result.
This decouples authorization from deployments. You update policies in OPA and see the effect right away. Teams can own their rules without touching TypeScript. You keep a single source of truth for RBAC across your portal. Decisions can allow or deny. They can be conditional with filters that map to rules in Backstage plugins.
Common uses include guarding catalog reads and writes. Protecting TechDocs updates. Locking down admin tasks in custom backend APIs. You can also call an evaluatePolicy helper inside your own plugins for richer inputs and checks. If OPA is unreachable, you can choose a safe fallback to allow or deny. The module works with the Backstage permission framework you already have. It focuses on authorization, not authentication.
If you run a self hosted Backstage and want flexible policy control, this plugin gives you a clean path to manage permissions with Rego.
Installation Instructions
These instructions apply to self-hosted Backstage only.
Install the backend module with the new backend system
Add the package
yarn --cwd packages/backend add @parsifal-m/plugin-permission-backend-module-opa-wrapper
Register the module
Edit packages/backend/src/index.ts
import { createBackend } from '@backstage/backend-defaults';
const backend = createBackend();
backend.add(import('@backstage/plugin-app-backend/alpha'));
backend.add(import('@backstage/plugin-auth-backend'));
// other plugins you already use
backend.add(import('@parsifal-m/plugin-permission-backend-module-opa-wrapper'));
backend.start();
This registers the OPA permission policy. All permission checks flow through OPA.
Configure OPA
Add this to app-config.yaml
permission:
opa:
baseUrl: 'http://localhost:8181'
policy:
policyEntryPoint: 'rbac_policy/decision'
policyFallbackDecision: 'deny'
policyEntryPoint is the OPA rule path you want to evaluate
policyFallbackDecision can be allow or deny
If the OPA server is not reachable the fallback decision is used
Install with the old backend system
Status
This module targets the new backend system in Backstage
There are no supported steps for the old backend system in this plugin
No frontend changes
This plugin runs in the backend
It does not export React components
You do not need to change the app package
Your existing permission checks continue to work
They now resolve through OPA
Example OPA policy
You can name your decision rule as you see fit
The plugin expects a result object in the OPA response
package backstage_policy
import future.keywords.if
CONDITIONAL(plugin_id, resource_type, conditions) := conditional_decision if {
conditional_decision := {
"result": "CONDITIONAL",
"pluginId": plugin_id,
"resourceType": resource_type,
"conditions": conditions,
}
}
default decision := {"result": "DENY"}
permission := input.permission.name
claims := input.identity.claims
decision := {"result": "ALLOW"} if {
permission == "catalog.entity.read"
}
decision := CONDITIONAL("catalog", "catalog-entity", {"anyOf": [{
"resourceType": "catalog-entity",
"rule": "IS_ENTITY_OWNER",
"params": {"claims": claims},
}]}) if {
permission == "catalog.entity.delete"
}
decision := CONDITIONAL("catalog", "catalog-entity", {"anyOf": [{
"resourceType": "catalog-entity",
"rule": "IS_ENTITY_KIND",
"params": {"kinds": ["Component"]},
}]}) if {
permission == "catalog.entity.update"
}
What the backend sends to OPA
The plugin sends input shaped like this
export type PermissionsFrameworkPolicyInput = {
permission: {
name: string;
};
identity?: {
user: string | undefined;
claims: string[];
};
};
OPA should return an object with a result field
If the decision is conditional include conditions as needed
Changelog
This changelog is produced from commits made to the OPA Permissions Wrapper plugin since a year ago, and based on the code located here. It may not contain information about all commits. Releases and version bumps are intentionally omitted. This changelog is generated by AI.
Breaking changes
- #263 Remove backend middleware. Use the wrapper plugin helper to protect routes. merged 7 months ago
- #266 Deprecate the opa authz package. All functionality lives in the wrapper plugin. Switch imports to the wrapper plugin. merged 7 months ago
Features
- #230 Enable OPA for permissions without the permissions framework. Control frontend rendering. Protect backend routes with OPA. merged 11 months ago
- #278 Add evaluatePolicy overload. Allow custom response types from OPA. merged 4 months ago
Bug fixes
- #280 Accept not in policy conditionals. none did not work with the permissions framework. merged 4 months ago
Documentation
- #293 Fix OPA configuration entry point naming in docs. merged 1 month ago
- #277 Fix README script command. merged 5 months ago
- #276 Fix typos in docs. merged 6 months ago
- #270 Minor doc fix. merged 7 months ago
Refactors
Set up Backstage in minutes with Roadie
Focus on using Backstage, rather than building and maintaining it.