From 3eb2b178ee87a41ad936510b28a14e552a10b0ba Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Sat, 27 Jul 2024 09:16:06 -0500 Subject: [PATCH] [NBS Docs] Refactor Top Level Permissions Docs Signed-off-by: Andre Wanlin --- docs/permissions/custom-rules--old.md | 165 +++++++++++++++++++++ docs/permissions/custom-rules.md | 76 +++++++--- docs/permissions/getting-started--new.md | 36 ----- docs/permissions/getting-started--old.md | 169 ++++++++++++++++++++++ docs/permissions/getting-started.md | 160 +++++++------------- docs/permissions/writing-a-policy--old.md | 148 +++++++++++++++++++ docs/permissions/writing-a-policy.md | 53 ++++++- 7 files changed, 634 insertions(+), 173 deletions(-) create mode 100644 docs/permissions/custom-rules--old.md delete mode 100644 docs/permissions/getting-started--new.md create mode 100644 docs/permissions/getting-started--old.md create mode 100644 docs/permissions/writing-a-policy--old.md diff --git a/docs/permissions/custom-rules--old.md b/docs/permissions/custom-rules--old.md new file mode 100644 index 0000000000..ee3cac09f2 --- /dev/null +++ b/docs/permissions/custom-rules--old.md @@ -0,0 +1,165 @@ +--- +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 { + 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 { + 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 diff --git a/docs/permissions/custom-rules.md b/docs/permissions/custom-rules.md index 57565755fb..46cc36a38f 100644 --- a/docs/permissions/custom-rules.md +++ b/docs/permissions/custom-rules.md @@ -4,19 +4,23 @@ title: Defining custom permission rules 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)! +::: + 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`. +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/extensions/permissionsPolicyExtension.ts`. -We use Zod in our example below. To install, run: +We use `zod` and `@backstage/catalog-model` in our example below. To install them run: -```bash -yarn workspace backend add zod +```bash title="from your Backstage root directory" +yarn --cwd packages/backend add zod @backstage/catalog-model ``` -```typescript title="packages/backend/src/plugins/permission.ts" +```ts title="packages/backend/src/extensions/permissionsPolicyExtension.ts" ... import type { Entity } from '@backstage/catalog-model'; @@ -55,9 +59,9 @@ 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`. +Still in the `packages/backend/src/extensions/permissionsPolicyExtension.ts` file, let's use the condition we just created in our `CustomPermissionPolicy`. -```ts title="packages/backend/src/plugins/permission.ts" +```ts title="packages/backend/src/extensions/permissionsPolicyExtension.ts" ... /* highlight-remove-next-line */ import { createCatalogPermissionRule } from '@backstage/plugin-catalog-backend/alpha'; @@ -98,7 +102,7 @@ export const isInSystemRule = createCatalogPermissionRule({ const isInSystem = createConditionFactory(isInSystemRule); -class TestPermissionPolicy implements PermissionPolicy { +class CustomPermissionPolicy implements PermissionPolicy { async handle( request: PolicyQuery, user?: PolicyQueryUser, @@ -135,25 +139,49 @@ class TestPermissionPolicy implements PermissionPolicy { 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`. +The api for providing custom rules may differ between plugins, but there should typically be some extension point that you can use to create a module that adds your rule. For the catalog, this extension point is exposed via `catalogPermissionExtensionPoint`. Here's the steps you'll need to take to add the `isInSystemRule` we created above to the catalog: -```typescript title="packages/backend/src/plugins/catalog.ts" -import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; -/* highlight-add-next-line */ -import { isInSystemRule } from './permission'; +1. We will be using the `@backstage/plugin-catalog-node` package as it contains the extension point we need. Run this to add it: -... + ```bash title="from your Backstage root directory" + yarn --cwd packages/backend add @backstage/plugin-catalog-node + ``` -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - const builder = await CatalogBuilder.create(env); - /* highlight-add-next-line */ - builder.addPermissionRules(isInSystemRule); - ... - return router; -} -``` +2. Next create a `catalogPermissionRules.ts` file in the `packages/backend/src/extensions` folder. +3. Then add this as the contents of the new `catalogPermissionRules.ts` file: + + ```typescript title="packages/backend/src/extensions/catalogPermissionRules.ts" + import { createBackendModule } from '@backstage/backend-plugin-api'; + import { catalogPermissionExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; + import { isInSystemRule } from './permissionPolicyExtension'; + + export default createBackendModule({ + pluginId: 'catalog', + moduleId: 'permission-rules', + register(reg) { + reg.registerInit({ + deps: { catalog: catalogPermissionExtensionPoint }, + async init({ catalog }) { + catalog.addPermissionRules(isInSystemRule); + }, + }); + }, + }); + ``` + +4. Next we need to add this to the backend by adding the following line: + + ```ts title="packages/backend/src/index.ts" + // catalog plugin + backend.add(import('@backstage/plugin-catalog-backend/alpha')); + backend.add( + import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'), + ); + /* highlight-add-next-line */ + backend.add(import('./extensions/catalogPermissionRules')); + ``` + +5. Now when you run you Backstage instance - `yarn dev` - the rule will be added to the catalog plugin. The updated policy will allow catalog entity resource permissions if any of the following are true: diff --git a/docs/permissions/getting-started--new.md b/docs/permissions/getting-started--new.md deleted file mode 100644 index b6198a222f..0000000000 --- a/docs/permissions/getting-started--new.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -id: getting-started--new -title: Getting Started -description: How to get started with the permission framework as an integrator ---- - -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 - -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! - -### 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. - -## Enable and test the permissions system - -All you need to do now is enable the permissions system in your Backstage instance! - -1. Set the property `permission.enabled` to `true` in `app-config.yaml`. - -```yaml title="app-config.yaml" -permission: - enabled: true -``` - -Congratulations! Now that the framework is 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)! diff --git a/docs/permissions/getting-started--old.md b/docs/permissions/getting-started--old.md new file mode 100644 index 0000000000..af6bb2d2e0 --- /dev/null +++ b/docs/permissions/getting-started--old.md @@ -0,0 +1,169 @@ +--- +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: + + + +:::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 { + return { result: AuthorizeResult.ALLOW }; + } + } + + export default async function createPlugin( + env: PluginEnvironment, + ): Promise { + 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 { + /* highlight-add-start */ + async handle(request: PolicyQuery): Promise { + 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. + +![Entity detail page showing disabled unregister entity context menu entry](../assets/permissions/disabled-unregister-entity.png) + +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)! diff --git a/docs/permissions/getting-started.md b/docs/permissions/getting-started.md index e8132e571b..a94d11f1dd 100644 --- a/docs/permissions/getting-started.md +++ b/docs/permissions/getting-started.md @@ -4,14 +4,8 @@ title: Getting Started description: How to get started with the permission framework as an integrator --- -If you prefer to watch a video instead, you can start with this video introduction: - - - -:::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. - +:::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)! ::: 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. @@ -22,13 +16,7 @@ The permissions framework depends on a few other Backstage systems, which must b ### 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). +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! ### Supply an identity resolver to populate group membership on sign in @@ -38,33 +26,40 @@ Like many other parts of Backstage, the permissions framework relies on informat [The IdentityResolver docs](../auth/identity-resolver.md) describe the process for resolving group membership on sign in. -## Optionally add cookie-based authentication +## Test Permission Policy -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. +To get help validate the permission framework is setup we'll create a Test Permission Policy: -## Integrating the permission framework with your Backstage instance +1. Backstage ships with a default Allow All Policy, we want to remove that as it would override our Test Permission Policy. To do this remove the following line: -### 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 + ```ts title="packages/backend/src/index.ts" + // permission plugin + backend.add(import('@backstage/plugin-permission-backend/alpha')); + /* highlight-remove-start */ + backend.add( + import('@backstage/plugin-permission-backend-module-allow-all-policy'), + ); + /* highlight-remove-end */ ``` -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. +2. Now we need to add the `@backstage/backend-plugin-api` package: - ```typescript title="packages/backend/src/plugins/permission.ts" - import { createRouter } from '@backstage/plugin-permission-backend'; + ```bash title="from your Backstage root directory" + yarn --cwd packages/backend add @backstage/backend-plugin-api + ``` + +3. Next we will create an `extensions` folder under `packages/backend/src` +4. In this new `extensions` folder we will add a new file called: `permissionsPolicyExtension.ts` +5. Copy the following into the new `permissionsPolicyExtension.ts` file: + + ```ts title="packages/backend/src/extensions/permissionsPolicyExtension.ts" + import { createBackendModule } from '@backstage/backend-plugin-api'; import { - AuthorizeResult, PolicyDecision, + AuthorizeResult, } from '@backstage/plugin-permission-common'; import { PermissionPolicy } from '@backstage/plugin-permission-node'; - import { Router } from 'express'; - import { PluginEnvironment } from '../types'; + import { policyExtensionPoint } from '@backstage/plugin-permission-node/alpha'; class TestPermissionPolicy implements PermissionPolicy { async handle(): Promise { @@ -72,48 +67,34 @@ The permissions framework uses a new `permission-backend` plugin to accept autho } } - export default async function createPlugin( - env: PluginEnvironment, - ): Promise { - return await createRouter({ - config: env.config, - logger: env.logger, - discovery: env.discovery, - policy: new TestPermissionPolicy(), - identity: env.identity, - }); - } + export default createBackendModule({ + pluginId: 'permission', + moduleId: 'permission-policy', + register(reg) { + reg.registerInit({ + deps: { policy: policyExtensionPoint }, + async init({ policy }) { + policy.setPolicy(new TestPermissionPolicy()); + }, + }); + }, + }); ``` -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: +6. We now need to register this in the backend. We will do this by adding the follow line: ```ts title="packages/backend/src/index.ts" - import proxy from './plugins/proxy'; - import techdocs from './plugins/techdocs'; - import search from './plugins/search'; + // permission plugin + backend.add(import('@backstage/plugin-permission-backend/alpha')); /* 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)); - // .. - } + backend.add(import('./extensions/permissionPolicyExtension')); ``` -### 2. Enable and test the permissions system +You now have a Test Permission Policy in place, this will help us test that the permission framework is working in the next section. -Now that the permission backend is running, it’s time to enable the permissions framework and make sure it’s working properly. +## Enable and test the permissions system + +Now lets test end to end that the permissions framework is setup and configured properly we will use the Test Permission Policy we create above as is, then modify it do deny access which will confirm everything is working as expected. Here's how to do that: 1. Set the property `permission.enabled` to `true` in `app-config.yaml`. @@ -122,44 +103,11 @@ Now that the permission backend is running, it’s time to enable the 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: +2. Now run `yarn dev`, Backstage should load up in your browser +3. You should see that you have entities in your Catalog, pretty simple +4. Let's change this line in our Test Permission Policy `return { result: AuthorizeResult.ALLOW };` to be `return { result: AuthorizeResult.DENY };` +5. Run `yarn dev` once again, Backstage should load up in your browser +6. This time you should not see any entities in your Catalog, if you do then something went wrong along the way and you'll need to review the steps above +7. Revert the change we made in step 4 so that the line looks like this: `return { result: AuthorizeResult.ALLOW };` - ```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 { - /* highlight-add-start */ - async handle(request: PolicyQuery): Promise { - 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. - -![Entity detail page showing disabled unregister entity context menu entry](../assets/permissions/disabled-unregister-entity.png) - -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)! +Congratulations! Now that the framework is fully configured, you can craft a permission policy that works best for your organization by [writing your own policy](./writing-a-policy.md)! diff --git a/docs/permissions/writing-a-policy--old.md b/docs/permissions/writing-a-policy--old.md new file mode 100644 index 0000000000..b85df244ef --- /dev/null +++ b/docs/permissions/writing-a-policy--old.md @@ -0,0 +1,148 @@ +--- +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 { + 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 { + /* highlight-add-start */ + async handle( + request: PolicyQuery, + user?: PolicyQueryUser, + ): Promise { + /* 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 { + /* 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. diff --git a/docs/permissions/writing-a-policy.md b/docs/permissions/writing-a-policy.md index 0f2d4fe091..ea2f537244 100644 --- a/docs/permissions/writing-a-policy.md +++ b/docs/permissions/writing-a-policy.md @@ -4,16 +4,40 @@ title: Writing a permission policy 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)! +::: + 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" +```ts title="packages/backend/src/extensions/permissionsPolicyExtension.ts" class TestPermissionPolicy implements PermissionPolicy { - async handle( - request: PolicyQuery, - _user?: PolicyQueryUser, - ): Promise { + async handle(): Promise { + return { result: AuthorizeResult.ALLOW }; + } +} +``` + +That is a very simple example and it's not really doing anything helpful, let's expand this a little more. + +First, let's rename this from `TestPermissionPolicy` to `CustomPermissionPolicy` as you'll build on adding to it as your permissions needs require. Then we'll add a check for a permission. Here's what the full `permissionsPolicyExtension.ts` will look like: + +```ts title="packages/backend/src/extensions/permissionsPolicyExtension.ts" +import { createBackendModule } from '@backstage/backend-plugin-api'; +import { + PolicyDecision, + AuthorizeResult, +} from '@backstage/plugin-permission-common'; +import { + PermissionPolicy, + PolicyQuery, +} from '@backstage/plugin-permission-node'; +import { policyExtensionPoint } from '@backstage/plugin-permission-node/alpha'; + +class CustomPermissionPolicy implements PermissionPolicy { + async handle(request: PolicyQuery): Promise { if (request.permission.name === 'catalog.entity.delete') { return { result: AuthorizeResult.DENY, @@ -23,8 +47,23 @@ class TestPermissionPolicy implements PermissionPolicy { return { result: AuthorizeResult.ALLOW }; } } + +export default createBackendModule({ + pluginId: 'permission', + moduleId: 'permission-policy', + register(reg) { + reg.registerInit({ + deps: { policy: policyExtensionPoint }, + async init({ policy }) { + policy.setPolicy(new CustomPermissionPolicy()); + }, + }); + }, +}); ``` +Now with this policy in place the ability to delete entities in the Catalog is not allowed for anyone. The following sections will expand on the concepts used here. + ## 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). @@ -54,7 +93,7 @@ import { } from '@backstage/plugin-catalog-common/alpha'; /* highlight-add-end */ -class TestPermissionPolicy implements PermissionPolicy { +class CustomPermissionPolicy implements PermissionPolicy { /* highlight-remove-next-line */ async handle(request: PolicyQuery): Promise { /* highlight-add-start */ @@ -117,7 +156,7 @@ import { } from '@backstage/plugin-catalog-common/alpha'; /* highlight-remove-end */ -class TestPermissionPolicy implements PermissionPolicy { +class CustomPermissionPolicy implements PermissionPolicy { async handle( request: PolicyQuery, user?: PolicyQueryUser,