Skip to main content
Version: Next

Defining custom permission rules

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 a new file called permissionRules.ts. Create this file in the src/ directory of your permission policy module (the package scaffolded by yarn new in the Getting Started section).

Permission rule parameter schemas accept libraries that implement Standard Schema, validate synchronously, and support JSON Schema conversion. Async refinements and transforms are not supported. We use Zod v4 and @backstage/catalog-model in the example below. To install them, run:

from your Backstage root directory
yarn --cwd plugins/permission-backend-module-custom add zod@4 @backstage/catalog-model
plugins/permission-backend-module-custom/src/permissionRules.ts
import type { Entity } from '@backstage/catalog-model';
import { catalogEntityPermissionResourceRef } from '@backstage/plugin-catalog-node/alpha';
import {
createConditionFactory,
createPermissionRule,
} from '@backstage/plugin-permission-node';
import * as z from 'zod';

export const isInSystemRule = createPermissionRule({
name: 'IS_IN_SYSTEM',
description: 'Checks if an entity is part of the system provided',
resourceRef: catalogEntityPermissionResourceRef,
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.

Since we defined the rule in the permission policy module's src/ directory, we can import the condition directly in our policy class:

...
import { isInSystem } from '../permissionRules';

export class CustomPolicy implements PermissionPolicy {
constructor(private readonly userInfo: UserInfoService) {}

async handle(
request: PolicyQuery,
user?: PolicyQueryUser,
): Promise<PolicyDecision> {
if (isResourcePermission(request.permission, 'catalog-entity')) {
const ownershipRefs = user
? (await this.userInfo.getUserInfo(user.credentials)).ownershipEntityRefs
: [];
return createCatalogConditionalDecision(
request.permission,
catalogConditions.isEntityOwner({
claims: ownershipRefs,
}),
{
anyOf: [
catalogConditions.isEntityOwner({
claims: ownershipRefs,
}),
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.

warning

The PermissionsRegistryService is a fairly new addition and not yet supported by all plugins as they might still be using the old createPermissionIntegrationRouter that cannot be extended. If you encounter errors when installing custom rules for a plugin, the plugin may need to be switched to using the PermissionsRegistryService first.

To install custom rules in a plugin, we need to use the PermissionsRegistryService. Here are the steps you'll need to take to add the isInSystemRule we created above to the catalog:

  1. Export isInSystemRule from your permission policy module by adding it to the module's src/index.ts:

    export { isInSystemRule } from './permissionRules';
    export { permissionModuleCustom as default } from './module';
  2. Create a catalogPermissionRules.ts file in the packages/backend/src/extensions folder with the following content:

    packages/backend/src/extensions/catalogPermissionRules.ts
    import {
    coreServices,
    createBackendModule,
    } from '@backstage/backend-plugin-api';
    import { isInSystemRule } from '@internal/backstage-plugin-permission-backend-module-custom';

    export default createBackendModule({
    pluginId: 'catalog',
    moduleId: 'permission-rules',
    register(reg) {
    reg.registerInit({
    deps: { permissionsRegistry: coreServices.permissionsRegistry },
    async init({ permissionsRegistry }) {
    permissionsRegistry.addPermissionRules([isInSystemRule]);
    },
    });
    },
    });
  3. 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'));
  4. Now when you run your Backstage instance — yarn start — 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