diff --git a/.changeset/brown-rats-judge.md b/.changeset/brown-rats-judge.md new file mode 100644 index 0000000000..0076c65d77 --- /dev/null +++ b/.changeset/brown-rats-judge.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-permission-backend-module-allow-all-policy': patch +--- + +Internal refactor to use new `PolicyQueryUser` type. diff --git a/.changeset/eleven-foxes-sip.md b/.changeset/eleven-foxes-sip.md new file mode 100644 index 0000000000..8b21ddb20f --- /dev/null +++ b/.changeset/eleven-foxes-sip.md @@ -0,0 +1,11 @@ +--- +'@backstage/plugin-permission-backend': patch +'@backstage/plugin-permission-node': patch +--- + +The `PermissionPolicy` interface has been updated to align with the recent changes to the Backstage auth system. The second argument to the `handle` method is now of the new `PolicyQueryUser` type. This type maintains the old fields from the `BackstageIdentityResponse`, which are now all deprecated. Instead, two new fields have been added, which allows access to the same information: + +- `credentials` - A `BackstageCredentials` object, which is useful for making requests to other services on behalf of the user as part of evaluating the policy. This replaces the deprecated `token` field. See the [Auth Service documentation](https://backstage.io/docs/backend-system/core-services/auth#creating-request-tokens) for information about how to create a token using these credentials. +- `info` - A `BackstageUserInfo` object, which contains the same information as the deprecated `identity`, except for the `type` field that was redundant. + +Most existing policies can be updated by replacing the `BackstageIdentityResponse` type with `PolicyQueryUser`, which is exported from `@backstage/plugin-permission-node`, as well as replacing any occurrences of `user?.identity` with `user?.info`. diff --git a/docs/backend-system/building-backends/08-migrating.md b/docs/backend-system/building-backends/08-migrating.md index d38acb695e..c6db44078e 100644 --- a/docs/backend-system/building-backends/08-migrating.md +++ b/docs/backend-system/building-backends/08-migrating.md @@ -1254,7 +1254,6 @@ In order to add your own permission policy you'll need to do the following: ```ts import { createBackendModule } from '@backstage/backend-plugin-api'; -import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; import { PolicyDecision, AuthorizeResult, @@ -1262,13 +1261,14 @@ import { import { PermissionPolicy, PolicyQuery, + PolicyQueryUser, } from '@backstage/plugin-permission-node'; import { policyExtensionPoint } from '@backstage/plugin-permission-node/alpha'; class CustomPermissionPolicy implements PermissionPolicy { async handle( request: PolicyQuery, - user?: BackstageIdentityResponse, + user?: PolicyQueryUser, ): Promise { // TODO: Add code here that inspects the incoming request and user, and returns AuthorizeResult.ALLOW, AuthorizeResult.DENY, or AuthorizeResult.CONDITIONAL as needed. See the docs at https://backstage.io/docs/permissions/writing-a-policy for more information diff --git a/docs/features/kubernetes/proxy.md b/docs/features/kubernetes/proxy.md index 2da797d8ef..85ad86f83b 100644 --- a/docs/features/kubernetes/proxy.md +++ b/docs/features/kubernetes/proxy.md @@ -60,7 +60,6 @@ This feature assumes your backstage instance has enabled the [permissions framew A sample policy like: ```typescript -import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; import { AuthorizeResult, PolicyDecision, @@ -68,12 +67,13 @@ import { import { PermissionPolicy, PolicyQuery, + PolicyQueryUser, } from '@backstage/plugin-permission-node'; class KubernetesDenyAllProxyEndpointPolicy implements PermissionPolicy { async handle( request: PolicyQuery, - user?: BackstageIdentityResponse, + user?: PolicyQueryUser, ): Promise { if (request.permission.name === 'kubernetes.proxy') { return { diff --git a/docs/features/software-templates/authorizing-scaffolder-template-details.md b/docs/features/software-templates/authorizing-scaffolder-template-details.md index 127677757c..942431c993 100644 --- a/docs/features/software-templates/authorizing-scaffolder-template-details.md +++ b/docs/features/software-templates/authorizing-scaffolder-template-details.md @@ -66,14 +66,14 @@ import { class ExamplePermissionPolicy implements PermissionPolicy { async handle( request: PolicyQuery, - user?: BackstageIdentityResponse, + user?: PolicyQueryUser, ): Promise { /* highlight-add-start */ if ( isPermission(request.permission, templateParameterReadPermission) || isPermission(request.permission, templateStepReadPermission) ) { - if (user?.identity.userEntityRef === 'user:default/spiderman') + if (user?.info.userEntityRef === 'user:default/spiderman') return createScaffolderTemplateConditionalDecision(request.permission, { not: scaffolderTemplateConditions.hasTag({ tag: 'secret' }), }); @@ -109,11 +109,11 @@ import { class ExamplePermissionPolicy implements PermissionPolicy { async handle( request: PolicyQuery, - user?: BackstageIdentityResponse, + user?: PolicyQueryUser, ): Promise { /* highlight-add-start */ if (isPermission(request.permission, actionExecutePermission)) { - if (user?.identity.userEntityRef === 'user:default/spiderman') { + if (user?.info.userEntityRef === 'user:default/spiderman') { return createScaffolderActionConditionalDecision(request.permission, { not: scaffolderActionConditions.hasActionId({ actionId: 'debug:log', @@ -147,11 +147,11 @@ import { class ExamplePermissionPolicy implements PermissionPolicy { async handle( request: PolicyQuery, - user?: BackstageIdentityResponse, + user?: PolicyQueryUser, ): Promise { /* highlight-add-start */ if (isPermission(request.permission, actionExecutePermission)) { - if (user?.identity.userEntityRef === 'user:default/spiderman') { + if (user?.info.userEntityRef === 'user:default/spiderman') { return createScaffolderActionConditionalDecision(request.permission, { not: { allOf: [ @@ -190,25 +190,25 @@ import { class ExamplePermissionPolicy implements PermissionPolicy { async handle( request: PolicyQuery, - user?: BackstageIdentityResponse, + user?: PolicyQueryUser, ): Promise { /* highlight-add-start */ if (isPermission(request.permission, taskCreatePermission)) { - if (user?.identity.userEntityRef === 'user:default/spiderman') { + if (user?.info.userEntityRef === 'user:default/spiderman') { return { result: AuthorizeResult.ALLOW, }; } } if (isPermission(request.permission, taskCancelPermission)) { - if (user?.identity.userEntityRef === 'user:default/spiderman') { + if (user?.info.userEntityRef === 'user:default/spiderman') { return { result: AuthorizeResult.ALLOW, }; } } if (isPermission(request.permission, taskReadPermission)) { - if (user?.identity.userEntityRef === 'user:default/spiderman') { + if (user?.info.userEntityRef === 'user:default/spiderman') { return { result: AuthorizeResult.ALLOW, }; @@ -239,7 +239,6 @@ Instead of the changes in `permission.ts` noted in the above example you will ma ```ts title="packages/backend/src/index.ts" import { createBackendModule } from '@backstage/backend-plugin-api'; -import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; import { PolicyDecision, AuthorizeResult, @@ -247,13 +246,14 @@ import { import { PermissionPolicy, PolicyQuery, + PolicyQueryUser, } from '@backstage/plugin-permission-node'; import { policyExtensionPoint } from '@backstage/plugin-permission-node/alpha'; class ExamplePermissionPolicy implements PermissionPolicy { async handle( request: PolicyQuery, - user?: BackstageIdentityResponse, + user?: PolicyQueryUser, ): Promise { // Various scaffolder permission checks ... diff --git a/docs/permissions/custom-rules.md b/docs/permissions/custom-rules.md index 9063ed7257..57565755fb 100644 --- a/docs/permissions/custom-rules.md +++ b/docs/permissions/custom-rules.md @@ -66,9 +66,8 @@ import { catalogConditions, createCatalogConditionalDecision, createCatalogPermi /* highlight-remove-next-line */ import { createConditionFactory } from '@backstage/plugin-permission-node'; /* highlight-add-next-line */ -import { PermissionPolicy, PolicyQuery, createConditionFactory } from '@backstage/plugin-permission-node'; +import { PermissionPolicy, PolicyQuery, PolicyQueryUser, createConditionFactory } from '@backstage/plugin-permission-node'; /* highlight-add-start */ -import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; import { AuthorizeResult, PolicyDecision, isResourcePermission } from '@backstage/plugin-permission-common'; /* highlight-add-end */ ... @@ -102,21 +101,21 @@ const isInSystem = createConditionFactory(isInSystemRule); class TestPermissionPolicy implements PermissionPolicy { async handle( request: PolicyQuery, - user?: BackstageIdentityResponse, + user?: PolicyQueryUser, ): Promise { if (isResourcePermission(request.permission, 'catalog-entity')) { return createCatalogConditionalDecision( request.permission, /* highlight-remove-start */ catalogConditions.isEntityOwner({ - claims: user?.identity.ownershipEntityRefs ?? [], + claims: user?.info.ownershipEntityRefs ?? [], }), /* highlight-remove-end */ /* highlight-add-start */ { anyOf: [ catalogConditions.isEntityOwner({ - claims: user?.identity.ownershipEntityRefs ?? [], + claims: user?.info.ownershipEntityRefs ?? [], }), isInSystem({ systemRef: 'interviewing' }), ], diff --git a/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md b/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md index aacd60af8c..724edb7ccb 100644 --- a/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md +++ b/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md @@ -169,15 +169,12 @@ Before running this step, please make sure you followed the steps described in [ In order to test the logic above, the integrators of your backstage instance need to change their permission policy to return `DENY` for our newly-created permission: ```ts title="packages/backend/src/plugins/permission.ts" -/* highlight-add-start */ -import { - BackstageIdentityResponse, -} from '@backstage/plugin-auth-node'; -/* highlight-add-end */ import { PermissionPolicy, - /* highlight-add-next-line */ + /* highlight-add-start */ PolicyQuery, + PolicyQueryUser, + /* highlight-add-end */ } from '@backstage/plugin-permission-node'; /* highlight-add-start */ import { isPermission } from '@backstage/plugin-permission-common'; @@ -190,7 +187,7 @@ class TestPermissionPolicy implements PermissionPolicy { /* highlight-add-start */ async handle( request: PolicyQuery, - _user?: BackstageIdentityResponse, + _user?: PolicyQueryUser, ): Promise { if (isPermission(request.permission, todoListCreatePermission)) { return { diff --git a/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md b/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md index b3aeb13a68..7ad551e926 100644 --- a/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md +++ b/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md @@ -237,12 +237,12 @@ Let's go back to the permission policy's handle function and try to authorize ou ```ts title="packages/backend/src/plugins/permission.ts" import { - BackstageIdentityResponse, 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 */ @@ -262,9 +262,9 @@ import { async handle( request: PolicyQuery, /* highlight-remove-next-line */ - _user?: BackstageIdentityResponse, + _user?: PolicyQueryUser, /* highlight-add-next-line */ - user?: BackstageIdentityResponse, + user?: PolicyQueryUser, ): Promise { if (isPermission(request.permission, todoListCreatePermission)) { return { @@ -276,7 +276,7 @@ async handle( return createTodoListConditionalDecision( request.permission, todoListConditions.isOwner({ - userId: user?.identity.userEntityRef ?? '', + userId: user?.info.userEntityRef ?? '', }), ); } diff --git a/docs/permissions/writing-a-policy.md b/docs/permissions/writing-a-policy.md index 3d4371f038..0f2d4fe091 100644 --- a/docs/permissions/writing-a-policy.md +++ b/docs/permissions/writing-a-policy.md @@ -10,7 +10,10 @@ That policy looked like this: ```typescript title="packages/backend/src/plugins/permission.ts" class TestPermissionPolicy implements PermissionPolicy { - async handle(request: PolicyQuery): Promise { + async handle( + request: PolicyQuery, + _user?: PolicyQueryUser, + ): Promise { if (request.permission.name === 'catalog.entity.delete') { return { result: AuthorizeResult.DENY, @@ -35,14 +38,6 @@ As we confirmed in the previous section, we know that this now prevents us from Let's change the policy to the following: ```ts -/* highlight-remove-next-line */ -import { IdentityClient } from '@backstage/plugin-auth-node'; -/* highlight-add-start */ -import { - BackstageIdentityResponse, - IdentityClient -} from '@backstage/plugin-auth-node'; - /* highlight-add-end */ import { AuthorizeResult, PolicyDecision, @@ -65,7 +60,7 @@ class TestPermissionPolicy implements PermissionPolicy { /* highlight-add-start */ async handle( request: PolicyQuery, - user?: BackstageIdentityResponse, + user?: PolicyQueryUser, ): Promise { /* highlight-add-end */ /* highlight-remove-next-line */ @@ -81,7 +76,7 @@ class TestPermissionPolicy implements PermissionPolicy { return createCatalogConditionalDecision( request.permission, catalogConditions.isEntityOwner({ - claims: user?.identity.ownershipEntityRefs ?? [], + claims: user?.info.ownershipEntityRefs ?? [], }), ); /* highlight-add-end */ @@ -95,7 +90,7 @@ 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 `BackstageIdentityResponse` object, from which we can grab the user's `ownershipEntityRefs`. We provide an empty array as a fallback since the user may be anonymous. +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! @@ -125,7 +120,7 @@ import { class TestPermissionPolicy implements PermissionPolicy { async handle( request: PolicyQuery, - user?: BackstageIdentityResponse, + user?: PolicyQueryUser, ): Promise { /* highlight-remove-next-line */ if (isPermission(request.permission, catalogEntityDeletePermission)) { @@ -134,7 +129,7 @@ class TestPermissionPolicy implements PermissionPolicy { return createCatalogConditionalDecision( request.permission, catalogConditions.isEntityOwner({ - claims: user?.identity.ownershipEntityRefs ?? [], + claims: user?.info.ownershipEntityRefs ?? [], }), ); } diff --git a/plugins/permission-backend-module-policy-allow-all/src/policy.ts b/plugins/permission-backend-module-policy-allow-all/src/policy.ts index da853e47db..90ae21f341 100644 --- a/plugins/permission-backend-module-policy-allow-all/src/policy.ts +++ b/plugins/permission-backend-module-policy-allow-all/src/policy.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; import { AuthorizeResult, PolicyDecision, @@ -22,12 +21,13 @@ import { import { PermissionPolicy, PolicyQuery, + PolicyQueryUser, } from '@backstage/plugin-permission-node'; export class AllowAllPermissionPolicy implements PermissionPolicy { async handle( _request: PolicyQuery, - _user?: BackstageIdentityResponse, + _user?: PolicyQueryUser, ): Promise { return { result: AuthorizeResult.ALLOW, diff --git a/plugins/permission-backend/src/service/router.test.ts b/plugins/permission-backend/src/service/router.test.ts index be90c5f628..f12bacc61d 100644 --- a/plugins/permission-backend/src/service/router.test.ts +++ b/plugins/permission-backend/src/service/router.test.ts @@ -186,6 +186,13 @@ describe('createRouter', () => { mockCredentials.user().principal.userEntityRef, ], }, + info: { + userEntityRef: mockCredentials.user().principal.userEntityRef, + ownershipEntityRefs: [ + mockCredentials.user().principal.userEntityRef, + ], + }, + credentials: mockCredentials.user(), }, ); expect(response.body).toEqual({ diff --git a/plugins/permission-backend/src/service/router.ts b/plugins/permission-backend/src/service/router.ts index cc45d0b41a..fd33f0b629 100644 --- a/plugins/permission-backend/src/service/router.ts +++ b/plugins/permission-backend/src/service/router.ts @@ -22,10 +22,7 @@ import { errorHandler, } from '@backstage/backend-common'; import { InputError } from '@backstage/errors'; -import { - BackstageIdentityResponse, - IdentityApi, -} from '@backstage/plugin-auth-node'; +import { IdentityApi } from '@backstage/plugin-auth-node'; import { AuthorizeResult, EvaluatePermissionRequest, @@ -40,6 +37,7 @@ import { ApplyConditionsRequestEntry, ApplyConditionsResponseEntry, PermissionPolicy, + PolicyQueryUser, } from '@backstage/plugin-permission-node'; import { PermissionIntegrationClient } from './PermissionIntegrationClient'; import { memoize } from 'lodash'; @@ -130,9 +128,9 @@ const handleRequest = async ( ); }); - let user: BackstageIdentityResponse | undefined; + let user: PolicyQueryUser | undefined; if (auth.isPrincipal(credentials, 'user')) { - const { ownershipEntityRefs } = await userInfo.getUserInfo(credentials); + const info = await userInfo.getUserInfo(credentials); const { token } = await auth.getPluginRequestToken({ onBehalfOf: credentials, targetPluginId: 'catalog', // TODO: unknown at this point @@ -141,9 +139,11 @@ const handleRequest = async ( identity: { type: 'user', userEntityRef: credentials.principal.userEntityRef, - ownershipEntityRefs, + ownershipEntityRefs: info.ownershipEntityRefs, }, token, + credentials, + info, }; } diff --git a/plugins/permission-node/api-report.md b/plugins/permission-node/api-report.md index 290c586f2c..81460ef630 100644 --- a/plugins/permission-node/api-report.md +++ b/plugins/permission-node/api-report.md @@ -8,7 +8,9 @@ import { AnyOfCriteria } from '@backstage/plugin-permission-common'; import { AuthorizePermissionRequest } from '@backstage/plugin-permission-common'; import { AuthorizePermissionResponse } from '@backstage/plugin-permission-common'; import { AuthService } from '@backstage/backend-plugin-api'; -import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; +import { BackstageCredentials } from '@backstage/backend-plugin-api'; +import { BackstageUserIdentity } from '@backstage/plugin-auth-node'; +import { BackstageUserInfo } from '@backstage/backend-plugin-api'; import { ConditionalPolicyDecision } from '@backstage/plugin-permission-common'; import { Config } from '@backstage/config'; import { DefinitivePolicyDecision } from '@backstage/plugin-permission-common'; @@ -246,10 +248,7 @@ export type PermissionIntegrationRouterOptions< // @public export interface PermissionPolicy { // (undocumented) - handle( - request: PolicyQuery, - user?: BackstageIdentityResponse, - ): Promise; + handle(request: PolicyQuery, user?: PolicyQueryUser): Promise; } // @public @@ -272,6 +271,15 @@ export type PolicyQuery = { permission: Permission; }; +// @public +export type PolicyQueryUser = { + token: string; + expiresInSeconds?: number; + identity: BackstageUserIdentity; + credentials: BackstageCredentials; + info: BackstageUserInfo; +}; + // @public export class ServerPermissionClient implements PermissionsService { // (undocumented) diff --git a/plugins/permission-node/src/policy/index.ts b/plugins/permission-node/src/policy/index.ts index f151cfb4fb..fd47ed7e62 100644 --- a/plugins/permission-node/src/policy/index.ts +++ b/plugins/permission-node/src/policy/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export type { PermissionPolicy, PolicyQuery } from './types'; +export type { PermissionPolicy, PolicyQuery, PolicyQueryUser } from './types'; diff --git a/plugins/permission-node/src/policy/types.ts b/plugins/permission-node/src/policy/types.ts index 0f469cfbfe..a7b8b75218 100644 --- a/plugins/permission-node/src/policy/types.ts +++ b/plugins/permission-node/src/policy/types.ts @@ -18,7 +18,11 @@ import { Permission, PolicyDecision, } from '@backstage/plugin-permission-common'; -import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; +import { BackstageUserIdentity } from '@backstage/plugin-auth-node'; +import { + BackstageCredentials, + BackstageUserInfo, +} from '@backstage/backend-plugin-api'; /** * A query to be evaluated by the {@link PermissionPolicy}. @@ -34,6 +38,44 @@ export type PolicyQuery = { permission: Permission; }; +/** + * The context within which a policy query is evaluated. + * + * @public + */ +export type PolicyQueryUser = { + /** + * The token used to authenticate the user within Backstage. + * + * @deprecated User the `credentials` field in combination with `coreServices.auth` to generate a request token instead. + */ + token: string; + + /** + * The number of seconds until the token expires. If not set, it can be assumed that the token does not expire. + * + * @deprecated This field is deprecated and will be removed in a future release. + */ + expiresInSeconds?: number; + + /** + * A plaintext description of the identity that is encapsulated within the token. + * + * @deprecated Use the `info` field instead. + */ + identity: BackstageUserIdentity; + + /** + * The credentials of the user making the request. + */ + credentials: BackstageCredentials; + + /** + * The information for the user making the request. + */ + info: BackstageUserInfo; +}; + /** * A policy to evaluate authorization requests for any permissioned action performed in Backstage. * @@ -51,8 +93,5 @@ export type PolicyQuery = { * @public */ export interface PermissionPolicy { - handle( - request: PolicyQuery, - user?: BackstageIdentityResponse, - ): Promise; + handle(request: PolicyQuery, user?: PolicyQueryUser): Promise; }