docs: create new page

Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
Camila Belo
2024-08-29 15:34:21 +02:00
parent a27aefa0f6
commit d6c4b41d26
@@ -4,6 +4,10 @@ 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 new backend system](../../backend-system/index.md) which is the default since Backstage [version 1.24](../../releases/v1.24.0.md). If you are still on the old backend system, you may want to read [its own article](./02-adding-a-basic-permission-check--old.md) instead, and [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).
@@ -57,18 +61,20 @@ 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';
import { HttpAuthService, LoggerService } from '@backstage/backend-plugin-api';
/* 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 { AuthService, HttpAuthService, LoggerService } from '@backstage/backend-plugin-api';
import { AuthorizeResult, PermissionEvaluator } 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;
logger: LoggerService;
/* highlight-add-next-line */
auth: AuthService;
identity: IdentityApi;
/* highlight-add-next-line */
permissions: PermissionEvaluator;
@@ -78,9 +84,9 @@ export async function createRouter(
options: RouterOptions,
): Promise<express.Router> {
/* highlight-remove-next-line */
const { logger, identity } = options;
const { logger, httpAuth } = options;
/* highlight-add-next-line */
const { logger, identity, permissions } = options;
const { logger, auth, httpAuth, permissions } = options;
/* highlight-add-start */
const permissionIntegrationRouter = createPermissionIntegrationRouter({
@@ -109,13 +115,14 @@ export async function createRouter(
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,
})
await permissions.authorize(
[{ permission: todoListCreatePermission }],
await auth.getPluginRequestToken({
onBehalfOf: credentials,
targetPluginId: 'permission',
}),
)
)[0];
if (decision.result === AuthorizeResult.DENY) {
@@ -134,30 +141,47 @@ export async function createRouter(
// ...
```
Pass the `permissions` object to the plugin in `packages/backend/src/plugins/todolist.ts`:
Pass the `auth` and `permissions` objects to the plugin in `plugins/todo-list-backend/src/plugin.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';
```ts title="plugins/todo-list-backend/src/plugin.ts"
import { coreServices, createBackendPlugin } from '@backstage/backend-plugin-api';
import { createRouter } from './service/router';
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,
});
}
export const exampleTodoListPlugin = createBackendPlugin({
pluginId: 'todolist',
register(env) {
env.registerInit({
deps: {
logger: coreServices.logger,
/* highlight-add-start */
auth: coreServices.auth,
permissions: coreServices.permissions,
/* highlight-add-end */
httpAuth: coreServices.httpAuth,
httpRouter: coreServices.httpRouter,
},
/* highlight-remove-next-line */
async init({ logger, httpAuth, httpRouter }) {
/* highlight-add-next-line */
async init({ logger, auth, httpAuth, httpRouter, permissions }) {
httpRouter.use(
await createRouter({
logger,
/* highlight-add-start */
auth,
permissions,
/* highlight-add-end */
httpAuth,
}),
);
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.
@@ -168,7 +192,15 @@ Before running this step, please make sure you followed the steps described in [
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"
```ts title="packages/backend/src/extensions/permissionsPolicyExtension.ts"
import { createBackendModule } from '@backstage/backend-plugin-api';
import {
PolicyDecision,
/* highlight-add-start */
isPermission,
/* highlight-add-end */
AuthorizeResult,
} from '@backstage/plugin-permission-common';
import {
PermissionPolicy,
/* highlight-add-start */
@@ -177,9 +209,9 @@ import {
/* 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 */
import { policyExtensionPoint } from '@backstage/plugin-permission-node/alpha';
class TestPermissionPolicy implements PermissionPolicy {
/* highlight-remove-next-line */
@@ -200,6 +232,19 @@ class TestPermissionPolicy implements PermissionPolicy {
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.
@@ -217,42 +262,28 @@ if (isPermission(request.permission, todoListCreatePermission)) {
}
```
At this point everything is working but if you run `yarn tsc` you'll get some errors, let's fix those up.
At this point everything is working but if you run `yarn tsc` you'll get an error, let's fix this up.
First we'll clean up the `plugins/todo-list-backend/src/service/router.test.ts`:
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 */
import { mockServices } from '@backstage/backend-test-utils';
describe('createRouter', () => {
let app: express.Express;
beforeAll(async () => {
const router = await createRouter({
logger: getVoidLogger(),
identity: {} as DefaultIdentityClient,
logger: mockServices.logger.mock(),
/* highlight-add-next-line */
permissions: permissionEvaluator,
auth: mockServices.auth.mock(),
httpAuth: mockServices.httpAuth.mock(),
/* highlight-add-next-line */
permissions: mockServices.permissions.mock(),
});
app = express().use(router);
});
@@ -272,112 +303,4 @@ describe('createRouter', () => {
});
```
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.