chore: remove old backend system guides
Signed-off-by: aramissennyeydd <aramis.sennyey@doordash.com>
This commit is contained in:
@@ -1,165 +0,0 @@
|
||||
---
|
||||
id: custom-rules--old
|
||||
title: Defining custom permission rules
|
||||
description: How to define custom permission rules for existing resources
|
||||
---
|
||||
|
||||
:::info
|
||||
This documentation is written for the old backend which has been replaced by [the new backend system](../backend-system/index.md), being the default since Backstage [version 1.24](../releases/v1.24.0.md). If have migrated to the new backend system, you may want to read [its own article](./custom-rules.md) instead. Otherwise, [consider migrating](../backend-system/building-backends/08-migrating.md)!
|
||||
:::
|
||||
|
||||
For some use cases, you may want to define custom [rules](../references/glossary.md#rule-permission-plugin) in addition to the ones provided by a plugin. In the [previous section](./writing-a-policy.md) we used the `isEntityOwner` rule to control access for catalog entities. Let's extend this policy with a custom rule that checks what [system](https://backstage.io/docs/features/software-catalog/system-model#system) an entity is part of.
|
||||
|
||||
## Define a custom rule
|
||||
|
||||
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 and create a condition in `packages/backend/src/plugins/permission.ts`.
|
||||
|
||||
We use Zod in our example below. To install, run:
|
||||
|
||||
```bash
|
||||
yarn workspace backend add zod
|
||||
```
|
||||
|
||||
```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';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const isInSystemRule = createCatalogPermissionRule({
|
||||
name: 'IS_IN_SYSTEM',
|
||||
description: 'Checks if an entity is part of the system provided',
|
||||
resourceType: 'catalog-entity',
|
||||
paramsSchema: z.object({
|
||||
systemRef: z
|
||||
.string()
|
||||
.describe('SystemRef to check the resource is part of'),
|
||||
}),
|
||||
apply: (resource: Entity, { systemRef }) => {
|
||||
if (!resource.relations) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return resource.relations
|
||||
.filter(relation => relation.type === 'partOf')
|
||||
.some(relation => relation.targetRef === systemRef);
|
||||
},
|
||||
toQuery: ({ systemRef }) => ({
|
||||
key: 'relations.partOf',
|
||||
values: [systemRef],
|
||||
}),
|
||||
});
|
||||
|
||||
const isInSystem = createConditionFactory(isInSystemRule);
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
For a more detailed explanation on defining rules, refer to the [documentation for plugin authors](./plugin-authors/03-adding-a-resource-permission-check.md#adding-support-for-conditional-decisions).
|
||||
|
||||
Still in the `packages/backend/src/plugins/permission.ts` file, let's use the condition we just created in our `TestPermissionPolicy`.
|
||||
|
||||
```ts title="packages/backend/src/plugins/permission.ts"
|
||||
...
|
||||
/* highlight-remove-next-line */
|
||||
import { createCatalogPermissionRule } from '@backstage/plugin-catalog-backend/alpha';
|
||||
/* highlight-add-next-line */
|
||||
import { catalogConditions, createCatalogConditionalDecision, createCatalogPermissionRule } from '@backstage/plugin-catalog-backend/alpha';
|
||||
/* highlight-remove-next-line */
|
||||
import { createConditionFactory } from '@backstage/plugin-permission-node';
|
||||
/* highlight-add-next-line */
|
||||
import { PermissionPolicy, PolicyQuery, PolicyQueryUser, createConditionFactory } from '@backstage/plugin-permission-node';
|
||||
/* highlight-add-start */
|
||||
import { AuthorizeResult, PolicyDecision, isResourcePermission } from '@backstage/plugin-permission-common';
|
||||
/* highlight-add-end */
|
||||
...
|
||||
|
||||
export const isInSystemRule = createCatalogPermissionRule({
|
||||
name: 'IS_IN_SYSTEM',
|
||||
description: 'Checks if an entity is part of the system provided',
|
||||
resourceType: 'catalog-entity',
|
||||
paramsSchema: z.object({
|
||||
systemRef: z
|
||||
.string()
|
||||
.describe('SystemRef to check the resource is part of'),
|
||||
}),
|
||||
apply: (resource: Entity, { systemRef }) => {
|
||||
if (!resource.relations) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return resource.relations
|
||||
.filter(relation => relation.type === 'partOf')
|
||||
.some(relation => relation.targetRef === systemRef);
|
||||
},
|
||||
toQuery: ({ systemRef }) => ({
|
||||
key: 'relations.partOf',
|
||||
values: [systemRef],
|
||||
}),
|
||||
});
|
||||
|
||||
const isInSystem = createConditionFactory(isInSystemRule);
|
||||
|
||||
class TestPermissionPolicy implements PermissionPolicy {
|
||||
async handle(
|
||||
request: PolicyQuery,
|
||||
user?: PolicyQueryUser,
|
||||
): Promise<PolicyDecision> {
|
||||
if (isResourcePermission(request.permission, 'catalog-entity')) {
|
||||
return createCatalogConditionalDecision(
|
||||
request.permission,
|
||||
/* highlight-remove-start */
|
||||
catalogConditions.isEntityOwner({
|
||||
claims: user?.info.ownershipEntityRefs ?? [],
|
||||
}),
|
||||
/* highlight-remove-end */
|
||||
/* highlight-add-start */
|
||||
{
|
||||
anyOf: [
|
||||
catalogConditions.isEntityOwner({
|
||||
claims: user?.info.ownershipEntityRefs ?? [],
|
||||
}),
|
||||
isInSystem({ systemRef: 'interviewing' }),
|
||||
],
|
||||
},
|
||||
/* highlight-add-end */
|
||||
);
|
||||
}
|
||||
|
||||
return { result: AuthorizeResult.ALLOW };
|
||||
}
|
||||
}
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
## Provide the rule during plugin setup
|
||||
|
||||
Now that we have a custom rule defined and added to our policy, we need provide it to the catalog plugin. This step is important because the catalog plugin will use the rule's `toQuery` and `apply` methods while evaluating conditional authorize results. There's no guarantee that the catalog and permission backends are running on the same server, so we must explicitly link the rule to ensure that it's available at runtime.
|
||||
|
||||
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 title="packages/backend/src/plugins/catalog.ts"
|
||||
import { CatalogBuilder } from '@backstage/plugin-catalog-backend';
|
||||
/* highlight-add-next-line */
|
||||
import { isInSystemRule } from './permission';
|
||||
|
||||
...
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
/* highlight-add-next-line */
|
||||
builder.addPermissionRules(isInSystemRule);
|
||||
...
|
||||
return router;
|
||||
}
|
||||
```
|
||||
|
||||
The updated policy will allow catalog entity resource permissions if any of the following are true:
|
||||
|
||||
- User owns the target entity
|
||||
- Target entity is part of the 'interviewing' system
|
||||
@@ -5,7 +5,7 @@ description: How to define custom permission rules for existing resources
|
||||
---
|
||||
|
||||
:::info
|
||||
This documentation is written for [the new backend system](../backend-system/index.md) which is the default since Backstage [version 1.24](../releases/v1.24.0.md). If you are still on the old backend system, you may want to read [its own article](./custom-rules--old.md) instead, and [consider migrating](../backend-system/building-backends/08-migrating.md)!
|
||||
This documentation is written for [the new backend system](../backend-system/index.md) which is the default since Backstage [version 1.24](../releases/v1.24.0.md). If you are still on the old backend system, you may want to read [its own article](https://github.com/backstage/backstage/blob/v1.37.0/docs/permissions/custom-rules--old.md) instead, and [consider migrating](../backend-system/building-backends/08-migrating.md)!
|
||||
:::
|
||||
|
||||
For some use cases, you may want to define custom [rules](../references/glossary.md#rule-permission-plugin) in addition to the ones provided by a plugin. In the [previous section](./writing-a-policy.md) we used the `isEntityOwner` rule to control access for catalog entities. Let's extend this policy with a custom rule that checks what [system](https://backstage.io/docs/features/software-catalog/system-model#system) an entity is part of.
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
---
|
||||
id: getting-started--old
|
||||
title: Getting Started
|
||||
description: How to get started with the permission framework as an integrator
|
||||
---
|
||||
|
||||
:::info
|
||||
This documentation is written for the old backend which has been replaced by [the new backend system](../backend-system/index.md), being the default since Backstage [version 1.24](../releases/v1.24.0.md). If have migrated to the new backend system, you may want to read [its own article](./getting-started.md) instead. Otherwise, [consider migrating](../backend-system/building-backends/08-migrating.md)!
|
||||
:::
|
||||
|
||||
If you prefer to watch a video instead, you can start with this video introduction:
|
||||
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/EQr9tFClgG0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
|
||||
:::note Note
|
||||
|
||||
This video was recorded in the January 2022 Contributors Session using `@backstage/create-app@0.4.14`. Some aspects of the demo may have changed in later releases.
|
||||
|
||||
:::
|
||||
|
||||
Backstage integrators control permissions by writing a policy. In general terms, a policy is simply an async function which receives a request to authorize a specific action for a user and (optional) resource, and returns a decision on whether to authorize that permission. Integrators can implement their own policies from scratch, or adopt reusable policies written by others.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The permissions framework depends on a few other Backstage systems, which must be set up before we can dive into writing a policy.
|
||||
|
||||
### Upgrade to the latest version of Backstage
|
||||
|
||||
The permissions framework itself is new to Backstage and still evolving quickly. To ensure your version of Backstage has all the latest permission-related functionality, it’s important to upgrade to the latest version. The [Backstage upgrade helper](https://backstage.github.io/upgrade-helper/) is a great tool to help ensure that you’ve made all the necessary changes during the upgrade!
|
||||
|
||||
### Enable service-to-service authentication
|
||||
|
||||
Service-to-service authentication allows Backstage backend code to verify that a given request originates from elsewhere in the Backstage backend. This is useful for tasks like collation of catalog entities in the search index. This type of request shouldn’t be permissioned, so it’s important to configure this feature before trying to use the permissions framework.
|
||||
|
||||
To set up service-to-service authentication, follow the [service-to-service authentication docs](../auth/service-to-service-auth.md).
|
||||
|
||||
### Supply an identity resolver to populate group membership on sign in
|
||||
|
||||
**Note**: If you are working off of an existing Backstage instance, you likely already have some form of an identity resolver set up.
|
||||
|
||||
Like many other parts of Backstage, the permissions framework relies on information about group membership. This simplifies authoring policies through the use of groups, rather than requiring each user to be listed in the configuration. Group membership is also often useful for conditional permissions, for example allowing permissions to act on an entity to be granted when a user is a member of a group that owns that entity.
|
||||
|
||||
[The IdentityResolver docs](../auth/identity-resolver.md) describe the process for resolving group membership on sign in.
|
||||
|
||||
## Optionally add cookie-based authentication
|
||||
|
||||
Asset requests initiated by the browser will not include a token in the `Authorization` header. If these requests check authorization through the permission framework, as done in plugins like TechDocs, then you'll need to set up cookie-based authentication. Refer to the ["Authenticate API requests"](https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/authenticate-api-requests.md) tutorial for a demonstration on how to implement this behavior.
|
||||
|
||||
## Integrating the permission framework with your Backstage instance
|
||||
|
||||
### 1. Set up the permission backend
|
||||
|
||||
The permissions framework uses a new `permission-backend` plugin to accept authorization requests from other plugins across your Backstage instance. The Backstage backend does not include this permission backend by default, so you will need to add it:
|
||||
|
||||
1. Add `@backstage/plugin-permission-backend` as a dependency of your Backstage backend:
|
||||
|
||||
```bash title="From your Backstage root directory"
|
||||
yarn --cwd packages/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 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 };
|
||||
}
|
||||
}
|
||||
|
||||
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:
|
||||
|
||||
```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'));
|
||||
// ..
|
||||
|
||||
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
|
||||
|
||||
Now that the permission backend is running, it’s time to enable the permissions framework and make sure it’s working properly.
|
||||
|
||||
1. Set the property `permission.enabled` to `true` in `app-config.yaml`.
|
||||
|
||||
```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:
|
||||
|
||||
```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 {
|
||||
/* 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.
|
||||
|
||||

|
||||
|
||||
Now that the framework is fully configured, you can craft a permission policy that works best for your organization by utilizing a provided authorization method or by [writing your own policy](./writing-a-policy.md)!
|
||||
@@ -5,7 +5,7 @@ description: How to get started with the permission framework as an integrator
|
||||
---
|
||||
|
||||
:::info
|
||||
This documentation is written for [the new backend system](../backend-system/index.md) which is the default since Backstage [version 1.24](../releases/v1.24.0.md). If you are still on the old backend system, you may want to read [its own article](./getting-started--old.md) instead, and [consider migrating](../backend-system/building-backends/08-migrating.md)!
|
||||
This documentation is written for [the new backend system](../backend-system/index.md) which is the default since Backstage [version 1.24](../releases/v1.24.0.md). If you are still on the old backend system, you may want to read [its own article](https://github.com/backstage/backstage/blob/v1.37.0/docs/permissions/getting-started--old.md) instead, and [consider migrating](../backend-system/building-backends/08-migrating.md)!
|
||||
:::
|
||||
|
||||
Backstage integrators control permissions by writing a policy. In general terms, a policy is simply an async function which receives a request to authorize a specific action for a user and (optional) resource, and returns a decision on whether to authorize that permission. Integrators can implement their own policies from scratch, or adopt reusable policies written by others.
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
---
|
||||
id: 01-setup--old
|
||||
title: 1. Tutorial setup
|
||||
description: How to get started with the permission framework as a plugin author
|
||||
---
|
||||
|
||||
:::info
|
||||
This documentation is written for the old backend which has been replaced by [the new backend system](../../backend-system/index.md), being the default since Backstage [version 1.24](../../releases/v1.24.0.md). If have migrated to the new backend system, you may want to read [its own article](./01-setup.md) instead. Otherwise, [consider migrating](../../backend-system/building-backends/08-migrating.md)!
|
||||
:::
|
||||
|
||||
The following tutorial is designed to help plugin authors add support for permissions to their plugins. We'll add support for permissions to example `todo-list` and `todo-list-backend` plugins, but the process should be similar for other plugins!
|
||||
|
||||
The rest of this page is focused on adding the `todo-list` and `todo-list-backend` plugins to your Backstage instance. If you want to add support for permissions to your own plugin instead, feel free to skip to the [next section](./02-adding-a-basic-permission-check.md).
|
||||
|
||||
## Setup for the Tutorial
|
||||
|
||||
We will use a "Todo list" feature, composed of the `todo-list` and `todo-list-backend` plugins, as well as their dependency, `todo-list-common`.
|
||||
|
||||
The source code is available here:
|
||||
|
||||
- [todo-list](https://github.com/backstage/backstage/blob/master/plugins/example-todo-list)
|
||||
- [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:
|
||||
|
||||
```bash
|
||||
$ cd $(mktemp -d)
|
||||
git clone --depth 1 --quiet --no-checkout --filter=blob:none https://github.com/backstage/backstage.git .
|
||||
git checkout master -- plugins/example-todo-list/
|
||||
git checkout master -- plugins/example-todo-list-backend/
|
||||
git checkout master -- plugins/example-todo-list-common/
|
||||
sed -i '' 's/workspace:\^/\*/g' plugins/example-todo-list/package.json
|
||||
sed -i '' 's/workspace:\^/\*/g' plugins/example-todo-list-backend/package.json
|
||||
sed -i '' 's/workspace:\^/\*/g' plugins/example-todo-list-common/package.json
|
||||
for file in plugins/*; do mv "$file" "$OLDPWD/${file/example-todo/todo}"; done
|
||||
cd -
|
||||
```
|
||||
|
||||
The `plugins` directory of your project should now include `todo-list`, `todo-list-backend`, and `todo-list-common`.
|
||||
|
||||
**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:
|
||||
|
||||
```sh title="From your Backstage root directory"
|
||||
yarn --cwd packages/backend add @internal/plugin-todo-list-backend @internal/plugin-todo-list-common
|
||||
yarn --cwd packages/app add @internal/plugin-todo-list
|
||||
```
|
||||
|
||||
3. Include the backend and frontend plugin in your application:
|
||||
|
||||
Create a new `packages/backend/src/plugins/todolist.ts` with the following content:
|
||||
|
||||
```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';
|
||||
import { PluginEnvironment } from '../types';
|
||||
|
||||
export default async function createPlugin({
|
||||
logger,
|
||||
discovery,
|
||||
}: PluginEnvironment): Promise<Router> {
|
||||
return await createRouter({
|
||||
logger,
|
||||
identity: DefaultIdentityClient.create({
|
||||
discovery,
|
||||
issuer: await discovery.getExternalBaseUrl('auth'),
|
||||
}),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Apply the following changes to `packages/backend/src/index.ts`:
|
||||
|
||||
```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'));
|
||||
/* 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));
|
||||
/* 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`:
|
||||
|
||||
```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 />} />
|
||||
{/* 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:
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Integrate the new plugin
|
||||
|
||||
If you play with the UI, you will notice that it is possible to perform a few actions:
|
||||
|
||||
- create a new todo item (`POST /todos`)
|
||||
- view todo items (`GET /todos`)
|
||||
- edit an existing todo item (`PUT /todos`)
|
||||
|
||||
Let's try to bring authorization on top of each one of them.
|
||||
@@ -5,7 +5,7 @@ description: How to get started with the permission framework as a plugin author
|
||||
---
|
||||
|
||||
:::info
|
||||
This documentation is written for [the new backend system](../../backend-system/index.md) which is the default since Backstage [version 1.24](../../releases/v1.24.0.md). If you are still on the old backend system, you may want to read [its own article](./01-setup--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)!
|
||||
This documentation is written for [the new backend system](../../backend-system/index.md) which is the default since Backstage [version 1.24](../../releases/v1.24.0.md). If you are still on the old backend system, you may want to read [its own article](https://github.com/backstage/backstage/blob/v1.37.0/docs/permissions/plugin-authors/01-setup--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)!
|
||||
:::
|
||||
|
||||
The following tutorial is designed to help plugin authors add support for permissions to their plugins. We'll add support for permissions to example `todo-list` and `todo-list-backend` plugins, but the process should be similar for other plugins!
|
||||
|
||||
@@ -1,387 +0,0 @@
|
||||
---
|
||||
id: 02-adding-a-basic-permission-check--old
|
||||
title: 2. Adding a basic permission check
|
||||
description: Explains how to add a basic permission check to a Backstage plugin
|
||||
---
|
||||
|
||||
:::info
|
||||
This documentation is written for the old backend which has been replaced by [the new backend system](../../backend-system/index.md), being the default since Backstage [version 1.24](../../releases/v1.24.0.md). If have migrated to the new backend system, you may want to read [its own article](./02-adding-a-basic-permission-check.md) instead. Otherwise, [consider migrating](../../backend-system/building-backends/08-migrating.md)!
|
||||
:::
|
||||
|
||||
If the outcome of a permission check doesn't need to change for different [resources](../../references/glossary.md#resource-permission-plugin), you can use a _basic permission check_. For this kind of check, we simply need to define a permission, and call `authorize` with it.
|
||||
|
||||
For this tutorial, we'll use a basic permission check to authorize the `create` endpoint in our todo-backend. This will allow Backstage integrators to control whether each of their users is authorized to create todos by adjusting their [permission policy](../../references/glossary.md#policy-permission-plugin).
|
||||
|
||||
We'll start by creating a new permission, and then we'll use the permission api to call `authorize` with it during todo creation.
|
||||
|
||||
## Creating a new permission
|
||||
|
||||
Let's navigate to the file `plugins/todo-list-common/src/permissions.ts` and add our first permission:
|
||||
|
||||
```ts title="plugins/todo-list-common/src/permissions.ts"
|
||||
import { createPermission } from '@backstage/plugin-permission-common';
|
||||
|
||||
/* highlight-remove-start */
|
||||
export const tempExamplePermission = createPermission({
|
||||
name: 'temp.example.noop',
|
||||
attributes: {},
|
||||
/* highlight-remove-end */
|
||||
/* highlight-add-start */
|
||||
export const todoListCreatePermission = createPermission({
|
||||
name: 'todo.list.create',
|
||||
attributes: { action: 'create' },
|
||||
/* highlight-add-end */
|
||||
});
|
||||
|
||||
/* highlight-remove-next-line */
|
||||
export const todoListPermissions = [tempExamplePermission];
|
||||
/* highlight-add-next-line */
|
||||
export const todoListPermissions = [todoListCreatePermission];
|
||||
```
|
||||
|
||||
For this tutorial, we've automatically exported all permissions from this file (see `plugins/todo-list-common/src/index.ts`).
|
||||
|
||||
:::note Note
|
||||
|
||||
We use a separate `todo-list-common` package since all permissions authorized by your plugin should be exported from a ["common-library" package](https://backstage.io/docs/tooling/cli/build-system#package-roles). This allows Backstage integrators to reference them in frontend components as well as permission policies.
|
||||
|
||||
:::
|
||||
|
||||
## Authorizing using the new permission
|
||||
|
||||
Install the following module:
|
||||
|
||||
```
|
||||
$ yarn workspace @internal/plugin-todo-list-backend \
|
||||
add @backstage/plugin-permission-common @backstage/plugin-permission-node @internal/plugin-todo-list-common
|
||||
```
|
||||
|
||||
Edit `plugins/todo-list-backend/src/service/router.ts`:
|
||||
|
||||
```ts title="plugins/todo-list-backend/src/service/router.ts"
|
||||
/* highlight-remove-start */
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { IdentityApi } from '@backstage/plugin-auth-node';
|
||||
/* highlight-remove-end */
|
||||
/* highlight-add-start */
|
||||
import { InputError, NotAllowedError } from '@backstage/errors';
|
||||
import { getBearerTokenFromAuthorizationHeader, IdentityApi } from '@backstage/plugin-auth-node';
|
||||
import { PermissionEvaluator, AuthorizeResult } from '@backstage/plugin-permission-common';
|
||||
import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node';
|
||||
import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
/* highlight-add-end */
|
||||
|
||||
export interface RouterOptions {
|
||||
logger: Logger;
|
||||
identity: IdentityApi;
|
||||
/* highlight-add-next-line */
|
||||
permissions: PermissionEvaluator;
|
||||
}
|
||||
|
||||
export async function createRouter(
|
||||
options: RouterOptions,
|
||||
): Promise<express.Router> {
|
||||
/* highlight-remove-next-line */
|
||||
const { logger, identity } = options;
|
||||
/* highlight-add-next-line */
|
||||
const { logger, identity, permissions } = options;
|
||||
|
||||
/* highlight-add-start */
|
||||
const permissionIntegrationRouter = createPermissionIntegrationRouter({
|
||||
permissions: [todoListCreatePermission],
|
||||
});
|
||||
/* highlight-add-end */
|
||||
|
||||
const router = Router();
|
||||
router.use(express.json());
|
||||
|
||||
router.get('/health', (_, response) => {
|
||||
logger.info('PONG!');
|
||||
response.json({ status: 'ok' });
|
||||
});
|
||||
|
||||
/* highlight-add-next-line */
|
||||
router.use(permissionIntegrationRouter);
|
||||
|
||||
router.get('/todos', async (_req, res) => {
|
||||
res.json(getAll());
|
||||
});
|
||||
|
||||
router.post('/todos', async (req, res) => {
|
||||
let author: string | undefined = undefined;
|
||||
|
||||
const user = await identity.getIdentity({ request: req });
|
||||
author = user?.identity.userEntityRef;
|
||||
/* highlight-add-start */
|
||||
const token = getBearerTokenFromAuthorizationHeader(
|
||||
req.header('authorization'),
|
||||
);
|
||||
const decision = (
|
||||
await permissions.authorize([{ permission: todoListCreatePermission }], {
|
||||
token,
|
||||
})
|
||||
)[0];
|
||||
|
||||
if (decision.result === AuthorizeResult.DENY) {
|
||||
throw new NotAllowedError('Unauthorized');
|
||||
}
|
||||
/* highlight-add-end */
|
||||
|
||||
if (!isTodoCreateRequest(req.body)) {
|
||||
throw new InputError('Invalid payload');
|
||||
}
|
||||
|
||||
const todo = add({ title: req.body.title, author });
|
||||
res.json(todo);
|
||||
});
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
Pass the `permissions` object to the plugin in `packages/backend/src/plugins/todolist.ts`:
|
||||
|
||||
```ts title="packages/backend/src/plugins/todolist.ts"
|
||||
import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
|
||||
import { createRouter } from '@internal/plugin-todo-list-backend';
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
|
||||
export default async function createPlugin({
|
||||
logger,
|
||||
discovery,
|
||||
/* highlight-add-next-line */
|
||||
permissions,
|
||||
}: PluginEnvironment): Promise<Router> {
|
||||
return await createRouter({
|
||||
logger,
|
||||
identity: DefaultIdentityClient.create({
|
||||
discovery,
|
||||
issuer: await discovery.getExternalBaseUrl('auth'),
|
||||
}),
|
||||
/* highlight-add-next-line */
|
||||
permissions,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
That's it! Now your plugin is fully configured. Let's try to test the logic by denying the permission.
|
||||
|
||||
## Test the authorized create endpoint
|
||||
|
||||
Before running this step, please make sure you followed the steps described in [Getting started](../getting-started.md) section.
|
||||
|
||||
In order to test the logic above, the integrators of your backstage instance need to change their permission policy to return `DENY` for our newly-created permission:
|
||||
|
||||
```ts title="packages/backend/src/plugins/permission.ts"
|
||||
import {
|
||||
PermissionPolicy,
|
||||
/* highlight-add-start */
|
||||
PolicyQuery,
|
||||
PolicyQueryUser,
|
||||
/* highlight-add-end */
|
||||
} from '@backstage/plugin-permission-node';
|
||||
/* highlight-add-start */
|
||||
import { isPermission } from '@backstage/plugin-permission-common';
|
||||
import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
/* highlight-add-end */
|
||||
|
||||
class TestPermissionPolicy implements PermissionPolicy {
|
||||
/* highlight-remove-next-line */
|
||||
async handle(): Promise<PolicyDecision> {
|
||||
/* highlight-add-start */
|
||||
async handle(
|
||||
request: PolicyQuery,
|
||||
_user?: PolicyQueryUser,
|
||||
): Promise<PolicyDecision> {
|
||||
if (isPermission(request.permission, todoListCreatePermission)) {
|
||||
return {
|
||||
result: AuthorizeResult.DENY,
|
||||
};
|
||||
}
|
||||
/* highlight-add-end */
|
||||
|
||||
return {
|
||||
result: AuthorizeResult.ALLOW,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Now the frontend should show an error whenever you try to create a new Todo item.
|
||||
|
||||
Let's flip the result back to `ALLOW` before moving on.
|
||||
|
||||
```ts
|
||||
if (isPermission(request.permission, todoListCreatePermission)) {
|
||||
return {
|
||||
/* highlight-remove-next-line */
|
||||
result: AuthorizeResult.DENY,
|
||||
/* highlight-add-next-line */
|
||||
result: AuthorizeResult.ALLOW,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
At this point everything is working but if you run `yarn tsc` you'll get some errors, let's fix those up.
|
||||
|
||||
First we'll clean up the `plugins/todo-list-backend/src/service/router.test.ts`:
|
||||
|
||||
```ts title="plugins/todo-list-backend/src/service/router.test.ts"
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
|
||||
/* highlight-add-next-line */
|
||||
import { PermissionEvaluator } from '@backstage/plugin-permission-common';
|
||||
import express from 'express';
|
||||
import request from 'supertest';
|
||||
|
||||
import { createRouter } from './router';
|
||||
|
||||
/* highlight-add-start */
|
||||
const mockedAuthorize: jest.MockedFunction<PermissionEvaluator['authorize']> =
|
||||
jest.fn();
|
||||
const mockedPermissionQuery: jest.MockedFunction<
|
||||
PermissionEvaluator['authorizeConditional']
|
||||
> = jest.fn();
|
||||
|
||||
const permissionEvaluator: PermissionEvaluator = {
|
||||
authorize: mockedAuthorize,
|
||||
authorizeConditional: mockedPermissionQuery,
|
||||
};
|
||||
/* highlight-add-end */
|
||||
|
||||
describe('createRouter', () => {
|
||||
let app: express.Express;
|
||||
|
||||
beforeAll(async () => {
|
||||
const router = await createRouter({
|
||||
logger: getVoidLogger(),
|
||||
identity: {} as DefaultIdentityClient,
|
||||
/* highlight-add-next-line */
|
||||
permissions: permissionEvaluator,
|
||||
});
|
||||
app = express().use(router);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('GET /health', () => {
|
||||
it('returns ok', async () => {
|
||||
const response = await request(app).get('/health');
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual({ status: 'ok' });
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Then we want to update the `plugins/todo-list-backend/src/service/standaloneServer.ts`:
|
||||
|
||||
```ts title="plugins/todo-list-backend/src/service/standaloneServer.ts"
|
||||
import {
|
||||
createServiceBuilder,
|
||||
loadBackendConfig,
|
||||
SingleHostDiscovery,
|
||||
/* highlight-add-next-line */
|
||||
ServerTokenManager,
|
||||
} from '@backstage/backend-common';
|
||||
import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
|
||||
/* highlight-add-next-line */
|
||||
import { ServerPermissionClient } from '@backstage/plugin-permission-node';
|
||||
import { Server } from 'http';
|
||||
import { Logger } from 'winston';
|
||||
import { createRouter } from './router';
|
||||
|
||||
export interface ServerOptions {
|
||||
port: number;
|
||||
enableCors: boolean;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export async function startStandaloneServer(
|
||||
options: ServerOptions,
|
||||
): Promise<Server> {
|
||||
const logger = options.logger.child({ service: 'todo-list-backend' });
|
||||
logger.debug('Starting application server...');
|
||||
const config = await loadBackendConfig({ logger, argv: process.argv });
|
||||
const discovery = SingleHostDiscovery.fromConfig(config);
|
||||
/* highlight-add-start */
|
||||
const tokenManager = ServerTokenManager.fromConfig(config, {
|
||||
logger,
|
||||
});
|
||||
const permissions = ServerPermissionClient.fromConfig(config, {
|
||||
discovery,
|
||||
tokenManager,
|
||||
});
|
||||
/* highlight-add-end */
|
||||
const router = await createRouter({
|
||||
logger,
|
||||
identity: DefaultIdentityClient.create({
|
||||
discovery,
|
||||
issuer: await discovery.getExternalBaseUrl('auth'),
|
||||
}),
|
||||
/* highlight-add-next-line */
|
||||
permissions,
|
||||
});
|
||||
|
||||
let service = createServiceBuilder(module)
|
||||
.setPort(options.port)
|
||||
.addRouter('/todo-list', router);
|
||||
if (options.enableCors) {
|
||||
service = service.enableCors({ origin: 'http://localhost:3000' });
|
||||
}
|
||||
|
||||
return await service.start().catch(err => {
|
||||
logger.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.hot?.accept();
|
||||
```
|
||||
|
||||
Finally, we need to update `plugins/todo-list-backend/src/plugin.ts`:
|
||||
|
||||
```ts title="plugins/todo-list-backend/src/plugin.ts"
|
||||
import { loggerToWinstonLogger } from '@backstage/backend-common';
|
||||
import {
|
||||
coreServices,
|
||||
createBackendPlugin,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { createRouter } from './service/router';
|
||||
|
||||
/**
|
||||
* The example TODO list backend plugin.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const exampleTodoListPlugin = createBackendPlugin({
|
||||
pluginId: 'exampleTodoList',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
identity: coreServices.identity,
|
||||
logger: coreServices.logger,
|
||||
httpRouter: coreServices.httpRouter,
|
||||
/* highlight-add-next-line */
|
||||
permissions: coreServices.permissions,
|
||||
},
|
||||
/* highlight-remove-next-line */
|
||||
async init({ identity, logger, httpRouter }) {
|
||||
/* highlight-add-next-line */
|
||||
async init({ identity, logger, httpRouter, permissions }) {
|
||||
httpRouter.use(
|
||||
await createRouter({
|
||||
identity,
|
||||
logger: loggerToWinstonLogger(logger),
|
||||
permissions,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Now when you run `yarn tsc` you should have no more errors.
|
||||
@@ -5,7 +5,7 @@ description: Explains how to add a basic permission check to a Backstage plugin
|
||||
---
|
||||
|
||||
:::info
|
||||
This documentation is written for [the new backend system](../../backend-system/index.md) which is the default since Backstage [version 1.24](../../releases/v1.24.0.md). If you are still on the old backend system, you may want to read [its own article](./02-adding-a-basic-permission-check--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)!
|
||||
This documentation is written for [the new backend system](../../backend-system/index.md) which is the default since Backstage [version 1.24](../../releases/v1.24.0.md). If you are still on the old backend system, you may want to read [its own article](https://github.com/backstage/backstage/blob/v1.37.0/docs/permissions/plugin-authors/02-adding-a-basic-permission-check--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)!
|
||||
:::
|
||||
|
||||
If the outcome of a permission check doesn't need to change for different [resources](../../references/glossary.md#resource-permission-plugin), you can use a _basic permission check_. For this kind of check, we simply need to define a permission, and call `authorize` with it.
|
||||
|
||||
@@ -1,299 +0,0 @@
|
||||
---
|
||||
id: 03-adding-a-resource-permission-check--old
|
||||
title: 3. Adding a resource permission check
|
||||
description: Explains how to add a resource permission check to a Backstage plugin
|
||||
---
|
||||
|
||||
:::info
|
||||
This documentation is written for the old backend which has been replaced by [the new backend system](../../backend-system/index.md), being the default since Backstage [version 1.24](../../releases/v1.24.0.md). If have migrated to the new backend system, you may want to read [its own article](./03-adding-a-resource-permission-check.md) instead. Otherwise, [consider migrating](../../backend-system/building-backends/08-migrating.md)!
|
||||
:::
|
||||
|
||||
When performing updates (or other operations) on specific [resources](../../references/glossary.md#resource-permission-plugin), the permissions framework allows for the decision to be based on characteristics of the resource itself. This means that it's possible to write policies that (for example) allow the operation for users that own a resource, and deny the operation otherwise.
|
||||
|
||||
## Creating the update permission
|
||||
|
||||
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).
|
||||
|
||||
```ts title="plugins/todo-list-common/src/permissions.ts"
|
||||
import { createPermission } from '@backstage/plugin-permission-common';
|
||||
|
||||
/* highlight-add-next-line */
|
||||
export const TODO_LIST_RESOURCE_TYPE = 'todo-item';
|
||||
|
||||
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-start */
|
||||
export const todoListPermissions = [
|
||||
todoListCreatePermission,
|
||||
todoListUpdatePermission,
|
||||
];
|
||||
/* highlight-add-end */
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## Setting up authorization for the update permission
|
||||
|
||||
To start, let's edit `plugins/todo-list-backend/src/service/router.ts` in the same manner as we did in the previous section:
|
||||
|
||||
```ts title="plugins/todo-list-backend/src/service/router.ts"
|
||||
/* highlight-remove-next-line */
|
||||
import { todoListCreatePermission } from '@internal/plugin-todo-list-common';
|
||||
/* highlight-add-start */
|
||||
import {
|
||||
todoListCreatePermission,
|
||||
todoListUpdatePermission,
|
||||
} from '@internal/plugin-todo-list-common';
|
||||
/* highlight-add-end */
|
||||
|
||||
// ...
|
||||
|
||||
const permissionIntegrationRouter = createPermissionIntegrationRouter({
|
||||
/* highlight-remove-next-line */
|
||||
permissions: [todoListCreatePermission],
|
||||
/* highlight-add-next-line */
|
||||
permissions: [todoListCreatePermission, todoListUpdatePermission],
|
||||
});
|
||||
|
||||
// ...
|
||||
|
||||
router.put('/todos', async (req, res) => {
|
||||
/* highlight-add-start */
|
||||
const token = getBearerTokenFromAuthorizationHeader(
|
||||
req.header('authorization'),
|
||||
);
|
||||
/* highlight-add-end */
|
||||
|
||||
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 (decision.result !== AuthorizeResult.ALLOW) {
|
||||
throw new NotAllowedError('Unauthorized');
|
||||
}
|
||||
/* highlight-add-end */
|
||||
|
||||
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.
|
||||
|
||||
This enables decisions based on characteristics of the resource, but it's important to note that policy authors will not have access to the resource ref inside of their permission policies. Instead, the policies will return conditional decisions, which we need to now support in our plugin.
|
||||
|
||||
## Adding support for conditional decisions
|
||||
|
||||
Install the missing module:
|
||||
|
||||
```bash
|
||||
$ yarn workspace @internal/plugin-todo-list-backend add zod
|
||||
```
|
||||
|
||||
Create a new `plugins/todo-list-backend/src/service/rules.ts` file and append the following code:
|
||||
|
||||
```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';
|
||||
import { Todo, TodoFilter } from './todos';
|
||||
|
||||
export const createTodoListPermissionRule = makeCreatePermissionRule<
|
||||
Todo,
|
||||
TodoFilter,
|
||||
typeof TODO_LIST_RESOURCE_TYPE
|
||||
>();
|
||||
|
||||
export const isOwner = createTodoListPermissionRule({
|
||||
name: 'IS_OWNER',
|
||||
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'),
|
||||
}),
|
||||
apply: (resource: Todo, { userId }) => {
|
||||
return resource.author === userId;
|
||||
},
|
||||
toQuery: ({ userId }) => {
|
||||
return {
|
||||
property: 'author',
|
||||
values: [userId],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const rules = { isOwner };
|
||||
```
|
||||
|
||||
`makeCreatePermissionRule` is a helper used to ensure that rules created for this plugin use consistent types for the resource and query.
|
||||
|
||||
:::note Note
|
||||
|
||||
To support custom rules defined by Backstage integrators, you must export `createTodoListPermissionRule` from the backend package and provide some way for custom rules to be passed in before the backend starts, likely via `createRouter`.
|
||||
|
||||
:::
|
||||
|
||||
We have created a new `isOwner` rule, which is going to be automatically used by the permission framework whenever a conditional response is returned in response to an authorized request with an attached `resourceRef`.
|
||||
Specifically, the `apply` function is used to understand whether the passed resource should be authorized or not.
|
||||
|
||||
Let's skip the `toQuery` function for now, we'll come back to that in the next section.
|
||||
|
||||
Now, let's create the new endpoint by editing `plugins/todo-list-backend/src/service/router.ts`. This uses the `createPermissionIntegrationRouter` helper to add the APIs needed by the permission framework to your plugin. You'll need to supply:
|
||||
|
||||
- `getResources`: a function that accepts an array of `resourceRefs` in the same format you expect to be passed to `authorize`, and returns an array of the corresponding resources.
|
||||
- `resourceType`: the same value used in the permission rule above.
|
||||
- `permissions`: the list of permissions that your plugin accepts.
|
||||
- `rules`: an array of all the permission rules you want to support in conditional decisions.
|
||||
|
||||
```ts title="plugins/todo-list-backend/src/service/router.ts"
|
||||
// ...
|
||||
import {
|
||||
/* highlight-add-next-line */
|
||||
TODO_LIST_RESOURCE_TYPE,
|
||||
todoListCreatePermission,
|
||||
todoListUpdatePermission,
|
||||
} from '@internal/plugin-todo-list-common';
|
||||
/* highlight-remove-next-line */
|
||||
import { add, getAll, update } from './todos';
|
||||
/* highlight-add-start */
|
||||
import { add, getAll, getTodo, update } from './todos';
|
||||
import { rules } from './rules';
|
||||
/* highlight-add-end */
|
||||
|
||||
export async function createRouter(
|
||||
options: RouterOptions,
|
||||
): Promise<express.Router> {
|
||||
const { logger, identity, permissions } = options;
|
||||
|
||||
const permissionIntegrationRouter = createPermissionIntegrationRouter({
|
||||
permissions: [todoListCreatePermission, todoListUpdatePermission],
|
||||
/* highlight-add-start */
|
||||
getResources: async resourceRefs => {
|
||||
return resourceRefs.map(getTodo);
|
||||
},
|
||||
resourceType: TODO_LIST_RESOURCE_TYPE,
|
||||
rules: Object.values(rules),
|
||||
/* highlight-add-end */
|
||||
});
|
||||
|
||||
const router = Router();
|
||||
router.use(express.json());
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Provide utilities for policy authors
|
||||
|
||||
Now that we have a new resource type and a corresponding rule, we need to export some utilities for policy authors to reference them.
|
||||
|
||||
Create a new `plugins/todo-list-backend/src/conditionExports.ts` file and add the following code:
|
||||
|
||||
```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';
|
||||
|
||||
const { conditions, createConditionalDecision } = createConditionExports({
|
||||
pluginId: 'todolist',
|
||||
resourceType: TODO_LIST_RESOURCE_TYPE,
|
||||
rules,
|
||||
});
|
||||
|
||||
export const todoListConditions = conditions;
|
||||
|
||||
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`:
|
||||
|
||||
```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.
|
||||
|
||||
```ts title="packages/backend/src/plugins/permission.ts"
|
||||
import {
|
||||
IdentityClient
|
||||
} from '@backstage/plugin-auth-node';
|
||||
import {
|
||||
PermissionPolicy,
|
||||
PolicyQuery,
|
||||
PolicyQueryUser,
|
||||
} 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 */
|
||||
|
||||
|
||||
async handle(
|
||||
request: PolicyQuery,
|
||||
/* highlight-remove-next-line */
|
||||
_user?: PolicyQueryUser,
|
||||
/* highlight-add-next-line */
|
||||
user?: PolicyQueryUser,
|
||||
): 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?.info.userEntityRef ?? '',
|
||||
}),
|
||||
);
|
||||
}
|
||||
/* highlight-add-end */
|
||||
|
||||
return {
|
||||
result: AuthorizeResult.ALLOW,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
For any incoming update requests, we now return a _Conditional Decision_. We are saying:
|
||||
|
||||
> Hey permission framework, I can't make a decision alone. Please go to the plugin with id `todolist` and ask it to apply these conditions.
|
||||
|
||||
To check that everything works as expected, you should now see an error in the UI whenever you try to edit an item that wasn’t created by you. Success!
|
||||
@@ -5,7 +5,7 @@ description: Explains how to add a resource permission check to a Backstage plug
|
||||
---
|
||||
|
||||
:::info
|
||||
This documentation is written for [the new backend system](../../backend-system/index.md) which is the default since Backstage [version 1.24](../../releases/v1.24.0.md). If you are still on the old backend system, you may want to read [its own article](./03-adding-a-resource-permission-check--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)!
|
||||
This documentation is written for [the new backend system](../../backend-system/index.md) which is the default since Backstage [version 1.24](../../releases/v1.24.0.md). If you are still on the old backend system, you may want to read [its own article](https://github.com/backstage/backstage/blob/v1.37.0/docs/permissions/plugin-authors/03-adding-a-resource-permission-check--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)!
|
||||
:::
|
||||
|
||||
When performing updates (or other operations) on specific [resources](../../references/glossary.md#resource-permission-plugin), the permissions framework allows for the decision to be based on characteristics of the resource itself. This means that it's possible to write policies that (for example) allow the operation for users that own a resource, and deny the operation otherwise.
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
---
|
||||
id: 04-authorizing-access-to-paginated-data--old
|
||||
title: 4. Authorizing access to paginated data
|
||||
description: Explains how to authorize access to paginated data in a Backstage plugin
|
||||
---
|
||||
|
||||
:::info
|
||||
This documentation is written for the old backend which has been replaced by [the new backend system](../../backend-system/index.md), being the default since Backstage [version 1.24](../../releases/v1.24.0.md). If have migrated to the new backend system, you may want to read [its own article](./04-authorizing-access-to-paginated-data.md) instead. Otherwise, [consider migrating](../../backend-system/building-backends/08-migrating.md)!
|
||||
:::
|
||||
|
||||
Authorizing `GET /todos` is similar to the update endpoint, in that it should be possible to authorize access based on the characteristics of each resource. However, we'll need to authorize a list of resources for this endpoint.
|
||||
|
||||
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`:
|
||||
|
||||
```ts
|
||||
router.get('/todos', async (req, res) => {
|
||||
/* highlight-add-next-line */
|
||||
const credentials = await httpAuth.credentials(req, { allow: ['user'] });
|
||||
|
||||
/* 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,
|
||||
})),
|
||||
{ credentials },
|
||||
);
|
||||
|
||||
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.
|
||||
|
||||
To avoid this situation, the permissions framework has support for filtering items in the data source itself. In this part of the tutorial, we'll describe the steps required to use that behavior.
|
||||
|
||||
:::note Note
|
||||
|
||||
In order to perform authorization filtering in this way, the data source must allow filters to be logically combined with AND, OR, and NOT operators. The conditional decisions returned by the permissions framework use a [nested object](https://backstage.io/docs/reference/plugin-permission-common.permissioncriteria) to combine conditions. If you're implementing a filter API from scratch, we recommend using the same shape for ease of interoperability. If not, you'll need to implement a function which transforms the nested object into your own format.
|
||||
|
||||
:::
|
||||
|
||||
## Creating the read permission
|
||||
|
||||
Let's add another permission to the plugin.
|
||||
|
||||
```ts title="plugins/todo-list-backend/src/service/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-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,
|
||||
/* highlight-add-start */
|
||||
todoListReadPermission,
|
||||
/* highlight-add-end */
|
||||
];
|
||||
```
|
||||
|
||||
## Using conditional policy decisions
|
||||
|
||||
So far we've only used the `PermissionsService.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 `PermissionsService.authorizeConditional` instead.
|
||||
|
||||
```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 {
|
||||
TODO_LIST_RESOURCE_TYPE,
|
||||
todoListCreatePermission,
|
||||
todoListUpdatePermission,
|
||||
/* highlight-add-next-line */
|
||||
todoListReadPermission,
|
||||
} from './permissions';
|
||||
|
||||
// ...
|
||||
|
||||
const permissionIntegrationRouter = createPermissionIntegrationRouter({
|
||||
/* highlight-remove-next-line */
|
||||
permissions: [todoListCreatePermission, todoListUpdatePermission],
|
||||
/* highlight-add-next-line */
|
||||
permissions: [todoListCreatePermission, todoListUpdatePermission, todoListReadPermission],
|
||||
getResources: async resourceRefs => {
|
||||
return resourceRefs.map(getTodo);
|
||||
},
|
||||
resourceType: TODO_LIST_RESOURCE_TYPE,
|
||||
rules: Object.values(rules),
|
||||
});
|
||||
|
||||
// ...
|
||||
|
||||
/* highlight-add-next-line */
|
||||
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 credentials = await httpAuth.credentials(req, { allow: ['user'] });
|
||||
|
||||
const decision = (
|
||||
await permissions.authorizeConditional([{ permission: todoListReadPermission }], {
|
||||
credentials,
|
||||
})
|
||||
)[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.
|
||||
|
||||
Since `TodoFilter` used in our plugin matches the structure of the conditions object, we can directly pass the output of our condition transformer. If the filters were structured differently, we'd need to transform it further before passing it to the api.
|
||||
|
||||
## Test the authorized read endpoint
|
||||
|
||||
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.
|
||||
|
||||
```ts title="packages/backend/src/plugins/permission.ts"
|
||||
import {
|
||||
todoListCreatePermission,
|
||||
todoListUpdatePermission,
|
||||
/* highlight-add-next-line */
|
||||
todoListReadPermission,
|
||||
} from '@internal/plugin-todo-list-common';
|
||||
|
||||
/* 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.
|
||||
@@ -5,7 +5,7 @@ description: Explains how to authorize access to paginated data in a Backstage p
|
||||
---
|
||||
|
||||
:::info
|
||||
This documentation is written for [the new backend system](../../backend-system/index.md) which is the default since Backstage [version 1.24](../../releases/v1.24.0.md). If you are still on the old backend system, you may want to read [its own article](./04-authorizing-access-to-paginated-data--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)!
|
||||
This documentation is written for [the new backend system](../../backend-system/index.md) which is the default since Backstage [version 1.24](../../releases/v1.24.0.md). If you are still on the old backend system, you may want to read [its own article](https://github.com/backstage/backstage/blob/v1.37.0/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)!
|
||||
:::
|
||||
|
||||
Authorizing `GET /todos` is similar to the update endpoint, in that it should be possible to authorize access based on the characteristics of each resource. However, we'll need to authorize a list of resources for this endpoint.
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
---
|
||||
id: writing-a-policy--old
|
||||
title: Writing a permission policy
|
||||
description: How to write your own permission policy as a Backstage integrator
|
||||
---
|
||||
|
||||
:::info
|
||||
This documentation is written for the old backend which has been replaced by [the new backend system](../backend-system/index.md), being the default since Backstage [version 1.24](../releases/v1.24.0.md). If have migrated to the new backend system, you may want to read [its own article](./writing-a-policy.md) instead. Otherwise, [consider migrating](../backend-system/building-backends/08-migrating.md)!
|
||||
:::
|
||||
|
||||
In the [previous section](./getting-started.md), we were able to set up the permission framework and make a simple change to our `TestPermissionPolicy` to confirm that policy is indeed wired up correctly.
|
||||
|
||||
That policy looked like this:
|
||||
|
||||
```typescript title="packages/backend/src/plugins/permission.ts"
|
||||
class TestPermissionPolicy implements PermissionPolicy {
|
||||
async handle(
|
||||
request: PolicyQuery,
|
||||
_user?: PolicyQueryUser,
|
||||
): Promise<PolicyDecision> {
|
||||
if (request.permission.name === 'catalog.entity.delete') {
|
||||
return {
|
||||
result: AuthorizeResult.DENY,
|
||||
};
|
||||
}
|
||||
|
||||
return { result: AuthorizeResult.ALLOW };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## What's in a policy?
|
||||
|
||||
Let's break this down a bit further. The request object of type [PolicyQuery](https://backstage.io/docs/reference/plugin-permission-node.policyquery) is a simple wrapper around [the Permission object](https://backstage.io/docs/reference/plugin-permission-common.permission). This permission object encapsulates information about the action that the user is attempting to perform (See [the Concepts page](./concepts.md) for more details).
|
||||
|
||||
In the policy above, we are checking to see if the provided action is a catalog entity delete action, which is the permission that the catalog plugin authors have created to represent the action of unregistering a catalog entity. If this is the case, we return a [Definitive Policy Decision](https://backstage.io/docs/reference/plugin-permission-common.definitivepolicydecision) of DENY. In all other cases, we return ALLOW (resulting in an allow-by-default behavior).
|
||||
|
||||
As we confirmed in the previous section, we know that this now prevents us from unregistering catalog components. Hooray! But you may notice that this prevents _anyone_ from unregistering a component, which is not a very realistic policy. Let's improve this policy by disabling the unregister action _unless you are the owner of this component_.
|
||||
|
||||
## Conditional decisions
|
||||
|
||||
Let's change the policy to the following:
|
||||
|
||||
```ts
|
||||
import {
|
||||
AuthorizeResult,
|
||||
PolicyDecision,
|
||||
/* highlight-add-next-line */
|
||||
isPermission,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
/* 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 {
|
||||
/* highlight-remove-next-line */
|
||||
async handle(request: PolicyQuery): Promise<PolicyDecision> {
|
||||
/* highlight-add-start */
|
||||
async handle(
|
||||
request: PolicyQuery,
|
||||
user?: PolicyQueryUser,
|
||||
): 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?.info.ownershipEntityRefs ?? [],
|
||||
}),
|
||||
);
|
||||
/* highlight-add-end */
|
||||
}
|
||||
return { result: AuthorizeResult.ALLOW };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Let's walk through the new code that we just added.
|
||||
|
||||
Instead of returning an Definitive Policy Decision, we use factory methods to construct a [Conditional Policy Decision](https://backstage.io/docs/reference/plugin-permission-common.conditionalpolicydecision) (See the [Concepts page](./concepts.md) for more details). Since the policy doesn't have enough information to determine if `user` is the entity owner, this criteria is encapsulated within the conditional decision. However, `createCatalogConditionalDecision` will not compile unless `request.permission` is a catalog entity [`ResourcePermission`](https://backstage.io/docs/reference/plugin-permission-common.resourcepermission). This type constraint ensures that policies return conditional decisions that are compatible with the requested permission. To address this, we use [`isPermission`](https://backstage.io/docs/reference/plugin-permission-common.ispermission) to ["narrow"](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) the type of `request.permission` to `ResourcePermission<'catalog-entity'>`. This matches the runtime behavior that was in place before, but you'll notice that the type of `request.permission` has changed within the scope of that `if` statement.
|
||||
|
||||
The `catalogConditions` object contains all of the rules defined by the catalog plugin. These rules can be combined to form a [`PermissionCriteria`](https://backstage.io/docs/reference/plugin-permission-common.permissioncriteria) object, but for this case we only need to use the `isEntityOwner` rule. This rule accepts a list of entity refs that represent User identity and Group membership used to determine ownership. The second argument to `PermissionPolicy#handle` provides us with a `PolicyQueryUser` object, from which we can grab the user's `ownershipEntityRefs`. We provide an empty array as a fallback since the user may be anonymous.
|
||||
|
||||
You should now be able to see in your Backstage app that the unregister entity button is enabled for entities that you own, but disabled for all other entities!
|
||||
|
||||
## Resource types
|
||||
|
||||
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.
|
||||
|
||||
```ts
|
||||
import {
|
||||
AuthorizeResult,
|
||||
PolicyDecision,
|
||||
/* highlight-remove-next-line */
|
||||
isPermission,
|
||||
isResourcePermission,
|
||||
/* highlight-add-next-line */
|
||||
} from '@backstage/plugin-permission-common';
|
||||
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?: PolicyQueryUser,
|
||||
): Promise<PolicyDecision> {
|
||||
/* 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({
|
||||
claims: user?.info.ownershipEntityRefs ?? [],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
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!
|
||||
|
||||
_Note:_ Some catalog permissions do not have the `'catalog-entity'` resource type, such as [`catalogEntityCreatePermission`](https://github.com/backstage/backstage/blob/1e5e9fb9de9856a49e60fc70c38a4e4e94c69570/plugins/catalog-common/src/permissions.ts#L49). In those cases, a definitive decision is required because conditions can't be applied to an entity that does not exist yet.
|
||||
@@ -5,7 +5,7 @@ description: How to write your own permission policy as a Backstage integrator
|
||||
---
|
||||
|
||||
:::info
|
||||
This documentation is written for [the new backend system](../backend-system/index.md) which is the default since Backstage [version 1.24](../releases/v1.24.0.md). If you are still on the old backend system, you may want to read [its own article](./writing-a-policy--old.md) instead, and [consider migrating](../backend-system/building-backends/08-migrating.md)!
|
||||
This documentation is written for [the new backend system](../backend-system/index.md) which is the default since Backstage [version 1.24](../releases/v1.24.0.md). If you are still on the old backend system, you may want to read [its own article](https://github.com/backstage/backstage/blob/v1.37.0/docs/permissions/writing-a-policy--old.md) instead, and [consider migrating](../backend-system/building-backends/08-migrating.md)!
|
||||
:::
|
||||
|
||||
In the [previous section](./getting-started.md), we were able to set up the permission framework and make a simple change to our `TestPermissionPolicy` to confirm that policy is indeed wired up correctly.
|
||||
|
||||
Reference in New Issue
Block a user