permission-node: update policy handler to work with new auth system

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2024-07-12 13:39:40 +02:00
parent 1a966daca7
commit ed10fd202c
15 changed files with 127 additions and 66 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-permission-backend-module-allow-all-policy': patch
---
Internal refactor to use new `PolicyQueryUser` type.
+11
View File
@@ -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`.
@@ -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<PolicyDecision> {
// 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
+2 -2
View File
@@ -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<PolicyDecision> {
if (request.permission.name === 'kubernetes.proxy') {
return {
@@ -66,14 +66,14 @@ import {
class ExamplePermissionPolicy implements PermissionPolicy {
async handle(
request: PolicyQuery,
user?: BackstageIdentityResponse,
user?: PolicyQueryUser,
): Promise<PolicyDecision> {
/* 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<PolicyDecision> {
/* 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<PolicyDecision> {
/* 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<PolicyDecision> {
/* 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<PolicyDecision> {
// Various scaffolder permission checks ...
+4 -5
View File
@@ -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<PolicyDecision> {
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' }),
],
@@ -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<PolicyDecision> {
if (isPermission(request.permission, todoListCreatePermission)) {
return {
@@ -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<PolicyDecision> {
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 ?? '',
}),
);
}
+9 -14
View File
@@ -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<PolicyDecision> {
async handle(
request: PolicyQuery,
_user?: PolicyQueryUser,
): Promise<PolicyDecision> {
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<PolicyDecision> {
/* 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<PolicyDecision> {
/* 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 ?? [],
}),
);
}
@@ -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<PolicyDecision> {
return {
result: AuthorizeResult.ALLOW,
@@ -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({
@@ -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,
};
}
+13 -5
View File
@@ -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<PolicyDecision>;
handle(request: PolicyQuery, user?: PolicyQueryUser): Promise<PolicyDecision>;
}
// @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)
+1 -1
View File
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export type { PermissionPolicy, PolicyQuery } from './types';
export type { PermissionPolicy, PolicyQuery, PolicyQueryUser } from './types';
+44 -5
View File
@@ -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<PolicyDecision>;
handle(request: PolicyQuery, user?: PolicyQueryUser): Promise<PolicyDecision>;
}