Merge pull request #16164 from backstage/permissions-tutorial-fixes

docs: miscellaneous permissions plugin tutorial fixes
This commit is contained in:
Ben Lambert
2023-02-06 13:02:58 +01:00
committed by GitHub
5 changed files with 76 additions and 22 deletions
+1 -1
View File
@@ -48,7 +48,7 @@ The source code is available here:
Create a new `packages/backend/src/plugins/todolist.ts` with the following content:
```javascript
```typescript
import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
import { createRouter } from '@internal/plugin-todo-list-backend';
import { Router } from 'express';
@@ -146,7 +146,7 @@ In order to test the logic above, the integrators of your backstage instance nee
- async handle(): Promise<PolicyDecision> {
+ async handle(
+ request: PolicyQuery,
+ user?: BackstageIdentityResponse,
+ _user?: BackstageIdentityResponse,
+ ): Promise<PolicyDecision> {
+ if (isPermission(request.permission, todoListCreatePermission)) {
+ return {
@@ -204,7 +204,7 @@ First we'll clean up the `plugins/todo-list-backend/src/service/router.test.ts`:
const router = await createRouter({
logger: getVoidLogger(),
identity: {} as DefaultIdentityClient,
+ permissions: toPermissionEvaluator,
+ permissions: permissionEvaluator,
});
app = express().use(router);
});
@@ -235,7 +235,7 @@ Then we want to update the `plugins/todo-list-backend/src/service/standaloneServ
+ ServerTokenManager,
} from '@backstage/backend-common';
import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
import { ServerPermissionClient } from '@backstage/plugin-permission-node';
+ import { ServerPermissionClient } from '@backstage/plugin-permission-node';
import { Server } from 'http';
import { Logger } from 'winston';
import { createRouter } from './router';
@@ -285,4 +285,44 @@ Then we want to update the `plugins/todo-list-backend/src/service/standaloneServ
module.hot?.accept();
```
Finally, we need to update `plugins/todo-list-backend/src/plugin.ts`:
```diff
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.
*
* @alpha
*/
export const exampleTodoListPlugin = createBackendPlugin({
id: 'exampleTodoList',
register(env) {
env.registerInit({
deps: {
identity: coreServices.identity,
logger: coreServices.logger,
httpRouter: coreServices.httpRouter,
+ permissions: coreServices.permissions,
},
- async init({ identity, logger, httpRouter }) {
+ 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.
@@ -76,7 +76,7 @@ This enables decisions based on characteristics of the resource, but it's import
Install the missing module:
```
$ yarn workspace @internal/plugin-todo-list-backend add @backstage/plugin-permission-node
$ yarn workspace @internal/plugin-todo-list-backend add @backstage/plugin-permission-node zod
```
Create a new `plugins/todo-list-backend/src/service/rules.ts` file and append the following code:
@@ -89,7 +89,8 @@ import { Todo, TodoFilter } from './todos';
export const createTodoListPermissionRule = makeCreatePermissionRule<
Todo,
TodoFilter
TodoFilter,
typeof TODO_LIST_RESOURCE_TYPE
>();
export const isOwner = createTodoListPermissionRule({
@@ -97,8 +98,8 @@ export const isOwner = createTodoListPermissionRule({
description: 'Should allow only if the todo belongs to the user',
resourceType: TODO_LIST_RESOURCE_TYPE,
paramsSchema: z.object({
userId: z.string().describe('User ID to match on the resource')
})
userId: z.string().describe('User ID to match on the resource'),
}),
apply: (resource: Todo, { userId }) => {
return resource.author === userId;
},
@@ -187,6 +188,7 @@ Make sure `todoListConditions` and `createTodoListConditionalDecision` are expor
```diff
export * from './service/router';
+ export * from './conditionExports';
export { exampleTodoListPlugin } from './plugin';
```
## Test the authorized update endpoint
@@ -209,7 +211,6 @@ Let's go back to the permission policy's handle function and try to authorize ou
+ import {
+ todoListCreatePermission,
+ todoListUpdatePermission,
+ TODO_LIST_RESOURCE_TYPE,
+ } from '@internal/plugin-todo-list-common';
+ import {
+ todoListConditions,
@@ -217,7 +218,11 @@ Let's go back to the permission policy's handle function and try to authorize ou
+ } from '@internal/plugin-todo-list-backend';
...
async handle(
request: PolicyQuery,
- _user?: BackstageIdentityResponse,
+ user?: BackstageIdentityResponse,
): Promise<PolicyDecision> {
if (isPermission(request.permission, todoListCreatePermission)) {
return {
result: AuthorizeResult.ALLOW,
@@ -236,6 +241,7 @@ Let's go back to the permission policy's handle function and try to authorize ou
return {
result: AuthorizeResult.ALLOW,
};
}
```
For any incoming update requests, we now return a _Conditional Decision_. We are saying:
@@ -15,7 +15,7 @@ One possible solution may leverage the batching functionality to authorize all o
- res.json(getAll())
+ const items = getAll();
+ const decisions = await permissions.authorize(
+ items.map(({ id }) => ({ permission: todosListRead, resourceRef: id })),
+ items.map(({ id }) => ({ permission: todoListReadPermission, resourceRef: id })),
+ );
+ const filteredItems = decisions.filter(
@@ -53,7 +53,7 @@ Let's add another permission to the plugin.
resourceType: TODO_LIST_RESOURCE_TYPE,
});
+
+ export const todosListRead = createPermission({
+ export const todoListReadPermission = createPermission({
+ name: 'todos.list.read',
+ attributes: { action: 'read' },
+ resourceType: TODO_LIST_RESOURCE_TYPE,
@@ -81,10 +81,9 @@ So far we've only used the `PermissionEvaluator.authorize` method, which will ev
import {
todosListCreate,
todosListUpdate,
+ todosListRead,
+ todoListReadPermission,
TODO_LIST_RESOURCE_TYPE,
} from './permissions';
+ import { rules } from './rules';
+ const transformConditions: ConditionTransformer<TodoFilter> = createConditionTransformer(Object.values(rules));
@@ -95,7 +94,7 @@ So far we've only used the `PermissionEvaluator.authorize` method, which will ev
+ );
+
+ const decision = (
+ await permissions.authorizeConditional([{ permission: todosListRead }], {
+ await permissions.authorizeConditional([{ permission: todoListReadPermission }], {
+ token,
+ })
+ )[0];
@@ -110,7 +109,6 @@ So far we've only used the `PermissionEvaluator.authorize` method, which will ev
+ } else {
+ res.json(getAll());
+ }
+ }
- res.json(getAll());
});
```
@@ -121,7 +119,7 @@ Since `TodoFilter` used in our plugin matches the structure of the conditions ob
## Test the authorized read endpoint
Let's update our permission policy to return a conditional result whenever a `todosListRead` permission is received. In this case, we can reuse the decision returned for the `todosListCreate` permission.
Let's update our permission policy to return a conditional result whenever a `todoListReadPermission` permission is received. In this case, we can reuse the decision returned for the `todosListCreate` permission.
```diff
// packages/backend/src/plugins/permission.ts
@@ -132,7 +130,6 @@ import {
todoListCreatePermission,
todoListUpdatePermission,
+ todoListReadPermission,
TODO_LIST_RESOURCE_TYPE,
} from '@internal/plugin-todo-list-common';
...
@@ -56,7 +56,11 @@ Let's make the following changes in `plugins/todo-list/src/components/TodoListPa
- Add
- </Button>
+ {!loadingPermission && (
+ <Button disabled={!canAddTodo} variant="contained" onClick={handleAdd}>
+ <Button
+ disabled={!canAddTodo}
+ variant="contained"
+ onClick={() => onAdd(title.current)}
+ >
+ Add
+ </Button>
+ )}
@@ -118,7 +122,10 @@ Providing a disabled state can be a helpful signal to users, but there may be ca
- <Grid item>
- <AddTodo onAdd={handleAdd} />
- </Grid>
+ <RequirePermission permission={todoListCreatePermission} errorPage={<></>}>
+ <RequirePermission
+ permission={todoListCreatePermission}
+ errorPage={<></>}
+ >
+ <Grid item>
+ <AddTodo onAdd={handleAdd} />
+ </Grid>
@@ -149,11 +156,15 @@ Providing a disabled state can be a helpful signal to users, but there may be ca
onChange={e => (title.current = e.target.value)}
/>
- {!loadingPermission && (
- <Button disabled={!canAddTodo} variant="contained" onClick={handleAdd}>
- <Button
- disabled={!canAddTodo}
- variant="contained"
- onClick={() => onAdd(title.current)}
- >
- Add
- </Button>
- )}
+ <Button variant="contained" onClick={handleAdd}>
+ <Button variant="contained" onClick={() => onAdd(title.current)}>
+ Add
+ </Button>
</Box>