Skip to main content

Writing a permission policy

info

This documentation is written for the old backend which has been replaced by the new backend system, being the default since Backstage version 1.24. If have migrated to the new backend system, you may want to read its own article instead. Otherwise, consider migrating!

In the previous section, we were able to set up the permission framework and make a simple change to our TestPermissionPolicy to confirm that policy is indeed wired up correctly.

That policy looked like this:

packages/backend/src/plugins/permission.ts
class TestPermissionPolicy implements PermissionPolicy {
async handle(
request: PolicyQuery,
_user?: PolicyQueryUser,
): Promise<PolicyDecision> {
if (request.permission.name === 'catalog.entity.delete') {
return {
result: AuthorizeResult.DENY,
};
}

return { result: AuthorizeResult.ALLOW };
}
}

What's in a policy?

Let's break this down a bit further. The request object of type PolicyQuery is a simple wrapper around the Permission object. This permission object encapsulates information about the action that the user is attempting to perform (See the Concepts page for more details).

In the policy above, we are checking to see if the provided action is a catalog entity delete action, which is the permission that the catalog plugin authors have created to represent the action of unregistering a catalog entity. If this is the case, we return a Definitive Policy Decision of DENY. In all other cases, we return ALLOW (resulting in an allow-by-default behavior).

As we confirmed in the previous section, we know that this now prevents us from unregistering catalog components. Hooray! But you may notice that this prevents anyone from unregistering a component, which is not a very realistic policy. Let's improve this policy by disabling the unregister action unless you are the owner of this component.

Conditional decisions

Let's change the policy to the following:

import {
AuthorizeResult,
PolicyDecision,
isPermission,
} from '@backstage/plugin-permission-common';
import {
catalogConditions,
createCatalogConditionalDecision,
} from '@backstage/plugin-catalog-backend/alpha';
import {
catalogEntityDeletePermission,
} from '@backstage/plugin-catalog-common/alpha';

class TestPermissionPolicy implements PermissionPolicy {
async handle(request: PolicyQuery): Promise<PolicyDecision> {
async handle(
request: PolicyQuery,
user?: PolicyQueryUser,
): Promise<PolicyDecision> {
if (request.permission.name === 'catalog.entity.delete') {
if (isPermission(request.permission, catalogEntityDeletePermission)) {
return {
result: AuthorizeResult.DENY,
};
return createCatalogConditionalDecision(
request.permission,
catalogConditions.isEntityOwner({
claims: user?.info.ownershipEntityRefs ?? [],
}),
);
}
return { result: AuthorizeResult.ALLOW };
}
}

Let's walk through the new code that we just added.

Instead of returning an Definitive Policy Decision, we use factory methods to construct a Conditional Policy Decision (See the Concepts page for more details). Since the policy doesn't have enough information to determine if user is the entity owner, this criteria is encapsulated within the conditional decision. However, createCatalogConditionalDecision will not compile unless request.permission is a catalog entity ResourcePermission. This type constraint ensures that policies return conditional decisions that are compatible with the requested permission. To address this, we use isPermission to "narrow" the type of request.permission to ResourcePermission<'catalog-entity'>. This matches the runtime behavior that was in place before, but you'll notice that the type of request.permission has changed within the scope of that if statement.

The catalogConditions object contains all of the rules defined by the catalog plugin. These rules can be combined to form a PermissionCriteria object, but for this case we only need to use the isEntityOwner rule. This rule accepts a list of entity refs that represent User identity and Group membership used to determine ownership. The second argument to PermissionPolicy#handle provides us with a PolicyQueryUser object, from which we can grab the user's ownershipEntityRefs. We provide an empty array as a fallback since the user may be anonymous.

You should now be able to see in your Backstage app that the unregister entity button is enabled for entities that you own, but disabled for all other entities!

Resource types

Now let's say we want to prevent all actions on catalog entities unless performed by the owner. One way to achieve this may be to simply update the if statement and check for each permission. If you choose to write your policy this way, it will certainly work! However, it may be difficult to maintain as the policy grows, and it may not be obvious if certain permissions are left out. We can author this same policy in a more scalable way by checking the resource type of the requested permission.

import {
AuthorizeResult,
PolicyDecision,
isPermission,
isResourcePermission,
} from '@backstage/plugin-permission-common';
import {
catalogConditions,
createCatalogConditionalDecision,
} from '@backstage/plugin-catalog-backend/alpha';
import {
catalogEntityDeletePermission,
} from '@backstage/plugin-catalog-common/alpha';

class TestPermissionPolicy implements PermissionPolicy {
async handle(
request: PolicyQuery,
user?: PolicyQueryUser,
): Promise<PolicyDecision> {
if (isPermission(request.permission, catalogEntityDeletePermission)) {
if (isResourcePermission(request.permission, 'catalog-entity')) {
return createCatalogConditionalDecision(
request.permission,
catalogConditions.isEntityOwner({
claims: user?.info.ownershipEntityRefs ?? [],
}),
);
}

return { result: AuthorizeResult.ALLOW };
}
}

In this example, we use isResourcePermission to match all permissions with a resource type of catalog-entity. Just like isPermission, this helper will "narrow" the type of request.permission and enable the use of createCatalogConditionalDecision. In addition to the behavior you observed before, you should also see that catalog entities are no longer visible unless you are the owner - success!

Note: Some catalog permissions do not have the 'catalog-entity' resource type, such as catalogEntityCreatePermission. In those cases, a definitive decision is required because conditions can't be applied to an entity that does not exist yet.