Skip to main content

Local Development

The OPA Permissions Wrapper Module has a self-contained dev setup that lets you run and test it in isolation — no full Backstage app or database required.

Prerequisites

  • Node 22 or 24
  • A running OPA server at http://localhost:8181 with the rbac_policy loaded — run docker-compose up -d from the repo root
  • Dependencies installed: yarn install --immutable

Starting the plugin

yarn workspace @parsifal-m/plugin-permission-backend-module-opa-wrapper start

The backend starts at http://localhost:7007.

How it works

The dev backend directly instantiates OpaClient and OpaPermissionPolicy, then wires them into a single POST /evaluate endpoint. Send a permission name and optional user identity, get back the raw OPA decision. No permission framework validation, no resourceRef requirements, no auth headers needed.

The root app-config.yaml in this repo already has the required config:

permission:
opa:
baseUrl: 'http://localhost:8181'
policy:
policyEntryPoint: 'rbac_policy/decision'
policyFallbackDecision: 'allow'

Testing the endpoint

POST /api/opa-permission-wrapper-dev/evaluate

Send permissionName and optionally override userEntityRef and ownershipEntityRefs. The defaults simulate a non-admin user (user:default/mock with no group memberships).

Testing a DENY decision

Any permission with no matching rule in rbac_policy hits the default decision := {"result": "DENY"}:

curl -X POST http://localhost:7007/api/opa-permission-wrapper-dev/evaluate \
-H 'Content-Type: application/json' \
-d '{"permissionName": "scaffolder.task.create"}'

Expected response:

{ "result": "DENY" }

Testing a CONDITIONAL decision

catalog.entity.read is delegated to catalog_rules, which returns CONDITIONAL for non-admins (only Component-kind entities are readable):

curl -X POST http://localhost:7007/api/opa-permission-wrapper-dev/evaluate \
-H 'Content-Type: application/json' \
-d '{"permissionName": "catalog.entity.read"}'

Expected response:

{
"result": "CONDITIONAL",
"pluginId": "catalog",
"resourceType": "catalog-entity",
"conditions": {
"anyOf": [
{
"resourceType": "catalog-entity",
"rule": "IS_ENTITY_KIND",
"params": { "kinds": ["Component"] }
}
]
}
}

Testing an ALLOW decision (admin user)

Override ownershipEntityRefs to include group:default/maintainers, which satisfies is_admin in rbac_policy:

curl -X POST http://localhost:7007/api/opa-permission-wrapper-dev/evaluate \
-H 'Content-Type: application/json' \
-d '{
"permissionName": "catalog.entity.read",
"ownershipEntityRefs": ["group:default/maintainers"]
}'

Expected response:

{ "result": "ALLOW" }

Testing with a custom user

You can fully override the identity sent to OPA:

curl -X POST http://localhost:7007/api/opa-permission-wrapper-dev/evaluate \
-H 'Content-Type: application/json' \
-d '{
"permissionName": "catalog.entity.delete",
"userEntityRef": "user:default/alice",
"ownershipEntityRefs": ["user:default/alice", "group:default/team-a"]
}'

This maps directly to the input.identity object OPA receives:

{
"permission": { "name": "catalog.entity.delete" },
"identity": {
"user": "user:default/alice",
"claims": ["user:default/alice", "group:default/team-a"]
}
}

Modifying the policy

The rbac_policy and catalog_rules Rego files live in policies/ at the repo root. OPA runs with --watch in docker-compose, so any change to those files reloads without restarting OPA or the dev backend. Send another request immediately after saving to see the updated decision.

Fallback behaviour

If OPA is unreachable, the module returns the configured fallback (allow by default in this repo). To test the deny fallback, stop OPA — the next request returns { "result": "DENY" } and logs a warning.