Skip to main content

3. Adding a resource permission check

info

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!

When performing updates (or other operations) on specific resources, the permissions framework allows for the decision to be based on characteristics of the resource itself. This means that it's possible to write policies that (for example) allow the operation for users that own a resource, and deny the operation otherwise.

Creating the update permission

Let's add a new permission to the file plugins/todo-list-common/src/permissions.ts from the previous section.

plugins/todo-list-common/src/permissions.ts
import { createPermission } from '@backstage/plugin-permission-common';

export const TODO_LIST_RESOURCE_TYPE = 'todo-item';

export const todoListCreatePermission = createPermission({
name: 'todo.list.create',
attributes: { action: 'create' },
});

export const todoListUpdatePermission = createPermission({
name: 'todo.list.update',
attributes: { action: 'update' },
resourceType: TODO_LIST_RESOURCE_TYPE,
});

export const todoListPermissions = [todoListCreatePermission];
export const todoListPermissions = [
todoListCreatePermission,
todoListUpdatePermission,
];

Notice that unlike todoListCreatePermission, the todoListUpdatePermission permission contains a resourceType field. This field indicates to the permission framework that this permission is intended to be authorized in the context of a resource with type 'todo-item'. You can use whatever string you like as the resource type, as long as you use the same value consistently for each type of resource.

Setting up authorization for the update permission

To start, let's edit plugins/todo-list-backend/src/service/router.ts in the same manner as we did in the previous section:

plugins/todo-list-backend/src/service/router.ts
import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
import {
todoListCreatePermission,
todoListUpdatePermission,
} from '@internal/plugin-todo-list-common';

// ...

const permissionIntegrationRouter = createPermissionIntegrationRouter({
permissions: [todoListCreatePermission],
permissions: [todoListCreatePermission, todoListUpdatePermission],
});

// ...

router.put('/todos', async (req, res) => {
const credentials = await httpAuth.credentials(req, { allow: ['user'] });

if (!isTodoUpdateRequest(req.body)) {
throw new InputError('Invalid payload');
}
const decision = (
await permissions.authorize(
[{ permission: todoListUpdatePermission, resourceRef: req.body.id }],
{ credentials },
)
)[0];

if (decision.result !== AuthorizeResult.ALLOW) {
throw new NotAllowedError('Unauthorized');
}

res.json(update(req.body));
});

Important: Notice that we are passing an extra resourceRef field, with the id of the todo item as the value.

This enables decisions based on characteristics of the resource, but it's important to note that policy authors will not have access to the resource ref inside of their permission policies. Instead, the policies will return conditional decisions, which we need to now support in our plugin.

Adding support for conditional decisions

Install the missing module:

$ yarn workspace @internal/plugin-todo-list-backend add zod

Create a new plugins/todo-list-backend/src/service/rules.ts file and append the following code:

plugins/todo-list-backend/src/service/rules.ts
import { makeCreatePermissionRule } from '@backstage/plugin-permission-node';
import { TODO_LIST_RESOURCE_TYPE } from '@internal/plugin-todo-list-common';
import { z } from 'zod';
import { Todo, TodoFilter } from './todos';

export const createTodoListPermissionRule = makeCreatePermissionRule<
Todo,
TodoFilter,
typeof TODO_LIST_RESOURCE_TYPE
>();

export const isOwner = createTodoListPermissionRule({
name: 'IS_OWNER',
description: 'Should allow only if the todo belongs to the user',
resourceType: TODO_LIST_RESOURCE_TYPE,
paramsSchema: z.object({
userId: z.string().describe('User ID to match on the resource'),
}),
apply: (resource: Todo, { userId }) => {
return resource.author === userId;
},
toQuery: ({ userId }) => {
return {
property: 'author',
values: [userId],
};
},
});

export const rules = { isOwner };

makeCreatePermissionRule is a helper used to ensure that rules created for this plugin use consistent types for the resource and query.

Note

To support custom rules defined by Backstage integrators, you must export createTodoListPermissionRule from the backend package and provide some way for custom rules to be passed in before the backend starts, likely via extension point.

