Defining custom permission rules
This documentation is written for the new backend system which is the default since Backstage version 1.24. If you are still on the old backend system, you may want to read its own article instead, and consider migrating!
For some use cases, you may want to define custom rules in addition to the ones provided by a plugin. In the previous section we used the isEntityOwner
rule to control access for catalog entities. Let's extend this policy with a custom rule that checks what system an entity is part of.
Define a custom rule
Plugins should export a rule factory that provides type-safety that ensures compatibility with the plugin's backend. The catalog plugin exports createCatalogPermissionRule
from @backstage/plugin-catalog-backend/alpha
for this purpose. Note: the /alpha
path segment is temporary until this API is marked as stable. For this example, we'll define the rule and create a condition in packages/backend/src/extensions/permissionsPolicyExtension.ts
.
We use zod
and @backstage/catalog-model
in our example below. To install them run:
yarn --cwd packages/backend add zod @backstage/catalog-model
...
import type { Entity } from '@backstage/catalog-model';
import { createCatalogPermissionRule } from '@backstage/plugin-catalog-backend/alpha';
import { createConditionFactory } from '@backstage/plugin-permission-node';
import { z } from 'zod';
export const isInSystemRule = createCatalogPermissionRule({
name: 'IS_IN_SYSTEM',
description: 'Checks if an entity is part of the system provided',
resourceType: 'catalog-entity',
paramsSchema: z.object({
systemRef: z
.string()
.describe('SystemRef to check the resource is part of'),
}),
apply: (resource: Entity, { systemRef }) => {
if (!resource.relations) {
return false;
}
return resource.relations
.filter(relation => relation.type === 'partOf')
.some(relation => relation.targetRef === systemRef);
},
toQuery: ({ systemRef }) => ({
key: 'relations.partOf',
values: [systemRef],
}),
});
const isInSystem = createConditionFactory(isInSystemRule);
...
For a more detailed explanation on defining rules, refer to the documentation for plugin authors.
Still in the packages/backend/src/extensions/permissionsPolicyExtension.ts
file, let's use the condition we just created in our CustomPermissionPolicy
.
...
import { createCatalogPermissionRule } from '@backstage/plugin-catalog-backend/alpha';
import { catalogConditions, createCatalogConditionalDecision, createCatalogPermissionRule } from '@backstage/plugin-catalog-backend/alpha';
import { createConditionFactory } from '@backstage/plugin-permission-node';
import { PermissionPolicy, PolicyQuery, PolicyQueryUser, createConditionFactory } from '@backstage/plugin-permission-node';
import { AuthorizeResult, PolicyDecision, isResourcePermission } from '@backstage/plugin-permission-common';
...
export const isInSystemRule = createCatalogPermissionRule({
name: 'IS_IN_SYSTEM',
description: 'Checks if an entity is part of the system provided',
resourceType: 'catalog-entity',
paramsSchema: z.object({
systemRef: z
.string()
.describe('SystemRef to check the resource is part of'),
}),
apply: (resource: Entity, { systemRef }) => {
if (!resource.relations) {
return false;
}
return resource.relations
.filter(relation => relation.type === 'partOf')
.some(relation => relation.targetRef === systemRef);
},
toQuery: ({ systemRef }) => ({
key: 'relations.partOf',
values: [systemRef],
}),
});
const isInSystem = createConditionFactory(isInSystemRule);
class CustomPermissionPolicy implements PermissionPolicy {
async handle(
request: PolicyQuery,
user?: PolicyQueryUser,
): Promise<PolicyDecision> {
if (isResourcePermission(request.permission, 'catalog-entity')) {
return createCatalogConditionalDecision(
request.permission,
catalogConditions.isEntityOwner({
claims: user?.info.ownershipEntityRefs ?? [],
}),
{
anyOf: [
catalogConditions.isEntityOwner({
claims: user?.info.ownershipEntityRefs ?? [],
}),
isInSystem({ systemRef: 'interviewing' }),
],
},
);
}
return { result: AuthorizeResult.ALLOW };
}
}
...
Provide the rule during plugin setup
Now that we have a custom rule defined and added to our policy, we need provide it to the catalog plugin. This step is important because the catalog plugin will use the rule's toQuery
and apply
methods while evaluating conditional authorize results. There's no guarantee that the catalog and permission backends are running on the same server, so we must explicitly link the rule to ensure that it's available at runtime.
The api for providing custom rules may differ between plugins, but there should typically be an extension point that you can use in your created module to add your rule. For the catalog, this extension point is exposed via catalogPermissionExtensionPoint
. Here's the steps you'll need to take to add the isInSystemRule
we created above to the catalog:
-
We will be using the
@backstage/plugin-catalog-node
package as it contains the extension point we need. Run this to add it:from your Backstage root directoryyarn --cwd packages/backend add @backstage/plugin-catalog-node
-
Next create a
catalogPermissionRules.ts
file in thepackages/backend/src/extensions
folder. -
Then add this as the contents of the new
catalogPermissionRules.ts
file:packages/backend/src/extensions/catalogPermissionRules.tsimport { createBackendModule } from '@backstage/backend-plugin-api';
import { catalogPermissionExtensionPoint } from '@backstage/plugin-catalog-node/alpha';
import { isInSystemRule } from './permissionPolicyExtension';
export default createBackendModule({
pluginId: 'catalog',
moduleId: 'permission-rules',
register(reg) {
reg.registerInit({
deps: { catalog: catalogPermissionExtensionPoint },
async init({ catalog }) {
catalog.addPermissionRules(isInSystemRule);
},
});
},
}); -
Next we need to add this to the backend by adding the following line:
packages/backend/src/index.ts// catalog plugin
backend.add(import('@backstage/plugin-catalog-backend'));
backend.add(
import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'),
);
backend.add(import('./extensions/catalogPermissionRules')); -
Now when you run you Backstage instance -
yarn dev
- the rule will be added to the catalog plugin.
The updated policy will allow catalog entity resource permissions if any of the following are true:
- User owns the target entity
- Target entity is part of the 'interviewing' system