Skip to main content

Core Concepts

Publishing Lifecycle

The RBAC UI facilitates the creation and management of RBAC policies. Policies go through a publishing lifecycle that includes the following stages:

  1. Draft: Newly created policies are in draft state and do not affect Backstage behavior. You can save draft policies while working on them and return to them later.
  2. Published: Draft policies can be published, replacing the existing policy and becoming active. Only one published policy exists at a time in Backstage.
  3. Inactive: When a new policy is published, the previously active policy becomes inactive.
  4. Republish: Inactive policies can be republished at any time to become active again.

Policy Overview

The RBAC policy is configured by defining one or more roles, to which you can add users/groups as members. Each role can have multiple permission decisions configured, which match one or more permissions, and specify the authorization decision that should be made for those permissions.

Users can be a member of multiple roles, and roles can overlap in the permissions they match. When a given permission matches multiple times, the first match will be the one used to determine the decision returned.

Matching permissions

Permissions are a core concept in the Backstage permission framework. Each plugin can define a set of permissions that control what users can see and do within that plugin. Each permission is an object with the following fields:

  • name: a string which uniquely identifies the permission, such as catalog.entity.read
  • attributes: an object containing attributes which describe the characteristics of the permission, to allow matching multiple permissions when making a policy decision. We expect the number of supported attributes to increase over time, but for now, the only attribute is action, which can be set to "create", "read", "update", or "delete".
  • resourceType: the type of resource expected to be supplied when authorizing this permission, if applicable (for example catalog-entity).

In the RBAC UI, it's possible to match a single specific permission by name, or to match using a combination of resourceType and actions in order to match multiple permissions with a single entry in the list. It's also possible to match all permissions with a single entry - this is mostly useful for defining a fallback at the end of your policy, in case no other permission decision in the policy matched a request.

Specifying decisions

The RBAC UI supports definitive allow or deny decisions, as well as conditional decisions whose result can vary based on characteristics of the resource being authorized. Conditional decisions can only be returned for permissions with a resourceType - the RBAC UI handles enabling and disabling the "conditional" option as needed.

Conditional decisions contain one or more conditions, which are exported by permissioned plugins and tied to a resourceType. The condition HAS_ANNOTATION, for example, is tied to the catalog-entity resourceType, and checks whether a catalog entity has a certain annotation. Multiple conditions can be combined using "any of" (return allow when any one of these conditions is true), or "all of" (only return allow when all of these conditions is true). It's also possible to negate conditions using "not", and combinations of conditions and logical operators can be deeply nested if needed.

Additional features

Import / Export

The RBAC plugin allows users to export existing policies, a feature which can be found within the policy page. You can import the resulting yaml file back into RBAC, which will result in a draft policy identical to the exported policy. This can be useful when working with multiple Backstage instances with similar configurations.

Default policy

The RBAC plugin allows Backstage administrators to configure a default policy for deployment. Setting a default policy allows RBAC to enforce an active policy from the start, before any administrator has interacted with the RBAC UI. You can set a default policy by:

  1. Create a draft policy in the RBAC UI that represents the policy that you would like to become the default policy.
  2. Go to the draft policy page, and download the policy by clicking "Export." Alternatively, you can export any previous version or the active policy from the respective policy pages.
  3. Add a defaultPolicy field under the rbac field in your app-config.yaml, and point it to the file you saved from the previous step:
permission:
rbac:
defaultPolicy:
$include: ./default-rbac-policy.yaml
  1. You should now see that your Backstage instance behaves as configured in your default policy, even though you have not yet authored any other policies using the RBAC UI.

NOTE: The default policy will be active if and only if no other active policies already exist.

Fallback policy

By default, when a request to the RBAC backend fails, RBAC will automatically switch to denying all permissions. You can configure this behavior by specifying a fallback policy in your app-config.yaml in the exact same way as the default policy:

Here is a sample fallback policy to allow all permissions for administrators and grant read-only access to everyone else for the items they own:

fallback-rbac-policy.yaml
name: Fallback Policy
description: Fallback Policy
options:
resolutionStrategy: any-allow
roles:
- name: Admins
members:
- group:default/devs # add your own group/users here
permissions:
- match: '*'
decision: allow
- name: Everyone else
members: '*'
permissions:
- match:
actions:
- read
resourceType: catalog-entity
decision:
pluginId: catalog
resourceType: catalog-entity
conditions:
anyOf:
- rule: IS_ENTITY_OWNER
params:
claims:
- ':backstageUser'

The fallback policy should be included in the app-config.yaml file, as illustrated below:

app-config.yaml
permission:
rbac:
fallbackPolicy:
$include: ./fallback-rbac-policy.yaml

Decision resolution strategy

