2. Adding a basic permission check
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:
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
).
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
:
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
:
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:
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
:
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.