Skip to main content

2. Adding a basic 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!

If the outcome of a permission check doesn't need to change for different resources, you can use a basic permission check. For this kind of check, we simply need to define a permission, and call authorize with it.

For this tutorial, we'll use a basic permission check to authorize the create endpoint in our todo-backend. This will allow Backstage integrators to control whether each of their users is authorized to create todos by adjusting their permission policy.

We'll start by creating a new permission, and then we'll use the permission api to call authorize with it during todo creation.

Creating a new permission

Let's navigate to the file plugins/todo-list-common/src/permissions.ts and add our first permission:

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

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

export const todoListPermissions = [tempExamplePermission];
export const todoListPermissions = [todoListCreatePermission];

For this tutorial, we've automatically exported all permissions from this file (see plugins/todo-list-common/src/index.ts).

Note

We use a separate todo-list-common package since all permissions authorized by your plugin should be exported from a "common-library" package. This allows Backstage integrators to reference them in frontend components as well as permission policies.

Authorizing using the new permission

Install the following module:

$ yarn workspace @internal/plugin-todo-list-backend \
add @backstage/plugin-permission-common @backstage/plugin-permission-node @internal/plugin-todo-list-common

Edit plugins/todo-list-backend/src/service/router.ts:

plugins/todo-list-backend/src/service/router.ts
import { InputError } from '@backstage/errors';
import { LoggerService, HttpAuthService } from '@backstage/backend-plugin-api';
import { InputError, NotAllowedError } from '@backstage/errors';
import { LoggerService, HttpAuthService, PermissionsService } from '@backstage/backend-plugin-api';
import { AuthorizeResult } from '@backstage/plugin-permission-common';
import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node';
import { todoListCreatePermission } from '@internal/plugin-todo-list-common';

export interface RouterOptions {
logger: LoggerService;
httpAuth: HttpAuthService;
permissions: PermissionsService;
}

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

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

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

router.get('/health', (_, response) => {
logger.info('PONG!');
response.json({ status: 'ok' });
});

router.use(permissionIntegrationRouter);

router.get('/todos', async (_req, res) => {
res.json(getAll());
});

router.post('/todos', async (req, res) => {
let author: string | undefined = undefined;

const user = await identity.getIdentity({ request: req });
author = user?.identity.userEntityRef;
const credentials = await httpAuth.credentials(req, { allow: ['user'] });
const decision = (
await permissions.authorize(
[{ permission: todoListCreatePermission }],
{ credentials },
)
)[0];

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

if (!isTodoCreateRequest(req.body)) {
throw new InputError('Invalid payload');
}

const todo = add({ title: req.body.title, author });
res.json(todo);
});

// ...

Pass the permissions object to the plugin in plugins/todo-list-backend/src/plugin.ts:

plugins/todo-list-backend/src/plugin.ts
import { coreServices, createBackendPlugin } from '@backstage/backend-plugin-api';
import { createRouter } from './service/router';

export const exampleTodoListPlugin = createBackendPlugin({
pluginId: 'todolist',
register(env) {
env.registerInit({
deps: {
logger: coreServices.logger,
httpAuth: coreServices.httpAuth,
httpRouter: coreServices.httpRouter,
permissions: coreServices.permissions,
},
async init({ logger, httpAuth, httpRouter }) {
async init({ logger, httpAuth, httpRouter, permissions }) {
httpRouter.use(
await createRouter({
logger,
httpAuth,
permissions,
}),
);
httpRouter.addAuthPolicy({
path: '/health',
allow: 'unauthenticated',
});
},
});
},
});

That's it! Now your plugin is fully configured. Let's try to test the logic by denying the permission.

Test the authorized create endpoint

Before running this step, please make sure you followed the steps described in Getting started section.

In order to test the logic above, the integrators of your backstage instance need to change their permission policy to return DENY for our newly-created permission:

packages/backend/src/extensions/permissionsPolicyExtension.ts
import { createBackendModule } from '@backstage/backend-plugin-api';
import {
PolicyDecision,
isPermission,
AuthorizeResult,
} from '@backstage/plugin-permission-common';
import {
PermissionPolicy,
PolicyQuery,
PolicyQueryUser,
} from '@backstage/plugin-permission-node';
import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
import { policyExtensionPoint } from '@backstage/plugin-permission-node/alpha';

class TestPermissionPolicy implements PermissionPolicy {
async handle(): Promise<PolicyDecision> {
async handle(
request: PolicyQuery,
_user?: PolicyQueryUser,
): Promise<PolicyDecision> {
if (isPermission(request.permission, todoListCreatePermission)) {
return {
result: AuthorizeResult.DENY,
};
}

return {
result: AuthorizeResult.ALLOW,
};
}

export default createBackendModule({
pluginId: 'permission',
moduleId: 'permission-policy',
register(reg) {
reg.registerInit({
deps: { policy: policyExtensionPoint },
async init({ policy }) {
policy.setPolicy(new TestPermissionPolicy());
},
});
},
});

Now the frontend should show an error whenever you try to create a new Todo item.

Let's flip the result back to ALLOW before moving on.

if (isPermission(request.permission, todoListCreatePermission)) {
return {
result: AuthorizeResult.DENY,
result: AuthorizeResult.ALLOW,
};
}

At this point everything is working but if you run yarn tsc you'll get an error, let's fix this up.

Clean up the plugins/todo-list-backend/src/service/router.test.ts:

plugins/todo-list-backend/src/service/router.test.ts
import express from 'express';
import request from 'supertest';

import { createRouter } from './router';
import { mockServices } from '@backstage/backend-test-utils';

describe('createRouter', () => {
let app: express.Express;

beforeAll(async () => {
const router = await createRouter({
logger: mockServices.logger.mock(),
httpAuth: mockServices.httpAuth.mock(),
permissions: mockServices.permissions.mock(),
});
app = express().use(router);
});

beforeEach(() => {
jest.resetAllMocks();
});

describe('GET /health', () => {
it('returns ok', async () => {
const response = await request(app).get('/health');

expect(response.status).toEqual(200);
expect(response.body).toEqual({ status: 'ok' });
});
});
});

Now when you run yarn tsc you should have no more errors.