Add titles to codeblocks and switch from diff codeblock to language codeblock
Signed-off-by: Paul Schultz <pschultz@pobox.com>
This commit is contained in:
@@ -10,7 +10,7 @@ For some use cases, you may want to define custom [rules](./concepts.md#resource
|
||||
|
||||
Plugins should export a rule factory that provides type-safety that ensures compatibility with the plugin's backend. The catalog plugin exports `createCatalogPermissionRule` from `@backstage/plugin-catalog-backend/alpha` for this purpose. Note: the `/alpha` path segment is temporary until this API is marked as stable. For this example, we'll define the rule in `packages/backend/src/plugins/permission.ts`, but you can put it anywhere that's accessible by your `backend` package.
|
||||
|
||||
```typescript
|
||||
```typescript title="packages/backend/src/plugins/permission.ts"
|
||||
import type { Entity } from '@backstage/catalog-model';
|
||||
import { createCatalogPermissionRule } from '@backstage/plugin-catalog-backend/alpha';
|
||||
import { createConditionFactory } from '@backstage/plugin-permission-node';
|
||||
@@ -51,9 +51,7 @@ Now that we have a custom rule defined, we need provide it to the catalog plugin
|
||||
|
||||
The api for providing custom rules may differ between plugins, but there should typically be some integration point during the creation of the backend router. For the catalog, this integration point is exposed via `CatalogBuilder.addPermissionRules`.
|
||||
|
||||
```typescript
|
||||
// packages/backend/src/plugins/catalog.ts
|
||||
|
||||
```typescript title="packages/backend/src/plugins/catalog.ts"
|
||||
import { isInSystemRule } from './permission';
|
||||
// The CatalogBuilder with the addPermissionRules function is in the alpha path
|
||||
import { CatalogBuilder } from '@backstage/plugin-catalog-backend/alpha';
|
||||
@@ -76,12 +74,9 @@ The new rule is now ready for use in a permission policy!
|
||||
|
||||
Let's bring this all together by extending the example policy from the previous section.
|
||||
|
||||
```diff
|
||||
// packages/backend/src/plugins/permission.ts
|
||||
|
||||
+ import { isInSystem } from './catalog';
|
||||
|
||||
...
|
||||
```ts title="packages/backend/src/plugins/permission.ts"
|
||||
/* highlight-add-next-line */
|
||||
import { isInSystem } from './catalog';
|
||||
|
||||
class TestPermissionPolicy implements PermissionPolicy {
|
||||
async handle(
|
||||
@@ -91,17 +86,21 @@ class TestPermissionPolicy implements PermissionPolicy {
|
||||
if (isResourcePermission(request.permission, 'catalog-entity')) {
|
||||
return createCatalogConditionalDecision(
|
||||
request.permission,
|
||||
- catalogConditions.isEntityOwner({
|
||||
- claims: user?.identity.ownershipEntityRefs ?? [],
|
||||
- }),
|
||||
+ {
|
||||
+ anyOf: [
|
||||
+ catalogConditions.isEntityOwner({
|
||||
+ claims: user?.identity.ownershipEntityRefs ?? []
|
||||
+ }),
|
||||
+ isInSystem('interviewing')
|
||||
+ ]
|
||||
+ }
|
||||
/* highlight-remove-start */
|
||||
catalogConditions.isEntityOwner({
|
||||
claims: user?.identity.ownershipEntityRefs ?? [],
|
||||
}),
|
||||
/* highlight-remove-end */
|
||||
/* highlight-add-start */
|
||||
{
|
||||
anyOf: [
|
||||
catalogConditions.isEntityOwner({
|
||||
claims: user?.identity.ownershipEntityRefs ?? []
|
||||
}),
|
||||
isInSystem('interviewing')
|
||||
]
|
||||
}
|
||||
/* highlight-add-end */
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,27 +12,29 @@ However, there are some cases where the integrator needs to supplement the polic
|
||||
|
||||
If your Backstage permission policy may return a `DENY` for users requesting the `catalogEntityCreatePermission`, it may make sense, for example, to remove access to the `/catalog-import` page entirely:
|
||||
|
||||
```diff
|
||||
// packages/app/src/App.tsx
|
||||
|
||||
...
|
||||
|
||||
+ import { RequirePermission } from '@backstage/plugin-permission-react';
|
||||
+ import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common';
|
||||
|
||||
...
|
||||
|
||||
- <Route path="/catalog-import" element={<CatalogImportPage />} />
|
||||
+ <Route
|
||||
+ path="/catalog-import"
|
||||
+ element={
|
||||
+ <RequirePermission permission={catalogEntityCreatePermission}>
|
||||
+ <CatalogImportPage />
|
||||
+ </RequirePermission>
|
||||
+ }
|
||||
+ />
|
||||
...
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
/* highlight-add-start */
|
||||
import { RequirePermission } from '@backstage/plugin-permission-react';
|
||||
import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common';
|
||||
/* highlight-add-end */
|
||||
|
||||
const routes = (
|
||||
<FlatRoutes>
|
||||
{/* highlight-remove-next-line */}
|
||||
<Route path="/catalog-import" element={<CatalogImportPage />} />
|
||||
{/* highlight-add-start */}
|
||||
<Route
|
||||
path="/catalog-import"
|
||||
element={
|
||||
<RequirePermission permission={catalogEntityCreatePermission}>
|
||||
<CatalogImportPage />
|
||||
</RequirePermission>
|
||||
}
|
||||
/>
|
||||
{/* highlight-add-end */}
|
||||
{/* ... */}
|
||||
</FlatRoutes>
|
||||
);
|
||||
```
|
||||
|
||||
With this change, users who are denied the `catalogEntityCreatePermission` should now be unable to access the `/catalog-import` page.
|
||||
|
||||
@@ -46,64 +46,67 @@ The permissions framework uses a new `permission-backend` plugin to accept autho
|
||||
|
||||
1. Add `@backstage/plugin-permission-backend` as a dependency of your Backstage backend:
|
||||
|
||||
```bash
|
||||
# From your Backstage root directory
|
||||
$ yarn add --cwd packages/backend @backstage/plugin-permission-backend
|
||||
```
|
||||
```bash
|
||||
# From your Backstage root directory
|
||||
$ yarn add --cwd packages/backend @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 { createRouter } from '@backstage/plugin-permission-backend';
|
||||
import {
|
||||
AuthorizeResult,
|
||||
PolicyDecision,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
import { PermissionPolicy } from '@backstage/plugin-permission-node';
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
```typescript title="packages/backend/src/plugins/permission.ts"
|
||||
import { createRouter } from '@backstage/plugin-permission-backend';
|
||||
import {
|
||||
AuthorizeResult,
|
||||
PolicyDecision,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
import { PermissionPolicy } from '@backstage/plugin-permission-node';
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
|
||||
class TestPermissionPolicy implements PermissionPolicy {
|
||||
async handle(): Promise<PolicyDecision> {
|
||||
return { result: AuthorizeResult.ALLOW };
|
||||
}
|
||||
}
|
||||
class TestPermissionPolicy implements PermissionPolicy {
|
||||
async handle(): Promise<PolicyDecision> {
|
||||
return { result: AuthorizeResult.ALLOW };
|
||||
}
|
||||
}
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
return await createRouter({
|
||||
config: env.config,
|
||||
logger: env.logger,
|
||||
discovery: env.discovery,
|
||||
policy: new TestPermissionPolicy(),
|
||||
identity: env.identity,
|
||||
});
|
||||
}
|
||||
```
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
return await createRouter({
|
||||
config: env.config,
|
||||
logger: env.logger,
|
||||
discovery: env.discovery,
|
||||
policy: new TestPermissionPolicy(),
|
||||
identity: env.identity,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
3. Wire up the permission policy in `packages/backend/src/index.ts`. [The index in the example backend](https://github.com/backstage/backstage/blob/master/packages/backend/src/index.ts) shows how to do this. You’ll need to import the module from the previous step, create a plugin environment, and add the router to the express app:
|
||||
|
||||
```diff
|
||||
import proxy from './plugins/proxy';
|
||||
import techdocs from './plugins/techdocs';
|
||||
import search from './plugins/search';
|
||||
+ import permission from './plugins/permission';
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
import proxy from './plugins/proxy';
|
||||
import techdocs from './plugins/techdocs';
|
||||
import search from './plugins/search';
|
||||
/* highlight-add-next-line */
|
||||
import permission from './plugins/permission';
|
||||
|
||||
...
|
||||
async function main() {
|
||||
const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs'));
|
||||
const searchEnv = useHotMemoize(module, () => createEnv('search'));
|
||||
const appEnv = useHotMemoize(module, () => createEnv('app'));
|
||||
/* highlight-add-next-line */
|
||||
const permissionEnv = useHotMemoize(module, () => createEnv('permission'));
|
||||
// ..
|
||||
|
||||
const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs'));
|
||||
const searchEnv = useHotMemoize(module, () => createEnv('search'));
|
||||
const appEnv = useHotMemoize(module, () => createEnv('app'));
|
||||
+ const permissionEnv = useHotMemoize(module, () => createEnv('permission'));
|
||||
|
||||
...
|
||||
|
||||
apiRouter.use('/techdocs', await techdocs(techdocsEnv));
|
||||
apiRouter.use('/proxy', await proxy(proxyEnv));
|
||||
apiRouter.use('/search', await search(searchEnv));
|
||||
+ apiRouter.use('/permission', await permission(permissionEnv));
|
||||
```
|
||||
apiRouter.use('/techdocs', await techdocs(techdocsEnv));
|
||||
apiRouter.use('/proxy', await proxy(proxyEnv));
|
||||
apiRouter.use('/search', await search(searchEnv));
|
||||
/* highlight-add-next-line */
|
||||
apiRouter.use('/permission', await permission(permissionEnv));
|
||||
// ..
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Enable and test the permissions system
|
||||
|
||||
@@ -111,40 +114,46 @@ Now that the permission backend is running, it’s time to enable the permission
|
||||
|
||||
1. Set the property `permission.enabled` to `true` in `app-config.yaml`.
|
||||
|
||||
```yaml
|
||||
permission:
|
||||
enabled: true
|
||||
```
|
||||
```yaml title="app-config.yaml"
|
||||
permission:
|
||||
enabled: true
|
||||
```
|
||||
|
||||
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 { createRouter } from '@backstage/plugin-permission-backend';
|
||||
import {
|
||||
AuthorizeResult,
|
||||
PolicyDecision,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
- import { PermissionPolicy } from '@backstage/plugin-permission-node';
|
||||
+ import {
|
||||
+ PermissionPolicy,
|
||||
+ PolicyQuery,
|
||||
+ } from '@backstage/plugin-permission-node';
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
```ts title="packages/backend/src/plugins/permission.ts"
|
||||
import { createRouter } from '@backstage/plugin-permission-backend';
|
||||
import {
|
||||
AuthorizeResult,
|
||||
PolicyDecision,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
/* highlight-remove-next-line */
|
||||
import { PermissionPolicy } from '@backstage/plugin-permission-node';
|
||||
/* highlight-add-start */
|
||||
import {
|
||||
PermissionPolicy,
|
||||
PolicyQuery,
|
||||
} from '@backstage/plugin-permission-node';
|
||||
/* highlight-add-end */
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
|
||||
class TestPermissionPolicy implements PermissionPolicy {
|
||||
- async handle(): Promise<PolicyDecision> {
|
||||
+ async handle(request: PolicyQuery): Promise<PolicyDecision> {
|
||||
+ if (request.permission.name === 'catalog.entity.delete') {
|
||||
+ return {
|
||||
+ result: AuthorizeResult.DENY,
|
||||
+ };
|
||||
+ }
|
||||
+
|
||||
return { result: AuthorizeResult.ALLOW };
|
||||
class TestPermissionPolicy implements PermissionPolicy {
|
||||
/* highlight-remove-next-line */
|
||||
async handle(): Promise<PolicyDecision> {
|
||||
/* highlight-add-start */
|
||||
async handle(request: PolicyQuery): Promise<PolicyDecision> {
|
||||
if (request.permission.name === 'catalog.entity.delete') {
|
||||
return {
|
||||
result: AuthorizeResult.DENY,
|
||||
};
|
||||
}
|
||||
/* highlight-add-end */
|
||||
|
||||
return { result: AuthorizeResult.ALLOW };
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
3. Now that you’ve made this change, you should find that the unregister entity menu option on the catalog entity page is disabled.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ The source code is available here:
|
||||
- [todo-list-backend](https://github.com/backstage/backstage/blob/master/plugins/example-todo-list-backend)
|
||||
- [todo-list-common](https://github.com/backstage/backstage/blob/master/plugins/example-todo-list-common)
|
||||
|
||||
1. Copy-paste the three folders into the plugins folder of your backstage application repository (removing the `example-` prefix from each folder) or run the following script from the root of your backstage application:
|
||||
1. Copy-paste the three folders into the plugins folder of your backstage application repository (removing the `example-` prefix from each folder) or run the following script from the root of your backstage application:
|
||||
|
||||
```bash
|
||||
$ cd $(mktemp -d)
|
||||
@@ -37,7 +37,7 @@ The source code is available here:
|
||||
|
||||
**Important**: if you are on **Windows**, make sure you have WSL and git installed on your machine before executing the script above.
|
||||
|
||||
2. Add these packages as dependencies for your Backstage app:
|
||||
2. Add these packages as dependencies for your Backstage app:
|
||||
|
||||
```sh
|
||||
# From your Backstage root directory
|
||||
@@ -45,11 +45,11 @@ The source code is available here:
|
||||
$ yarn add --cwd packages/app @internal/plugin-todo-list
|
||||
```
|
||||
|
||||
3. Include the backend and frontend plugin in your application:
|
||||
3. Include the backend and frontend plugin in your application:
|
||||
|
||||
Create a new `packages/backend/src/plugins/todolist.ts` with the following content:
|
||||
|
||||
```typescript
|
||||
```typescript 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';
|
||||
@@ -71,40 +71,47 @@ The source code is available here:
|
||||
|
||||
Apply the following changes to `packages/backend/src/index.ts`:
|
||||
|
||||
```diff
|
||||
import techdocs from './plugins/techdocs';
|
||||
+ import todoList from './plugins/todolist';
|
||||
import search from './plugins/search';
|
||||
|
||||
...
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
import techdocs from './plugins/techdocs';
|
||||
/* highlight-add-next-line */
|
||||
import todoList from './plugins/todolist';
|
||||
import search from './plugins/search';
|
||||
|
||||
async function main() {
|
||||
const searchEnv = useHotMemoize(module, () => createEnv('search'));
|
||||
const appEnv = useHotMemoize(module, () => createEnv('app'));
|
||||
+ const todoListEnv = useHotMemoize(module, () => createEnv('todolist'));
|
||||
|
||||
...
|
||||
/* highlight-add-next-line */
|
||||
const todoListEnv = useHotMemoize(module, () => createEnv('todolist'));
|
||||
// ..
|
||||
|
||||
apiRouter.use('/proxy', await proxy(proxyEnv));
|
||||
apiRouter.use('/search', await search(searchEnv));
|
||||
apiRouter.use('/permission', await permission(permissionEnv));
|
||||
+ apiRouter.use('/todolist', await todoList(todoListEnv));
|
||||
/* highlight-add-next-line */
|
||||
apiRouter.use('/todolist', await todoList(todoListEnv));
|
||||
// Add backends ABOVE this line; this 404 handler is the catch-all fallback
|
||||
apiRouter.use(notFoundHandler());
|
||||
// ..
|
||||
}
|
||||
```
|
||||
|
||||
Apply the following changes to `packages/app/src/App.tsx`:
|
||||
|
||||
```diff
|
||||
+ import { TodoListPage } from '@internal/plugin-todo-list';
|
||||
|
||||
...
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
/* highlight-add-next-line */
|
||||
import { TodoListPage } from '@internal/plugin-todo-list';
|
||||
|
||||
const routes = (
|
||||
<FlatRoutes>
|
||||
<Route path="/search" element={<SearchPage />}>
|
||||
{searchPage}
|
||||
</Route>
|
||||
<Route path="/settings" element={<UserSettingsPage />} />
|
||||
+ <Route path="/todo-list" element={<TodoListPage />} />
|
||||
{/* highlight-add-next-line */}
|
||||
<Route path="/todo-list" element={<TodoListPage />} />
|
||||
{/* ... */}
|
||||
</FlatRoutes>
|
||||
);
|
||||
```
|
||||
|
||||
Now if you start your application you should be able to reach the `/todo-list` page:
|
||||
|
||||
@@ -14,19 +14,25 @@ We'll start by creating a new permission, and then we'll use the permission api
|
||||
|
||||
Let's navigate to the file `plugins/todo-list-common/src/permissions.ts` and add our first permission:
|
||||
|
||||
```diff
|
||||
import { createPermission } from '@backstage/plugin-permission-common';
|
||||
```ts title="plugins/todo-list-common/src/permissions.ts"
|
||||
import { createPermission } from '@backstage/plugin-permission-common';
|
||||
|
||||
- export const tempExamplePermission = createPermission({
|
||||
- name: 'temp.example.noop',
|
||||
- attributes: {},
|
||||
+ export const todoListCreatePermission = createPermission({
|
||||
+ name: 'todo.list.create',
|
||||
+ attributes: { action: 'create' },
|
||||
});
|
||||
/* 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 */
|
||||
});
|
||||
|
||||
- export const todoListPermissions = [tempExamplePermission];
|
||||
+ export const todoListPermissions = [todoListCreatePermission];
|
||||
/* 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`).
|
||||
@@ -44,81 +50,86 @@ $ yarn workspace @internal/plugin-todo-list-backend \
|
||||
|
||||
Edit `plugins/todo-list-backend/src/service/router.ts`:
|
||||
|
||||
```diff
|
||||
...
|
||||
```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 { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
/* highlight-add-end */
|
||||
|
||||
- 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';
|
||||
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;
|
||||
|
||||
export interface RouterOptions {
|
||||
logger: Logger;
|
||||
identity: IdentityApi;
|
||||
+ permissions: PermissionEvaluator;
|
||||
}
|
||||
router.post('/todos', async (req, res) => {
|
||||
let author: string | undefined = undefined;
|
||||
|
||||
export async function createRouter(
|
||||
options: RouterOptions,
|
||||
): Promise<express.Router> {
|
||||
- const { logger, identity } = options;
|
||||
+ const { logger, identity, permissions } = options;
|
||||
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 */
|
||||
|
||||
router.post('/todos', async (req, res) => {
|
||||
let author: string | undefined = undefined;
|
||||
if (!isTodoCreateRequest(req.body)) {
|
||||
throw new InputError('Invalid payload');
|
||||
}
|
||||
|
||||
const user = await identity.getIdentity({ request: req });
|
||||
author = user?.identity.userEntityRef;
|
||||
+ const token = getBearerTokenFromAuthorizationHeader(
|
||||
+ req.header('authorization'),
|
||||
+ );
|
||||
+ const decision = (
|
||||
+ await permissions.authorize([{ permission: todoListCreatePermission }], {
|
||||
+ token,
|
||||
+ })
|
||||
+ )[0];
|
||||
|
||||
+ if (decision.result === AuthorizeResult.DENY) {
|
||||
+ throw new NotAllowedError('Unauthorized');
|
||||
+ }
|
||||
|
||||
if (!isTodoCreateRequest(req.body)) {
|
||||
throw new InputError('Invalid payload');
|
||||
}
|
||||
|
||||
const todo = add({ title: req.body.title, author });
|
||||
res.json(todo);
|
||||
});
|
||||
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`:
|
||||
|
||||
```diff
|
||||
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="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({
|
||||
export default async function createPlugin({
|
||||
logger,
|
||||
discovery,
|
||||
/* highlight-add-next-line */
|
||||
permissions,
|
||||
}: PluginEnvironment): Promise<Router> {
|
||||
return await createRouter({
|
||||
logger,
|
||||
discovery,
|
||||
+ permissions,
|
||||
}: PluginEnvironment): Promise<Router> {
|
||||
return await createRouter({
|
||||
logger,
|
||||
identity: DefaultIdentityClient.create({
|
||||
discovery,
|
||||
issuer: await discovery.getExternalBaseUrl('auth'),
|
||||
}),
|
||||
+ permissions,
|
||||
});
|
||||
}
|
||||
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.
|
||||
@@ -129,200 +140,219 @@ 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:
|
||||
|
||||
```diff
|
||||
// packages/backend/src/plugins/permission.ts
|
||||
```ts title="packages/backend/src/plugins/permission.ts"
|
||||
/* highlight-add-start */
|
||||
import {
|
||||
BackstageIdentityResponse,
|
||||
} from '@backstage/plugin-auth-node';
|
||||
/* highlight-add-end */
|
||||
import {
|
||||
PermissionPolicy,
|
||||
/* highlight-add-next-line */
|
||||
PolicyQuery,
|
||||
} 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 {
|
||||
+ BackstageIdentityResponse,
|
||||
+ } from '@backstage/plugin-auth-node';
|
||||
import {
|
||||
PermissionPolicy,
|
||||
+ PolicyQuery,
|
||||
} from '@backstage/plugin-permission-node';
|
||||
+ import { isPermission } from '@backstage/plugin-permission-common';
|
||||
+ import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
|
||||
class TestPermissionPolicy implements PermissionPolicy {
|
||||
- async handle(): Promise<PolicyDecision> {
|
||||
+ async handle(
|
||||
+ request: PolicyQuery,
|
||||
+ _user?: BackstageIdentityResponse,
|
||||
+ ): Promise<PolicyDecision> {
|
||||
+ if (isPermission(request.permission, todoListCreatePermission)) {
|
||||
+ return {
|
||||
+ result: AuthorizeResult.DENY,
|
||||
+ };
|
||||
+ }
|
||||
+
|
||||
class TestPermissionPolicy implements PermissionPolicy {
|
||||
/* highlight-remove-next-line */
|
||||
async handle(): Promise<PolicyDecision> {
|
||||
/* highlight-add-start */
|
||||
async handle(
|
||||
request: PolicyQuery,
|
||||
_user?: BackstageIdentityResponse,
|
||||
): Promise<PolicyDecision> {
|
||||
if (isPermission(request.permission, todoListCreatePermission)) {
|
||||
return {
|
||||
result: AuthorizeResult.ALLOW,
|
||||
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.
|
||||
|
||||
```diff
|
||||
if (isPermission(request.permission, todoListCreatePermission)) {
|
||||
return {
|
||||
- result: AuthorizeResult.DENY,
|
||||
+ result: AuthorizeResult.ALLOW,
|
||||
};
|
||||
}
|
||||
```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`:
|
||||
|
||||
```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';
|
||||
```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';
|
||||
import { createRouter } from './router';
|
||||
|
||||
+ const mockedAuthorize: jest.MockedFunction<PermissionEvaluator['authorize']> =
|
||||
+ jest.fn();
|
||||
+ const mockedPermissionQuery: jest.MockedFunction<
|
||||
+ PermissionEvaluator['authorizeConditional']
|
||||
+ > = jest.fn();
|
||||
/* 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,
|
||||
+ };
|
||||
const permissionEvaluator: PermissionEvaluator = {
|
||||
authorize: mockedAuthorize,
|
||||
authorizeConditional: mockedPermissionQuery,
|
||||
};
|
||||
/* highlight-add-end */
|
||||
|
||||
describe('createRouter', () => {
|
||||
let app: express.Express;
|
||||
describe('createRouter', () => {
|
||||
let app: express.Express;
|
||||
|
||||
beforeAll(async () => {
|
||||
const router = await createRouter({
|
||||
logger: getVoidLogger(),
|
||||
identity: {} as DefaultIdentityClient,
|
||||
+ 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' });
|
||||
});
|
||||
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`, 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';
|
||||
```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 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' });
|
||||
}
|
||||
|
||||
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);
|
||||
+ 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,
|
||||
});
|
||||
return await service.start().catch(err => {
|
||||
logger.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
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';
|
||||
```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.
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export const exampleTodoListPlugin = createBackendPlugin({
|
||||
pluginId: '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,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
/**
|
||||
* The example TODO list backend plugin.
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
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.
|
||||
|
||||
@@ -10,24 +10,29 @@ When performing updates (or other operations) on specific [resources](../concept
|
||||
|
||||
Let's add a new permission to the file `plugins/todo-list-common/src/permissions.ts` from [the previous section](./02-adding-a-basic-permission-check.md).
|
||||
|
||||
```diff
|
||||
import { createPermission } from '@backstage/plugin-permission-common';
|
||||
```ts title="plugins/todo-list-common/src/permissions.ts"
|
||||
import { createPermission } from '@backstage/plugin-permission-common';
|
||||
|
||||
+ export const TODO_LIST_RESOURCE_TYPE = 'todo-item';
|
||||
+
|
||||
export const todoListCreatePermission = createPermission({
|
||||
name: 'todo.list.create',
|
||||
attributes: { action: 'create' },
|
||||
});
|
||||
+
|
||||
+ export const todoListUpdatePermission = createPermission({
|
||||
+ name: 'todo.list.update',
|
||||
+ attributes: { action: 'update' },
|
||||
+ resourceType: TODO_LIST_RESOURCE_TYPE,
|
||||
+ });
|
||||
/* highlight-add-next-line */
|
||||
export const TODO_LIST_RESOURCE_TYPE = 'todo-item';
|
||||
|
||||
- export const todoListPermissions = [todoListCreatePermission];
|
||||
+ export const todoListPermissions = [todoListCreatePermission, todoListUpdatePermission];
|
||||
export const todoListCreatePermission = createPermission({
|
||||
name: 'todo.list.create',
|
||||
attributes: { action: 'create' },
|
||||
});
|
||||
|
||||
/* highlight-add-start */
|
||||
export const todoListUpdatePermission = createPermission({
|
||||
name: 'todo.list.update',
|
||||
attributes: { action: 'update' },
|
||||
resourceType: TODO_LIST_RESOURCE_TYPE,
|
||||
});
|
||||
/* highlight-add-end */
|
||||
|
||||
/* highlight-remove-next-line */
|
||||
export const todoListPermissions = [todoListCreatePermission];
|
||||
/* highlight-add-next-line */
|
||||
export const todoListPermissions = [todoListCreatePermission, todoListUpdatePermission];
|
||||
```
|
||||
|
||||
Notice that unlike `todoListCreatePermission`, the `todoListUpdatePermission` permission contains a `resourceType` field. This field indicates to the permission framework that this permission is intended to be authorized in the context of a resource with type `'todo-item'`. You can use whatever string you like as the resource type, as long as you use the same value consistently for each type of resource.
|
||||
@@ -36,35 +41,39 @@ Notice that unlike `todoListCreatePermission`, the `todoListUpdatePermission` pe
|
||||
|
||||
To start, let's edit `plugins/todo-list-backend/src/service/router.ts` in the same manner as we did in the previous section:
|
||||
|
||||
```diff
|
||||
- import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
+ import { todoListCreatePermission, todoListUpdatePermission } from '@internal/plugin-todo-list-common';
|
||||
```ts title="plugins/todo-list-backend/src/service/router.ts"
|
||||
/* highlight-remove-next-line */
|
||||
import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
/* highlight-add-next-line */
|
||||
import { todoListCreatePermission, todoListUpdatePermission } from '@internal/plugin-todo-list-common';
|
||||
|
||||
...
|
||||
router.put('/todos', async (req, res) => {
|
||||
/* highlight-add-start */
|
||||
const token = getBearerTokenFromAuthorizationHeader(
|
||||
req.header('authorization'),
|
||||
);
|
||||
/* highlight-add-end */
|
||||
|
||||
router.put('/todos', async (req, res) => {
|
||||
+ const token = getBearerTokenFromAuthorizationHeader(
|
||||
+ req.header('authorization'),
|
||||
+ );
|
||||
if (!isTodoUpdateRequest(req.body)) {
|
||||
throw new InputError('Invalid payload');
|
||||
}
|
||||
/* highlight-add-start */
|
||||
const decision = (
|
||||
await permissions.authorize(
|
||||
[{ permission: todoListUpdatePermission, resourceRef: req.body.id }],
|
||||
{
|
||||
token,
|
||||
},
|
||||
)
|
||||
)[0];
|
||||
|
||||
if (!isTodoUpdateRequest(req.body)) {
|
||||
throw new InputError('Invalid payload');
|
||||
}
|
||||
+ const decision = (
|
||||
+ await permissions.authorize(
|
||||
+ [{ permission: todoListUpdatePermission, resourceRef: req.body.id }],
|
||||
+ {
|
||||
+ token,
|
||||
+ },
|
||||
+ )
|
||||
+ )[0];
|
||||
+
|
||||
+ if (decision.result !== AuthorizeResult.ALLOW) {
|
||||
+ throw new NotAllowedError('Unauthorized');
|
||||
+ }
|
||||
if (decision.result !== AuthorizeResult.ALLOW) {
|
||||
throw new NotAllowedError('Unauthorized');
|
||||
}
|
||||
/* highlight-add-end */
|
||||
|
||||
res.json(update(req.body));
|
||||
});
|
||||
res.json(update(req.body));
|
||||
});
|
||||
```
|
||||
|
||||
**Important:** Notice that we are passing an extra `resourceRef` field, with the `id` of the todo item as the value.
|
||||
@@ -75,13 +84,13 @@ This enables decisions based on characteristics of the resource, but it's import
|
||||
|
||||
Install the missing module:
|
||||
|
||||
```
|
||||
```bash
|
||||
$ 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:
|
||||
|
||||
```typescript
|
||||
```typescript title="plugins/todo-list-backend/src/service/rules.ts"
|
||||
import { makeCreatePermissionRule } from '@backstage/plugin-permission-node';
|
||||
import { TODO_LIST_RESOURCE_TYPE } from '@internal/plugin-todo-list-common';
|
||||
import { z } from 'zod';
|
||||
@@ -130,35 +139,43 @@ Now, let's create the new endpoint by editing `plugins/todo-list-backend/src/ser
|
||||
- `permissions`: the list of permissions that your plugin accepts.
|
||||
- `rules`: an array of all the permission rules you want to support in conditional decisions.
|
||||
|
||||
```diff
|
||||
...
|
||||
```ts title="plugins/todo-list-backend/src/service/router.ts"
|
||||
/* highlight-remove-next-line */
|
||||
import { add, getAll, update } from './todos';
|
||||
/* highlight-add-start */
|
||||
import { add, getAll, getTodo, update } from './todos';
|
||||
import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node';
|
||||
import { TODO_LIST_RESOURCE_TYPE, todoListPermissions } from '@internal/plugin-todo-list-common';
|
||||
import { rules } from './rules';
|
||||
/* highlight-add-end */
|
||||
|
||||
- import { add, getAll, update } from './todos';
|
||||
+ import { add, getAll, getTodo, update } from './todos';
|
||||
+ import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node';
|
||||
+ import { TODO_LIST_RESOURCE_TYPE, todoListPermissions } from '@internal/plugin-todo-list-common';
|
||||
+ import { rules } from './rules';
|
||||
export async function createRouter(
|
||||
options: RouterOptions,
|
||||
): Promise<express.Router> {
|
||||
const { logger, identity, permissions } = options;
|
||||
|
||||
export async function createRouter(
|
||||
options: RouterOptions,
|
||||
): Promise<express.Router> {
|
||||
const { logger, identity, permissions } = options;
|
||||
/* highlight-add-start */
|
||||
const permissionIntegrationRouter = createPermissionIntegrationRouter({
|
||||
getResources: async resourceRefs => {
|
||||
return resourceRefs.map(getTodo);
|
||||
},
|
||||
resourceType: TODO_LIST_RESOURCE_TYPE,
|
||||
permissions: todoListPermissions,
|
||||
rules: Object.values(rules),
|
||||
});
|
||||
/* highlight-add-end */
|
||||
|
||||
+ const permissionIntegrationRouter = createPermissionIntegrationRouter({
|
||||
+ getResources: async resourceRefs => {
|
||||
+ return resourceRefs.map(getTodo);
|
||||
+ },
|
||||
+ resourceType: TODO_LIST_RESOURCE_TYPE,
|
||||
+ permissions: todoListPermissions,
|
||||
+ rules: Object.values(rules),
|
||||
+ });
|
||||
const router = Router();
|
||||
router.use(express.json());
|
||||
|
||||
const router = Router();
|
||||
router.use(express.json());
|
||||
/* highlight-add-next-line */
|
||||
router.use(permissionIntegrationRouter);
|
||||
|
||||
+ router.use(permissionIntegrationRouter);
|
||||
|
||||
router.post('/todos', async (req, res) => {
|
||||
router.post('/todos', async (req, res) => {
|
||||
// ..
|
||||
}
|
||||
// ..
|
||||
}
|
||||
```
|
||||
|
||||
## Provide utilities for policy authors
|
||||
@@ -167,7 +184,7 @@ Now that we have a new resource type and a corresponding rule, we need to export
|
||||
|
||||
Create a new `plugins/todo-list-backend/src/conditionExports.ts` file and add the following code:
|
||||
|
||||
```typescript
|
||||
```typescript title="plugins/todo-list-backend/src/conditionExports.ts"
|
||||
import { TODO_LIST_RESOURCE_TYPE } from '@internal/plugin-todo-list-common';
|
||||
import { createConditionExports } from '@backstage/plugin-permission-node';
|
||||
import { rules } from './service/rules';
|
||||
@@ -185,63 +202,68 @@ export const createTodoListConditionalDecision = createConditionalDecision;
|
||||
|
||||
Make sure `todoListConditions` and `createTodoListConditionalDecision` are exported from the `todo-list-backend` package by editing `plugins/todo-list-backend/src/index.ts`:
|
||||
|
||||
```diff
|
||||
export * from './service/router';
|
||||
+ export * from './conditionExports';
|
||||
export { exampleTodoListPlugin } from './plugin';
|
||||
```ts title="plugins/todo-list-backend/src/index.ts"
|
||||
export * from './service/router';
|
||||
/* highlight-add-next-line */
|
||||
export * from './conditionExports';
|
||||
export { exampleTodoListPlugin } from './plugin';
|
||||
```
|
||||
|
||||
## Test the authorized update endpoint
|
||||
|
||||
Let's go back to the permission policy's handle function and try to authorize our new permission with an `isOwner` condition.
|
||||
|
||||
```diff
|
||||
// packages/backend/src/plugins/permission.ts
|
||||
```ts title="packages/backend/src/plugins/permission.ts"
|
||||
import {
|
||||
BackstageIdentityResponse,
|
||||
IdentityClient
|
||||
} from '@backstage/plugin-auth-node';
|
||||
import {
|
||||
PermissionPolicy,
|
||||
PolicyQuery,
|
||||
} from '@backstage/plugin-permission-node';
|
||||
import { isPermission } from '@backstage/plugin-permission-common';
|
||||
/* highlight-remove-next-line */
|
||||
import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
/* highlight-add-start */
|
||||
import {
|
||||
todoListCreatePermission,
|
||||
todoListUpdatePermission,
|
||||
} from '@internal/plugin-todo-list-common';
|
||||
import {
|
||||
todoListConditions,
|
||||
createTodoListConditionalDecision,
|
||||
} from '@internal/plugin-todo-list-backend';
|
||||
/* highlight-add-end */
|
||||
|
||||
import {
|
||||
BackstageIdentityResponse,
|
||||
IdentityClient
|
||||
} from '@backstage/plugin-auth-node';
|
||||
import {
|
||||
PermissionPolicy,
|
||||
PolicyQuery,
|
||||
} from '@backstage/plugin-permission-node';
|
||||
import { isPermission } from '@backstage/plugin-permission-common';
|
||||
- import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
+ import {
|
||||
+ todoListCreatePermission,
|
||||
+ todoListUpdatePermission,
|
||||
+ } from '@internal/plugin-todo-list-common';
|
||||
+ import {
|
||||
+ todoListConditions,
|
||||
+ createTodoListConditionalDecision,
|
||||
+ } 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,
|
||||
};
|
||||
}
|
||||
|
||||
+ if (isPermission(request.permission, todoListUpdatePermission)) {
|
||||
+ return createTodoListConditionalDecision(
|
||||
+ request.permission,
|
||||
+ todoListConditions.isOwner({
|
||||
+ userId: user?.identity.userEntityRef ?? '',
|
||||
+ }),
|
||||
+ );
|
||||
+ }
|
||||
+
|
||||
async handle(
|
||||
request: PolicyQuery,
|
||||
/* highlight-remove-next-line */
|
||||
_user?: BackstageIdentityResponse,
|
||||
/* highlight-add-next-line */
|
||||
user?: BackstageIdentityResponse,
|
||||
): Promise<PolicyDecision> {
|
||||
if (isPermission(request.permission, todoListCreatePermission)) {
|
||||
return {
|
||||
result: AuthorizeResult.ALLOW,
|
||||
};
|
||||
}
|
||||
/* highlight-add-start */
|
||||
if (isPermission(request.permission, todoListUpdatePermission)) {
|
||||
return createTodoListConditionalDecision(
|
||||
request.permission,
|
||||
todoListConditions.isOwner({
|
||||
userId: user?.identity.userEntityRef ?? '',
|
||||
}),
|
||||
);
|
||||
}
|
||||
/* highlight-add-end */
|
||||
|
||||
return {
|
||||
result: AuthorizeResult.ALLOW,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
For any incoming update requests, we now return a _Conditional Decision_. We are saying:
|
||||
|
||||
@@ -8,21 +8,25 @@ Authorizing `GET /todos` is similar to the update endpoint, in that it should be
|
||||
|
||||
One possible solution may leverage the batching functionality to authorize all of the todos, and then returning only the ones for which the decision was `ALLOW`:
|
||||
|
||||
```diff
|
||||
router.get('/todos', async (req, res) => {
|
||||
+ const token = IdentityClient.getBearerToken(req.header('authorization'));
|
||||
```ts
|
||||
router.get('/todos', async (req, res) => {
|
||||
/* highlight-add-next-line */
|
||||
const token = IdentityClient.getBearerToken(req.header('authorization'));
|
||||
|
||||
- res.json(getAll())
|
||||
+ const items = getAll();
|
||||
+ const decisions = await permissions.authorize(
|
||||
+ items.map(({ id }) => ({ permission: todoListReadPermission, resourceRef: id })),
|
||||
+ );
|
||||
/* highlight-remove-next-line */
|
||||
res.json(getAll())
|
||||
/* highlight-add-start */
|
||||
const items = getAll();
|
||||
const decisions = await permissions.authorize(
|
||||
items.map(({ id }) => ({ permission: todoListReadPermission, resourceRef: id })),
|
||||
);
|
||||
|
||||
+ const filteredItems = decisions.filter(
|
||||
+ decision => decision.result === AuthorizeResult.ALLOW,
|
||||
+ );
|
||||
+ res.json(filteredItems);
|
||||
});
|
||||
const filteredItems = decisions.filter(
|
||||
decision => decision.result === AuthorizeResult.ALLOW,
|
||||
);
|
||||
res.json(filteredItems);
|
||||
/* highlight-add-end */
|
||||
});
|
||||
```
|
||||
|
||||
This approach will work for simple cases, but it has a downside: it forces us to retrieve all the elements upfront and authorize them one by one. This forces the plugin implementation to handle concerns like pagination, which is currently handled by the data source.
|
||||
@@ -35,82 +39,93 @@ To avoid this situation, the permissions framework has support for filtering ite
|
||||
|
||||
Let's add another permission to the plugin.
|
||||
|
||||
```diff
|
||||
// plugins/todo-list-backend/src/service/permissions.ts
|
||||
```ts title="plugins/todo-list-backend/src/service/permissions.ts"
|
||||
import { createPermission } from '@backstage/plugin-permission-common';
|
||||
|
||||
import { createPermission } from '@backstage/plugin-permission-common';
|
||||
export const TODO_LIST_RESOURCE_TYPE = 'todo-item';
|
||||
|
||||
export const TODO_LIST_RESOURCE_TYPE = 'todo-item';
|
||||
export const todoListCreatePermission = createPermission({
|
||||
name: 'todo.list.create',
|
||||
attributes: { action: 'create' },
|
||||
});
|
||||
|
||||
export const todoListCreatePermission = createPermission({
|
||||
name: 'todo.list.create',
|
||||
attributes: { action: 'create' },
|
||||
});
|
||||
export const todoListUpdatePermission = createPermission({
|
||||
name: 'todo.list.update',
|
||||
attributes: { action: 'update' },
|
||||
resourceType: TODO_LIST_RESOURCE_TYPE,
|
||||
});
|
||||
|
||||
export const todoListUpdatePermission = createPermission({
|
||||
name: 'todo.list.update',
|
||||
attributes: { action: 'update' },
|
||||
resourceType: TODO_LIST_RESOURCE_TYPE,
|
||||
});
|
||||
+
|
||||
+ export const todoListReadPermission = createPermission({
|
||||
+ name: 'todos.list.read',
|
||||
+ attributes: { action: 'read' },
|
||||
+ resourceType: TODO_LIST_RESOURCE_TYPE,
|
||||
+ });
|
||||
/* highlight-add-start */
|
||||
export const todoListReadPermission = createPermission({
|
||||
name: 'todos.list.read',
|
||||
attributes: { action: 'read' },
|
||||
resourceType: TODO_LIST_RESOURCE_TYPE,
|
||||
});
|
||||
/* highlight-add-end */
|
||||
|
||||
- export const todoListPermissions = [todoListCreatePermission, todoListUpdatePermission];
|
||||
+ export const todoListPermissions = [todoListCreatePermission, todoListUpdatePermission, todoListReadPermission];
|
||||
/* highlight-add-next-line */
|
||||
export const todoListPermissions = [todoListCreatePermission, todoListUpdatePermission];
|
||||
/* highlight-add-next-line */
|
||||
export const todoListPermissions = [todoListCreatePermission, todoListUpdatePermission, todoListReadPermission];
|
||||
```
|
||||
|
||||
## Using conditional policy decisions
|
||||
|
||||
So far we've only used the `PermissionEvaluator.authorize` method, which will evaluate conditional decisions before returning a result. In this step, we want to evaluate conditional decisions within our plugin, so we'll use `PermissionEvaluator.authorizeConditional` instead.
|
||||
|
||||
```diff
|
||||
// plugins/todo-list-backend/src/service/router.ts
|
||||
```ts title="plugins/todo-list-backend/src/service/router.ts"
|
||||
/* highlight-remove-next-line */
|
||||
import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node';
|
||||
/* highlight-add-start */
|
||||
import {
|
||||
createPermissionIntegrationRouter,
|
||||
createConditionTransformer,
|
||||
ConditionTransformer,
|
||||
} from '@backstage/plugin-permission-node';
|
||||
/* highlight-add-end */
|
||||
/* highlight-remove-next-line */
|
||||
import { add, getAll, getTodo, update } from './todos';
|
||||
/* highlight-add-next-line */
|
||||
import { add, getAll, getTodo, TodoFilter, update } from './todos';
|
||||
import {
|
||||
todosListCreate,
|
||||
todosListUpdate,
|
||||
/* highlight-add-next-line */
|
||||
todoListReadPermission,
|
||||
TODO_LIST_RESOURCE_TYPE,
|
||||
} from './permissions';
|
||||
|
||||
- import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node';
|
||||
+ import {
|
||||
+ createPermissionIntegrationRouter,
|
||||
+ createConditionTransformer,
|
||||
+ ConditionTransformer,
|
||||
+ } from '@backstage/plugin-permission-node';
|
||||
- import { add, getAll, getTodo, update } from './todos';
|
||||
+ import { add, getAll, getTodo, TodoFilter, update } from './todos';
|
||||
import {
|
||||
todosListCreate,
|
||||
todosListUpdate,
|
||||
+ todoListReadPermission,
|
||||
TODO_LIST_RESOURCE_TYPE,
|
||||
} from './permissions';
|
||||
/* highlight-add-next-line */
|
||||
const transformConditions: ConditionTransformer<TodoFilter> = createConditionTransformer(Object.values(rules));
|
||||
|
||||
+ const transformConditions: ConditionTransformer<TodoFilter> = createConditionTransformer(Object.values(rules));
|
||||
/* highlight-remove-next-line */
|
||||
router.get('/todos', async (_req, res) => {
|
||||
/* highlight-add-start */
|
||||
router.get('/todos', async (req, res) => {
|
||||
const token = getBearerTokenFromAuthorizationHeader(
|
||||
req.header('authorization'),
|
||||
);
|
||||
|
||||
- router.get('/todos', async (_req, res) => {
|
||||
+ router.get('/todos', async (req, res) => {
|
||||
+ const token = getBearerTokenFromAuthorizationHeader(
|
||||
+ req.header('authorization'),
|
||||
+ );
|
||||
+
|
||||
+ const decision = (
|
||||
+ await permissions.authorizeConditional([{ permission: todoListReadPermission }], {
|
||||
+ token,
|
||||
+ })
|
||||
+ )[0];
|
||||
+
|
||||
+ if (decision.result === AuthorizeResult.DENY) {
|
||||
+ throw new NotAllowedError('Unauthorized');
|
||||
+ }
|
||||
+
|
||||
+ if (decision.result === AuthorizeResult.CONDITIONAL) {
|
||||
+ const filter = transformConditions(decision.conditions);
|
||||
+ res.json(getAll(filter));
|
||||
+ } else {
|
||||
+ res.json(getAll());
|
||||
+ }
|
||||
- res.json(getAll());
|
||||
});
|
||||
const decision = (
|
||||
await permissions.authorizeConditional([{ permission: todoListReadPermission }], {
|
||||
token,
|
||||
})
|
||||
)[0];
|
||||
|
||||
if (decision.result === AuthorizeResult.DENY) {
|
||||
throw new NotAllowedError('Unauthorized');
|
||||
}
|
||||
|
||||
if (decision.result === AuthorizeResult.CONDITIONAL) {
|
||||
const filter = transformConditions(decision.conditions);
|
||||
res.json(getAll(filter));
|
||||
} else {
|
||||
res.json(getAll());
|
||||
}
|
||||
/* highlight-add-end */
|
||||
/* highlight-remove-next-line */
|
||||
res.json(getAll());
|
||||
});
|
||||
```
|
||||
|
||||
To make the process of handling conditional decisions easier, the permission framework provides a `createConditionTransformer` helper. This function accepts an array of permission rules, and returns a transformer function which converts the conditions to the format needed by the plugin using the `toQuery` method defined on each rule.
|
||||
@@ -121,31 +136,29 @@ Since `TodoFilter` used in our plugin matches the structure of the conditions ob
|
||||
|
||||
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
|
||||
|
||||
...
|
||||
|
||||
```ts title="packages/backend/src/plugins/permission.ts"
|
||||
import {
|
||||
todoListCreatePermission,
|
||||
todoListUpdatePermission,
|
||||
+ todoListReadPermission,
|
||||
/* highlight-add-next-line */
|
||||
todoListReadPermission,
|
||||
} from '@internal/plugin-todo-list-common';
|
||||
|
||||
...
|
||||
|
||||
- if (isPermission(request.permission, todoListUpdatePermission)) {
|
||||
+ if (
|
||||
+ isPermission(request.permission, todoListUpdatePermission) ||
|
||||
+ isPermission(request.permission, todoListReadPermission)
|
||||
+ ) {
|
||||
return createTodoListConditionalDecision(
|
||||
request.permission,
|
||||
todoListConditions.isOwner({
|
||||
userId: user?.identity.userEntityRef
|
||||
}),
|
||||
);
|
||||
}
|
||||
/* highlight-remove-next-line */
|
||||
if (isPermission(request.permission, todoListUpdatePermission)) {
|
||||
/* highlight-add-start */
|
||||
if (
|
||||
isPermission(request.permission, todoListUpdatePermission) ||
|
||||
isPermission(request.permission, todoListReadPermission)
|
||||
) {
|
||||
/* highlight-add-end */
|
||||
return createTodoListConditionalDecision(
|
||||
request.permission,
|
||||
todoListConditions.isOwner({
|
||||
userId: user?.identity.userEntityRef
|
||||
}),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Once the changes to the permission policy are saved, the UI should show only the todo items you've created.
|
||||
|
||||
@@ -14,81 +14,79 @@ Take, for example, the "Add" button in our todo list application. When a user cl
|
||||
|
||||
Let's start by adding the packages we will need:
|
||||
|
||||
```
|
||||
```bash
|
||||
$ yarn workspace @internal/plugin-todo-list \
|
||||
add @backstage/plugin-permission-react @internal/plugin-todo-list-common
|
||||
```
|
||||
|
||||
Let's make the following changes in `plugins/todo-list/src/components/TodoListPage/TodoListPage.tsx`:
|
||||
|
||||
```diff
|
||||
...
|
||||
```tsx title="plugins/todo-list/src/components/TodoListPage/TodoListPage.tsx"
|
||||
import {
|
||||
alertApiRef,
|
||||
discoveryApiRef,
|
||||
fetchApiRef,
|
||||
useApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
/* highlight-add-start */
|
||||
import { usePermission } from '@backstage/plugin-permission-react';
|
||||
import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
/* highlight-add-end */
|
||||
|
||||
import {
|
||||
alertApiRef,
|
||||
discoveryApiRef,
|
||||
fetchApiRef,
|
||||
useApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
+ import { usePermission } from '@backstage/plugin-permission-react';
|
||||
+ import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
function AddTodo({ onAdd }: { onAdd: (title: string) => any }) {
|
||||
const title = useRef('');
|
||||
/* highlight-add-next-line */
|
||||
const { loading: loadingPermission, allowed: canAddTodo } = usePermission({ permission: todoListCreatePermission });
|
||||
|
||||
...
|
||||
|
||||
function AddTodo({ onAdd }: { onAdd: (title: string) => any }) {
|
||||
const title = useRef('');
|
||||
+ const { loading: loadingPermission, allowed: canAddTodo } = usePermission({ permission: todoListCreatePermission });
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="body1">Add todo</Typography>
|
||||
<Box
|
||||
component="span"
|
||||
alignItems="flex-end"
|
||||
display="flex"
|
||||
flexDirection="row"
|
||||
>
|
||||
<TextField
|
||||
placeholder="Write something here..."
|
||||
onChange={e => (title.current = e.target.value)}
|
||||
/>
|
||||
- <Button variant="contained" onClick={handleAdd}>
|
||||
- Add
|
||||
- </Button>
|
||||
+ {!loadingPermission && (
|
||||
+ <Button
|
||||
+ disabled={!canAddTodo}
|
||||
+ variant="contained"
|
||||
+ onClick={() => onAdd(title.current)}
|
||||
+ >
|
||||
+ Add
|
||||
+ </Button>
|
||||
+ )}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
...
|
||||
return (
|
||||
<>
|
||||
<Typography variant="body1">Add todo</Typography>
|
||||
<Box
|
||||
component="span"
|
||||
alignItems="flex-end"
|
||||
display="flex"
|
||||
flexDirection="row"
|
||||
>
|
||||
<TextField
|
||||
placeholder="Write something here..."
|
||||
onChange={e => (title.current = e.target.value)}
|
||||
/>
|
||||
{/* highlight-remove-start */}
|
||||
<Button variant="contained" onClick={handleAdd}>
|
||||
Add
|
||||
</Button>
|
||||
{/* highlight-remove-end */}
|
||||
{/* highlight-add-start */}
|
||||
{!loadingPermission && (
|
||||
<Button
|
||||
disabled={!canAddTodo}
|
||||
variant="contained"
|
||||
onClick={() => onAdd(title.current)}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
)}
|
||||
{/* highlight-add-end */}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Here we are using the [`usePermission` hook](https://backstage.io/docs/reference/plugin-permission-react.usepermission) to communicate with the permission policy and receive a decision on whether this user is authorized to create a todo list item.
|
||||
|
||||
It's really that simple! Let's change our policy to test the disabled button:
|
||||
|
||||
```diff
|
||||
// packages/backend/src/plugins/permission.ts
|
||||
```ts title="packages/backend/src/plugins/permission.ts"
|
||||
|
||||
...
|
||||
|
||||
if (isPermission(request.permission, todoListCreatePermission)) {
|
||||
return {
|
||||
- result: AuthorizeResult.ALLOW,
|
||||
+ result: AuthorizeResult.DENY,
|
||||
};
|
||||
}
|
||||
|
||||
...
|
||||
if (isPermission(request.permission, todoListCreatePermission)) {
|
||||
return {
|
||||
/* highlight-remove-next-line */
|
||||
result: AuthorizeResult.ALLOW,
|
||||
/* highlight-add-next-line */
|
||||
result: AuthorizeResult.DENY,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
And now you should see that you are not able to create a todo item from the frontend!
|
||||
@@ -97,104 +95,111 @@ And now you should see that you are not able to create a todo item from the fron
|
||||
|
||||
Providing a disabled state can be a helpful signal to users, but there may be cases where hiding the element is preferred. For such cases, you can use the provided [`RequirePermission` component](https://backstage.io/docs/reference/plugin-permission-react.requirepermission):
|
||||
|
||||
```diff
|
||||
// plugins/todo-list/src/components/TodoListPage/TodoListPage.tsx
|
||||
```tsx title="plugins/todo-list/src/components/TodoListPage/TodoListPage.tsx"
|
||||
import {
|
||||
alertApiRef,
|
||||
discoveryApiRef,
|
||||
fetchApiRef,
|
||||
useApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
/* highlight-remove-next-line */
|
||||
import { usePermission } from '@backstage/plugin-permission-react';
|
||||
/* highlight-add-next-line */
|
||||
import { RequirePermission } from '@backstage/plugin-permission-react';
|
||||
import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
|
||||
...
|
||||
export const TodoListPage = () => {
|
||||
// ..
|
||||
<Grid container spacing={3} direction="column">
|
||||
{/* highlight-remove-start */}
|
||||
<Grid item>
|
||||
<AddTodo onAdd={handleAdd} />
|
||||
</Grid>
|
||||
{/* highlight-remove-end */}
|
||||
{/* highlight-add-start */}
|
||||
<RequirePermission
|
||||
permission={todoListCreatePermission}
|
||||
errorPage={<></>}
|
||||
>
|
||||
<Grid item>
|
||||
<AddTodo onAdd={handleAdd} />
|
||||
</Grid>
|
||||
</RequirePermission>
|
||||
{/* highlight-add-end */}
|
||||
<Grid item>
|
||||
<TodoList key={key} onEdit={setEdit} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
}
|
||||
|
||||
import {
|
||||
alertApiRef,
|
||||
discoveryApiRef,
|
||||
fetchApiRef,
|
||||
useApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
- import { usePermission } from '@backstage/plugin-permission-react';
|
||||
+ import { RequirePermission } from '@backstage/plugin-permission-react';
|
||||
import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
function AddTodo({ onAdd }: { onAdd: (title: string) => any }) {
|
||||
const title = useRef('');
|
||||
/* highlight-remove-next-line */
|
||||
const { loading: loadingPermission, allowed: canAddTodo } = usePermission({ permission: todoListCreatePermission });
|
||||
|
||||
...
|
||||
|
||||
export const TodoListPage = () => {
|
||||
|
||||
...
|
||||
|
||||
<Grid container spacing={3} direction="column">
|
||||
- <Grid item>
|
||||
- <AddTodo onAdd={handleAdd} />
|
||||
- </Grid>
|
||||
+ <RequirePermission
|
||||
+ permission={todoListCreatePermission}
|
||||
+ errorPage={<></>}
|
||||
+ >
|
||||
+ <Grid item>
|
||||
+ <AddTodo onAdd={handleAdd} />
|
||||
+ </Grid>
|
||||
+ </RequirePermission>
|
||||
<Grid item>
|
||||
<TodoList key={key} onEdit={setEdit} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
...
|
||||
|
||||
|
||||
function AddTodo({ onAdd }: { onAdd: (title: string) => any }) {
|
||||
const title = useRef('');
|
||||
- const { loading: loadingPermission, allowed: canAddTodo } = usePermission({ permission: todoListCreatePermission });
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="body1">Add todo</Typography>
|
||||
<Box
|
||||
component="span"
|
||||
alignItems="flex-end"
|
||||
display="flex"
|
||||
flexDirection="row"
|
||||
>
|
||||
<TextField
|
||||
placeholder="Write something here..."
|
||||
onChange={e => (title.current = e.target.value)}
|
||||
/>
|
||||
- {!loadingPermission && (
|
||||
- <Button
|
||||
- disabled={!canAddTodo}
|
||||
- variant="contained"
|
||||
- onClick={() => onAdd(title.current)}
|
||||
- >
|
||||
- Add
|
||||
- </Button>
|
||||
- )}
|
||||
+ <Button variant="contained" onClick={() => onAdd(title.current)}>
|
||||
+ Add
|
||||
+ </Button>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
...
|
||||
return (
|
||||
<>
|
||||
<Typography variant="body1">Add todo</Typography>
|
||||
<Box
|
||||
component="span"
|
||||
alignItems="flex-end"
|
||||
display="flex"
|
||||
flexDirection="row"
|
||||
>
|
||||
<TextField
|
||||
placeholder="Write something here..."
|
||||
onChange={e => (title.current = e.target.value)}
|
||||
/>
|
||||
{/* highlight-remove-start */}
|
||||
{!loadingPermission && (
|
||||
<Button
|
||||
disabled={!canAddTodo}
|
||||
variant="contained"
|
||||
onClick={() => onAdd(title.current)}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
)}
|
||||
{/* highlight-remove-end */}
|
||||
{/* highlight-add-start */}
|
||||
<Button variant="contained" onClick={() => onAdd(title.current)}>
|
||||
Add
|
||||
</Button>
|
||||
{/* highlight-add-end */}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
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';
|
||||
|
||||
...
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
/* highlight-add-start */
|
||||
import { RequirePermission } from '@backstage/plugin-permission-react';
|
||||
import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
/* highlight-add-end */
|
||||
|
||||
const routes = (
|
||||
<FlatRoutes>
|
||||
<Route path="/search" element={<SearchPage />}>
|
||||
{searchPage}
|
||||
</Route>
|
||||
<Route path="/settings" element={<UserSettingsPage />} />
|
||||
+ <Route path="/todo-list" element={
|
||||
// You might want to create a "read" permission for this, we are just using this one as an example
|
||||
+ <RequirePermission permission={todoListCreatePermission}>
|
||||
+ <TodoListPage />
|
||||
+ </RequirePermission>
|
||||
{/* highlight-add-next-line */}
|
||||
<Route path="/todo-list" element={
|
||||
{/* You might want to create a "read" permission for this, we are just using this one as an example */}
|
||||
{/* highlight-add-start */}
|
||||
<RequirePermission permission={todoListCreatePermission}>
|
||||
<TodoListPage />
|
||||
</RequirePermission>
|
||||
{/* highlight-add-end */}}
|
||||
{/* ... */}
|
||||
</Route>
|
||||
</FlatRoutes>
|
||||
);
|
||||
```
|
||||
|
||||
Now if you try to navigate to `https://localhost:3000/todo-list` you'll get and error page if you do not have permission.
|
||||
|
||||
@@ -8,9 +8,7 @@ In the [previous section](./getting-started.md), we were able to set up the perm
|
||||
|
||||
That policy looked like this:
|
||||
|
||||
```typescript
|
||||
// packages/backend/src/plugins/permission.ts
|
||||
|
||||
```typescript title="packages/backend/src/plugins/permission.ts"
|
||||
class TestPermissionPolicy implements PermissionPolicy {
|
||||
async handle(request: PolicyQuery): Promise<PolicyDecision> {
|
||||
if (request.permission.name === 'catalog.entity.delete') {
|
||||
@@ -36,49 +34,61 @@ As we confirmed in the previous section, we know that this now prevents us from
|
||||
|
||||
Let's change the policy to the following:
|
||||
|
||||
```diff
|
||||
- import { IdentityClient } from '@backstage/plugin-auth-node';
|
||||
+ import {
|
||||
+ BackstageIdentityResponse,
|
||||
+ IdentityClient
|
||||
+ } from '@backstage/plugin-auth-node';
|
||||
import {
|
||||
```ts
|
||||
/* highlight-remove-next-line */
|
||||
import { IdentityClient } from '@backstage/plugin-auth-node';
|
||||
/* highlight-add-start */
|
||||
import {
|
||||
BackstageIdentityResponse,
|
||||
IdentityClient
|
||||
} from '@backstage/plugin-auth-node';
|
||||
/* highlight-add-end */
|
||||
import {
|
||||
AuthorizeResult,
|
||||
PolicyDecision,
|
||||
+ isPermission,
|
||||
/* highlight-add-next-line */
|
||||
isPermission,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
+ import {
|
||||
+ catalogConditions,
|
||||
+ createCatalogConditionalDecision,
|
||||
+ } from '@backstage/plugin-catalog-backend/alpha';
|
||||
+ import {
|
||||
+ catalogEntityDeletePermission,
|
||||
+ } from '@backstage/plugin-catalog-common/alpha';
|
||||
/* highlight-add-start */
|
||||
import {
|
||||
catalogConditions,
|
||||
createCatalogConditionalDecision,
|
||||
} from '@backstage/plugin-catalog-backend/alpha';
|
||||
import {
|
||||
catalogEntityDeletePermission,
|
||||
} from '@backstage/plugin-catalog-common/alpha';
|
||||
/* highlight-add-end */
|
||||
|
||||
...
|
||||
|
||||
class TestPermissionPolicy implements PermissionPolicy {
|
||||
- async handle(request: PolicyQuery): Promise<PolicyDecision> {
|
||||
+ async handle(
|
||||
+ request: PolicyQuery,
|
||||
+ user?: BackstageIdentityResponse,
|
||||
+ ): Promise<PolicyDecision> {
|
||||
- if (request.permission.name === 'catalog.entity.delete') {
|
||||
+ if (isPermission(request.permission, catalogEntityDeletePermission)) {
|
||||
- return {
|
||||
- result: AuthorizeResult.DENY,
|
||||
- };
|
||||
+ return createCatalogConditionalDecision(
|
||||
+ request.permission,
|
||||
+ catalogConditions.isEntityOwner({
|
||||
+ claims: user?.identity.ownershipEntityRefs ?? [],
|
||||
+ }),
|
||||
+ );
|
||||
}
|
||||
|
||||
return { result: AuthorizeResult.ALLOW };
|
||||
class TestPermissionPolicy implements PermissionPolicy {
|
||||
/* highlight-remove-next-line */
|
||||
async handle(request: PolicyQuery): Promise<PolicyDecision> {
|
||||
/* highlight-add-start */
|
||||
async handle(
|
||||
request: PolicyQuery,
|
||||
user?: BackstageIdentityResponse,
|
||||
): Promise<PolicyDecision> {
|
||||
/* highlight-add-end */
|
||||
/* highlight-remove-next-line */
|
||||
if (request.permission.name === 'catalog.entity.delete') {
|
||||
/* highlight-add-next-line */
|
||||
if (isPermission(request.permission, catalogEntityDeletePermission)) {
|
||||
/* highlight-remove-start */
|
||||
return {
|
||||
result: AuthorizeResult.DENY,
|
||||
};
|
||||
/* highlight-remove-end */
|
||||
/* highlight-add-start */
|
||||
return createCatalogConditionalDecision(
|
||||
request.permission,
|
||||
catalogConditions.isEntityOwner({
|
||||
claims: user?.identity.ownershipEntityRefs ?? [],
|
||||
}),
|
||||
);
|
||||
/* highlight-add-end */
|
||||
}
|
||||
return { result: AuthorizeResult.ALLOW };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Let's walk through the new code that we just added.
|
||||
@@ -93,30 +103,34 @@ You should now be able to see in your Backstage app that the unregister entity b
|
||||
|
||||
Now let's say we want to prevent all actions on catalog entities unless performed by the owner. One way to achieve this may be to simply update the `if` statement and check for each permission. If you choose to write your policy this way, it will certainly work! However, it may be difficult to maintain as the policy grows, and it may not be obvious if certain permissions are left out. We can author this same policy in a more scalable way by checking the resource type of the requested permission.
|
||||
|
||||
```diff
|
||||
```ts
|
||||
import {
|
||||
AuthorizeResult,
|
||||
PolicyDecision,
|
||||
- isPermission,
|
||||
+ isResourcePermission,
|
||||
/* highlight-remove-next-line */
|
||||
isPermission,
|
||||
isResourcePermission,
|
||||
/* highlight-add-next-line */
|
||||
} from '@backstage/plugin-permission-common';
|
||||
import {
|
||||
catalogConditions,
|
||||
createCatalogConditionalDecision,
|
||||
} from '@backstage/plugin-catalog-backend/alpha';
|
||||
- import {
|
||||
- catalogEntityDeletePermission,
|
||||
- } from '@backstage/plugin-catalog-common/alpha';
|
||||
|
||||
...
|
||||
import {
|
||||
catalogConditions,
|
||||
createCatalogConditionalDecision,
|
||||
} from '@backstage/plugin-catalog-backend/alpha';
|
||||
/* highlight-remove-start */
|
||||
import {
|
||||
catalogEntityDeletePermission,
|
||||
} from '@backstage/plugin-catalog-common/alpha';
|
||||
/* highlight-remove-end */
|
||||
|
||||
class TestPermissionPolicy implements PermissionPolicy {
|
||||
async handle(
|
||||
request: PolicyQuery,
|
||||
user?: BackstageIdentityResponse,
|
||||
): Promise<PolicyDecision> {
|
||||
- if (isPermission(request.permission, catalogEntityDeletePermission)) {
|
||||
+ if (isResourcePermission(request.permission, 'catalog-entity')) {
|
||||
/* highlight-remove-next-line */
|
||||
if (isPermission(request.permission, catalogEntityDeletePermission)) {
|
||||
/* highlight-add-next-line */
|
||||
if (isResourcePermission(request.permission, 'catalog-entity')) {
|
||||
return createCatalogConditionalDecision(
|
||||
request.permission,
|
||||
catalogConditions.isEntityOwner({
|
||||
@@ -127,6 +141,7 @@ class TestPermissionPolicy implements PermissionPolicy {
|
||||
|
||||
return { result: AuthorizeResult.ALLOW };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In this example, we use [`isResourcePermission`](https://backstage.io/docs/reference/plugin-permission-common.isresourcepermission) to match all permissions with a resource type of `catalog-entity`. Just like `isPermission`, this helper will "narrow" the type of `request.permission` and enable the use of `createCatalogConditionalDecision`. In addition to the behavior you observed before, you should also see that catalog entities are no longer visible unless you are the owner - success!
|
||||
|
||||
Reference in New Issue
Block a user