From a9621deada91a63393393256f036392bd8f6ad30 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 30 Jan 2025 09:38:20 +0100 Subject: [PATCH 01/10] permission-node: add PermissionResourceRef Signed-off-by: Patrik Oldsberg --- .changeset/tidy-forks-pay.md | 5 ++ plugins/permission-node/report.api.md | 73 +++++++++++++++-- .../src/integration/createConditionExports.ts | 51 +++++++++++- .../src/integration/createConditionFactory.ts | 2 +- .../createPermissionIntegrationRouter.ts | 21 ++++- .../createPermissionResourceRef.ts | 56 +++++++++++++ .../src/integration/createPermissionRule.ts | 78 ++++++++++++++++++- .../permission-node/src/integration/index.ts | 4 + 8 files changed, 272 insertions(+), 18 deletions(-) create mode 100644 .changeset/tidy-forks-pay.md create mode 100644 plugins/permission-node/src/integration/createPermissionResourceRef.ts diff --git a/.changeset/tidy-forks-pay.md b/.changeset/tidy-forks-pay.md new file mode 100644 index 0000000000..ec9a99641b --- /dev/null +++ b/.changeset/tidy-forks-pay.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-permission-node': patch +--- + +Added a new `createPermissionResourceRef` utility that encapsulates the constants and types related to a permission resource types. The `createConditionExports` and `createPermissionRule` functions have also been adapted to accept these references as arguments, deprecating their older counterparts. diff --git a/plugins/permission-node/report.api.md b/plugins/permission-node/report.api.md index a55cbbf9c5..2bbc548202 100644 --- a/plugins/permission-node/report.api.md +++ b/plugins/permission-node/report.api.md @@ -83,7 +83,23 @@ export const createConditionAuthorizer: ( ) => (decision: PolicyDecision, resource: TResource | undefined) => boolean; // @public -export const createConditionExports: < +export function createConditionExports< + TResourceType extends string, + TResource, + TRules extends Record>, +>(options: { + resourceRef: PermissionResourceRef; + rules: TRules; +}): { + conditions: Conditions; + createConditionalDecision: ( + permission: ResourcePermission, + conditions: PermissionCriteria>, + ) => ConditionalPolicyDecision; +}; + +// @public @deprecated (undocumented) +export function createConditionExports< TResourceType extends string, TResource, TRules extends Record>, @@ -91,7 +107,7 @@ export const createConditionExports: < pluginId: string; resourceType: TResourceType; rules: TRules; -}) => { +}): { conditions: Conditions; createConditionalDecision: ( permission: ResourcePermission, @@ -164,15 +180,48 @@ export type CreatePermissionIntegrationRouterResourceOptions< ) => Promise>; }; +// @public (undocumented) +export function createPermissionResourceRef(): { + with(options: { + pluginId: TPluginId; + resourceType: TResourceType; + }): PermissionResourceRef; +}; + // @public -export const createPermissionRule: < +export function createPermissionRule< + TResource, + TQuery, + TResourceType extends string, + TParams extends PermissionRuleParams = undefined, +>( + rule: CreatePermissionRuleOptions, +): PermissionRule; + +// @public @deprecated +export function createPermissionRule< TResource, TQuery, TResourceType extends string, TParams extends PermissionRuleParams = undefined, >( rule: PermissionRule, -) => PermissionRule; +): PermissionRule; + +// @public (undocumented) +export type CreatePermissionRuleOptions< + TResource, + TQuery, + TResourceType extends string, + TParams extends PermissionRuleParams = PermissionRuleParams, +> = { + name: string; + description: string; + resourceRef: PermissionResourceRef; + paramsSchema?: z.ZodSchema; + apply(resource: TResource, params: NoInfer_2): boolean; + toQuery(params: NoInfer_2): PermissionCriteria; +}; // @public export const isAndCriteria: ( @@ -189,7 +238,7 @@ export const isOrCriteria: ( criteria: PermissionCriteria, ) => criteria is AnyOfCriteria; -// @public +// @public @deprecated export const makeCreatePermissionRule: < TResource, TQuery, @@ -253,6 +302,20 @@ export interface PermissionPolicy { handle(request: PolicyQuery, user?: PolicyQueryUser): Promise; } +// @public (undocumented) +export type PermissionResourceRef< + TResource = unknown, + TQuery = unknown, + TResourceType extends string = string, + TPluginId extends string = string, +> = { + readonly $$type: '@backstage/PermissionResourceRef'; + readonly pluginId: TPluginId; + readonly resourceType: TResourceType; + readonly TQuery: TQuery; + readonly TResource: TResource; +}; + // @public export type PermissionRule< TResource, diff --git a/plugins/permission-node/src/integration/createConditionExports.ts b/plugins/permission-node/src/integration/createConditionExports.ts index 1c82434add..768e009ca4 100644 --- a/plugins/permission-node/src/integration/createConditionExports.ts +++ b/plugins/permission-node/src/integration/createConditionExports.ts @@ -23,6 +23,7 @@ import { } from '@backstage/plugin-permission-common'; import { PermissionRule } from '../types'; import { createConditionFactory } from './createConditionFactory'; +import { PermissionResourceRef } from './createPermissionResourceRef'; /** * A utility type for mapping a single {@link PermissionRule} to its @@ -73,7 +74,25 @@ export type Conditions< * * @public */ -export const createConditionExports = < +export function createConditionExports< + TResourceType extends string, + TResource, + TRules extends Record>, +>(options: { + resourceRef: PermissionResourceRef; + rules: TRules; +}): { + conditions: Conditions; + createConditionalDecision: ( + permission: ResourcePermission, + conditions: PermissionCriteria>, + ) => ConditionalPolicyDecision; +}; +/** + * @public + * @deprecated Use the version of `createConditionExports` that accepts a `resourceRef` option instead. + */ +export function createConditionExports< TResourceType extends string, TResource, TRules extends Record>, @@ -87,8 +106,32 @@ export const createConditionExports = < permission: ResourcePermission, conditions: PermissionCriteria>, ) => ConditionalPolicyDecision; -} => { - const { pluginId, resourceType, rules } = options; +}; +export function createConditionExports< + TResourceType extends string, + TResource, + TRules extends Record>, +>( + options: + | { + resourceRef: PermissionResourceRef; + rules: TRules; + } + | { + pluginId: string; + resourceType: TResourceType; + rules: TRules; + }, +): { + conditions: Conditions; + createConditionalDecision: ( + permission: ResourcePermission, + conditions: PermissionCriteria>, + ) => ConditionalPolicyDecision; +} { + const { rules } = options; + const { pluginId, resourceType } = + 'resourceRef' in options ? options.resourceRef : options; return { conditions: Object.entries(rules).reduce( @@ -108,4 +151,4 @@ export const createConditionExports = < conditions, }), }; -}; +} diff --git a/plugins/permission-node/src/integration/createConditionFactory.ts b/plugins/permission-node/src/integration/createConditionFactory.ts index 7f8cb1ced8..e2de9dd90d 100644 --- a/plugins/permission-node/src/integration/createConditionFactory.ts +++ b/plugins/permission-node/src/integration/createConditionFactory.ts @@ -29,7 +29,7 @@ import { PermissionRule } from '../types'; * The rule itself defines _how_ to check a given resource, whereas a condition also includes _what_ * to verify. * - * Plugin authors should generally use the {@link createConditionExports} in order to efficiently + * Plugin authors should generally use the {@link (createConditionExports:1)} in order to efficiently * create multiple condition factories. This helper should generally only be used to construct * condition factories for third-party rules that aren't part of the backend plugin with which * they're intended to integrate. diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts index dbd7e45c74..37973176ed 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts @@ -39,6 +39,7 @@ import { isOrCriteria, } from './util'; import { NotImplementedError } from '@backstage/errors'; +import { PermissionResourceRef } from './createPermissionResourceRef'; const permissionCriteriaSchema: z.ZodSchema< PermissionCriteria @@ -163,10 +164,22 @@ const applyConditions = ( * * @public */ -export const createConditionAuthorizer = ( +export function createConditionAuthorizer( + permissionRuleAccessor: PermissionRuleAccessor, +): (decision: PolicyDecision, resource: TResource | undefined) => boolean; +/** + * @public + * @deprecated Use the version of `createConditionAuthorizer` that accepts a `PermissionRuleAccessor` instead. + */ +export function createConditionAuthorizer( rules: PermissionRule[], -) => { - const getRule = createGetRule(rules); +): (decision: PolicyDecision, resource: TResource | undefined) => boolean; +export function createConditionAuthorizer( + rules: + | PermissionRule[] + | PermissionRuleAccessor, +): (decision: PolicyDecision, resource: TResource | undefined) => boolean { + const getRule = typeof rules === 'function' ? rules : createGetRule(rules); return ( decision: PolicyDecision, @@ -178,7 +191,7 @@ export const createConditionAuthorizer = ( return decision.result === AuthorizeResult.ALLOW; }; -}; +} /** * Options for creating a permission integration router specific diff --git a/plugins/permission-node/src/integration/createPermissionResourceRef.ts b/plugins/permission-node/src/integration/createPermissionResourceRef.ts new file mode 100644 index 0000000000..1ef7dd9d29 --- /dev/null +++ b/plugins/permission-node/src/integration/createPermissionResourceRef.ts @@ -0,0 +1,56 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @public + */ +export type PermissionResourceRef< + TResource = unknown, + TQuery = unknown, + TResourceType extends string = string, + TPluginId extends string = string, +> = { + readonly $$type: '@backstage/PermissionResourceRef'; + readonly pluginId: TPluginId; + readonly resourceType: TResourceType; + readonly TQuery: TQuery; + readonly TResource: TResource; +}; + +/** + * @public + */ +export function createPermissionResourceRef(): { + with(options: { + pluginId: TPluginId; + resourceType: TResourceType; + }): PermissionResourceRef; +} { + return { + with(options: { + pluginId: TPluginId; + resourceType: TResourceType; + }): PermissionResourceRef { + return { + $$type: '@backstage/PermissionResourceRef', + pluginId: options.pluginId, + resourceType: options.resourceType, + TQuery: null as TQuery, + TResource: null as TResource, + }; + }, + }; +} diff --git a/plugins/permission-node/src/integration/createPermissionRule.ts b/plugins/permission-node/src/integration/createPermissionRule.ts index 44187e58a6..2e9833c32e 100644 --- a/plugins/permission-node/src/integration/createPermissionRule.ts +++ b/plugins/permission-node/src/integration/createPermissionRule.ts @@ -14,22 +14,91 @@ * limitations under the License. */ -import { PermissionRuleParams } from '@backstage/plugin-permission-common'; +import { + PermissionCriteria, + PermissionRuleParams, +} from '@backstage/plugin-permission-common'; import { PermissionRule } from '../types'; +import { z } from 'zod'; +import { PermissionResourceRef } from './createPermissionResourceRef'; +import { NoInfer } from './util'; /** - * Helper function to ensure that {@link PermissionRule} definitions are typed correctly. + * @public + */ +export type CreatePermissionRuleOptions< + TResource, + TQuery, + TResourceType extends string, + TParams extends PermissionRuleParams = PermissionRuleParams, +> = { + name: string; + description: string; + + resourceRef: PermissionResourceRef; + + /** + * A ZodSchema that reflects the structure of the parameters that are passed to + */ + paramsSchema?: z.ZodSchema; + + /** + * Apply this rule to a resource already loaded from a backing data source. The params are + * arguments supplied for the rule; for example, a rule could be `isOwner` with entityRefs as the + * params. + */ + apply(resource: TResource, params: NoInfer): boolean; + + /** + * Translate this rule to criteria suitable for use in querying a backing data store. The criteria + * can be used for loading a collection of resources efficiently with conditional criteria already + * applied. + */ + toQuery(params: NoInfer): PermissionCriteria; +}; + +/** + * Helper function to create a {@link PermissionRule} for a specific resource type using a {@link PermissionResourceRef}. * * @public */ -export const createPermissionRule = < +export function createPermissionRule< + TResource, + TQuery, + TResourceType extends string, + TParams extends PermissionRuleParams = undefined, +>( + rule: CreatePermissionRuleOptions, +): PermissionRule; +/** + * Helper function to ensure that {@link PermissionRule} definitions are typed correctly. + * + * @deprecated Use the version of `createPermissionRule` that accepts a `resourceRef` option instead. + * @public + */ +export function createPermissionRule< TResource, TQuery, TResourceType extends string, TParams extends PermissionRuleParams = undefined, >( rule: PermissionRule, -) => rule; +): PermissionRule; +export function createPermissionRule< + TResource, + TQuery, + TResourceType extends string, + TParams extends PermissionRuleParams = undefined, +>( + rule: + | PermissionRule + | CreatePermissionRuleOptions, +): PermissionRule { + if ('resourceRef' in rule) { + return { ...rule, resourceType: rule.resourceRef.resourceType }; + } + return rule; +} /** * Helper for making plugin-specific createPermissionRule functions, that have @@ -38,6 +107,7 @@ export const createPermissionRule = < * consistent types for the resource and query. * * @public + * @deprecated Use {@link (createPermissionRule:1)} directly instead with the resourceRef option. */ export const makeCreatePermissionRule = () => diff --git a/plugins/permission-node/src/integration/index.ts b/plugins/permission-node/src/integration/index.ts index 7702fea95b..0a783676db 100644 --- a/plugins/permission-node/src/integration/index.ts +++ b/plugins/permission-node/src/integration/index.ts @@ -19,4 +19,8 @@ export * from './createConditionExports'; export * from './createConditionTransformer'; export * from './createPermissionIntegrationRouter'; export * from './createPermissionRule'; +export { + createPermissionResourceRef, + type PermissionResourceRef, +} from './createPermissionResourceRef'; export { isAndCriteria, isOrCriteria, isNotCriteria } from './util'; From b71f634e3bb91d794e349357922a8809fdca0a86 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 30 Jan 2025 09:39:38 +0100 Subject: [PATCH 02/10] permission-node: added PermissionRuleAccessor Signed-off-by: Patrik Oldsberg --- .changeset/gold-rabbits-change.md | 5 ++++ plugins/permission-node/report.api.md | 27 ++++++++++++++----- .../integration/createConditionTransformer.ts | 27 ++++++++++++++----- plugins/permission-node/src/types.ts | 17 ++++++++++++ 4 files changed, 63 insertions(+), 13 deletions(-) create mode 100644 .changeset/gold-rabbits-change.md diff --git a/.changeset/gold-rabbits-change.md b/.changeset/gold-rabbits-change.md new file mode 100644 index 0000000000..0ede93af58 --- /dev/null +++ b/.changeset/gold-rabbits-change.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-permission-node': patch +--- + +Added a new `PermissionRuleAccessor` type that encapsulates a lookup function for permission rules, which can be created by the new `PermissionsRegistryService` via the `getRuleAccessor` method. The `createConditionTransformer` and `createConditionAuthorizer` functions have been adapted to receive these accessors as arguments, with their older counterparts being deprecated. diff --git a/plugins/permission-node/report.api.md b/plugins/permission-node/report.api.md index 2bbc548202..2e447ddb1e 100644 --- a/plugins/permission-node/report.api.md +++ b/plugins/permission-node/report.api.md @@ -78,9 +78,14 @@ export type ConditionTransformer = ( ) => PermissionCriteria; // @public -export const createConditionAuthorizer: ( +export function createConditionAuthorizer( + permissionRuleAccessor: PermissionRuleAccessor, +): (decision: PolicyDecision, resource: TResource | undefined) => boolean; + +// @public @deprecated (undocumented) +export function createConditionAuthorizer( rules: PermissionRule[], -) => (decision: PolicyDecision, resource: TResource | undefined) => boolean; +): (decision: PolicyDecision, resource: TResource | undefined) => boolean; // @public export function createConditionExports< @@ -124,12 +129,15 @@ export const createConditionFactory: < ) => (params: TParams) => PermissionCondition; // @public -export const createConditionTransformer: < +export function createConditionTransformer( + permissionRuleAccessor: PermissionRuleAccessor, +): ConditionTransformer; + +// @public @deprecated (undocumented) +export function createConditionTransformer< TQuery, TRules extends PermissionRule[], ->( - permissionRules: [...TRules], -) => ConditionTransformer; +>(permissionRules: [...TRules]): ConditionTransformer; // @public export function createPermissionIntegrationRouter< @@ -331,6 +339,13 @@ export type PermissionRule< toQuery(params: NoInfer_2): PermissionCriteria; }; +// @public +export type PermissionRuleAccessor< + TResource = unknown, + TQuery = unknown, + TResourceType extends string = string, +> = (name: string) => PermissionRule; + // @public export type PolicyQuery = { permission: Permission; diff --git a/plugins/permission-node/src/integration/createConditionTransformer.ts b/plugins/permission-node/src/integration/createConditionTransformer.ts index 4b6f933f79..a7dbabc903 100644 --- a/plugins/permission-node/src/integration/createConditionTransformer.ts +++ b/plugins/permission-node/src/integration/createConditionTransformer.ts @@ -20,7 +20,7 @@ import { PermissionCondition, PermissionCriteria, } from '@backstage/plugin-permission-common'; -import { PermissionRule } from '../types'; +import { PermissionRule, PermissionRuleAccessor } from '../types'; import { createGetRule, isAndCriteria, @@ -76,13 +76,26 @@ export type ConditionTransformer = ( * * @public */ -export const createConditionTransformer = < +export function createConditionTransformer( + permissionRuleAccessor: PermissionRuleAccessor, +): ConditionTransformer; +/** + * @public + * @deprecated Use the version of `createConditionTransformer` that accepts a `PermissionRuleAccessor` instead. + */ +export function createConditionTransformer< TQuery, TRules extends PermissionRule[], ->( - permissionRules: [...TRules], -): ConditionTransformer => { - const getRule = createGetRule(permissionRules); +>(permissionRules: [...TRules]): ConditionTransformer; +export function createConditionTransformer( + permissionRules: + | PermissionRule[] + | PermissionRuleAccessor, +): ConditionTransformer { + const getRule = + typeof permissionRules === 'function' + ? permissionRules + : createGetRule(permissionRules); return conditions => mapConditions(conditions, getRule); -}; +} diff --git a/plugins/permission-node/src/types.ts b/plugins/permission-node/src/types.ts index 0a7e06ed91..da071aa4d9 100644 --- a/plugins/permission-node/src/types.ts +++ b/plugins/permission-node/src/types.ts @@ -66,3 +66,20 @@ export type PermissionRule< */ toQuery(params: NoInfer): PermissionCriteria; }; + +/** + * A function that can be used to look up permission rules by name for a particular resource type. + * + * @remarks + * + * Accessed via {@link @backstage/backend-plugin-api#PermissionsRegistryService.getRuleAccessor}. + * + * Will throw an error if a rule with the provided name does not exist. + * + * @public + */ +export type PermissionRuleAccessor< + TResource = unknown, + TQuery = unknown, + TResourceType extends string = string, +> = (name: string) => PermissionRule; From 72cddf297160cc1c6f18c6858553695b1268149f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 30 Jan 2025 09:41:55 +0100 Subject: [PATCH 03/10] packages/backend-plugin-api: updated PermissionsRegistryService to use rule accessors and resource refs Signed-off-by: Patrik Oldsberg --- .changeset/gorgeous-shoes-stare.md | 5 +++ .changeset/witty-ducks-cross.md | 6 +++ .../permissionsRegistryServiceFactory.ts | 11 +++++- packages/backend-plugin-api/report.api.md | 23 +++++++++--- .../definitions/PermissionsRegistryService.ts | 37 +++++++++++++++---- .../src/next/services/mockServices.ts | 1 + plugins/permission-node/report.api.md | 3 ++ .../createPermissionIntegrationRouter.ts | 12 +++++- 8 files changed, 83 insertions(+), 15 deletions(-) create mode 100644 .changeset/gorgeous-shoes-stare.md create mode 100644 .changeset/witty-ducks-cross.md diff --git a/.changeset/gorgeous-shoes-stare.md b/.changeset/gorgeous-shoes-stare.md new file mode 100644 index 0000000000..c5748b14bc --- /dev/null +++ b/.changeset/gorgeous-shoes-stare.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Added the new `getRuleAccessor` method to `mockServices.permissionsRegistry`. diff --git a/.changeset/witty-ducks-cross.md b/.changeset/witty-ducks-cross.md new file mode 100644 index 0000000000..b374f0a116 --- /dev/null +++ b/.changeset/witty-ducks-cross.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-plugin-api': patch +'@backstage/backend-defaults': patch +--- + +Updated `PermissionsRegistryService` to use `PermissionResourceRef`s and added the `getRuleAccessor` method. diff --git a/packages/backend-defaults/src/entrypoints/permissionsRegistry/permissionsRegistryServiceFactory.ts b/packages/backend-defaults/src/entrypoints/permissionsRegistry/permissionsRegistryServiceFactory.ts index 17bcb21091..4619ff634b 100644 --- a/packages/backend-defaults/src/entrypoints/permissionsRegistry/permissionsRegistryServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/permissionsRegistry/permissionsRegistryServiceFactory.ts @@ -15,6 +15,7 @@ */ import { + PermissionsRegistryService, coreServices, createServiceFactory, } from '@backstage/backend-plugin-api'; @@ -52,7 +53,10 @@ export const permissionsRegistryServiceFactory = createServiceFactory({ 'Cannot add permission resource types after the plugin has started', ); } - router.addResourceType(resource); + router.addResourceType({ + ...resource, + resourceType: resource.resourceRef.resourceType, + }); }, addPermissions(permissions) { if (started) { @@ -70,6 +74,9 @@ export const permissionsRegistryServiceFactory = createServiceFactory({ } router.addPermissionRules(rules); }, - }; + getRuleAccessor(resourceRef) { + return router.getRuleAccessor(resourceRef); + }, + } satisfies PermissionsRegistryService; }, }); diff --git a/packages/backend-plugin-api/report.api.md b/packages/backend-plugin-api/report.api.md index a4f9b9fa1d..bb8cd63539 100644 --- a/packages/backend-plugin-api/report.api.md +++ b/packages/backend-plugin-api/report.api.md @@ -19,7 +19,9 @@ import { Knex } from 'knex'; import { Permission } from '@backstage/plugin-permission-common'; import { PermissionAttributes } from '@backstage/plugin-permission-common'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; +import { PermissionResourceRef } from '@backstage/plugin-permission-node'; import { PermissionRule } from '@backstage/plugin-permission-node'; +import { PermissionRuleAccessor } from '@backstage/plugin-permission-node'; import { QueryPermissionRequest } from '@backstage/plugin-permission-common'; import { QueryPermissionResponse } from '@backstage/plugin-permission-common'; import { Readable } from 'stream'; @@ -466,23 +468,34 @@ export interface LoggerService { export interface PermissionsRegistryService { addPermissionRules(rules: PermissionRule[]): void; addPermissions(permissions: Permission[]): void; - addResourceType( + addResourceType( options: PermissionsRegistryServiceAddResourceTypeOptions< TResourceType, - TResource + TResource, + TQuery >, ): void; + getRuleAccessor( + resourceRef: PermissionResourceRef, + ): PermissionRuleAccessor; } // @public export type PermissionsRegistryServiceAddResourceTypeOptions< TResourceType extends string, TResource, + TQuery, > = { - resourceType: TResourceType; + resourceRef: PermissionResourceRef; permissions?: Array; - rules: PermissionRule>[]; - getResources?(resourceRefs: string[]): Promise>; + rules: PermissionRule< + NoInfer_2, + NoInfer_2, + NoInfer_2 + >[]; + getResources?( + resourceRefs: string[], + ): Promise | undefined>>; }; // @public diff --git a/packages/backend-plugin-api/src/services/definitions/PermissionsRegistryService.ts b/packages/backend-plugin-api/src/services/definitions/PermissionsRegistryService.ts index 46236aa6cc..bc9d8dd223 100644 --- a/packages/backend-plugin-api/src/services/definitions/PermissionsRegistryService.ts +++ b/packages/backend-plugin-api/src/services/definitions/PermissionsRegistryService.ts @@ -15,7 +15,11 @@ */ import { Permission } from '@backstage/plugin-permission-common'; -import { PermissionRule } from '@backstage/plugin-permission-node'; +import { + PermissionResourceRef, + PermissionRule, + PermissionRuleAccessor, +} from '@backstage/plugin-permission-node'; /** * Prevent use of type parameter from contributing to type inference. @@ -33,11 +37,12 @@ type NoInfer = T extends infer S ? S : never; export type PermissionsRegistryServiceAddResourceTypeOptions< TResourceType extends string, TResource, + TQuery, > = { /** - * The identifier for the resource type. + * The {@link @backstage/plugin-permission-node#PermissionResourceRef} that identifies the resource type. */ - resourceType: TResourceType; + resourceRef: PermissionResourceRef; /** * Permissions that are available for this resource type. @@ -47,7 +52,11 @@ export type PermissionsRegistryServiceAddResourceTypeOptions< /** * Permission rules that are available for this resource type. */ - rules: PermissionRule>[]; + rules: PermissionRule< + NoInfer, + NoInfer, + NoInfer + >[]; /** * The function used to load associated resources based in the provided @@ -59,7 +68,9 @@ export type PermissionsRegistryServiceAddResourceTypeOptions< * resolve conditional decisions except when requesting resources directly * from the plugin. */ - getResources?(resourceRefs: string[]): Promise>; + getResources?( + resourceRefs: string[], + ): Promise | undefined>>; }; /** @@ -122,10 +133,22 @@ export interface PermissionsRegistryService { * called by the `permission-backend` when authorization conditions relating * to this plugin need to be evaluated. */ - addResourceType( + addResourceType( options: PermissionsRegistryServiceAddResourceTypeOptions< TResourceType, - TResource + TResource, + TQuery >, ): void; + + /** + * Returns a lookup function that can be used to look up rules for the provided resource by name. + * + * @remarks + * + * Primarily intended for use with {@link @backstage/plugin-permission-node#createConditionAuthorizer} and {@link @backstage/plugin-permission-node#createConditionTransformer}. + */ + getRuleAccessor( + resourceRef: PermissionResourceRef, + ): PermissionRuleAccessor; } diff --git a/packages/backend-test-utils/src/next/services/mockServices.ts b/packages/backend-test-utils/src/next/services/mockServices.ts index 15026d5dda..9c6b033505 100644 --- a/packages/backend-test-utils/src/next/services/mockServices.ts +++ b/packages/backend-test-utils/src/next/services/mockServices.ts @@ -489,6 +489,7 @@ export namespace mockServices { addPermissionRules: jest.fn(), addPermissions: jest.fn(), addResourceType: jest.fn(), + getRuleAccessor: jest.fn(), })); } diff --git a/plugins/permission-node/report.api.md b/plugins/permission-node/report.api.md index 2e447ddb1e..3859427ddf 100644 --- a/plugins/permission-node/report.api.md +++ b/plugins/permission-node/report.api.md @@ -173,6 +173,9 @@ export function createPermissionIntegrationRouter< TResource >, ): void; + getRuleAccessor( + resourceRef: PermissionResourceRef, + ): PermissionRuleAccessor; }; // @public diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts index 37973176ed..a85fe2d085 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts @@ -30,7 +30,7 @@ import { PermissionCriteria, PolicyDecision, } from '@backstage/plugin-permission-common'; -import { PermissionRule } from '../types'; +import { PermissionRule, PermissionRuleAccessor } from '../types'; import { NoInfer, createGetRule, @@ -439,6 +439,9 @@ export function createPermissionIntegrationRouter< TResource >, ): void; + getRuleAccessor( + resourceRef: PermissionResourceRef, + ): PermissionRuleAccessor; } { const store = new PermissionIntegrationMetadataStore(); @@ -528,5 +531,12 @@ export function createPermissionIntegrationRouter< ) { store.addResourceType(resource); }, + getRuleAccessor( + resourceRef: PermissionResourceRef, + ): PermissionRuleAccessor { + return store.getRuleMapper( + resourceRef.resourceType, + ) as PermissionRuleAccessor; + }, }); } From b4183adc9d1fa5b74ec947875a0faf19be4623a0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 30 Jan 2025 09:45:07 +0100 Subject: [PATCH 04/10] catalog-node: add entity permission resource ref Signed-off-by: Patrik Oldsberg --- .changeset/fresh-bears-thank.md | 5 +++++ plugins/catalog-node/report-alpha.api.md | 9 +++++++++ plugins/catalog-node/src/alpha.ts | 13 +++++++++++++ 3 files changed, 27 insertions(+) create mode 100644 .changeset/fresh-bears-thank.md diff --git a/.changeset/fresh-bears-thank.md b/.changeset/fresh-bears-thank.md new file mode 100644 index 0000000000..baeb14f036 --- /dev/null +++ b/.changeset/fresh-bears-thank.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-node': minor +--- + +Added new `catalogEntityPermissionResourceRef` export via the `/alpha` sub-path. diff --git a/plugins/catalog-node/report-alpha.api.md b/plugins/catalog-node/report-alpha.api.md index 92dfaefb7a..59f32cf76c 100644 --- a/plugins/catalog-node/report-alpha.api.md +++ b/plugins/catalog-node/report-alpha.api.md @@ -12,6 +12,7 @@ import { EntityProvider } from '@backstage/plugin-catalog-node'; import { ExtensionPoint } from '@backstage/backend-plugin-api'; import { LocationAnalyzer } from '@backstage/plugin-catalog-node'; import { Permission } from '@backstage/plugin-permission-common'; +import { PermissionResourceRef } from '@backstage/plugin-permission-node'; import { PermissionRule } from '@backstage/plugin-permission-node'; import { PermissionRuleParams } from '@backstage/plugin-permission-common'; import { PlaceholderResolver } from '@backstage/plugin-catalog-node'; @@ -34,6 +35,14 @@ export interface CatalogAnalysisExtensionPoint { // @alpha (undocumented) export const catalogAnalysisExtensionPoint: ExtensionPoint; +// @alpha (undocumented) +export const catalogEntityPermissionResourceRef: PermissionResourceRef< + Entity, + EntitiesSearchFilter, + 'catalog-entity', + 'catalog' +>; + // @alpha (undocumented) export interface CatalogLocationsExtensionPoint { setAllowedLocationTypes(locationTypes: Array): void; diff --git a/plugins/catalog-node/src/alpha.ts b/plugins/catalog-node/src/alpha.ts index 63b817fbb3..ec9d4bf435 100644 --- a/plugins/catalog-node/src/alpha.ts +++ b/plugins/catalog-node/src/alpha.ts @@ -21,6 +21,19 @@ import { } from '@backstage/backend-plugin-api'; import { catalogServiceRef as _catalogServiceRef } from './catalogService'; import { CatalogApi, CatalogClient } from '@backstage/catalog-client'; +import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common/alpha'; +import { createPermissionResourceRef } from '@backstage/plugin-permission-node'; +import { Entity } from '@backstage/catalog-model'; +import { EntitiesSearchFilter } from '@backstage/plugin-catalog-node'; + +/** @alpha */ +export const catalogEntityPermissionResourceRef = createPermissionResourceRef< + Entity, + EntitiesSearchFilter +>().with({ + pluginId: 'catalog', + resourceType: RESOURCE_TYPE_CATALOG_ENTITY, +}); /** * @alpha From ec07d0a5794c4f9449ca6599454246a97248915f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 30 Jan 2025 09:55:10 +0100 Subject: [PATCH 05/10] permission-node: updated createPermissionRule to improve type inference Signed-off-by: Patrik Oldsberg --- plugins/permission-node/report.api.md | 14 ++++++++--- .../src/integration/createPermissionRule.ts | 23 +++++++++++++++---- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/plugins/permission-node/report.api.md b/plugins/permission-node/report.api.md index 3859427ddf..adfeec0039 100644 --- a/plugins/permission-node/report.api.md +++ b/plugins/permission-node/report.api.md @@ -203,10 +203,17 @@ export function createPermissionResourceRef(): { export function createPermissionRule< TResource, TQuery, + TQueryOutput extends TQuery, TResourceType extends string, TParams extends PermissionRuleParams = undefined, >( - rule: CreatePermissionRuleOptions, + rule: CreatePermissionRuleOptions< + TResource, + TQuery, + TQueryOutput, + TResourceType, + TParams + >, ): PermissionRule; // @public @deprecated @@ -223,15 +230,16 @@ export function createPermissionRule< export type CreatePermissionRuleOptions< TResource, TQuery, + TQueryOutput extends TQuery, TResourceType extends string, - TParams extends PermissionRuleParams = PermissionRuleParams, + TParams extends PermissionRuleParams, > = { name: string; description: string; resourceRef: PermissionResourceRef; paramsSchema?: z.ZodSchema; apply(resource: TResource, params: NoInfer_2): boolean; - toQuery(params: NoInfer_2): PermissionCriteria; + toQuery(params: NoInfer_2): PermissionCriteria; }; // @public diff --git a/plugins/permission-node/src/integration/createPermissionRule.ts b/plugins/permission-node/src/integration/createPermissionRule.ts index 2e9833c32e..4625fb5af8 100644 --- a/plugins/permission-node/src/integration/createPermissionRule.ts +++ b/plugins/permission-node/src/integration/createPermissionRule.ts @@ -29,8 +29,9 @@ import { NoInfer } from './util'; export type CreatePermissionRuleOptions< TResource, TQuery, + TQueryOutput extends TQuery, TResourceType extends string, - TParams extends PermissionRuleParams = PermissionRuleParams, + TParams extends PermissionRuleParams, > = { name: string; description: string; @@ -54,7 +55,7 @@ export type CreatePermissionRuleOptions< * can be used for loading a collection of resources efficiently with conditional criteria already * applied. */ - toQuery(params: NoInfer): PermissionCriteria; + toQuery(params: NoInfer): PermissionCriteria; }; /** @@ -65,10 +66,17 @@ export type CreatePermissionRuleOptions< export function createPermissionRule< TResource, TQuery, + TQueryOutput extends TQuery, TResourceType extends string, TParams extends PermissionRuleParams = undefined, >( - rule: CreatePermissionRuleOptions, + rule: CreatePermissionRuleOptions< + TResource, + TQuery, + TQueryOutput, + TResourceType, + TParams + >, ): PermissionRule; /** * Helper function to ensure that {@link PermissionRule} definitions are typed correctly. @@ -87,12 +95,19 @@ export function createPermissionRule< export function createPermissionRule< TResource, TQuery, + TQueryOutput extends TQuery, TResourceType extends string, TParams extends PermissionRuleParams = undefined, >( rule: | PermissionRule - | CreatePermissionRuleOptions, + | CreatePermissionRuleOptions< + TResource, + TQuery, + TQueryOutput, + TResourceType, + TParams + >, ): PermissionRule { if ('resourceRef' in rule) { return { ...rule, resourceType: rule.resourceRef.resourceType }; From ef73f971589a33624431f39f08e72178b2f59114 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 30 Jan 2025 10:04:19 +0100 Subject: [PATCH 06/10] catalog-backend: update to use permission resource ref Signed-off-by: Patrik Oldsberg --- .changeset/hungry-chefs-relax.md | 5 ++++ .../src/permissions/conditionExports.ts | 5 ++-- .../permissions/rules/createPropertyRule.ts | 8 +++---- .../src/permissions/rules/hasAnnotation.ts | 8 +++---- .../src/permissions/rules/hasLabel.ts | 8 +++---- .../src/permissions/rules/isEntityKind.ts | 8 +++---- .../src/permissions/rules/isEntityOwner.ts | 8 +++---- .../src/service/CatalogBuilder.ts | 23 ++++++++++++++----- 8 files changed, 44 insertions(+), 29 deletions(-) create mode 100644 .changeset/hungry-chefs-relax.md diff --git a/.changeset/hungry-chefs-relax.md b/.changeset/hungry-chefs-relax.md new file mode 100644 index 0000000000..05235d4f47 --- /dev/null +++ b/.changeset/hungry-chefs-relax.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Updated permission integration to use new permission resource ref. diff --git a/plugins/catalog-backend/src/permissions/conditionExports.ts b/plugins/catalog-backend/src/permissions/conditionExports.ts index c9406cdc0e..446ab1fb57 100644 --- a/plugins/catalog-backend/src/permissions/conditionExports.ts +++ b/plugins/catalog-backend/src/permissions/conditionExports.ts @@ -14,13 +14,12 @@ * limitations under the License. */ -import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common/alpha'; import { createConditionExports } from '@backstage/plugin-permission-node'; import { permissionRules } from './rules'; +import { catalogEntityPermissionResourceRef } from '@backstage/plugin-catalog-node/alpha'; const { conditions, createConditionalDecision } = createConditionExports({ - pluginId: 'catalog', - resourceType: RESOURCE_TYPE_CATALOG_ENTITY, + resourceRef: catalogEntityPermissionResourceRef, rules: permissionRules, }); diff --git a/plugins/catalog-backend/src/permissions/rules/createPropertyRule.ts b/plugins/catalog-backend/src/permissions/rules/createPropertyRule.ts index 73f40da8c8..d0a37802e0 100644 --- a/plugins/catalog-backend/src/permissions/rules/createPropertyRule.ts +++ b/plugins/catalog-backend/src/permissions/rules/createPropertyRule.ts @@ -14,16 +14,16 @@ * limitations under the License. */ +import { catalogEntityPermissionResourceRef } from '@backstage/plugin-catalog-node/alpha'; +import { createPermissionRule } from '@backstage/plugin-permission-node'; import { get } from 'lodash'; -import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common/alpha'; -import { createCatalogPermissionRule } from './util'; import { z } from 'zod'; export const createPropertyRule = (propertyType: 'metadata' | 'spec') => - createCatalogPermissionRule({ + createPermissionRule({ name: `HAS_${propertyType.toUpperCase()}`, description: `Allow entities with the specified ${propertyType} subfield`, - resourceType: RESOURCE_TYPE_CATALOG_ENTITY, + resourceRef: catalogEntityPermissionResourceRef, paramsSchema: z.object({ key: z .string() diff --git a/plugins/catalog-backend/src/permissions/rules/hasAnnotation.ts b/plugins/catalog-backend/src/permissions/rules/hasAnnotation.ts index 315d7fa8d0..32e0019715 100644 --- a/plugins/catalog-backend/src/permissions/rules/hasAnnotation.ts +++ b/plugins/catalog-backend/src/permissions/rules/hasAnnotation.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common/alpha'; +import { catalogEntityPermissionResourceRef } from '@backstage/plugin-catalog-node/alpha'; +import { createPermissionRule } from '@backstage/plugin-permission-node'; import { z } from 'zod'; -import { createCatalogPermissionRule } from './util'; /** * A catalog {@link @backstage/plugin-permission-node#PermissionRule} which @@ -26,10 +26,10 @@ import { createCatalogPermissionRule } from './util'; * * @alpha */ -export const hasAnnotation = createCatalogPermissionRule({ +export const hasAnnotation = createPermissionRule({ name: 'HAS_ANNOTATION', description: 'Allow entities with the specified annotation', - resourceType: RESOURCE_TYPE_CATALOG_ENTITY, + resourceRef: catalogEntityPermissionResourceRef, paramsSchema: z.object({ annotation: z.string().describe('Name of the annotation to match on'), value: z diff --git a/plugins/catalog-backend/src/permissions/rules/hasLabel.ts b/plugins/catalog-backend/src/permissions/rules/hasLabel.ts index 2d6289dd26..e825b05b04 100644 --- a/plugins/catalog-backend/src/permissions/rules/hasLabel.ts +++ b/plugins/catalog-backend/src/permissions/rules/hasLabel.ts @@ -14,19 +14,19 @@ * limitations under the License. */ -import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common/alpha'; +import { catalogEntityPermissionResourceRef } from '@backstage/plugin-catalog-node/alpha'; +import { createPermissionRule } from '@backstage/plugin-permission-node'; import { z } from 'zod'; -import { createCatalogPermissionRule } from './util'; /** * A catalog {@link @backstage/plugin-permission-node#PermissionRule} which * filters for entities with a specified label in its metadata. * @alpha */ -export const hasLabel = createCatalogPermissionRule({ +export const hasLabel = createPermissionRule({ name: 'HAS_LABEL', description: 'Allow entities with the specified label', - resourceType: RESOURCE_TYPE_CATALOG_ENTITY, + resourceRef: catalogEntityPermissionResourceRef, paramsSchema: z.object({ label: z.string().describe('Name of the label to match on'), }), diff --git a/plugins/catalog-backend/src/permissions/rules/isEntityKind.ts b/plugins/catalog-backend/src/permissions/rules/isEntityKind.ts index 568aae4838..6291692556 100644 --- a/plugins/catalog-backend/src/permissions/rules/isEntityKind.ts +++ b/plugins/catalog-backend/src/permissions/rules/isEntityKind.ts @@ -14,19 +14,19 @@ * limitations under the License. */ -import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common/alpha'; +import { catalogEntityPermissionResourceRef } from '@backstage/plugin-catalog-node/alpha'; +import { createPermissionRule } from '@backstage/plugin-permission-node'; import { z } from 'zod'; -import { createCatalogPermissionRule } from './util'; /** * A catalog {@link @backstage/plugin-permission-node#PermissionRule} which * filters for entities with a specified kind. * @alpha */ -export const isEntityKind = createCatalogPermissionRule({ +export const isEntityKind = createPermissionRule({ name: 'IS_ENTITY_KIND', description: 'Allow entities matching a specified kind', - resourceType: RESOURCE_TYPE_CATALOG_ENTITY, + resourceRef: catalogEntityPermissionResourceRef, paramsSchema: z.object({ kinds: z .array(z.string()) diff --git a/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts b/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts index 86493315dc..de336ba966 100644 --- a/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts +++ b/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts @@ -15,9 +15,9 @@ */ import { RELATION_OWNED_BY } from '@backstage/catalog-model'; -import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common/alpha'; +import { createPermissionRule } from '@backstage/plugin-permission-node'; import { z } from 'zod'; -import { createCatalogPermissionRule } from './util'; +import { catalogEntityPermissionResourceRef } from '@backstage/plugin-catalog-node/alpha'; /** * A catalog {@link @backstage/plugin-permission-node#PermissionRule} which @@ -25,10 +25,10 @@ import { createCatalogPermissionRule } from './util'; * * @alpha */ -export const isEntityOwner = createCatalogPermissionRule({ +export const isEntityOwner = createPermissionRule({ name: 'IS_ENTITY_OWNER', description: 'Allow entities owned by a specified claim', - resourceType: RESOURCE_TYPE_CATALOG_ENTITY, + resourceRef: catalogEntityPermissionResourceRef, paramsSchema: z.object({ claims: z .array(z.string()) diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index bf7f31919c..7e00d6dc62 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -34,7 +34,7 @@ import { import { ScmIntegrations } from '@backstage/integration'; import { createHash } from 'crypto'; import { Router } from 'express'; -import lodash, { keyBy } from 'lodash'; +import lodash from 'lodash'; import { AuditorService, @@ -118,6 +118,7 @@ import { DefaultLocationService } from './DefaultLocationService'; import { DefaultRefreshService } from './DefaultRefreshService'; import { basicEntityFilter } from './request'; import { entitiesResponseToObjects } from './response'; +import { catalogEntityPermissionResourceRef } from '@backstage/plugin-catalog-node/alpha'; /** * This is a duplicate of the alpha `CatalogPermissionRule` type, for use in the stable API. @@ -558,7 +559,13 @@ export class CatalogBuilder { const entitiesCatalog = new AuthorizedEntitiesCatalog( unauthorizedEntitiesCatalog, permissionsService, - createConditionTransformer(this.permissionRules), + permissionsRegistry + ? createConditionTransformer( + permissionsRegistry.getRuleAccessor( + catalogEntityPermissionResourceRef, + ), + ) + : createConditionTransformer(this.permissionRules), ); const catalogPermissionResource = { @@ -579,9 +586,10 @@ export class CatalogBuilder { }, }); - const entitiesByRef = keyBy( - entitiesResponseToObjects(entities), - stringifyEntityRef, + const entitiesByRef = Object.fromEntries( + entitiesResponseToObjects(entities) + .filter((x): x is Entity => Boolean(x)) + .map(entity => [stringifyEntityRef(entity), entity]), ); return resourceRefs.map( @@ -597,7 +605,10 @@ export class CatalogBuilder { | ReturnType | undefined; if (permissionsRegistry) { - permissionsRegistry.addResourceType(catalogPermissionResource); + permissionsRegistry.addResourceType({ + ...catalogPermissionResource, + resourceRef: catalogEntityPermissionResourceRef, + }); } else { permissionIntegrationRouter = createPermissionIntegrationRouter( catalogPermissionResource, From 0c344381065497381016e8ffa39ce733d6eaafab Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 30 Jan 2025 10:47:58 +0100 Subject: [PATCH 07/10] docs/permissions: updated to use resource refs and rule accessor Signed-off-by: Patrik Oldsberg --- docs/permissions/custom-rules.md | 20 ++++++------- .../03-adding-a-resource-permission-check.md | 29 +++++++++---------- ...04-authorizing-access-to-paginated-data.md | 13 ++++++--- 3 files changed, 33 insertions(+), 29 deletions(-) diff --git a/docs/permissions/custom-rules.md b/docs/permissions/custom-rules.md index ca93652f73..41a1966a34 100644 --- a/docs/permissions/custom-rules.md +++ b/docs/permissions/custom-rules.md @@ -24,14 +24,14 @@ yarn --cwd packages/backend add zod @backstage/catalog-model ... import type { Entity } from '@backstage/catalog-model'; -import { createCatalogPermissionRule } from '@backstage/plugin-catalog-backend/alpha'; -import { createConditionFactory } from '@backstage/plugin-permission-node'; +import { catalogEntityPermissionResourceRef } from '@backstage/plugin-catalog-backend/alpha'; +import { createConditionFactory, createPermissionRule } from '@backstage/plugin-permission-node'; import { z } from 'zod'; -export const isInSystemRule = createCatalogPermissionRule({ +export const isInSystemRule = createPermissionRule({ name: 'IS_IN_SYSTEM', description: 'Checks if an entity is part of the system provided', - resourceType: 'catalog-entity', + resourceRef: catalogEntityPermissionResourceRef, paramsSchema: z.object({ systemRef: z .string() @@ -64,22 +64,22 @@ Still in the `packages/backend/src/extensions/permissionsPolicyExtension.ts` fil ```ts title="packages/backend/src/extensions/permissionsPolicyExtension.ts" ... /* highlight-remove-next-line */ -import { createCatalogPermissionRule } from '@backstage/plugin-catalog-backend/alpha'; +import { catalogEntityPermissionResourceRef } from '@backstage/plugin-catalog-backend/alpha'; /* highlight-add-next-line */ -import { catalogConditions, createCatalogConditionalDecision, createCatalogPermissionRule } from '@backstage/plugin-catalog-backend/alpha'; +import { catalogEntityPermissionResourceRef, createCatalogConditionalDecision, catalogConditions } from '@backstage/plugin-catalog-backend/alpha'; /* highlight-remove-next-line */ -import { createConditionFactory } from '@backstage/plugin-permission-node'; +import { createConditionFactory, createPermissionRule } from '@backstage/plugin-permission-node'; /* highlight-add-next-line */ -import { PermissionPolicy, PolicyQuery, PolicyQueryUser, createConditionFactory } from '@backstage/plugin-permission-node'; +import { createConditionFactory, createPermissionRule, PermissionPolicy, PolicyQuery, PolicyQueryUser } from '@backstage/plugin-permission-node'; /* highlight-add-start */ import { AuthorizeResult, PolicyDecision, isResourcePermission } from '@backstage/plugin-permission-common'; /* highlight-add-end */ ... -export const isInSystemRule = createCatalogPermissionRule({ +export const isInSystemRule = createPermissionRule({ name: 'IS_IN_SYSTEM', description: 'Checks if an entity is part of the system provided', - resourceType: 'catalog-entity', + resourceRef: catalogEntityPermissionResourceRef, paramsSchema: z.object({ systemRef: z .string() 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 8d0e5c75e9..dd8e88bb94 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 @@ -130,16 +130,18 @@ import { TODO_LIST_RESOURCE_TYPE } from '@internal/plugin-todo-list-common'; import { z } from 'zod'; import { Todo, TodoFilter } from './todos'; -export const createTodoListPermissionRule = makeCreatePermissionRule< +export const todoListPermissionResourceRef = createPermissionResourceRef< Todo, - TodoFilter, - typeof TODO_LIST_RESOURCE_TYPE ->(); + TodoFilter +>().with({ + pluginId: 'todolist', + type: TODO_LIST_RESOURCE_TYPE, +}); -export const isOwner = createTodoListPermissionRule({ +export const isOwner = createPermissionRule({ name: 'IS_OWNER', description: 'Should allow only if the todo belongs to the user', - resourceType: TODO_LIST_RESOURCE_TYPE, + resourceType: todoListPermissionResourceRef, paramsSchema: z.object({ userId: z.string().describe('User ID to match on the resource'), }), @@ -157,11 +159,11 @@ export const isOwner = createTodoListPermissionRule({ export const rules = { isOwner }; ``` -`makeCreatePermissionRule` is a helper used to ensure that rules created for this plugin use consistent types for the resource and query. +The `todoListPermissionResourceRef` is a utility that encapsulates the types and constants related to the resource type. It ensures that the resource and query types are consistent across all rules created for this resource. :::note Note -To support custom rules defined by Backstage integrators, you must export `createTodoListPermissionRule` from the backend package and provide some way for custom rules to be passed in before the backend starts, likely via `extension point`. +To support custom rules defined by Backstage integrators, you must export `todoListPermissionResourceRef` from the backend package, or a `*-node` package if you want to enable the creation of third-party modules. ::: @@ -186,14 +188,12 @@ import { } from '@backstage/backend-plugin-api'; import { createRouter } from './service/router'; import { - /* highlight-add-next-line */ - TODO_LIST_RESOURCE_TYPE, todoListCreatePermission, todoListUpdatePermission, } from '@internal/plugin-todo-list-common'; /* highlight-add-start */ import { getTodo } from './todos'; -import { rules } from './rules'; +import { todoListPermissionResourceRef, rules } from './rules'; /* highlight-add-end */ // ... @@ -206,7 +206,7 @@ permissionsRegistry.addPermissions([ /* highlight-remove-end */ /* highlight-add-start */ permissionsRegistry.addResourceType({ - resourceType: TODO_LIST_RESOURCE_TYPE, + resourceRef: todoListPermissionResourceRef, permissions: [todoListCreatePermission, todoListUpdatePermission], rules: Object.values(rules), getResources: async resourceRefs => { @@ -225,11 +225,10 @@ Create a new `plugins/todo-list-backend/src/conditionExports.ts` file and add th ```typescript title="plugins/todo-list-backend/src/conditionExports.ts" import { TODO_LIST_RESOURCE_TYPE } from '@internal/plugin-todo-list-common'; import { createConditionExports } from '@backstage/plugin-permission-node'; -import { rules } from './service/rules'; +import { todoListPermissionResourceRef, rules } from './service/rules'; const { conditions, createConditionalDecision } = createConditionExports({ - pluginId: 'todolist', - resourceType: TODO_LIST_RESOURCE_TYPE, + resourceRef: todoListPermissionResourceRef, rules, }); diff --git a/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md b/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md index 10941e6305..1a65c1f51a 100644 --- a/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md +++ b/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md @@ -100,7 +100,7 @@ import { // ... permissionsRegistry.addResourceType({ - resourceType: TODO_LIST_RESOURCE_TYPE, + resourceRef: todoListPermissionResourceRef, /* highlight-remove-next-line */ permissions: [todoListCreatePermission, todoListUpdatePermission], /* highlight-add-next-line */ @@ -129,17 +129,22 @@ import { import { add, getAll, getTodo, update } from './todos'; /* highlight-add-next-line */ import { add, getAll, getTodo, TodoFilter, update } from './todos'; +/* highlight-add-next-line */ +import { todoListPermissionResourceRef } from './rules'; import { todoListCreatePermission, todoListUpdatePermission, /* highlight-add-next-line */ todoListReadPermission, -} from './permissions'; +} from '@internal/plugin-todo-list-common'; // ... -/* highlight-add-next-line */ -const transformConditions: ConditionTransformer = createConditionTransformer(Object.values(rules)); +/* highlight-add-start */ +const transformConditions = createConditionTransformer( + permissionsRegistry.getRuleAccessor(todoListPermissionResourceRef) +); +/* highlight-add-end */ /* highlight-remove-next-line */ router.get('/todos', async (_req, res) => { From 7500c99ecc79ebfee748d196d6d8fb1e75e658e7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 30 Jan 2025 15:38:22 +0100 Subject: [PATCH 08/10] permission-node: refactor PermissionRuleAccessor to PermissionRuleset Signed-off-by: Patrik Oldsberg --- .changeset/gold-rabbits-change.md | 2 +- .changeset/gorgeous-shoes-stare.md | 2 +- .changeset/witty-ducks-cross.md | 2 +- ...04-authorizing-access-to-paginated-data.md | 2 +- .../permissionsRegistryServiceFactory.ts | 4 +-- packages/backend-plugin-api/report.api.md | 6 ++--- .../definitions/PermissionsRegistryService.ts | 6 ++--- .../src/next/services/mockServices.ts | 2 +- .../src/service/CatalogBuilder.ts | 2 +- plugins/permission-node/report.api.md | 14 +++++----- .../integration/createConditionTransformer.ts | 12 ++++----- .../createPermissionIntegrationRouter.ts | 27 ++++++++++--------- plugins/permission-node/src/types.ts | 8 +++--- 13 files changed, 48 insertions(+), 41 deletions(-) diff --git a/.changeset/gold-rabbits-change.md b/.changeset/gold-rabbits-change.md index 0ede93af58..4d124a5d51 100644 --- a/.changeset/gold-rabbits-change.md +++ b/.changeset/gold-rabbits-change.md @@ -2,4 +2,4 @@ '@backstage/plugin-permission-node': patch --- -Added a new `PermissionRuleAccessor` type that encapsulates a lookup function for permission rules, which can be created by the new `PermissionsRegistryService` via the `getRuleAccessor` method. The `createConditionTransformer` and `createConditionAuthorizer` functions have been adapted to receive these accessors as arguments, with their older counterparts being deprecated. +Added a new `PermissionRuleset` type that encapsulates a lookup function for permission rules, which can be created by the new `PermissionsRegistryService` via the `getPermissionRuleset` method. The `createConditionTransformer` and `createConditionAuthorizer` functions have been adapted to receive these accessors as arguments, with their older counterparts being deprecated. diff --git a/.changeset/gorgeous-shoes-stare.md b/.changeset/gorgeous-shoes-stare.md index c5748b14bc..a67a185892 100644 --- a/.changeset/gorgeous-shoes-stare.md +++ b/.changeset/gorgeous-shoes-stare.md @@ -2,4 +2,4 @@ '@backstage/backend-test-utils': patch --- -Added the new `getRuleAccessor` method to `mockServices.permissionsRegistry`. +Added the new `getPermissionRuleset` method to `mockServices.permissionsRegistry`. diff --git a/.changeset/witty-ducks-cross.md b/.changeset/witty-ducks-cross.md index b374f0a116..f0defba897 100644 --- a/.changeset/witty-ducks-cross.md +++ b/.changeset/witty-ducks-cross.md @@ -3,4 +3,4 @@ '@backstage/backend-defaults': patch --- -Updated `PermissionsRegistryService` to use `PermissionResourceRef`s and added the `getRuleAccessor` method. +Updated `PermissionsRegistryService` to use `PermissionResourceRef`s and added the `getPermissionRuleset` method. diff --git a/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md b/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md index 1a65c1f51a..c03183bbf0 100644 --- a/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md +++ b/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md @@ -142,7 +142,7 @@ import { /* highlight-add-start */ const transformConditions = createConditionTransformer( - permissionsRegistry.getRuleAccessor(todoListPermissionResourceRef) + permissionsRegistry.getPermissionRuleset(todoListPermissionResourceRef) ); /* highlight-add-end */ diff --git a/packages/backend-defaults/src/entrypoints/permissionsRegistry/permissionsRegistryServiceFactory.ts b/packages/backend-defaults/src/entrypoints/permissionsRegistry/permissionsRegistryServiceFactory.ts index 4619ff634b..993fb4e946 100644 --- a/packages/backend-defaults/src/entrypoints/permissionsRegistry/permissionsRegistryServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/permissionsRegistry/permissionsRegistryServiceFactory.ts @@ -74,8 +74,8 @@ export const permissionsRegistryServiceFactory = createServiceFactory({ } router.addPermissionRules(rules); }, - getRuleAccessor(resourceRef) { - return router.getRuleAccessor(resourceRef); + getPermissionRuleset(resourceRef) { + return router.getPermissionRuleset(resourceRef); }, } satisfies PermissionsRegistryService; }, diff --git a/packages/backend-plugin-api/report.api.md b/packages/backend-plugin-api/report.api.md index bb8cd63539..384ae31481 100644 --- a/packages/backend-plugin-api/report.api.md +++ b/packages/backend-plugin-api/report.api.md @@ -21,7 +21,7 @@ import { PermissionAttributes } from '@backstage/plugin-permission-common'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; import { PermissionResourceRef } from '@backstage/plugin-permission-node'; import { PermissionRule } from '@backstage/plugin-permission-node'; -import { PermissionRuleAccessor } from '@backstage/plugin-permission-node'; +import { PermissionRuleset } from '@backstage/plugin-permission-node'; import { QueryPermissionRequest } from '@backstage/plugin-permission-common'; import { QueryPermissionResponse } from '@backstage/plugin-permission-common'; import { Readable } from 'stream'; @@ -475,9 +475,9 @@ export interface PermissionsRegistryService { TQuery >, ): void; - getRuleAccessor( + getPermissionRuleset( resourceRef: PermissionResourceRef, - ): PermissionRuleAccessor; + ): PermissionRuleset; } // @public diff --git a/packages/backend-plugin-api/src/services/definitions/PermissionsRegistryService.ts b/packages/backend-plugin-api/src/services/definitions/PermissionsRegistryService.ts index bc9d8dd223..edd41e0cbc 100644 --- a/packages/backend-plugin-api/src/services/definitions/PermissionsRegistryService.ts +++ b/packages/backend-plugin-api/src/services/definitions/PermissionsRegistryService.ts @@ -18,7 +18,7 @@ import { Permission } from '@backstage/plugin-permission-common'; import { PermissionResourceRef, PermissionRule, - PermissionRuleAccessor, + PermissionRuleset, } from '@backstage/plugin-permission-node'; /** @@ -148,7 +148,7 @@ export interface PermissionsRegistryService { * * Primarily intended for use with {@link @backstage/plugin-permission-node#createConditionAuthorizer} and {@link @backstage/plugin-permission-node#createConditionTransformer}. */ - getRuleAccessor( + getPermissionRuleset( resourceRef: PermissionResourceRef, - ): PermissionRuleAccessor; + ): PermissionRuleset; } diff --git a/packages/backend-test-utils/src/next/services/mockServices.ts b/packages/backend-test-utils/src/next/services/mockServices.ts index 9c6b033505..baeae53a77 100644 --- a/packages/backend-test-utils/src/next/services/mockServices.ts +++ b/packages/backend-test-utils/src/next/services/mockServices.ts @@ -489,7 +489,7 @@ export namespace mockServices { addPermissionRules: jest.fn(), addPermissions: jest.fn(), addResourceType: jest.fn(), - getRuleAccessor: jest.fn(), + getPermissionRuleset: jest.fn(), })); } diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index 7e00d6dc62..7f70e2732d 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -561,7 +561,7 @@ export class CatalogBuilder { permissionsService, permissionsRegistry ? createConditionTransformer( - permissionsRegistry.getRuleAccessor( + permissionsRegistry.getPermissionRuleset( catalogEntityPermissionResourceRef, ), ) diff --git a/plugins/permission-node/report.api.md b/plugins/permission-node/report.api.md index adfeec0039..9131d6e2a9 100644 --- a/plugins/permission-node/report.api.md +++ b/plugins/permission-node/report.api.md @@ -79,7 +79,7 @@ export type ConditionTransformer = ( // @public export function createConditionAuthorizer( - permissionRuleAccessor: PermissionRuleAccessor, + permissionRuleset: PermissionRuleset, ): (decision: PolicyDecision, resource: TResource | undefined) => boolean; // @public @deprecated (undocumented) @@ -130,7 +130,7 @@ export const createConditionFactory: < // @public export function createConditionTransformer( - permissionRuleAccessor: PermissionRuleAccessor, + permissionRuleset: PermissionRuleset, ): ConditionTransformer; // @public @deprecated (undocumented) @@ -173,9 +173,9 @@ export function createPermissionIntegrationRouter< TResource >, ): void; - getRuleAccessor( + getPermissionRuleset( resourceRef: PermissionResourceRef, - ): PermissionRuleAccessor; + ): PermissionRuleset; }; // @public @@ -351,11 +351,13 @@ export type PermissionRule< }; // @public -export type PermissionRuleAccessor< +export type PermissionRuleset< TResource = unknown, TQuery = unknown, TResourceType extends string = string, -> = (name: string) => PermissionRule; +> = { + getRuleByName(name: string): PermissionRule; +}; // @public export type PolicyQuery = { diff --git a/plugins/permission-node/src/integration/createConditionTransformer.ts b/plugins/permission-node/src/integration/createConditionTransformer.ts index a7dbabc903..21608a1f50 100644 --- a/plugins/permission-node/src/integration/createConditionTransformer.ts +++ b/plugins/permission-node/src/integration/createConditionTransformer.ts @@ -20,7 +20,7 @@ import { PermissionCondition, PermissionCriteria, } from '@backstage/plugin-permission-common'; -import { PermissionRule, PermissionRuleAccessor } from '../types'; +import { PermissionRule, PermissionRuleset } from '../types'; import { createGetRule, isAndCriteria, @@ -77,11 +77,11 @@ export type ConditionTransformer = ( * @public */ export function createConditionTransformer( - permissionRuleAccessor: PermissionRuleAccessor, + permissionRuleset: PermissionRuleset, ): ConditionTransformer; /** * @public - * @deprecated Use the version of `createConditionTransformer` that accepts a `PermissionRuleAccessor` instead. + * @deprecated Use the version of `createConditionTransformer` that accepts a `PermissionRuleset` instead. */ export function createConditionTransformer< TQuery, @@ -90,11 +90,11 @@ export function createConditionTransformer< export function createConditionTransformer( permissionRules: | PermissionRule[] - | PermissionRuleAccessor, + | PermissionRuleset, ): ConditionTransformer { const getRule = - typeof permissionRules === 'function' - ? permissionRules + 'getRuleByName' in permissionRules + ? (n: string) => permissionRules.getRuleByName(n) : createGetRule(permissionRules); return conditions => mapConditions(conditions, getRule); diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts index a85fe2d085..b215044ed8 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts @@ -30,7 +30,7 @@ import { PermissionCriteria, PolicyDecision, } from '@backstage/plugin-permission-common'; -import { PermissionRule, PermissionRuleAccessor } from '../types'; +import { PermissionRule, PermissionRuleset } from '../types'; import { NoInfer, createGetRule, @@ -165,11 +165,11 @@ const applyConditions = ( * @public */ export function createConditionAuthorizer( - permissionRuleAccessor: PermissionRuleAccessor, + permissionRuleset: PermissionRuleset, ): (decision: PolicyDecision, resource: TResource | undefined) => boolean; /** * @public - * @deprecated Use the version of `createConditionAuthorizer` that accepts a `PermissionRuleAccessor` instead. + * @deprecated Use the version of `createConditionAuthorizer` that accepts a `PermissionRuleset` instead. */ export function createConditionAuthorizer( rules: PermissionRule[], @@ -177,9 +177,12 @@ export function createConditionAuthorizer( export function createConditionAuthorizer( rules: | PermissionRule[] - | PermissionRuleAccessor, + | PermissionRuleset, ): (decision: PolicyDecision, resource: TResource | undefined) => boolean { - const getRule = typeof rules === 'function' ? rules : createGetRule(rules); + const getRule = + 'getRuleByName' in rules + ? (n: string) => rules.getRuleByName(n) + : createGetRule(rules); return ( decision: PolicyDecision, @@ -439,9 +442,9 @@ export function createPermissionIntegrationRouter< TResource >, ): void; - getRuleAccessor( + getPermissionRuleset( resourceRef: PermissionResourceRef, - ): PermissionRuleAccessor; + ): PermissionRuleset; } { const store = new PermissionIntegrationMetadataStore(); @@ -531,12 +534,12 @@ export function createPermissionIntegrationRouter< ) { store.addResourceType(resource); }, - getRuleAccessor( + getPermissionRuleset( resourceRef: PermissionResourceRef, - ): PermissionRuleAccessor { - return store.getRuleMapper( - resourceRef.resourceType, - ) as PermissionRuleAccessor; + ): PermissionRuleset { + return { + getRuleByName: store.getRuleMapper(resourceRef.resourceType), + } as PermissionRuleset; }, }); } diff --git a/plugins/permission-node/src/types.ts b/plugins/permission-node/src/types.ts index da071aa4d9..975a78acf0 100644 --- a/plugins/permission-node/src/types.ts +++ b/plugins/permission-node/src/types.ts @@ -72,14 +72,16 @@ export type PermissionRule< * * @remarks * - * Accessed via {@link @backstage/backend-plugin-api#PermissionsRegistryService.getRuleAccessor}. + * Accessed via {@link @backstage/backend-plugin-api#PermissionsRegistryService.getPermissionRuleset}. * * Will throw an error if a rule with the provided name does not exist. * * @public */ -export type PermissionRuleAccessor< +export type PermissionRuleset< TResource = unknown, TQuery = unknown, TResourceType extends string = string, -> = (name: string) => PermissionRule; +> = { + getRuleByName(name: string): PermissionRule; +}; From 2c7d35f844a0d11c8309f94f0f855e620d3e7897 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 4 Feb 2025 12:32:11 +0100 Subject: [PATCH 09/10] backend-defaults: make permissionRegistry reject refs from other plugins Signed-off-by: Patrik Oldsberg --- .../permissionsRegistryServiceFactory.test.ts | 82 +++++++++++++++++++ .../permissionsRegistryServiceFactory.ts | 19 ++++- 2 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 packages/backend-defaults/src/entrypoints/permissionsRegistry/permissionsRegistryServiceFactory.test.ts diff --git a/packages/backend-defaults/src/entrypoints/permissionsRegistry/permissionsRegistryServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/permissionsRegistry/permissionsRegistryServiceFactory.test.ts new file mode 100644 index 0000000000..578c76378a --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/permissionsRegistry/permissionsRegistryServiceFactory.test.ts @@ -0,0 +1,82 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { startTestBackend } from '@backstage/backend-test-utils'; +import { createPermissionResourceRef } from '@backstage/plugin-permission-node'; +import { permissionsRegistryServiceFactory } from './permissionsRegistryServiceFactory'; + +describe('permissionsRegistryServiceFactory', () => { + it('should reject resource refs from other plugins', async () => { + await expect( + startTestBackend({ + features: [ + permissionsRegistryServiceFactory, + createBackendPlugin({ + pluginId: 'test', + register(reg) { + reg.registerInit({ + deps: { permissionsRegistry: coreServices.permissionsRegistry }, + async init({ permissionsRegistry }) { + permissionsRegistry.addResourceType({ + resourceRef: createPermissionResourceRef< + unknown, + unknown + >().with({ + pluginId: 'other', + resourceType: 'some-resource', + }), + rules: [], + }); + }, + }); + }, + }), + ], + }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Plugin 'test' startup failed; caused by Error: Resource type 'some-resource' belongs to plugin 'other', but was used with plugin 'test'"`, + ); + + await expect( + startTestBackend({ + features: [ + permissionsRegistryServiceFactory, + createBackendPlugin({ + pluginId: 'test', + register(reg) { + reg.registerInit({ + deps: { permissionsRegistry: coreServices.permissionsRegistry }, + async init({ permissionsRegistry }) { + permissionsRegistry.getPermissionRuleset( + createPermissionResourceRef().with({ + pluginId: 'other', + resourceType: 'some-resource', + }), + ); + }, + }); + }, + }), + ], + }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Plugin 'test' startup failed; caused by Error: Resource type 'some-resource' belongs to plugin 'other', but was used with plugin 'test'"`, + ); + }); +}); diff --git a/packages/backend-defaults/src/entrypoints/permissionsRegistry/permissionsRegistryServiceFactory.ts b/packages/backend-defaults/src/entrypoints/permissionsRegistry/permissionsRegistryServiceFactory.ts index 993fb4e946..7ff6eabf03 100644 --- a/packages/backend-defaults/src/entrypoints/permissionsRegistry/permissionsRegistryServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/permissionsRegistry/permissionsRegistryServiceFactory.ts @@ -19,7 +19,18 @@ import { coreServices, createServiceFactory, } from '@backstage/backend-plugin-api'; -import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node'; +import { + PermissionResourceRef, + createPermissionIntegrationRouter, +} from '@backstage/plugin-permission-node'; + +function assertRefPluginId(ref: PermissionResourceRef, pluginId: string) { + if (ref.pluginId !== pluginId) { + throw new Error( + `Resource type '${ref.resourceType}' belongs to plugin '${ref.pluginId}', but was used with plugin '${pluginId}'`, + ); + } +} /** * Permission system integration for registering resources and permissions. @@ -35,9 +46,11 @@ export const permissionsRegistryServiceFactory = createServiceFactory({ deps: { lifecycle: coreServices.lifecycle, httpRouter: coreServices.httpRouter, + pluginMetadata: coreServices.pluginMetadata, }, - async factory({ httpRouter, lifecycle }) { + async factory({ httpRouter, lifecycle, pluginMetadata }) { const router = createPermissionIntegrationRouter(); + const pluginId = pluginMetadata.getId(); httpRouter.use(router); @@ -53,6 +66,7 @@ export const permissionsRegistryServiceFactory = createServiceFactory({ 'Cannot add permission resource types after the plugin has started', ); } + assertRefPluginId(resource.resourceRef, pluginId); router.addResourceType({ ...resource, resourceType: resource.resourceRef.resourceType, @@ -75,6 +89,7 @@ export const permissionsRegistryServiceFactory = createServiceFactory({ router.addPermissionRules(rules); }, getPermissionRuleset(resourceRef) { + assertRefPluginId(resourceRef, pluginId); return router.getPermissionRuleset(resourceRef); }, } satisfies PermissionsRegistryService; From d1a4831725334a64ffcc12e67e92718d9db22a8a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 4 Feb 2025 15:39:10 +0100 Subject: [PATCH 10/10] permission-node: doc fixes Signed-off-by: Patrik Oldsberg --- .../definitions/PermissionsRegistryService.ts | 2 +- plugins/permission-node/src/types.ts | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/backend-plugin-api/src/services/definitions/PermissionsRegistryService.ts b/packages/backend-plugin-api/src/services/definitions/PermissionsRegistryService.ts index edd41e0cbc..7a3897a5bd 100644 --- a/packages/backend-plugin-api/src/services/definitions/PermissionsRegistryService.ts +++ b/packages/backend-plugin-api/src/services/definitions/PermissionsRegistryService.ts @@ -142,7 +142,7 @@ export interface PermissionsRegistryService { ): void; /** - * Returns a lookup function that can be used to look up rules for the provided resource by name. + * Returns the set of registered rules for this resource. * * @remarks * diff --git a/plugins/permission-node/src/types.ts b/plugins/permission-node/src/types.ts index 975a78acf0..b1585d1827 100644 --- a/plugins/permission-node/src/types.ts +++ b/plugins/permission-node/src/types.ts @@ -68,14 +68,12 @@ export type PermissionRule< }; /** - * A function that can be used to look up permission rules by name for a particular resource type. + * A set of registered rules for a particular resource type. * * @remarks * * Accessed via {@link @backstage/backend-plugin-api#PermissionsRegistryService.getPermissionRuleset}. * - * Will throw an error if a rule with the provided name does not exist. - * * @public */ export type PermissionRuleset< @@ -83,5 +81,12 @@ export type PermissionRuleset< TQuery = unknown, TResourceType extends string = string, > = { + /** + * Returns a resource permission rule by name. + * + * @remarks + * + * Will throw an error if a rule with the provided name does not exist. + */ getRuleByName(name: string): PermissionRule; };