From a27aefa0f6d63a62f059f2ae2441f83f0e36f327 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 29 Aug 2024 15:34:07 +0200 Subject: [PATCH] docs: create old page Signed-off-by: Camila Belo --- ...02-adding-a-basic-permission-check--old.md | 387 ++++++++++++++++++ 1 file changed, 387 insertions(+) create mode 100644 docs/permissions/plugin-authors/02-adding-a-basic-permission-check--old.md diff --git a/docs/permissions/plugin-authors/02-adding-a-basic-permission-check--old.md b/docs/permissions/plugin-authors/02-adding-a-basic-permission-check--old.md new file mode 100644 index 0000000000..7c51214359 --- /dev/null +++ b/docs/permissions/plugin-authors/02-adding-a-basic-permission-check--old.md @@ -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 { + /* 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 { + 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 { + /* highlight-add-start */ + async handle( + request: PolicyQuery, + _user?: PolicyQueryUser, + ): Promise { + 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 = + 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 { + 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.