docs: create old page

Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
Camila Belo
2024-08-29 15:34:07 +02:00
parent 7310b97bd4
commit a27aefa0f6
@@ -0,0 +1,387 @@
---
id: 02-adding-a-basic-permission-check
title: 2. Adding a basic permission check
description: Explains how to add a basic permission check to a Backstage plugin
---
:::info
This documentation is written for the old backend which has been replaced by [the new backend system](../../backend-system/index.md), being the default since Backstage [version 1.24](../../releases/v1.24.0.md). If have migrated to the new backend system, you may want to read [its own article](./02-adding-a-basic-permission-check.md) instead. Otherwise, [consider migrating](../../backend-system/building-backends/08-migrating.md)!
:::
If the outcome of a permission check doesn't need to change for different [resources](../../references/glossary.md#resource-permission-plugin), 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](../../references/glossary.md#policy-permission-plugin).
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:
```ts title="plugins/todo-list-common/src/permissions.ts"
import { createPermission } from '@backstage/plugin-permission-common';
/* highlight-remove-start */
export const tempExamplePermission = createPermission({
name: 'temp.example.noop',
attributes: {},
/* highlight-remove-end */
/* highlight-add-start */
export const todoListCreatePermission = createPermission({
name: 'todo.list.create',
attributes: { action: 'create' },
/* highlight-add-end */
});
/* highlight-remove-next-line */
export const todoListPermissions = [tempExamplePermission];
/* highlight-add-next-line */
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 Note
We use a separate `todo-list-common` package since all permissions authorized by your plugin should be exported from a ["common-library" package](https://backstage.io/docs/tooling/cli/build-system#package-roles). 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`:
```ts title="plugins/todo-list-backend/src/service/router.ts"
/* highlight-remove-start */
import { InputError } from '@backstage/errors';
import { IdentityApi } from '@backstage/plugin-auth-node';
/* highlight-remove-end */
/* highlight-add-start */
import { InputError, NotAllowedError } from '@backstage/errors';
import { getBearerTokenFromAuthorizationHeader, IdentityApi } from '@backstage/plugin-auth-node';
import { PermissionEvaluator, AuthorizeResult } from '@backstage/plugin-permission-common';
import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node';
import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
/* highlight-add-end */
export interface RouterOptions {
logger: Logger;
identity: IdentityApi;
/* highlight-add-next-line */
permissions: PermissionEvaluator;
}
export async function createRouter(
options: RouterOptions,
): Promise<express.Router> {
/* highlight-remove-next-line */
const { logger, identity } = options;
/* highlight-add-next-line */
const { logger, identity, permissions } = options;
/* highlight-add-start */
const permissionIntegrationRouter = createPermissionIntegrationRouter({
permissions: [todoListCreatePermission],
});
/* highlight-add-end */
const router = Router();
router.use(express.json());
router.get('/health', (_, response) => {
logger.info('PONG!');
response.json({ status: 'ok' });
});
/* highlight-add-next-line */
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;
/* highlight-add-start */
const token = getBearerTokenFromAuthorizationHeader(
req.header('authorization'),
);
const decision = (
await permissions.authorize([{ permission: todoListCreatePermission }], {
token,
})
)[0];
if (decision.result === AuthorizeResult.DENY) {
throw new NotAllowedError('Unauthorized');
}
/* highlight-add-end */
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 `packages/backend/src/plugins/todolist.ts`:
```ts title="packages/backend/src/plugins/todolist.ts"
import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
import { createRouter } from '@internal/plugin-todo-list-backend';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
export default async function createPlugin({
logger,
discovery,
/* highlight-add-next-line */
permissions,
}: PluginEnvironment): Promise<Router> {
return await createRouter({
logger,
identity: DefaultIdentityClient.create({
discovery,
issuer: await discovery.getExternalBaseUrl('auth'),
}),
/* highlight-add-next-line */
permissions,
});
}
```
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](../getting-started.md) 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:
```ts title="packages/backend/src/plugins/permission.ts"
import {
PermissionPolicy,
/* highlight-add-start */
PolicyQuery,
PolicyQueryUser,
/* highlight-add-end */
} from '@backstage/plugin-permission-node';
/* highlight-add-start */
import { isPermission } from '@backstage/plugin-permission-common';
import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
/* highlight-add-end */
class TestPermissionPolicy implements PermissionPolicy {
/* highlight-remove-next-line */
async handle(): Promise<PolicyDecision> {
/* highlight-add-start */
async handle(
request: PolicyQuery,
_user?: PolicyQueryUser,
): Promise<PolicyDecision> {
if (isPermission(request.permission, todoListCreatePermission)) {
return {
result: AuthorizeResult.DENY,
};
}
/* highlight-add-end */
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.
```ts
if (isPermission(request.permission, todoListCreatePermission)) {
return {
/* highlight-remove-next-line */
result: AuthorizeResult.DENY,
/* highlight-add-next-line */
result: AuthorizeResult.ALLOW,
};
}
```
At this point everything is working but if you run `yarn tsc` you'll get some errors, let's fix those up.
First we'll clean up the `plugins/todo-list-backend/src/service/router.test.ts`:
```ts title="plugins/todo-list-backend/src/service/router.test.ts"
import { getVoidLogger } from '@backstage/backend-common';
import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
/* highlight-add-next-line */
import { PermissionEvaluator } from '@backstage/plugin-permission-common';
import express from 'express';
import request from 'supertest';
import { createRouter } from './router';
/* highlight-add-start */
const mockedAuthorize: jest.MockedFunction<PermissionEvaluator['authorize']> =
jest.fn();
const mockedPermissionQuery: jest.MockedFunction<
PermissionEvaluator['authorizeConditional']
> = jest.fn();
const permissionEvaluator: PermissionEvaluator = {
authorize: mockedAuthorize,
authorizeConditional: mockedPermissionQuery,
};
/* highlight-add-end */
describe('createRouter', () => {
let app: express.Express;
beforeAll(async () => {
const router = await createRouter({
logger: getVoidLogger(),
identity: {} as DefaultIdentityClient,
/* highlight-add-next-line */
permissions: permissionEvaluator,
});
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' });
});
});
});
```
Then we want to update the `plugins/todo-list-backend/src/service/standaloneServer.ts`:
```ts title="plugins/todo-list-backend/src/service/standaloneServer.ts"
import {
createServiceBuilder,
loadBackendConfig,
SingleHostDiscovery,
/* highlight-add-next-line */
ServerTokenManager,
} from '@backstage/backend-common';
import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
/* highlight-add-next-line */
import { ServerPermissionClient } from '@backstage/plugin-permission-node';
import { Server } from 'http';
import { Logger } from 'winston';
import { createRouter } from './router';
export interface ServerOptions {
port: number;
enableCors: boolean;
logger: Logger;
}
export async function startStandaloneServer(
options: ServerOptions,
): Promise<Server> {
const logger = options.logger.child({ service: 'todo-list-backend' });
logger.debug('Starting application server...');
const config = await loadBackendConfig({ logger, argv: process.argv });
const discovery = SingleHostDiscovery.fromConfig(config);
/* highlight-add-start */
const tokenManager = ServerTokenManager.fromConfig(config, {
logger,
});
const permissions = ServerPermissionClient.fromConfig(config, {
discovery,
tokenManager,
});
/* highlight-add-end */
const router = await createRouter({
logger,
identity: DefaultIdentityClient.create({
discovery,
issuer: await discovery.getExternalBaseUrl('auth'),
}),
/* highlight-add-next-line */
permissions,
});
let service = createServiceBuilder(module)
.setPort(options.port)
.addRouter('/todo-list', router);
if (options.enableCors) {
service = service.enableCors({ origin: 'http://localhost:3000' });
}
return await service.start().catch(err => {
logger.error(err);
process.exit(1);
});
}
module.hot?.accept();
```
Finally, we need to update `plugins/todo-list-backend/src/plugin.ts`:
```ts title="plugins/todo-list-backend/src/plugin.ts"
import { loggerToWinstonLogger } from '@backstage/backend-common';
import {
coreServices,
createBackendPlugin,
} from '@backstage/backend-plugin-api';
import { createRouter } from './service/router';
/**
* The example TODO list backend plugin.
*
* @public
*/
export const exampleTodoListPlugin = createBackendPlugin({
pluginId: 'exampleTodoList',
register(env) {
env.registerInit({
deps: {
identity: coreServices.identity,
logger: coreServices.logger,
httpRouter: coreServices.httpRouter,
/* highlight-add-next-line */
permissions: coreServices.permissions,
},
/* highlight-remove-next-line */
async init({ identity, logger, httpRouter }) {
/* highlight-add-next-line */
async init({ identity, logger, httpRouter, permissions }) {
httpRouter.use(
await createRouter({
identity,
logger: loggerToWinstonLogger(logger),
permissions,
}),
);
},
});
},
});
```
Now when you run `yarn tsc` you should have no more errors.