Skip to main content
Version: Next

2. Adding a basic permission check

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';

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 router = Router();
router.use(express.json());

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

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 service and register the new permission 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';
import { todoListCreatePermission } from '@internal/plugin-todo-list-common';

export const exampleTodoListPlugin = createBackendPlugin({
pluginId: 'todolist',
register(env) {
env.registerInit({
deps: {
logger: coreServices.logger,
httpAuth: coreServices.httpAuth,
httpRouter: coreServices.httpRouter,
permissions: coreServices.permissions,
permissionsRegistry: coreServices.permissionsRegistry,
},
async init({ logger, httpAuth, httpRouter }) {
async init({ httpAuth, logger, httpRouter, permissions, permissionsRegistry }) {
permissionsRegistry.addPermissions([todoListCreatePermission]);

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​

To test the logic above, the integrators of your Backstage instance need to change their permission policy to return DENY for the newly-created permission. Update the CustomPolicy class in the permission policy module created during the Getting Started steps:

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';

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

return {
result: AuthorizeResult.ALLOW,
};
}
}

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.