Multiple decisions from multiple roles could be applicable to a single user when authorizing a permission request. By default, the any-allow strategy will be selected for a new policy. Please select the decision resolution strategy that makes sense for your policy.

Please note the selected strategy will be applied to the whole policy and greatly affects the outcome of permission decisions.

any-allow strategy

The first allow decision from all of the roles and decisions is the final result. A single explicit allow or an allow as a result of a conditional decision would result in a final allow decision, otherwise the decision is deny.

With this option, the order of roles and decisions does not matter.

first-match strategy

The first matching decision from the first matching role that is applicable to the user is the final result, regardless if that decision is an allow, deny or conditional.

With this option, the order in which you define roles and decisions matters.

How to change the decision resolution strategy for a policy

You can change the decision resolution strategy for a policy and read more about the options by following the steps below:

  1. Go to the draft policy page and click the more options icon on the top right.
  2. Select Options.
  3. Select which resolution strategy makes sense for your policy.
  4. Click Back to Policy when you are done. Your policy will be automatically updated with your selection, and it will be saved and/or published when save and publish your policy.

Integrating Custom Plugins

To integrate your custom plugins with RBAC you'll follow all the practices and conventions from the Permissions framework documentation. There is one extra step - adding the createPermissionIntegrationRouter - that you do need to make sure to take as it is the method that RBAC uses to find the permissions your custom plugin has made available. Here's what that looks like:

custom-plugin-backend/src/service/router.ts
import { errorHandler } from '@backstage/backend-common';
import { LoggerService } from '@backstage/backend-plugin-api';
import express from 'express';
import Router from 'express-promise-router';
import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node';
import { customPluginPermissions } from '@internal/custom-plugin-common';

export interface RouterOptions {
logger: LoggerService;
}

export async function createRouter(
options: RouterOptions,
): Promise<express.Router> {
const { logger } = options;

const router = Router();
router.use(express.json());

router.use(
createPermissionIntegrationRouter({
permissions: customPluginPermissions,
}),
);

router.get('/health', (_, response) => {
logger.info('PONG!');
response.json({ status: 'ok' });
});

router.use(errorHandler());
return router;
}
Note

customPluginPermissions shown above is an array of all the available permissions for your custom plugins

The last step you need to take to complete this is adding your custom plugin to the permissionedPlugins list:

app-config.yaml
permission:
enabled: true
permissionedPlugins:
- catalog
- scaffolder
- customplugin
rbac:
authorizedUsers:
- group:default/admins
- user:default/alice
- user:default/bob

Promoting policies through environments

The following is a best practice guide to safely promote policies through your Backstage deployment environments.

  1. Always use a lower environment (e.g. dev or staging) to create or update policies.
  2. We recommend using versions in your policy name (e.g. policy-v1, policy-v2).
  3. Test your policy in the lower environment thoroughly, you can use the policy tester, and we also recommend manually testing your policy as well.
  4. Once your policy is tested, use the policy export functionality to download your policy in yaml format.
  5. In your target environment (e.g. production), use the policy import functionality to upload your policy.
  6. Use the policy tester to perform a final round of testing.
  7. Click save & publish.

FAQ

This section covers the most common problems our first time adopters face and how to troubleshoot them. This section will cover permission topics that include both Open Source Backstage and RBAC.

Why do I see RBAC errors in the logs when Backstage starts up?

This behavior is simply due to a race condition where the frontend starts up faster than your backend and makes a request for the policy "too quickly". It can be safely ignored.

Why do my permissions not work on my groups but work on individuals?

When using a custom identity provider it's critical that both the user and the ownershipEntityRefs (the groups in which the user is a member) are returned. Otherwise, the permission plugin will interpret that the user does not belong to any groups during every permission check.

Why don't permissions from plugins appear in the permission decision list?

If a plugin has permissions but are not appearing in the RBAC UI permission decision list, it's likely due to not listing the plugin in the permissionedPlugins section of your app-config.yaml. See how to add your plugin to the permissioned list.

Why can I not change the resolution strategy?

The resolution strategy can only be changed when a policy is in draft mode. To change the resolution strategy you would need to duplicate the policy, modify the resolution strategy, and then publish the new policy.

Why is a user still able to access something I've denied via permissions? Or why is a user unable to access something I've granted them via permissions?

Both of these questions have similar resolutions, and it involves the selected resolution strategy.

In the first scenario, adopters often have selected Any Allow as the resolution strategy but have granted access/permission for that user in another permission decision. Another common cause for this issue is when the user belongs to multiple groups and is denied for belonging to one group but is given permission due to belonging to a different group that does have access.

Conversely, in the second scenario, adopters have selected First Match and the permission is being denied by an earlier permission decision denying access. With First Match, order of operations is critical.

Documentation