We have created a new isOwner rule, which is going to be automatically used by the permission framework whenever a conditional response is returned in response to an authorized request with an attached resourceRef. Specifically, the apply function is used to understand whether the passed resource should be authorized or not.

Let's skip the toQuery function for now, we'll come back to that in the next section.

Now, let's create the new endpoint by editing plugins/todo-list-backend/src/service/router.ts. This uses the createPermissionIntegrationRouter helper to add the APIs needed by the permission framework to your plugin. You'll need to supply:

  • getResources: a function that accepts an array of resourceRefs in the same format you expect to be passed to authorize, and returns an array of the corresponding resources.
  • resourceType: the same value used in the permission rule above.
  • permissions: the list of permissions that your plugin accepts.
  • rules: an array of all the permission rules you want to support in conditional decisions.
plugins/todo-list-backend/src/service/router.ts
// ...
import {
TODO_LIST_RESOURCE_TYPE,
todoListCreatePermission,
todoListUpdatePermission,
} from '@internal/plugin-todo-list-common';
import { add, getAll, update } from './todos';
import { add, getAll, getTodo, update } from './todos';
import { rules } from './rules';

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

const permissionIntegrationRouter = createPermissionIntegrationRouter({
permissions: [todoListCreatePermission, todoListUpdatePermission],
getResources: async resourceRefs => {
return resourceRefs.map(getTodo);
},
resourceType: TODO_LIST_RESOURCE_TYPE,
rules: Object.values(rules),
});

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

// ...
}

Provide utilities for policy authors

Now that we have a new resource type and a corresponding rule, we need to export some utilities for policy authors to reference them.

Create a new plugins/todo-list-backend/src/conditionExports.ts file and add the following code:

plugins/todo-list-backend/src/conditionExports.ts
import { TODO_LIST_RESOURCE_TYPE } from '@internal/plugin-todo-list-common';
import { createConditionExports } from '@backstage/plugin-permission-node';
import { rules } from './service/rules';

const { conditions, createConditionalDecision } = createConditionExports({
pluginId: 'todolist',
resourceType: TODO_LIST_RESOURCE_TYPE,
rules,
});

export const todoListConditions = conditions;

export const createTodoListConditionalDecision = createConditionalDecision;

Make sure todoListConditions and createTodoListConditionalDecision are exported from the todo-list-backend package by editing plugins/todo-list-backend/src/index.ts:

plugins/todo-list-backend/src/index.ts
export * from './service/router';
export * from './conditionExports';
export { exampleTodoListPlugin } from './plugin';

Test the authorized update endpoint

Let's go back to the permission policy's handle function and try to authorize our new permission with an isOwner condition.

packages/backend/src/plugins/permission.ts
import {
IdentityClient
} from '@backstage/plugin-auth-node';
import {
PermissionPolicy,
PolicyQuery,
PolicyQueryUser,
} from '@backstage/plugin-permission-node';
import { isPermission } from '@backstage/plugin-permission-common';
import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
import {
todoListCreatePermission,
todoListUpdatePermission,
} from '@internal/plugin-todo-list-common';
import {
todoListConditions,
createTodoListConditionalDecision,
} from '@internal/plugin-todo-list-backend';


async handle(
request: PolicyQuery,
_user?: PolicyQueryUser,
user?: PolicyQueryUser,
): Promise<PolicyDecision> {
if (isPermission(request.permission, todoListCreatePermission)) {
return {
result: AuthorizeResult.ALLOW,
};
}
if (isPermission(request.permission, todoListUpdatePermission)) {
return createTodoListConditionalDecision(
request.permission,
todoListConditions.isOwner({
userId: user?.info.userEntityRef ?? '',
}),
);
}

return {
result: AuthorizeResult.ALLOW,
};
}

For any incoming update requests, we now return a Conditional Decision. We are saying:

Hey permission framework, I can't make a decision alone. Please go to the plugin with id todolist and ask it to apply these conditions.

To check that everything works as expected, you should now see an error in the UI whenever you try to edit an item that wasn’t created by you. Success!