Implementing access control in Backstage: understanding the permission framework with permissions, conditions, and permission policies via createPermissionPolicy, and building role-based access control with the RBAC Backend Plugin, role assignment, the admin UI, and identity provider integration.

In episode 12, you organized the storage layer: database, cache, and search index. That stored data now needs guarding — who can see catalog entities, who can run templates, who can perform certain actions. Episode 13 covers authorization: Backstage's built-in permission framework and how to build role-based access control with the RBAC Backend Plugin. You remember authentication from episode 8 — this episode is its counterpart: authentication verifies who you are, authorization determines what you're allowed to do.
Backstage models authorization through permissions. A permission is a statement about an action, for example "view an entity", "run a template", or "delete an entity". Every time a user attempts a protected action, Backstage asks a question: is this principal allowed to perform this action?
There are two kinds of permissions:
Conditions allow decisions to be made not only from who the user is, but also from the properties of the target resource. Backstage provides built-in rules such as isEntityOwner or hasEntityMetadata, which can be combined with AND and OR logic to form complex conditions.
A common pattern example: the platform team may see all entities, while regular developers may only see entities they own or that mark them as a member. This is what makes permissions not merely on/off, but contextual to the resource.
The final decision rests with the permission policy. The permission backend sends a check request to the policy, and the policy answers allow or deny. The modern way to write policies is createPermissionPolicy, imported from the permission backend package:
import { createPermissionPolicy } from '@backstage/plugin-permission-backend';
const examplePolicy = createPermissionPolicy({
async handle(request, user) {
if (request.permission.name === 'catalog.entity.read') {
return { result: AuthorizationResult.ALLOW };
}
return { result: AuthorizationResult.DENY };
},
});The policy above is simple: allow all entity reads, deny everything else. In practice, a policy will combine user identity, resource conditions, and organizational policy. Policies are written as ordinary async handlers on top of @backstage/backend-defaults, so decisions can be logged and debugged like any other backend code.
A policy doesn't apply automatically — it must be registered to the permission backend when the backend initializes. Once registered and the service is restarted, every incoming permission check goes through that policy. You can test it right from the UI: try accessing a page as a regular user, then compare the behavior with an admin account.
yarn devWhile developing, it's good practice to write small test cases for each permission: who is allowed, who is denied, and what happens when the resource changes. This prevents regressions later — especially once the RBAC from the next section also connects to the same policies.
| Concept | Role | Example |
|---|---|---|
| Permission | Statement about a protected action | catalog.entity.read |
| Condition | Rule that depends on the resource | only entities owned by your team |
| Policy | Final decision maker | allow admin, restrict developers |
| Role | Collection of policies for a role | platform-admin |
The full flow: a plugin submits a permission check, the permission backend forwards it to the policy, the policy decides while considering conditions, then the result returns to the plugin. All of this runs transparently within the same request.
Important
Deny always wins. When a policy produces conflicting decisions, Backstage treats the deny result as the final decision. That's why the safest policy is explicit allow, not explicit deny — anything not explicitly allowed is automatically denied.
Writing manual policies for every role combination is exhausting. For role-based access control that can be managed dynamically, Backstage has the RBAC Backend Plugin — a community plugin that manages roles and policies through data, not code.
The RBAC backend stores roles and policies linked to roles. One role can be given several permissions, and a permission can be allow or deny:
roles:
- name: user/default/platform-engineer
permissions:
- policyEntity: 'entity'
permission: 'catalog-entity-read'
effect: 'allow'
conditions:
rule: 'IS_ENTITY_KIND'
params:
kinds: ['Component']With this pattern, adding a new role or changing access rights is just data — no rewriting policy code. The same permissions defined in the permission framework still apply; RBAC simply binds them to roles.
Once roles are defined, the next step is role assignment — linking users or groups to roles. Assignment can be done through the catalog: because users and groups in Backstage come from the catalog (and are often imported from an identity provider), a role only needs to mention a group like group/default/platform-eng.
The RBAC Backend Plugin also provides an admin UI in the frontend. With this UI, admins can view the role list, edit attached permissions, and see who belongs to each role — without touching code or restarting the backend.
Backstage RBAC's strength shows when connected to the identity provider you already have. Because users and groups are imported into the catalog from the provider (for example Microsoft Entra ID, a GitHub organization, or LDAP), the existing team structure directly becomes the basis for role assignment.
The flow: the identity provider supplies users and groups, the catalog replicates them as entities, RBAC links roles to groups, and permission checks decide access based on group membership. This keeps the access model aligned with the real organization, not a manual duplicate that easily goes stale.
It's also important to decide who can manage RBAC itself — usually a dedicated admin role held by a few people. A healthy authorization system always has an audit trail: who changed a role, when, and what effect that change had on user access.
Episode 13 brought you into the heart of Backstage authorization: permissions as statements about actions, conditions for contextual decisions, permission policies as the final decision makers, and the RBAC Backend Plugin for managing roles and assignments dynamically through the UI. This closes the security gap left after authentication in episode 8.
The key takeaways:
In the next episode, episode 14, you integrate with the outside world: secrets & external service integration — how Backstage forwards requests to external services through a backend proxy and keeps tokens secure throughout their lifecycle.