From 445460e2d19c4aec84037654be05d9c82c6c5f61 Mon Sep 17 00:00:00 2001 From: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Date: Wed, 19 Oct 2022 08:34:58 -0500 Subject: [PATCH 1/2] Updates to the To Do List permission tutorial Signed-off-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> --- docs/permissions/getting-started.md | 2 - .../02-adding-a-basic-permission-check.md | 126 +++++++++++++++++- .../05-frontend-authorization.md | 24 +++- 3 files changed, 144 insertions(+), 8 deletions(-) diff --git a/docs/permissions/getting-started.md b/docs/permissions/getting-started.md index 6e6bcd2d03..d2c467ff08 100644 --- a/docs/permissions/getting-started.md +++ b/docs/permissions/getting-started.md @@ -53,7 +53,6 @@ $ yarn workspace backend add @backstage/plugin-permission-backend 2. Add the following to a new file, `packages/backend/src/plugins/permission.ts`. This adds the permission-backend router, and configures it with a policy which allows everything. ```typescript -import { IdentityClient } from '@backstage/plugin-auth-node'; import { createRouter } from '@backstage/plugin-permission-backend'; import { AuthorizeResult, @@ -119,7 +118,6 @@ permission: 2. Update the PermissionPolicy in `packages/backend/src/plugins/permission.ts` to disable a permission that’s easy for us to test. This policy rejects any attempt to delete a catalog entity: ```diff - import { IdentityClient } from '@backstage/plugin-auth-node'; import { createRouter } from '@backstage/plugin-permission-backend'; import { AuthorizeResult, diff --git a/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md b/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md index 124605cb66..785b0bfdda 100644 --- a/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md +++ b/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md @@ -48,7 +48,9 @@ Edit `plugins/todo-list-backend/src/service/router.ts`: ... - import { InputError } from '@backstage/errors'; +- import { IdentityApi } from '@backstage/plugin-auth-node'; + import { InputError, NotAllowedError } from '@backstage/errors'; ++ import { getBearerTokenFromAuthorizationHeader, IdentityApi } from '@backstage/plugin-auth-node'; + import { PermissionEvaluator, AuthorizeResult } from '@backstage/plugin-permission-common'; + import { todoListCreatePermission } from '@internal/plugin-todo-list-common'; @@ -56,7 +58,7 @@ Edit `plugins/todo-list-backend/src/service/router.ts`: export interface RouterOptions { logger: Logger; - identity: IdentityClient; + identity: IdentityApi; + permissions: PermissionEvaluator; } @@ -69,11 +71,13 @@ Edit `plugins/todo-list-backend/src/service/router.ts`: ... router.post('/todos', async (req, res) => { - const token = IdentityClient.getBearerToken(req.header('authorization')); let author: string | undefined = undefined; - const user = token ? await identity.authenticate(token) : undefined; + const user = await identity.authenticate(token) : undefined; author = user?.identity.userEntityRef; ++ const token = getBearerTokenFromAuthorizationHeader( ++ req.header('authorization'), ++ ); + const decision = ( + await permissions.authorize([{ permission: todoListCreatePermission }], { + token, @@ -128,10 +132,8 @@ In order to test the logic above, the integrators of your backstage instance nee ```diff // packages/backend/src/plugins/permission.ts -- import { IdentityClient } from '@backstage/plugin-auth-node'; + import { + BackstageIdentityResponse, -+ IdentityClient + } from '@backstage/plugin-auth-node'; import { PermissionPolicy, @@ -170,3 +172,117 @@ Let's flip the result back to `ALLOW` before moving on. }; } ``` + +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`: + +```diff +import { getVoidLogger } from '@backstage/backend-common'; +import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; ++ import { PermissionEvaluator } from '@backstage/plugin-permission-common'; +import express from 'express'; +import request from 'supertest'; + +import { createRouter } from './router'; + ++ const mockedAuthorize: jest.MockedFunction = ++ jest.fn(); ++ const mockedPermissionQuery: jest.MockedFunction< ++ PermissionEvaluator['authorizeConditional'] ++> = jest.fn(); + ++ const permissionEvaluator: PermissionEvaluator = { ++ authorize: mockedAuthorize, ++ authorizeConditional: mockedPermissionQuery, ++}; + +describe('createRouter', () => { + let app: express.Express; + + beforeAll(async () => { + const router = await createRouter({ + logger: getVoidLogger(), + identity: {} as DefaultIdentityClient, ++ permissions: toPermissionEvaluator, + }); + 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`, first we need to add the `@backstage/plugin-permission-node` package to `plugins/todo-list-backend/package.json` and then we can make the following edits: + +```diff +import { + createServiceBuilder, + loadBackendConfig, + SingleHostDiscovery, ++ ServerTokenManager, +} from '@backstage/backend-common'; +import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; +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); ++ const tokenManager = ServerTokenManager.fromConfig(config, { ++ logger, ++ }); ++ const permissions = ServerPermissionClient.fromConfig(config, { ++ discovery, ++ tokenManager, ++ }); + const router = await createRouter({ + logger, + identity: DefaultIdentityClient.create({ + discovery, + issuer: await discovery.getExternalBaseUrl('auth'), + }), ++ 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(); +``` + +Now when you run `yarn tsc` you should have no more errors. diff --git a/docs/permissions/plugin-authors/05-frontend-authorization.md b/docs/permissions/plugin-authors/05-frontend-authorization.md index dc4df7839c..c3bdd26583 100644 --- a/docs/permissions/plugin-authors/05-frontend-authorization.md +++ b/docs/permissions/plugin-authors/05-frontend-authorization.md @@ -118,7 +118,7 @@ Providing a disabled state can be a helpful signal to users, but there may be ca - - - -+ ++ }> + + + @@ -165,3 +165,25 @@ Providing a disabled state can be a helpful signal to users, but there may be ca ``` Now you should find that the component for adding a todo list item does not render at all. Success! + +You can also use `RequirePermission` to prevent access to routes as well, here's how that would look in your `packages/app/src/App.tsx`: + +```diff ++ import { RequirePermission } from '@backstage/plugin-permission-react'; ++ import { todoListCreatePermission } from '@internal/plugin-todo-list-common'; + +... + + }> + {searchPage} + + } /> ++ ++ } /> ++ + +``` + +Now if you try to navigate to `https://localhost:3000/todo-list` you'll get and error page if you do not have permission. From 093ef4c5b340a2520cb1335e185a938d7a879a80 Mon Sep 17 00:00:00 2001 From: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Date: Mon, 24 Oct 2022 08:48:24 -0500 Subject: [PATCH 2/2] Improved formatting and content Signed-off-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> --- .../02-adding-a-basic-permission-check.md | 162 +++++++++--------- .../05-frontend-authorization.md | 4 +- 2 files changed, 83 insertions(+), 83 deletions(-) diff --git a/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md b/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md index 785b0bfdda..241b175987 100644 --- a/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md +++ b/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md @@ -73,7 +73,7 @@ Edit `plugins/todo-list-backend/src/service/router.ts`: router.post('/todos', async (req, res) => { let author: string | undefined = undefined; - const user = await identity.authenticate(token) : undefined; + const user = await identity.getIdentity({ request: req }); author = user?.identity.userEntityRef; + const token = getBearerTokenFromAuthorizationHeader( + req.header('authorization'), @@ -178,111 +178,111 @@ At this point everything is working but if you run `yarn tsc` you'll get some er First we'll clean up the `plugins/todo-list-backend/src/service/router.test.ts`: ```diff -import { getVoidLogger } from '@backstage/backend-common'; -import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; + import { getVoidLogger } from '@backstage/backend-common'; + import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; + import { PermissionEvaluator } from '@backstage/plugin-permission-common'; -import express from 'express'; -import request from 'supertest'; + import express from 'express'; + import request from 'supertest'; -import { createRouter } from './router'; + import { createRouter } from './router'; + const mockedAuthorize: jest.MockedFunction = -+ jest.fn(); ++ jest.fn(); + const mockedPermissionQuery: jest.MockedFunction< -+ PermissionEvaluator['authorizeConditional'] -+> = jest.fn(); ++ PermissionEvaluator['authorizeConditional'] ++ > = jest.fn(); + const permissionEvaluator: PermissionEvaluator = { -+ authorize: mockedAuthorize, -+ authorizeConditional: mockedPermissionQuery, -+}; ++ authorize: mockedAuthorize, ++ authorizeConditional: mockedPermissionQuery, ++ }; -describe('createRouter', () => { - let app: express.Express; + describe('createRouter', () => { + let app: express.Express; - beforeAll(async () => { - const router = await createRouter({ - logger: getVoidLogger(), - identity: {} as DefaultIdentityClient, -+ permissions: toPermissionEvaluator, + beforeAll(async () => { + const router = await createRouter({ + logger: getVoidLogger(), + identity: {} as DefaultIdentityClient, ++ permissions: toPermissionEvaluator, + }); + app = express().use(router); }); - app = express().use(router); - }); - beforeEach(() => { - jest.resetAllMocks(); - }); + beforeEach(() => { + jest.resetAllMocks(); + }); - describe('GET /health', () => { - it('returns ok', async () => { - const response = await request(app).get('/health'); + 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' }); + 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`, first we need to add the `@backstage/plugin-permission-node` package to `plugins/todo-list-backend/package.json` and then we can make the following edits: ```diff -import { - createServiceBuilder, - loadBackendConfig, - SingleHostDiscovery, -+ ServerTokenManager, -} from '@backstage/backend-common'; -import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; -import { ServerPermissionClient } from '@backstage/plugin-permission-node'; -import { Server } from 'http'; -import { Logger } from 'winston'; -import { createRouter } from './router'; + import { + createServiceBuilder, + loadBackendConfig, + SingleHostDiscovery, ++ ServerTokenManager, + } from '@backstage/backend-common'; + import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; + 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); -+ const tokenManager = ServerTokenManager.fromConfig(config, { -+ logger, -+ }); -+ const permissions = ServerPermissionClient.fromConfig(config, { -+ discovery, -+ tokenManager, -+ }); - const router = await createRouter({ - logger, - identity: DefaultIdentityClient.create({ - discovery, - issuer: await discovery.getExternalBaseUrl('auth'), - }), -+ permissions, - }); - - let service = createServiceBuilder(module) - .setPort(options.port) - .addRouter('/todo-list', router); - if (options.enableCors) { - service = service.enableCors({ origin: 'http://localhost:3000' }); + export interface ServerOptions { + port: number; + enableCors: boolean; + logger: Logger; } - return await service.start().catch(err => { - logger.error(err); - process.exit(1); - }); -} + 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); ++ const tokenManager = ServerTokenManager.fromConfig(config, { ++ logger, ++ }); ++ const permissions = ServerPermissionClient.fromConfig(config, { ++ discovery, ++ tokenManager, ++ }); + const router = await createRouter({ + logger, + identity: DefaultIdentityClient.create({ + discovery, + issuer: await discovery.getExternalBaseUrl('auth'), + }), ++ permissions, + }); -module.hot?.accept(); + 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(); ``` Now when you run `yarn tsc` you should have no more errors. diff --git a/docs/permissions/plugin-authors/05-frontend-authorization.md b/docs/permissions/plugin-authors/05-frontend-authorization.md index c3bdd26583..d494ea60ba 100644 --- a/docs/permissions/plugin-authors/05-frontend-authorization.md +++ b/docs/permissions/plugin-authors/05-frontend-authorization.md @@ -166,7 +166,7 @@ Providing a disabled state can be a helpful signal to users, but there may be ca Now you should find that the component for adding a todo list item does not render at all. Success! -You can also use `RequirePermission` to prevent access to routes as well, here's how that would look in your `packages/app/src/App.tsx`: +You can also use `RequirePermission` to prevent access to routes as well. Here's how that would look in your `packages/app/src/App.tsx`: ```diff + import { RequirePermission } from '@backstage/plugin-permission-react'; @@ -181,7 +181,7 @@ You can also use `RequirePermission` to prevent access to routes as well, here's + -+ } /> ++ + ```