3. Adding a resource permission check
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!
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.
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:
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 token = getBearerTokenFromAuthorizationHeader(
req.header('authorization'),
);
if (!isTodoUpdateRequest(req.body)) {
throw new InputError('Invalid payload');
}
const decision = (
await permissions.authorize(
[{ permission: todoListUpdatePermission, resourceRef: req.body.id }],
{
token,
},
)
)[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:
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.
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 createRouter
.
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 ofresourceRefs
in the same format you expect to be passed toauthorize
, 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.
// ...
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:
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
:
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.
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!