From 049d5d467672b4fb5d8805dbde90a1be1391bdf8 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 1 Jan 2025 15:56:10 +0100 Subject: [PATCH 01/15] permission-node: refactor integration router to be mutable Signed-off-by: Patrik Oldsberg --- .changeset/afraid-kids-jog.md | 5 + plugins/permission-node/report.api.md | 12 +- .../createPermissionIntegrationRouter.test.ts | 88 +++++ .../createPermissionIntegrationRouter.ts | 317 +++++++++--------- 4 files changed, 260 insertions(+), 162 deletions(-) create mode 100644 .changeset/afraid-kids-jog.md diff --git a/.changeset/afraid-kids-jog.md b/.changeset/afraid-kids-jog.md new file mode 100644 index 0000000000..39e4e5c5a1 --- /dev/null +++ b/.changeset/afraid-kids-jog.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-permission-node': patch +--- + +The returned router from `createPermissionIntegrationRouter` is now mutable, allowing for permissions and resources to be added after creation of the router. diff --git a/plugins/permission-node/report.api.md b/plugins/permission-node/report.api.md index 83730c6050..748ed58f3b 100644 --- a/plugins/permission-node/report.api.md +++ b/plugins/permission-node/report.api.md @@ -124,7 +124,7 @@ export function createPermissionIntegrationRouter< TResourceType3 extends string, TResource3, >( - options: + options?: | { permissions: Array; } @@ -140,7 +140,15 @@ export function createPermissionIntegrationRouter< TResourceType3, TResource3 >, -): express.Router; +): express.Router & { + addPermissions(permissions: Permission[]): void; + addResourceType( + resource: CreatePermissionIntegrationRouterResourceOptions< + TResourceType, + TResource + >, + ): void; +}; // @public export type CreatePermissionIntegrationRouterResourceOptions< diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts index 0062ef03b6..0bce007c89 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts @@ -981,6 +981,94 @@ describe('createPermissionIntegrationRouter', () => { ], }); }); + + it('returns a list of basic permissions together with permissions and rules from multiple resource types with mutation', async () => { + const aPermission = createPermission({ + name: 'a.permission', + attributes: {}, + }); + + const router = createPermissionIntegrationRouter(); + + const responseBefore = await request(express().use(router)).get( + '/.well-known/backstage/permissions/metadata', + ); + + expect(responseBefore.status).toEqual(200); + expect(responseBefore.body).toEqual({ + permissions: [], + rules: [], + }); + + router.addPermissions([aPermission, testPermission]); + + router.addResourceType({ + resourceType: 'test-resource', + permissions: [testPermission], + getResources: defaultMockedGetResources1, + rules: [testRule1, testRule2], + }); + + router.addResourceType({ + resourceType: 'test-resource-2', + permissions: [testPermission2], + getResources: defaultMockedGetResources2, + rules: [testRule3], + }); + + const responseAfter = await request(express().use(router)).get( + '/.well-known/backstage/permissions/metadata', + ); + + expect(responseAfter.status).toEqual(200); + expect(responseAfter.body).toEqual({ + permissions: [aPermission, testPermission, testPermission2], + rules: [ + { + name: testRule1.name, + description: testRule1.description, + resourceType: testRule1.resourceType, + paramsSchema: { + $schema: 'http://json-schema.org/draft-07/schema#', + additionalProperties: false, + properties: { + foo: { + type: 'string', + }, + bar: { + description: 'bar', + type: 'number', + }, + }, + required: ['foo', 'bar'], + type: 'object', + }, + }, + { + name: testRule2.name, + description: testRule2.description, + resourceType: testRule2.resourceType, + paramsSchema: { + $schema: 'http://json-schema.org/draft-07/schema#', + additionalProperties: false, + properties: {}, + type: 'object', + }, + }, + { + name: testRule3.name, + description: testRule3.description, + resourceType: testRule3.resourceType, + paramsSchema: { + $schema: 'http://json-schema.org/draft-07/schema#', + additionalProperties: false, + properties: {}, + type: 'object', + }, + }, + ], + }); + }); }); describe('createConditionAuthorizer', () => { diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts index de1e32169a..9ef5b564d8 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts @@ -250,6 +250,102 @@ export type PermissionIntegrationRouterOptions< >; }; +class PermissionIntegrationMetadataStore { + readonly #rulesByTypeByName = new Map< + string, + Map> + >(); + readonly #permissionsByName = new Map(); + readonly #resourcesByType = new Map< + string, + CreatePermissionIntegrationRouterResourceOptions + >(); + readonly #serializedRules = new Array(); + + getSerializedMetadata(): MetadataResponse { + return { + permissions: Array.from(this.#permissionsByName.values()), + rules: this.#serializedRules, + }; + } + + hasResourceType(type: string): boolean { + return this.#resourcesByType.has(type); + } + + async getResources( + resourceType: string, + refs: string[], + ): Promise> { + const resource = this.#resourcesByType.get(resourceType); + if (!resource?.getResources) { + throw new NotImplementedError( + `This plugin does not expose any permission rule or can't evaluate the conditions request for ${resourceType}`, + ); + } + + const uniqueRefs = Array.from(new Set(refs)); + const resources = await resource.getResources(uniqueRefs); + return Object.fromEntries( + uniqueRefs.map((ref, index) => [ref, resources[index]]), + ); + } + + getRuleMapper(resourceType: string) { + return (name: string): PermissionRule => { + const rule = this.#rulesByTypeByName.get(resourceType)?.get(name); + if (!rule) { + throw new Error(`Unexpected permission rule: ${name}`); + } + return rule; + }; + } + + addPermissions(permissions: Permission[]) { + for (const permission of permissions) { + // Permission naming conflicts are silently ignored + this.#permissionsByName.set(permission.name, permission); + } + } + + addResourceType( + resource: CreatePermissionIntegrationRouterResourceOptions, + ) { + const { resourceType } = resource; + + if (this.#resourcesByType.has(resourceType)) { + throw new Error( + `Refused to add permission resource with type '${resourceType}' because it already exists`, + ); + } + this.#resourcesByType.set(resourceType, resource); + + for (const rule of resource.rules) { + const rulesByName = + this.#rulesByTypeByName.get(resourceType) ?? new Map(); + this.#rulesByTypeByName.set(resourceType, rulesByName); + + if (rulesByName.has(rule.name)) { + throw new Error( + `Refused to add permission rule for type '${resourceType}' with name '${rule.name}' because it already exists`, + ); + } + rulesByName.set(rule.name, rule); + + this.#serializedRules.push({ + name: rule.name, + description: rule.description, + resourceType: rule.resourceType, + paramsSchema: zodToJsonSchema(rule.paramsSchema ?? z.object({})), + }); + } + + if (resource.permissions) { + this.addPermissions(resource.permissions); + } + } +} + /** * Create an express Router which provides an authorization route to allow * integration between the permission backend and other Backstage backend @@ -299,7 +395,7 @@ export function createPermissionIntegrationRouter< TResourceType3 extends string, TResource3, >( - options: + options?: | { permissions: Array } | CreatePermissionIntegrationRouterResourceOptions< TResourceType1, @@ -313,167 +409,80 @@ export function createPermissionIntegrationRouter< TResourceType3, TResource3 >, -): express.Router { - const optionsWithResources = options as PermissionIntegrationRouterOptions; - const allOptions = [ - optionsWithResources.resources ? optionsWithResources.resources : options, - ].flat(); - const allRules = allOptions.flatMap( - option => - ( - option as CreatePermissionIntegrationRouterResourceOptions< - TResourceType1, - TResource1 - > - ).rules || [], - ); +): express.Router & { + addPermissions(permissions: Permission[]): void; + addResourceType( + resource: CreatePermissionIntegrationRouterResourceOptions< + TResourceType, + TResource + >, + ): void; +} { + const store = new PermissionIntegrationMetadataStore(); - const allPermissions = Array.from( - new Map( - [ - ...((options as { permissions: Permission[] }).permissions || []), - ...(optionsWithResources.resources?.flatMap(o => o.permissions || []) || - []), - ].map(i => [i.name, i]), - ).values(), - ); + if (options) { + if ('resources' in options) { + // Not technically allowed by types, but it's historically been covered by tests + if ('permissions' in options) { + store.addPermissions(options.permissions as Permission[]); + } - const allResourceTypes = allOptions.reduce((acc, option) => { - if ( - isCreatePermissionIntegrationRouterResourceOptions( - option as - | { permissions: Array } - | CreatePermissionIntegrationRouterResourceOptions< - TResourceType1, - TResource1 - >, - ) - ) { - acc.push( - ( - option as CreatePermissionIntegrationRouterResourceOptions< - TResourceType1, - TResource1 - > - ).resourceType, - ); + for (const resource of options.resources) { + store.addResourceType(resource); + } + } else if ('resourceType' in options) { + store.addResourceType(options); + } else { + store.addPermissions(options.permissions); } - return acc; - }, [] as string[]); + } const router = Router(); - router.use(express.json()); + + router.use('/.well-known/backstage/permissions/', express.json()); router.get('/.well-known/backstage/permissions/metadata', (_, res) => { - const serializedRules: MetadataResponseSerializedRule[] = allRules.map( - rule => ({ - name: rule.name, - description: rule.description, - resourceType: rule.resourceType, - paramsSchema: zodToJsonSchema(rule.paramsSchema ?? z.object({})), - }), - ); - - const responseJson: MetadataResponse = { - permissions: allPermissions, - rules: serializedRules, - }; - - return res.json(responseJson); + res.json(store.getSerializedMetadata()); }); router.post( '/.well-known/backstage/permissions/apply-conditions', async (req, res: Response) => { - const ruleMapByResourceType: Record< - string, - ReturnType - > = {}; - const getResourcesByResourceType: Record< - string, - CreatePermissionIntegrationRouterResourceOptions< - TResourceType1, - TResource1 - >['getResources'] - > = {}; - - for (let option of allOptions) { - option = option as - | { permissions: Array } - | CreatePermissionIntegrationRouterResourceOptions< - TResourceType1, - TResource1 - >; - if (isCreatePermissionIntegrationRouterResourceOptions(option)) { - ruleMapByResourceType[option.resourceType] = createGetRule( - option.rules, - ); - - getResourcesByResourceType[option.resourceType] = option.getResources; - } - } - - const assertValidResourceTypes = ( - requests: ApplyConditionsRequestEntry[], - ) => { - const invalidResourceTypes = requests - .filter(request => !allResourceTypes.includes(request.resourceType)) - .map(request => request.resourceType); - - if (invalidResourceTypes.length) { - throw new InputError( - `Unexpected resource types: ${invalidResourceTypes.join(', ')}.`, - ); - } - }; - const parseResult = applyConditionsRequestSchema.safeParse(req.body); - if (!parseResult.success) { throw new InputError(parseResult.error.toString()); } - const body = parseResult.data; + const { items: requests } = parseResult.data; - assertValidResourceTypes(body.items); - - const resourceRefsByResourceType = body.items.reduce< - Record> - >((acc, item) => { - if (!acc[item.resourceType]) { - acc[item.resourceType] = new Set(); - } - acc[item.resourceType].add(item.resourceRef); - return acc; - }, {}); - - const resourcesByResourceType: Record> = {}; - for (const resourceType of Object.keys(resourceRefsByResourceType)) { - const getResources = getResourcesByResourceType[resourceType]; - if (!getResources) { - throw new NotImplementedError( - `This plugin does not expose any permission rule or can't evaluate the conditions request for ${resourceType}`, - ); - } - const resourceRefs = Array.from( - resourceRefsByResourceType[resourceType], + const invalidResourceTypes = requests.filter( + i => !store.hasResourceType(i.resourceType), + ); + if (invalidResourceTypes.length) { + throw new InputError( + `Unexpected resource types: ${invalidResourceTypes + .map(i => i.resourceType) + .join(', ')}.`, ); - const resources = await getResources(resourceRefs); - resourceRefs.forEach((resourceRef, index) => { - if (!resourcesByResourceType[resourceType]) { - resourcesByResourceType[resourceType] = {}; - } - resourcesByResourceType[resourceType][resourceRef] = resources[index]; - }); } - return res.json({ - items: body.items.map(request => ({ + const resourcesByType: Record> = {}; + for (const requestedType of new Set(requests.map(i => i.resourceType))) { + resourcesByType[requestedType] = await store.getResources( + requestedType, + requests + .filter(r => r.resourceType === requestedType) + .map(i => i.resourceRef), + ); + } + + res.json({ + items: requests.map(request => ({ id: request.id, result: applyConditions( request.conditions, - resourcesByResourceType[request.resourceType][request.resourceRef], - ruleMapByResourceType[request.resourceType], + resourcesByType[request.resourceType][request.resourceRef], + store.getRuleMapper(request.resourceType), ) ? AuthorizeResult.ALLOW : AuthorizeResult.DENY, @@ -482,29 +491,17 @@ export function createPermissionIntegrationRouter< }, ); - return router; -} - -function isCreatePermissionIntegrationRouterResourceOptions< - TResourceType extends string, - TResource, ->( - options: - | { permissions: Array } - | CreatePermissionIntegrationRouterResourceOptions< + return Object.assign(router, { + addPermissions(permissions: Permission[]) { + store.addPermissions(permissions); + }, + addResourceType( + resource: CreatePermissionIntegrationRouterResourceOptions< TResourceType, TResource >, -): options is CreatePermissionIntegrationRouterResourceOptions< - TResourceType, - TResource -> { - return ( - ( - options as CreatePermissionIntegrationRouterResourceOptions< - TResourceType, - TResource - > - ).resourceType !== undefined - ); + ) { + store.addResourceType(resource); + }, + }); } From 9ddfd94528b9d3f9f4d5eb282c3933aa417b1198 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 2 Jan 2025 01:37:15 +0100 Subject: [PATCH 02/15] backend-plugin-api: added PermissionsIntegrationsService definition Signed-off-by: Patrik Oldsberg --- .changeset/sharp-vans-protect.md | 5 + packages/backend-plugin-api/package.json | 1 + packages/backend-plugin-api/report.api.md | 29 +++++ .../PermissionIntegrationsService.ts | 116 ++++++++++++++++++ .../src/services/definitions/coreServices.ts | 13 ++ .../src/services/definitions/index.ts | 4 + yarn.lock | 1 + 7 files changed, 169 insertions(+) create mode 100644 .changeset/sharp-vans-protect.md create mode 100644 packages/backend-plugin-api/src/services/definitions/PermissionIntegrationsService.ts diff --git a/.changeset/sharp-vans-protect.md b/.changeset/sharp-vans-protect.md new file mode 100644 index 0000000000..a2f9237a56 --- /dev/null +++ b/.changeset/sharp-vans-protect.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': patch +--- + +Added new `PermissionIntegrationsService` that is used by plugins to register permissions, resource types, and rules into the permission system. This replaces the existing `createPermissionIntegrationRouter` from `@backstage/plugin-permission-node`. diff --git a/packages/backend-plugin-api/package.json b/packages/backend-plugin-api/package.json index 7e4fa19c84..8544741dc9 100644 --- a/packages/backend-plugin-api/package.json +++ b/packages/backend-plugin-api/package.json @@ -58,6 +58,7 @@ "@backstage/errors": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", "@backstage/plugin-permission-common": "workspace:^", + "@backstage/plugin-permission-node": "workspace:^", "@backstage/types": "workspace:^", "@types/express": "^4.17.6", "@types/luxon": "^3.0.0", diff --git a/packages/backend-plugin-api/report.api.md b/packages/backend-plugin-api/report.api.md index 8416c6c200..2a8300ccec 100644 --- a/packages/backend-plugin-api/report.api.md +++ b/packages/backend-plugin-api/report.api.md @@ -16,8 +16,10 @@ import { isChildPath } from '@backstage/cli-common'; import { JsonObject } from '@backstage/types'; import { JsonValue } from '@backstage/types'; 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 { PermissionRule } from '@backstage/plugin-permission-node'; import { QueryPermissionRequest } from '@backstage/plugin-permission-common'; import { QueryPermissionResponse } from '@backstage/plugin-permission-common'; import { Readable } from 'stream'; @@ -184,6 +186,11 @@ export namespace coreServices { const lifecycle: ServiceRef; const logger: ServiceRef; const permissions: ServiceRef; + const permissionIntegrations: ServiceRef< + PermissionIntegrationsService, + 'plugin', + 'singleton' + >; const pluginMetadata: ServiceRef< PluginMetadataService, 'plugin', @@ -425,6 +432,28 @@ export interface LoggerService { warn(message: string, meta?: Error | JsonObject): void; } +// @public +export interface PermissionIntegrationsService { + addPermissions(permissions: Permission[]): void; + addResourceType( + options: PermissionIntegrationsServiceAddResourceTypeOptions< + TResourceType, + TResource + >, + ): void; +} + +// @public +export type PermissionIntegrationsServiceAddResourceTypeOptions< + TResourceType extends string, + TResource, +> = { + resourceType: TResourceType; + permissions?: Array; + rules: PermissionRule>[]; + getResources?(resourceRefs: string[]): Promise>; +}; + // @public export interface PermissionsService extends PermissionEvaluator { authorize( diff --git a/packages/backend-plugin-api/src/services/definitions/PermissionIntegrationsService.ts b/packages/backend-plugin-api/src/services/definitions/PermissionIntegrationsService.ts new file mode 100644 index 0000000000..fd25a16b1c --- /dev/null +++ b/packages/backend-plugin-api/src/services/definitions/PermissionIntegrationsService.ts @@ -0,0 +1,116 @@ +/* + * 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 { Permission } from '@backstage/plugin-permission-common'; +import { PermissionRule } from '@backstage/plugin-permission-node'; + +/** + * Prevent use of type parameter from contributing to type inference. + * + * https://github.com/Microsoft/TypeScript/issues/14829#issuecomment-980401795 + * @ignore + */ +type NoInfer = T extends infer S ? S : never; + +/** + * Options for adding a resource type to the permission system. + * + * @public + */ +export type PermissionIntegrationsServiceAddResourceTypeOptions< + TResourceType extends string, + TResource, +> = { + /** + * The identifier for the resource type. + */ + resourceType: TResourceType; + + /** + * Permissions that are available for this resource type. + */ + permissions?: Array; + + /** + * Permission rules that are available for this resource type. + */ + rules: PermissionRule>[]; + + /** + * The function used to load associated resources based in the provided + * references. + * + * @remarks + * + * If this function is not provided the permission system will not be able to + * resolve conditional decisions except when requesting resources directly + * form the plugin. + */ + getResources?(resourceRefs: string[]): Promise>; +}; + +/** + * Permission system integration for registering resources and permissions. + * + * See the {@link https://backstage.io/docs/permissions/overview | permissions documentation} + * and the {@link https://backstage.io/docs/backend-system/core-services/permission-integrations | service documentation} + * for more details. + * + * @public + */ +export interface PermissionIntegrationsService { + /** + * Add permissions for this plugin to the permission system. + */ + addPermissions(permissions: Permission[]): void; + + /** + * Add a new resource type that is owned by this plugin to the permission + * system. + * + * @remarks + * + * To make this concrete, we can use the Backstage software catalog as an + * example. The catalog has conditional rules around access to specific + * _entities_ in the catalog. The _type_ of resource is captured here as + * `resourceType`, a string identifier (`catalog-entity` in this example) that + * can be provided with permission definitions. This is merely a _type_ to + * verify that conditions in an authorization policy are constructed + * correctly, not a reference to a specific resource. + * + * The `rules` parameter is an array of + * {@link @backstage/plugin-permission-node#PermissionRule}s that introduce + * conditional filtering logic for resources; for the catalog, these are + * things like `isEntityOwner` or `hasAnnotation`. Rules describe how to + * filter a list of resources, and the `conditions` returned allow these rules + * to be applied with specific parameters (such as 'group:default/team-a', or + * 'backstage.io/edit-url'). + * + * The `getResources` argument should load resources based on a reference + * identifier. For the catalog, this is an + * {@link @backstage/catalog-model#EntityRef}. For other plugins, this can be + * any serialized format. This is used to add a permission integrations API + * via the HTTP router service. This API will be called by the + * `permission-backend` when authorization conditions relating to this plugin + * need to be evaluated. + */ + addResourceType( + options: PermissionIntegrationsServiceAddResourceTypeOptions< + TResourceType, + TResource + >, + ): void; +} diff --git a/packages/backend-plugin-api/src/services/definitions/coreServices.ts b/packages/backend-plugin-api/src/services/definitions/coreServices.ts index f87bff023c..d65b3b85ea 100644 --- a/packages/backend-plugin-api/src/services/definitions/coreServices.ts +++ b/packages/backend-plugin-api/src/services/definitions/coreServices.ts @@ -174,6 +174,19 @@ export namespace coreServices { import('./PermissionsService').PermissionsService >({ id: 'core.permissions' }); + /** + * Permission system integration for registering resources and permissions. + * + * See {@link PermissionIntegrationsService} + * and {@link https://backstage.io/docs/backend-system/core-services/permission-integrations | the service docs} + * for more information. + * + * @public + */ + export const permissionIntegrations = createServiceRef< + import('./PermissionIntegrationsService').PermissionIntegrationsService + >({ id: 'core.permissionIntegrations' }); + /** * Built-in service for accessing metadata about the current plugin. * diff --git a/packages/backend-plugin-api/src/services/definitions/index.ts b/packages/backend-plugin-api/src/services/definitions/index.ts index 1df00bdead..4e82f1e089 100644 --- a/packages/backend-plugin-api/src/services/definitions/index.ts +++ b/packages/backend-plugin-api/src/services/definitions/index.ts @@ -50,6 +50,10 @@ export type { PermissionsService, PermissionsServiceRequestOptions, } from './PermissionsService'; +export type { + PermissionIntegrationsService, + PermissionIntegrationsServiceAddResourceTypeOptions, +} from './PermissionIntegrationsService'; export type { PluginMetadataService } from './PluginMetadataService'; export type { RootHttpRouterService } from './RootHttpRouterService'; export type { RootLifecycleService } from './RootLifecycleService'; diff --git a/yarn.lock b/yarn.lock index f40ec341af..2ed052002c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3774,6 +3774,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" "@backstage/plugin-permission-common": "workspace:^" + "@backstage/plugin-permission-node": "workspace:^" "@backstage/types": "workspace:^" "@types/express": ^4.17.6 "@types/luxon": ^3.0.0 From a19cb2b8a53ab78bdbf71a8f5f492523df769135 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 2 Jan 2025 11:01:17 +0100 Subject: [PATCH 03/15] backend-defaults: implement permissionIntegrations service Signed-off-by: Patrik Oldsberg --- .changeset/hungry-mirrors-sniff.md | 5 ++ packages/backend-defaults/package.json | 20 +++--- .../report-permissionIntegrations.api.md | 17 +++++ .../backend-defaults/src/CreateBackend.ts | 2 + .../permissionIntegrations/index.ts | 17 +++++ .../permissionIntegrationsServiceFactory.ts | 67 +++++++++++++++++++ 6 files changed, 120 insertions(+), 8 deletions(-) create mode 100644 .changeset/hungry-mirrors-sniff.md create mode 100644 packages/backend-defaults/report-permissionIntegrations.api.md create mode 100644 packages/backend-defaults/src/entrypoints/permissionIntegrations/index.ts create mode 100644 packages/backend-defaults/src/entrypoints/permissionIntegrations/permissionIntegrationsServiceFactory.ts diff --git a/.changeset/hungry-mirrors-sniff.md b/.changeset/hungry-mirrors-sniff.md new file mode 100644 index 0000000000..27ebf9d896 --- /dev/null +++ b/.changeset/hungry-mirrors-sniff.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +Added default implementation for the new `PermissionIntegrationsService`. diff --git a/packages/backend-defaults/package.json b/packages/backend-defaults/package.json index e29bb0da81..7ef216af3a 100644 --- a/packages/backend-defaults/package.json +++ b/packages/backend-defaults/package.json @@ -28,6 +28,7 @@ "./httpRouter": "./src/entrypoints/httpRouter/index.ts", "./lifecycle": "./src/entrypoints/lifecycle/index.ts", "./logger": "./src/entrypoints/logger/index.ts", + "./permissionIntegrations": "./src/entrypoints/permissionIntegrations/index.ts", "./permissions": "./src/entrypoints/permissions/index.ts", "./rootConfig": "./src/entrypoints/rootConfig/index.ts", "./rootHealth": "./src/entrypoints/rootHealth/index.ts", @@ -67,6 +68,9 @@ "logger": [ "src/entrypoints/logger/index.ts" ], + "permissionIntegrations": [ + "src/entrypoints/permissionIntegrations/index.ts" + ], "permissions": [ "src/entrypoints/permissions/index.ts" ], @@ -185,14 +189,6 @@ "yn": "^4.0.0", "zod": "^3.22.4" }, - "peerDependencies": { - "@google-cloud/cloud-sql-connector": "^1.4.0" - }, - "peerDependenciesMeta": { - "@google-cloud/cloud-sql-connector": { - "optional": true - } - }, "devDependencies": { "@aws-sdk/util-stream-node": "^3.350.0", "@backstage/backend-plugin-api": "workspace:^", @@ -214,5 +210,13 @@ "supertest": "^7.0.0", "wait-for-expect": "^3.0.2" }, + "peerDependencies": { + "@google-cloud/cloud-sql-connector": "^1.4.0" + }, + "peerDependenciesMeta": { + "@google-cloud/cloud-sql-connector": { + "optional": true + } + }, "configSchema": "config.d.ts" } diff --git a/packages/backend-defaults/report-permissionIntegrations.api.md b/packages/backend-defaults/report-permissionIntegrations.api.md new file mode 100644 index 0000000000..db24c04d65 --- /dev/null +++ b/packages/backend-defaults/report-permissionIntegrations.api.md @@ -0,0 +1,17 @@ +## API Report File for "@backstage/backend-defaults" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { PermissionIntegrationsService } from '@backstage/backend-plugin-api'; +import { ServiceFactory } from '@backstage/backend-plugin-api'; + +// @public +export const permissionIntegrationsServiceFactory: ServiceFactory< + PermissionIntegrationsService, + 'plugin', + 'singleton' +>; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/backend-defaults/src/CreateBackend.ts b/packages/backend-defaults/src/CreateBackend.ts index 4b0f497980..3cfb2b24ea 100644 --- a/packages/backend-defaults/src/CreateBackend.ts +++ b/packages/backend-defaults/src/CreateBackend.ts @@ -23,6 +23,7 @@ import { httpAuthServiceFactory } from '@backstage/backend-defaults/httpAuth'; import { httpRouterServiceFactory } from '@backstage/backend-defaults/httpRouter'; import { lifecycleServiceFactory } from '@backstage/backend-defaults/lifecycle'; import { loggerServiceFactory } from '@backstage/backend-defaults/logger'; +import { permissionIntegrationsServiceFactory } from '@backstage/backend-defaults/permissionIntegrations'; import { permissionsServiceFactory } from '@backstage/backend-defaults/permissions'; import { rootConfigServiceFactory } from '@backstage/backend-defaults/rootConfig'; import { rootHealthServiceFactory } from '@backstage/backend-defaults/rootHealth'; @@ -44,6 +45,7 @@ export const defaultServiceFactories = [ httpRouterServiceFactory, lifecycleServiceFactory, loggerServiceFactory, + permissionIntegrationsServiceFactory, permissionsServiceFactory, rootHealthServiceFactory, rootHttpRouterServiceFactory, diff --git a/packages/backend-defaults/src/entrypoints/permissionIntegrations/index.ts b/packages/backend-defaults/src/entrypoints/permissionIntegrations/index.ts new file mode 100644 index 0000000000..8e1daa15a1 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/permissionIntegrations/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 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. + */ + +export { permissionIntegrationsServiceFactory } from './permissionIntegrationsServiceFactory'; diff --git a/packages/backend-defaults/src/entrypoints/permissionIntegrations/permissionIntegrationsServiceFactory.ts b/packages/backend-defaults/src/entrypoints/permissionIntegrations/permissionIntegrationsServiceFactory.ts new file mode 100644 index 0000000000..3b668514ce --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/permissionIntegrations/permissionIntegrationsServiceFactory.ts @@ -0,0 +1,67 @@ +/* + * 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, + createServiceFactory, +} from '@backstage/backend-plugin-api'; +import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node'; + +/** + * Permission system integration for registering resources and permissions. + * + * See {@link @backstage/code-plugin-api#PermissionIntegrationsService} + * and {@link https://backstage.io/docs/backend-system/core-services/permission-integrations | the service docs} + * for more information. + * + * @public + */ +export const permissionIntegrationsServiceFactory = createServiceFactory({ + service: coreServices.permissionIntegrations, + deps: { + lifecycle: coreServices.lifecycle, + httpRouter: coreServices.httpRouter, + }, + async factory({ httpRouter, lifecycle }) { + const router = createPermissionIntegrationRouter(); + + httpRouter.use(router); + + let started = false; + lifecycle.addStartupHook(() => { + started = true; + }); + + return { + addResourceType(resource) { + if (started) { + throw new Error( + 'Cannot add permission resource types after the plugin has started', + ); + } + router.addResourceType(resource); + }, + addPermissions(permissions) { + if (started) { + throw new Error( + 'Cannot add permissions after the plugin has started', + ); + } + router.addPermissions(permissions); + }, + }; + }, +}); From 45e0c1d64598bdd948cf908ce7c03231d685890e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 8 Jan 2025 09:51:03 +0100 Subject: [PATCH 04/15] permissions: add support for adding permission rules separately from resource type Signed-off-by: Patrik Oldsberg --- .../permissionIntegrationsServiceFactory.ts | 8 ++++ packages/backend-plugin-api/report.api.md | 1 + .../PermissionIntegrationsService.ts | 15 ++++++ plugins/permission-node/report.api.md | 1 + .../createPermissionIntegrationRouter.test.ts | 9 +++- .../createPermissionIntegrationRouter.ts | 46 +++++++++++-------- 6 files changed, 60 insertions(+), 20 deletions(-) diff --git a/packages/backend-defaults/src/entrypoints/permissionIntegrations/permissionIntegrationsServiceFactory.ts b/packages/backend-defaults/src/entrypoints/permissionIntegrations/permissionIntegrationsServiceFactory.ts index 3b668514ce..b083439515 100644 --- a/packages/backend-defaults/src/entrypoints/permissionIntegrations/permissionIntegrationsServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/permissionIntegrations/permissionIntegrationsServiceFactory.ts @@ -62,6 +62,14 @@ export const permissionIntegrationsServiceFactory = createServiceFactory({ } router.addPermissions(permissions); }, + addPermissionRules(rules) { + if (started) { + throw new Error( + 'Cannot add permission rules after the plugin has started', + ); + } + router.addPermissionRules(rules); + }, }; }, }); diff --git a/packages/backend-plugin-api/report.api.md b/packages/backend-plugin-api/report.api.md index 2a8300ccec..224a057686 100644 --- a/packages/backend-plugin-api/report.api.md +++ b/packages/backend-plugin-api/report.api.md @@ -434,6 +434,7 @@ export interface LoggerService { // @public export interface PermissionIntegrationsService { + addPermissionRules(rules: PermissionRule[]): void; addPermissions(permissions: Permission[]): void; addResourceType( options: PermissionIntegrationsServiceAddResourceTypeOptions< diff --git a/packages/backend-plugin-api/src/services/definitions/PermissionIntegrationsService.ts b/packages/backend-plugin-api/src/services/definitions/PermissionIntegrationsService.ts index fd25a16b1c..2d4a2e0ffb 100644 --- a/packages/backend-plugin-api/src/services/definitions/PermissionIntegrationsService.ts +++ b/packages/backend-plugin-api/src/services/definitions/PermissionIntegrationsService.ts @@ -77,6 +77,21 @@ export interface PermissionIntegrationsService { */ addPermissions(permissions: Permission[]): void; + /** + * Adds a set of permission rules to the permission system for a resource type + * that is owned by this plugin. + * + * @remarks + * + * Rules should be created using corresponding `create*PermissionRule` + * functions exported by plugins, who in turn are created with + * `makeCreatePermissionRule`. + * + * Rules can be added either directly by the plugin itself or through a plugin + * module. + */ + addPermissionRules(rules: PermissionRule[]): void; + /** * Add a new resource type that is owned by this plugin to the permission * system. diff --git a/plugins/permission-node/report.api.md b/plugins/permission-node/report.api.md index 748ed58f3b..a55cbbf9c5 100644 --- a/plugins/permission-node/report.api.md +++ b/plugins/permission-node/report.api.md @@ -142,6 +142,7 @@ export function createPermissionIntegrationRouter< >, ): express.Router & { addPermissions(permissions: Permission[]): void; + addPermissionRules(rules: PermissionRule[]): void; addResourceType( resource: CreatePermissionIntegrationRouterResourceOptions< TResourceType, diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts index 0bce007c89..dbb5647157 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts @@ -1006,14 +1006,19 @@ describe('createPermissionIntegrationRouter', () => { resourceType: 'test-resource', permissions: [testPermission], getResources: defaultMockedGetResources1, - rules: [testRule1, testRule2], + rules: [testRule1], }); + router.addPermissionRules([testRule2]); + + // This one is for the resource added below, it should be possible to add rules before the resource typeof + router.addPermissionRules([testRule3]); + router.addResourceType({ resourceType: 'test-resource-2', permissions: [testPermission2], getResources: defaultMockedGetResources2, - rules: [testRule3], + rules: [], }); const responseAfter = await request(express().use(router)).get( diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts index 9ef5b564d8..f24f4827e7 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts @@ -308,6 +308,28 @@ class PermissionIntegrationMetadataStore { } } + addPermissionRules(rules: PermissionRule[]) { + for (const rule of rules) { + const rulesByName = + this.#rulesByTypeByName.get(rule.resourceType) ?? new Map(); + this.#rulesByTypeByName.set(rule.resourceType, rulesByName); + + if (rulesByName.has(rule.name)) { + throw new Error( + `Refused to add permission rule for type '${rule.resourceType}' with name '${rule.name}' because it already exists`, + ); + } + rulesByName.set(rule.name, rule); + + this.#serializedRules.push({ + name: rule.name, + description: rule.description, + resourceType: rule.resourceType, + paramsSchema: zodToJsonSchema(rule.paramsSchema ?? z.object({})), + }); + } + } + addResourceType( resource: CreatePermissionIntegrationRouterResourceOptions, ) { @@ -320,24 +342,8 @@ class PermissionIntegrationMetadataStore { } this.#resourcesByType.set(resourceType, resource); - for (const rule of resource.rules) { - const rulesByName = - this.#rulesByTypeByName.get(resourceType) ?? new Map(); - this.#rulesByTypeByName.set(resourceType, rulesByName); - - if (rulesByName.has(rule.name)) { - throw new Error( - `Refused to add permission rule for type '${resourceType}' with name '${rule.name}' because it already exists`, - ); - } - rulesByName.set(rule.name, rule); - - this.#serializedRules.push({ - name: rule.name, - description: rule.description, - resourceType: rule.resourceType, - paramsSchema: zodToJsonSchema(rule.paramsSchema ?? z.object({})), - }); + if (resource.rules) { + this.addPermissionRules(resource.rules); } if (resource.permissions) { @@ -411,6 +417,7 @@ export function createPermissionIntegrationRouter< >, ): express.Router & { addPermissions(permissions: Permission[]): void; + addPermissionRules(rules: PermissionRule[]): void; addResourceType( resource: CreatePermissionIntegrationRouterResourceOptions< TResourceType, @@ -495,6 +502,9 @@ export function createPermissionIntegrationRouter< addPermissions(permissions: Permission[]) { store.addPermissions(permissions); }, + addPermissionRules(rules: PermissionRule[]) { + store.addPermissionRules(rules); + }, addResourceType( resource: CreatePermissionIntegrationRouterResourceOptions< TResourceType, From dd05a977d93e2c8298ccd74765d31c155bc542d3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 8 Jan 2025 11:02:13 +0100 Subject: [PATCH 05/15] backend-test-utils: added mocks for permissions integrations service Signed-off-by: Patrik Oldsberg --- .changeset/curvy-ways-play.md | 5 +++++ packages/backend-test-utils/report.api.md | 14 ++++++++++++++ .../src/next/services/mockServices.ts | 10 ++++++++++ .../src/next/wiring/TestBackend.ts | 1 + 4 files changed, 30 insertions(+) create mode 100644 .changeset/curvy-ways-play.md diff --git a/.changeset/curvy-ways-play.md b/.changeset/curvy-ways-play.md new file mode 100644 index 0000000000..4cb0e3fd9f --- /dev/null +++ b/.changeset/curvy-ways-play.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': minor +--- + +Added mocks for the new `PermissionIntegrationsService`. diff --git a/packages/backend-test-utils/report.api.md b/packages/backend-test-utils/report.api.md index a79bd9d03c..804f06fcbb 100644 --- a/packages/backend-test-utils/report.api.md +++ b/packages/backend-test-utils/report.api.md @@ -33,6 +33,7 @@ import { LifecycleService } from '@backstage/backend-plugin-api'; import { LoggerService } from '@backstage/backend-plugin-api'; import { ParamsDictionary } from 'express-serve-static-core'; import { ParsedQs } from 'qs'; +import { PermissionIntegrationsService } from '@backstage/backend-plugin-api'; import { PermissionsService } from '@backstage/backend-plugin-api'; import { RootConfigService } from '@backstage/backend-plugin-api'; import { RootHealthService } from '@backstage/backend-plugin-api'; @@ -255,6 +256,19 @@ export namespace mockServices { ) => ServiceMock; } // (undocumented) + export namespace permissionIntegrations { + const // (undocumented) + factory: () => ServiceFactory< + PermissionIntegrationsService, + 'plugin', + 'singleton' + >; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; + } + // (undocumented) export namespace permissions { const // (undocumented) factory: () => ServiceFactory; diff --git a/packages/backend-test-utils/src/next/services/mockServices.ts b/packages/backend-test-utils/src/next/services/mockServices.ts index b9bca62767..83c0213296 100644 --- a/packages/backend-test-utils/src/next/services/mockServices.ts +++ b/packages/backend-test-utils/src/next/services/mockServices.ts @@ -53,6 +53,7 @@ import { MockRootLoggerService } from './MockRootLoggerService'; import { MockUserInfoService } from './MockUserInfoService'; import { mockCredentials } from './mockCredentials'; import { Knex } from 'knex'; +import { permissionIntegrationsServiceFactory } from '@backstage/backend-defaults/permissionIntegrations'; /** @internal */ function createLoggerMock() { @@ -468,6 +469,15 @@ export namespace mockServices { })); } + export namespace permissionIntegrations { + export const factory = () => permissionIntegrationsServiceFactory; + export const mock = simpleMock(coreServices.permissionIntegrations, () => ({ + addPermissionRules: jest.fn(), + addPermissions: jest.fn(), + addResourceType: jest.fn(), + })); + } + export namespace rootLifecycle { export const factory = () => rootLifecycleServiceFactory; export const mock = simpleMock(coreServices.rootLifecycle, () => ({ diff --git a/packages/backend-test-utils/src/next/wiring/TestBackend.ts b/packages/backend-test-utils/src/next/wiring/TestBackend.ts index 4471f6ed24..7c0edb069b 100644 --- a/packages/backend-test-utils/src/next/wiring/TestBackend.ts +++ b/packages/backend-test-utils/src/next/wiring/TestBackend.ts @@ -75,6 +75,7 @@ export const defaultServiceFactories = [ mockServices.lifecycle.factory(), mockServices.logger.factory(), mockServices.permissions.factory(), + mockServices.permissionIntegrations.factory(), mockServices.rootHealth.factory(), mockServices.rootLifecycle.factory(), mockServices.rootLogger.factory(), From 8805f938d534283dc1e193d4501e41f2a26c9684 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 8 Jan 2025 11:03:59 +0100 Subject: [PATCH 06/15] catalog-backend: add support for permission integrations service + test Signed-off-by: Patrik Oldsberg --- .changeset/loud-walls-build.md | 5 ++ plugins/catalog-backend/report.api.md | 2 + .../src/service/CatalogBuilder.ts | 19 ++++- .../src/service/CatalogPlugin.test.ts | 71 +++++++++++++++++++ .../src/service/CatalogPlugin.ts | 3 + 5 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 .changeset/loud-walls-build.md create mode 100644 plugins/catalog-backend/src/service/CatalogPlugin.test.ts diff --git a/.changeset/loud-walls-build.md b/.changeset/loud-walls-build.md new file mode 100644 index 0000000000..28a6ef3e0b --- /dev/null +++ b/.changeset/loud-walls-build.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +The catalog backend now supports the new `PermissionIntegrationsService`, which can be used to add custom permission rules. diff --git a/plugins/catalog-backend/report.api.md b/plugins/catalog-backend/report.api.md index ea7e11d0d7..db7a4818be 100644 --- a/plugins/catalog-backend/report.api.md +++ b/plugins/catalog-backend/report.api.md @@ -49,6 +49,7 @@ import { locationSpecToMetadataName as locationSpecToMetadataName_2 } from '@bac import { LoggerService } from '@backstage/backend-plugin-api'; import { Permission } from '@backstage/plugin-permission-common'; import { PermissionAuthorizer } from '@backstage/plugin-permission-common'; +import { PermissionIntegrationsService } from '@backstage/backend-plugin-api'; import { PermissionRule } from '@backstage/plugin-permission-node'; import { PermissionRuleParams } from '@backstage/plugin-permission-common'; import { PermissionsService } from '@backstage/backend-plugin-api'; @@ -198,6 +199,7 @@ export type CatalogEnvironment = { config: RootConfigService; reader: UrlReaderService; permissions: PermissionsService | PermissionAuthorizer; + permissionIntegrations?: PermissionIntegrationsService; scheduler?: SchedulerService; discovery?: DiscoveryService; auth?: AuthService; diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index 7ce308e796..ab3751eb90 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -114,6 +114,7 @@ import { RootConfigService, UrlReaderService, SchedulerService, + PermissionIntegrationsService, } from '@backstage/backend-plugin-api'; import { entitiesResponseToObjects } from './response'; @@ -136,6 +137,7 @@ export type CatalogEnvironment = { config: RootConfigService; reader: UrlReaderService; permissions: PermissionsService | PermissionAuthorizer; + permissionIntegrations?: PermissionIntegrationsService; scheduler?: SchedulerService; discovery?: DiscoveryService; auth?: AuthService; @@ -478,6 +480,7 @@ export class CatalogBuilder { logger, permissions, scheduler, + permissionIntegrations, discovery = HostDiscovery.fromConfig(config), } = this.env; @@ -554,7 +557,8 @@ export class CatalogBuilder { permissionsService, createConditionTransformer(this.permissionRules), ); - const permissionIntegrationRouter = createPermissionIntegrationRouter({ + + const catalogPermissionResource = { resourceType: RESOURCE_TYPE_CATALOG_ENTITY, getResources: async (resourceRefs: string[]) => { const { entities } = await unauthorizedEntitiesCatalog.entities({ @@ -584,7 +588,18 @@ export class CatalogBuilder { }, permissions: this.permissions, rules: this.permissionRules, - }); + } as const; + + let permissionIntegrationRouter: + | ReturnType + | undefined; + if (permissionIntegrations) { + permissionIntegrations.addResourceType(catalogPermissionResource); + } else { + permissionIntegrationRouter = createPermissionIntegrationRouter( + catalogPermissionResource, + ); + } const locationStore = new DefaultLocationStore(dbClient); const configLocationProvider = new ConfigLocationEntityProvider(config); diff --git a/plugins/catalog-backend/src/service/CatalogPlugin.test.ts b/plugins/catalog-backend/src/service/CatalogPlugin.test.ts new file mode 100644 index 0000000000..3390f9c901 --- /dev/null +++ b/plugins/catalog-backend/src/service/CatalogPlugin.test.ts @@ -0,0 +1,71 @@ +/* + * 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 request from 'supertest'; +import { startTestBackend } from '@backstage/backend-test-utils'; +import { catalogPlugin } from './CatalogPlugin'; +import { + coreServices, + createBackendModule, +} from '@backstage/backend-plugin-api'; +import { createCatalogPermissionRule } from '../permissions'; + +describe('catalogPlugin', () => { + it('should support custom permission rules', async () => { + const { server } = await startTestBackend({ + features: [ + catalogPlugin, + createBackendModule({ + pluginId: 'catalog', + moduleId: 'custom-rules', + register(reg) { + reg.registerInit({ + deps: { + permissionIntegrations: coreServices.permissionIntegrations, + }, + async init({ permissionIntegrations }) { + permissionIntegrations.addPermissionRules([ + createCatalogPermissionRule({ + name: 'test', + resourceType: 'catalog-entity', + description: 'Test permission rule', + apply() { + return true; + }, + toQuery() { + return { key: 'test' }; + }, + }), + ]); + }, + }); + }, + }), + ], + }); + + const res = await request(server).get( + '/api/catalog/.well-known/backstage/permissions/metadata', + ); + + expect(res.status).toBe(200); + expect(res.body.rules).toContainEqual({ + name: 'test', + resourceType: 'catalog-entity', + paramsSchema: expect.any(Object), + description: 'Test permission rule', + }); + }); +}); diff --git a/plugins/catalog-backend/src/service/CatalogPlugin.ts b/plugins/catalog-backend/src/service/CatalogPlugin.ts index 4da0b612cd..c076f68980 100644 --- a/plugins/catalog-backend/src/service/CatalogPlugin.ts +++ b/plugins/catalog-backend/src/service/CatalogPlugin.ts @@ -227,6 +227,7 @@ export const catalogPlugin = createBackendPlugin({ config: coreServices.rootConfig, reader: coreServices.urlReader, permissions: coreServices.permissions, + permissionIntegrations: coreServices.permissionIntegrations, database: coreServices.database, httpRouter: coreServices.httpRouter, lifecycle: coreServices.rootLifecycle, @@ -242,6 +243,7 @@ export const catalogPlugin = createBackendPlugin({ reader, database, permissions, + permissionIntegrations, httpRouter, lifecycle, scheduler, @@ -254,6 +256,7 @@ export const catalogPlugin = createBackendPlugin({ config, reader, permissions, + permissionIntegrations, database, scheduler, logger, From 4a941e749c94ca66c93d5ba8ca0e14c09da1e961 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 8 Jan 2025 11:10:16 +0100 Subject: [PATCH 07/15] catalog-node: deprecate catalogPermissionExtensionPoint Signed-off-by: Patrik Oldsberg --- .changeset/old-moons-end.md | 5 +++++ plugins/catalog-node/report-alpha.api.md | 6 +++--- plugins/catalog-node/src/extensions.ts | 3 +++ 3 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 .changeset/old-moons-end.md diff --git a/.changeset/old-moons-end.md b/.changeset/old-moons-end.md new file mode 100644 index 0000000000..36eaaec056 --- /dev/null +++ b/.changeset/old-moons-end.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-node': patch +--- + +Deprecated the alpha `catalogPermissionExtensionPoint` and related types, since the same functionality is now available via the new `PermissionIntegrationsService`. diff --git a/plugins/catalog-node/report-alpha.api.md b/plugins/catalog-node/report-alpha.api.md index 044bc14de3..92dfaefb7a 100644 --- a/plugins/catalog-node/report-alpha.api.md +++ b/plugins/catalog-node/report-alpha.api.md @@ -51,7 +51,7 @@ export interface CatalogModelExtensionPoint { // @alpha (undocumented) export const catalogModelExtensionPoint: ExtensionPoint; -// @alpha (undocumented) +// @alpha @deprecated (undocumented) export interface CatalogPermissionExtensionPoint { // (undocumented) addPermissionRules( @@ -63,10 +63,10 @@ export interface CatalogPermissionExtensionPoint { addPermissions(...permissions: Array>): void; } -// @alpha (undocumented) +// @alpha @deprecated (undocumented) export const catalogPermissionExtensionPoint: ExtensionPoint; -// @alpha (undocumented) +// @alpha @deprecated (undocumented) export type CatalogPermissionRuleInput< TParams extends PermissionRuleParams = PermissionRuleParams, > = PermissionRule; diff --git a/plugins/catalog-node/src/extensions.ts b/plugins/catalog-node/src/extensions.ts index a971e1588d..1861dbcea5 100644 --- a/plugins/catalog-node/src/extensions.ts +++ b/plugins/catalog-node/src/extensions.ts @@ -138,6 +138,7 @@ export const catalogModelExtensionPoint = /** * @alpha + * @deprecated Use the `coreServices.permissionIntegrations` instead. */ export type CatalogPermissionRuleInput< TParams extends PermissionRuleParams = PermissionRuleParams, @@ -145,6 +146,7 @@ export type CatalogPermissionRuleInput< /** * @alpha + * @deprecated Use the `coreServices.permissionIntegrations` instead. */ export interface CatalogPermissionExtensionPoint { addPermissions(...permissions: Array>): void; @@ -157,6 +159,7 @@ export interface CatalogPermissionExtensionPoint { /** * @alpha + * @deprecated Use the `coreServices.permissionIntegrations` instead. */ export const catalogPermissionExtensionPoint = createExtensionPoint({ From 521dedd6ea97a9ac331ce9116e67e02b827011fe Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 8 Jan 2025 12:27:21 +0100 Subject: [PATCH 08/15] docs/permissions: update to use new integrations service Signed-off-by: Patrik Oldsberg --- docs/permissions/custom-rules.md | 18 ++-- .../02-adding-a-basic-permission-check.md | 22 ++--- .../03-adding-a-resource-permission-check.md | 82 +++++++++++-------- ...04-authorizing-access-to-paginated-data.md | 48 +++++++---- 4 files changed, 100 insertions(+), 70 deletions(-) diff --git a/docs/permissions/custom-rules.md b/docs/permissions/custom-rules.md index 00f4d81939..fa56637bd6 100644 --- a/docs/permissions/custom-rules.md +++ b/docs/permissions/custom-rules.md @@ -139,7 +139,13 @@ class CustomPermissionPolicy implements PermissionPolicy { Now that we have a custom rule defined and added to our policy, we need provide it to the catalog plugin. This step is important because the catalog plugin will use the rule's `toQuery` and `apply` methods while evaluating conditional authorize results. There's no guarantee that the catalog and permission backends are running on the same server, so we must explicitly link the rule to ensure that it's available at runtime. -The api for providing custom rules may differ between plugins, but there should typically be an [extension point](../backend-system/architecture/05-extension-points.md) that you can use in your created module to add your rule. For the catalog, this extension point is exposed via `catalogPermissionExtensionPoint`. Here's the steps you'll need to take to add the `isInSystemRule` we created above to the catalog: +:::warning Warning + +The `PermissionIntegrationsService` is a fairly new addition and not yet supported by all plugins as they might still be using the old `createPermissionIntegrationRouter` that cannot be extended. If you encounter errors when installing custom rules for a plugin, the plugin may need to be switched to using the `PermissionIntegrationsService` first. + +::: + +To install custom rules in a plugin, we need to use the [`PermissionIntegrationsService`](../backend-system/core-services/permissionIntegrations.md). Here's the steps you'll need to take to add the `isInSystemRule` we created above to the catalog: 1. We will be using the `@backstage/plugin-catalog-node` package as it contains the extension point we need. Run this to add it: @@ -147,10 +153,10 @@ The api for providing custom rules may differ between plugins, but there should yarn --cwd packages/backend add @backstage/plugin-catalog-node ``` -2. Next create a `catalogPermissionRules.ts` file in the `packages/backend/src/extensions` folder. +2. Next create a `catalogPermissionRules.ts` file in the `packages/backend/src/modules` folder. 3. Then add this as the contents of the new `catalogPermissionRules.ts` file: - ```typescript title="packages/backend/src/extensions/catalogPermissionRules.ts" + ```typescript title="packages/backend/src/modules/catalogPermissionRules.ts" import { createBackendModule } from '@backstage/backend-plugin-api'; import { catalogPermissionExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { isInSystemRule } from './permissionPolicyExtension'; @@ -160,9 +166,9 @@ The api for providing custom rules may differ between plugins, but there should moduleId: 'permission-rules', register(reg) { reg.registerInit({ - deps: { catalog: catalogPermissionExtensionPoint }, - async init({ catalog }) { - catalog.addPermissionRules(isInSystemRule); + deps: { permissionIntegrations: coreServices.permissionIntegrations }, + async init({ permissionIntegrations }) { + permissionIntegrations.addPermissionRules([isInSystemRule]); }, }); }, diff --git a/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md b/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md index 2d39d5cef3..f6b4c11433 100644 --- a/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md +++ b/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md @@ -67,8 +67,6 @@ import { LoggerService, HttpAuthService } from '@backstage/backend-plugin-api'; import { InputError, NotAllowedError } from '@backstage/errors'; import { LoggerService, HttpAuthService, PermissionsService } from '@backstage/backend-plugin-api'; import { AuthorizeResult } from '@backstage/plugin-permission-common'; -import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node'; -import { todoListCreatePermission } from '@internal/plugin-todo-list-common'; /* highlight-add-end */ export interface RouterOptions { @@ -86,12 +84,6 @@ export async function createRouter( /* highlight-add-next-line */ const { logger, httpAuth, permissions } = options; - /* highlight-add-start */ - const permissionIntegrationRouter = createPermissionIntegrationRouter({ - permissions: [todoListCreatePermission], - }); - /* highlight-add-end */ - const router = Router(); router.use(express.json()); @@ -100,9 +92,6 @@ export async function createRouter( response.json({ status: 'ok' }); }); - /* highlight-add-next-line */ - router.use(permissionIntegrationRouter); - router.get('/todos', async (_req, res) => { res.json(getAll()); }); @@ -137,11 +126,13 @@ export async function createRouter( // ... ``` -Pass the `permissions` object to the plugin in `plugins/todo-list-backend/src/plugin.ts`: +Pass the `permissions` service and register the new permission to the plugin in `plugins/todo-list-backend/src/plugin.ts`: ```ts title="plugins/todo-list-backend/src/plugin.ts" import { coreServices, createBackendPlugin } from '@backstage/backend-plugin-api'; import { createRouter } from './service/router'; +/* highlight-add-next-line */ +import { todoListCreatePermission } from '@internal/plugin-todo-list-common'; export const exampleTodoListPlugin = createBackendPlugin({ pluginId: 'todolist', @@ -153,11 +144,16 @@ export const exampleTodoListPlugin = createBackendPlugin({ httpRouter: coreServices.httpRouter, /* highlight-add-next-line */ permissions: coreServices.permissions, + /* highlight-add-next-line */ + permissionIntegrations: coreServices.permissionIntegrations, }, /* highlight-remove-next-line */ async init({ logger, httpAuth, httpRouter }) { /* highlight-add-next-line */ - async init({ logger, httpAuth, httpRouter, permissions }) { + async init({ httpAuth, logger, httpRouter, permissions, permissionIntegrations }) { + /* highlight-add-next-line */ + permissionIntegrations.addPermissions([todoListCreatePermission]); + httpRouter.use( await createRouter({ logger, 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 3f5a9777b3..f33ae583bd 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 @@ -47,9 +47,9 @@ Notice that unlike `todoListCreatePermission`, the `todoListUpdatePermission` pe ## Setting up authorization for the update permission -To start, let's edit `plugins/todo-list-backend/src/service/router.ts` in the same manner as we did in the previous section: +To start, let's edit `plugins/todo-list-backend/src/plugin.ts` to add the new permission to our plugin: -```ts title="plugins/todo-list-backend/src/service/router.ts" +```ts title="plugins/todo-list-backend/src/plugin.ts" /* highlight-remove-next-line */ import { todoListCreatePermission } from '@internal/plugin-todo-list-common'; /* highlight-add-start */ @@ -61,14 +61,29 @@ import { // ... -const permissionIntegrationRouter = createPermissionIntegrationRouter({ - /* highlight-remove-next-line */ - permissions: [todoListCreatePermission], - /* highlight-add-next-line */ - permissions: [todoListCreatePermission, todoListUpdatePermission], -}); +/* highlight-remove-next-line */ +permissionIntegrations.addPermissions([todoListCreatePermission]); +/* highlight-add-start */ +permissionIntegrations.addPermissions([ + todoListCreatePermission, + todoListUpdatePermission, +]); +/* highlight-add-end */ // ... +``` + +Then let's edit `plugins/todo-list-backend/src/service/router.ts` in the same manner as we did in the previous section: + +```ts title="plugins/todo-list-backend/src/service/router.ts" +/* highlight-remove-next-line */ +import { todoListCreatePermission } from '@internal/plugin-todo-list-common'; +/* highlight-add-start */ +import { + todoListCreatePermission, + todoListUpdatePermission, +} from '@internal/plugin-todo-list-common'; +/* highlight-add-end */ router.put('/todos', async (req, res) => { /* highlight-add-start */ @@ -155,49 +170,50 @@ Specifically, the `apply` function is used to understand whether the passed reso Let's skip the `toQuery` function for now, we'll come back to that in the next section. -Now, let's create the new endpoint by editing `plugins/todo-list-backend/src/service/router.ts`. This uses the `createPermissionIntegrationRouter` helper to add the APIs needed by the permission framework to your plugin. You'll need to supply: +Now, let's add the new resource type to the permissions system via the +`PermissionIntegrationsService`. You'll need to supply: - `getResources`: a function that accepts an array of `resourceRefs` in the same format you expect to be passed to `authorize`, and returns an array of the corresponding resources. - `resourceType`: the same value used in the permission rule above. - `permissions`: the list of permissions that your plugin accepts. - `rules`: an array of all the permission rules you want to support in conditional decisions. -```ts title="plugins/todo-list-backend/src/service/router.ts" +```ts title="plugins/todo-list-backend/src/plugin.ts" // ... +import { + coreServices, + createBackendPlugin, +} 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-remove-next-line */ -import { add, getAll, update } from './todos'; /* highlight-add-start */ -import { add, getAll, getTodo, update } from './todos'; +import { getTodo } from './todos'; import { rules } from './rules'; /* highlight-add-end */ -export async function createRouter( - options: RouterOptions, -): Promise { - const { logger, identity, permissions } = options; +// ... - const permissionIntegrationRouter = createPermissionIntegrationRouter({ - permissions: [todoListCreatePermission, todoListUpdatePermission], - /* highlight-add-start */ - getResources: async resourceRefs => { - return resourceRefs.map(getTodo); - }, - resourceType: TODO_LIST_RESOURCE_TYPE, - rules: Object.values(rules), - /* highlight-add-end */ - }); - - const router = Router(); - router.use(express.json()); - - // ... -} +/* highlight-remove-start */ +permissionIntegrations.addPermissions([ + todoListCreatePermission, + todoListUpdatePermission, +]); +/* highlight-remove-end */ +/* highlight-add-start */ +permissionIntegrations.addResourceType({ + resourceType: TODO_LIST_RESOURCE_TYPE, + permissions: [todoListCreatePermission, todoListUpdatePermission], + rules: Object.values(rules), + getResources: async resourceRefs => { + return Promise.all(resourceRefs.map(getTodo)); + }, +}); +/* highlight-add-end */ ``` ## Provide utilities for policy authors 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 883884b6e5..d85d82510d 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 @@ -84,14 +84,41 @@ export const todoListPermissions = [ ## Using conditional policy decisions +As usual, we'll start by updating the permission integration to include the new permission: + +```ts title="plugins/todo-list-backend/src/plugin.ts" +import { + TODO_LIST_RESOURCE_TYPE, + todoListCreatePermission, + todoListUpdatePermission, + /* highlight-add-next-line */ + todoListReadPermission, +} from '@internal/plugin-todo-list-common'; + +// ... + +permissionIntegrations.addResourceType({ + resourceType: TODO_LIST_RESOURCE_TYPE, + /* highlight-remove-next-line */ + permissions: [todoListCreatePermission, todoListUpdatePermission], + /* highlight-add-next-line */ + permissions: [ + todoListCreatePermission, + todoListUpdatePermission, + todoListReadPermission, + ], + rules: Object.values(rules), + getResources: async resourceRefs => { + return Promise.all(resourceRefs.map(getTodo)); + }, +}); +``` + So far we've only used the `PermissionsService.authorize` method, which will evaluate conditional decisions before returning a result. In this step, we want to evaluate conditional decisions within our plugin, so we'll use `PermissionsService.authorizeConditional` instead. ```ts title="plugins/todo-list-backend/src/service/router.ts" -/* highlight-remove-next-line */ -import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node'; /* highlight-add-start */ import { - createPermissionIntegrationRouter, createConditionTransformer, ConditionTransformer, } from '@backstage/plugin-permission-node'; @@ -101,7 +128,6 @@ import { add, getAll, getTodo, update } from './todos'; /* highlight-add-next-line */ import { add, getAll, getTodo, TodoFilter, update } from './todos'; import { - TODO_LIST_RESOURCE_TYPE, todoListCreatePermission, todoListUpdatePermission, /* highlight-add-next-line */ @@ -110,20 +136,6 @@ import { // ... -const permissionIntegrationRouter = createPermissionIntegrationRouter({ - /* highlight-remove-next-line */ - permissions: [todoListCreatePermission, todoListUpdatePermission], - /* highlight-add-next-line */ - permissions: [todoListCreatePermission, todoListUpdatePermission, todoListReadPermission], - getResources: async resourceRefs => { - return resourceRefs.map(getTodo); - }, - resourceType: TODO_LIST_RESOURCE_TYPE, - rules: Object.values(rules), -}); - -// ... - /* highlight-add-next-line */ const transformConditions: ConditionTransformer = createConditionTransformer(Object.values(rules)); From 7fa644144c35d473e62d66da78fad753c28974d2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 8 Jan 2025 12:27:34 +0100 Subject: [PATCH 09/15] docs/backend-system/core-services: add permissionIntegrations Signed-off-by: Patrik Oldsberg --- .../core-services/permissionIntegrations.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 docs/backend-system/core-services/permissionIntegrations.md diff --git a/docs/backend-system/core-services/permissionIntegrations.md b/docs/backend-system/core-services/permissionIntegrations.md new file mode 100644 index 0000000000..664e2d58d4 --- /dev/null +++ b/docs/backend-system/core-services/permissionIntegrations.md @@ -0,0 +1,14 @@ +--- +id: permissions +title: Permission Integrations Service +sidebar_label: Permission Integrations +description: Documentation for the Permission Integrations service +--- + +This service allows your plugins to register new permissions, rules, and resource types and integrate with [the permissions framework](../../permissions/overview.md). + +## Using the service + +For a deep dive into how to use the `permissionIntegrations` service, see the [permission guide for plugin authors](../../permissions/plugin-authors/01-setup.md). + +If all you want to do is add new custom permission rules to an existing plugin, you can instead refer to the [custom permission rules guide](../../permissions/custom-rules.md). From dfe6e29c0cf8ec01a3498bc0bdb4f32ca7b9b31a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 9 Jan 2025 11:58:34 +0100 Subject: [PATCH 10/15] docs/backend/core-services/permissionIntegrations: add migration guide Signed-off-by: Patrik Oldsberg --- .../core-services/permissionIntegrations.md | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/docs/backend-system/core-services/permissionIntegrations.md b/docs/backend-system/core-services/permissionIntegrations.md index 664e2d58d4..c56e9c6c5a 100644 --- a/docs/backend-system/core-services/permissionIntegrations.md +++ b/docs/backend-system/core-services/permissionIntegrations.md @@ -12,3 +12,69 @@ This service allows your plugins to register new permissions, rules, and resourc For a deep dive into how to use the `permissionIntegrations` service, see the [permission guide for plugin authors](../../permissions/plugin-authors/01-setup.md). If all you want to do is add new custom permission rules to an existing plugin, you can instead refer to the [custom permission rules guide](../../permissions/custom-rules.md). + +## Migrating from `createPermissionIntegrationRouter` + +Before this service was introduced, plugins would use +`createPermissionIntegrationRouter` to implement the same functionality. To +migrate a plugin, locate the `createPermissionIntegrationRouter` call for your +router and remove it, but copy all options that are passed to it, for example: + +```ts +export async function createRouter() { + const router = Router(); + + /* highlight-remove-start */ + const permissionIntegrationRouter = createPermissionIntegrationRouter({ + resourceType: RESOURCE_TYPE_MY_RESOURCE, + permissions: [myResourcePermissions], + rules: [myResourceRule], + }); + + router.use(permissionIntegrationRouter); + /* highlight-remove-end */ + + // ... +} +``` + +Next, add a dependency on the `PermissionIntegrationsService` to your plugin, +and pass it the same options: + +```ts +export const examplePlugin = createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + logger: coreServices.logger, + /* highlight-add-next-line */ + permissionIntegrations: coreServices.permissionIntegrations, + }, + /* highlight-remove-next-line */ + async init({ logger }) { + /* highlight-add-next-line */ + async init({ logger, permissionIntegrations }) { + logger.log('This is a silly example plugin with no functionality'); + + /* highlight-add-start */ + permissionIntegrations.addResourceType({ + resourceType: RESOURCE_TYPE_MY_RESOURCE, + permissions: [myResourcePermissions], + rules: [myResourceRule], + }); + /* highlight-add-end */ + }, + }); + }, +}); +``` + +If you only passed the `permissions` option to +`createPermissionIntegrationRouter`, you will want to use +`permissionIntegrations.addPermissions` instead. + +If you passed multiple resources types to `createPermissionIntegrationRouter` +via the `resources` option, you will want to call +`permissionIntegrations.addResourceType` multiple times for each of those +resource types. From cf6bd48cab1df41026d95630b19f951a9dd927a1 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 9 Jan 2025 12:41:58 +0100 Subject: [PATCH 11/15] permissions: rename permissionIntegrations to permissionsRegistry Signed-off-by: Patrik Oldsberg --- .changeset/curvy-ways-play.md | 2 +- .changeset/hungry-mirrors-sniff.md | 2 +- .changeset/loud-walls-build.md | 2 +- .changeset/old-moons-end.md | 2 +- .changeset/sharp-vans-protect.md | 2 +- ...Integrations.md => permissionsRegistry.md} | 20 ++++++------- docs/permissions/custom-rules.md | 10 +++---- .../02-adding-a-basic-permission-check.md | 6 ++-- .../03-adding-a-resource-permission-check.md | 10 +++---- ...04-authorizing-access-to-paginated-data.md | 2 +- packages/backend-defaults/package.json | 6 ++-- ...i.md => report-permissionsRegistry.api.md} | 6 ++-- .../backend-defaults/src/CreateBackend.ts | 4 +-- .../index.ts | 2 +- .../permissionsRegistryServiceFactory.ts} | 6 ++-- packages/backend-plugin-api/report.api.md | 10 +++---- ...rvice.ts => PermissionsRegistryService.ts} | 8 +++--- .../src/services/definitions/coreServices.ts | 8 +++--- .../src/services/definitions/index.ts | 6 ++-- packages/backend-test-utils/report.api.md | 28 +++++++++---------- .../src/next/services/mockServices.ts | 8 +++--- .../src/next/wiring/TestBackend.ts | 2 +- plugins/catalog-backend/report.api.md | 4 +-- .../src/service/CatalogBuilder.ts | 10 +++---- .../src/service/CatalogPlugin.test.ts | 6 ++-- .../src/service/CatalogPlugin.ts | 6 ++-- plugins/catalog-node/src/extensions.ts | 6 ++-- 27 files changed, 92 insertions(+), 92 deletions(-) rename docs/backend-system/core-services/{permissionIntegrations.md => permissionsRegistry.md} (75%) rename packages/backend-defaults/{report-permissionIntegrations.api.md => report-permissionsRegistry.api.md} (64%) rename packages/backend-defaults/src/entrypoints/{permissionIntegrations => permissionsRegistry}/index.ts (86%) rename packages/backend-defaults/src/entrypoints/{permissionIntegrations/permissionIntegrationsServiceFactory.ts => permissionsRegistry/permissionsRegistryServiceFactory.ts} (91%) rename packages/backend-plugin-api/src/services/definitions/{PermissionIntegrationsService.ts => PermissionsRegistryService.ts} (94%) diff --git a/.changeset/curvy-ways-play.md b/.changeset/curvy-ways-play.md index 4cb0e3fd9f..c69b855098 100644 --- a/.changeset/curvy-ways-play.md +++ b/.changeset/curvy-ways-play.md @@ -2,4 +2,4 @@ '@backstage/backend-test-utils': minor --- -Added mocks for the new `PermissionIntegrationsService`. +Added mocks for the new `PermissionsRegistryService`. diff --git a/.changeset/hungry-mirrors-sniff.md b/.changeset/hungry-mirrors-sniff.md index 27ebf9d896..f855c1e1a9 100644 --- a/.changeset/hungry-mirrors-sniff.md +++ b/.changeset/hungry-mirrors-sniff.md @@ -2,4 +2,4 @@ '@backstage/backend-defaults': patch --- -Added default implementation for the new `PermissionIntegrationsService`. +Added default implementation for the new `PermissionsRegistryService`. diff --git a/.changeset/loud-walls-build.md b/.changeset/loud-walls-build.md index 28a6ef3e0b..0ddad96f2f 100644 --- a/.changeset/loud-walls-build.md +++ b/.changeset/loud-walls-build.md @@ -2,4 +2,4 @@ '@backstage/plugin-catalog-backend': minor --- -The catalog backend now supports the new `PermissionIntegrationsService`, which can be used to add custom permission rules. +The catalog backend now supports the new `PermissionsRegistryService`, which can be used to add custom permission rules. diff --git a/.changeset/old-moons-end.md b/.changeset/old-moons-end.md index 36eaaec056..d550ae6325 100644 --- a/.changeset/old-moons-end.md +++ b/.changeset/old-moons-end.md @@ -2,4 +2,4 @@ '@backstage/plugin-catalog-node': patch --- -Deprecated the alpha `catalogPermissionExtensionPoint` and related types, since the same functionality is now available via the new `PermissionIntegrationsService`. +Deprecated the alpha `catalogPermissionExtensionPoint` and related types, since the same functionality is now available via the new `PermissionsRegistryService`. diff --git a/.changeset/sharp-vans-protect.md b/.changeset/sharp-vans-protect.md index a2f9237a56..651c38be8b 100644 --- a/.changeset/sharp-vans-protect.md +++ b/.changeset/sharp-vans-protect.md @@ -2,4 +2,4 @@ '@backstage/backend-plugin-api': patch --- -Added new `PermissionIntegrationsService` that is used by plugins to register permissions, resource types, and rules into the permission system. This replaces the existing `createPermissionIntegrationRouter` from `@backstage/plugin-permission-node`. +Added new `PermissionsRegistryService` that is used by plugins to register permissions, resource types, and rules into the permission system. This replaces the existing `createPermissionIntegrationRouter` from `@backstage/plugin-permission-node`. diff --git a/docs/backend-system/core-services/permissionIntegrations.md b/docs/backend-system/core-services/permissionsRegistry.md similarity index 75% rename from docs/backend-system/core-services/permissionIntegrations.md rename to docs/backend-system/core-services/permissionsRegistry.md index c56e9c6c5a..4b0b87b894 100644 --- a/docs/backend-system/core-services/permissionIntegrations.md +++ b/docs/backend-system/core-services/permissionsRegistry.md @@ -1,15 +1,15 @@ --- id: permissions -title: Permission Integrations Service -sidebar_label: Permission Integrations -description: Documentation for the Permission Integrations service +title: Permissions Registry Service +sidebar_label: Permissions Registry +description: Documentation for the Permissions Registry service --- This service allows your plugins to register new permissions, rules, and resource types and integrate with [the permissions framework](../../permissions/overview.md). ## Using the service -For a deep dive into how to use the `permissionIntegrations` service, see the [permission guide for plugin authors](../../permissions/plugin-authors/01-setup.md). +For a deep dive into how to use the `permissionsRegistry` service, see the [permission guide for plugin authors](../../permissions/plugin-authors/01-setup.md). If all you want to do is add new custom permission rules to an existing plugin, you can instead refer to the [custom permission rules guide](../../permissions/custom-rules.md). @@ -38,7 +38,7 @@ export async function createRouter() { } ``` -Next, add a dependency on the `PermissionIntegrationsService` to your plugin, +Next, add a dependency on the `PermissionsRegistryService` to your plugin, and pass it the same options: ```ts @@ -49,16 +49,16 @@ export const examplePlugin = createBackendPlugin({ deps: { logger: coreServices.logger, /* highlight-add-next-line */ - permissionIntegrations: coreServices.permissionIntegrations, + permissionsRegistry: coreServices.permissionsRegistry, }, /* highlight-remove-next-line */ async init({ logger }) { /* highlight-add-next-line */ - async init({ logger, permissionIntegrations }) { + async init({ logger, permissionsRegistry }) { logger.log('This is a silly example plugin with no functionality'); /* highlight-add-start */ - permissionIntegrations.addResourceType({ + permissionsRegistry.addResourceType({ resourceType: RESOURCE_TYPE_MY_RESOURCE, permissions: [myResourcePermissions], rules: [myResourceRule], @@ -72,9 +72,9 @@ export const examplePlugin = createBackendPlugin({ If you only passed the `permissions` option to `createPermissionIntegrationRouter`, you will want to use -`permissionIntegrations.addPermissions` instead. +`permissionsRegistry.addPermissions` instead. If you passed multiple resources types to `createPermissionIntegrationRouter` via the `resources` option, you will want to call -`permissionIntegrations.addResourceType` multiple times for each of those +`permissionsRegistry.addResourceType` multiple times for each of those resource types. diff --git a/docs/permissions/custom-rules.md b/docs/permissions/custom-rules.md index fa56637bd6..ca93652f73 100644 --- a/docs/permissions/custom-rules.md +++ b/docs/permissions/custom-rules.md @@ -141,11 +141,11 @@ Now that we have a custom rule defined and added to our policy, we need provide :::warning Warning -The `PermissionIntegrationsService` is a fairly new addition and not yet supported by all plugins as they might still be using the old `createPermissionIntegrationRouter` that cannot be extended. If you encounter errors when installing custom rules for a plugin, the plugin may need to be switched to using the `PermissionIntegrationsService` first. +The `PermissionsRegistryService` is a fairly new addition and not yet supported by all plugins as they might still be using the old `createPermissionIntegrationRouter` that cannot be extended. If you encounter errors when installing custom rules for a plugin, the plugin may need to be switched to using the `PermissionsRegistryService` first. ::: -To install custom rules in a plugin, we need to use the [`PermissionIntegrationsService`](../backend-system/core-services/permissionIntegrations.md). Here's the steps you'll need to take to add the `isInSystemRule` we created above to the catalog: +To install custom rules in a plugin, we need to use the [`PermissionsRegistryService`](../backend-system/core-services/permissionsRegistry.md). Here's the steps you'll need to take to add the `isInSystemRule` we created above to the catalog: 1. We will be using the `@backstage/plugin-catalog-node` package as it contains the extension point we need. Run this to add it: @@ -166,9 +166,9 @@ To install custom rules in a plugin, we need to use the [`PermissionIntegrations moduleId: 'permission-rules', register(reg) { reg.registerInit({ - deps: { permissionIntegrations: coreServices.permissionIntegrations }, - async init({ permissionIntegrations }) { - permissionIntegrations.addPermissionRules([isInSystemRule]); + deps: { permissionsRegistry: coreServices.permissionsRegistry }, + async init({ permissionsRegistry }) { + permissionsRegistry.addPermissionRules([isInSystemRule]); }, }); }, diff --git a/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md b/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md index f6b4c11433..4053a81f99 100644 --- a/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md +++ b/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md @@ -145,14 +145,14 @@ export const exampleTodoListPlugin = createBackendPlugin({ /* highlight-add-next-line */ permissions: coreServices.permissions, /* highlight-add-next-line */ - permissionIntegrations: coreServices.permissionIntegrations, + permissionsRegistry: coreServices.permissionsRegistry, }, /* highlight-remove-next-line */ async init({ logger, httpAuth, httpRouter }) { /* highlight-add-next-line */ - async init({ httpAuth, logger, httpRouter, permissions, permissionIntegrations }) { + async init({ httpAuth, logger, httpRouter, permissions, permissionsRegistry }) { /* highlight-add-next-line */ - permissionIntegrations.addPermissions([todoListCreatePermission]); + permissionsRegistry.addPermissions([todoListCreatePermission]); httpRouter.use( await createRouter({ 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 f33ae583bd..8d0e5c75e9 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 @@ -62,9 +62,9 @@ import { // ... /* highlight-remove-next-line */ -permissionIntegrations.addPermissions([todoListCreatePermission]); +permissionsRegistry.addPermissions([todoListCreatePermission]); /* highlight-add-start */ -permissionIntegrations.addPermissions([ +permissionsRegistry.addPermissions([ todoListCreatePermission, todoListUpdatePermission, ]); @@ -171,7 +171,7 @@ Specifically, the `apply` function is used to understand whether the passed reso Let's skip the `toQuery` function for now, we'll come back to that in the next section. Now, let's add the new resource type to the permissions system via the -`PermissionIntegrationsService`. You'll need to supply: +`PermissionsRegistryService`. You'll need to supply: - `getResources`: a function that accepts an array of `resourceRefs` in the same format you expect to be passed to `authorize`, and returns an array of the corresponding resources. - `resourceType`: the same value used in the permission rule above. @@ -199,13 +199,13 @@ import { rules } from './rules'; // ... /* highlight-remove-start */ -permissionIntegrations.addPermissions([ +permissionsRegistry.addPermissions([ todoListCreatePermission, todoListUpdatePermission, ]); /* highlight-remove-end */ /* highlight-add-start */ -permissionIntegrations.addResourceType({ +permissionsRegistry.addResourceType({ resourceType: TODO_LIST_RESOURCE_TYPE, permissions: [todoListCreatePermission, todoListUpdatePermission], rules: Object.values(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 d85d82510d..3a0b358c85 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 @@ -97,7 +97,7 @@ import { // ... -permissionIntegrations.addResourceType({ +permissionsRegistry.addResourceType({ resourceType: TODO_LIST_RESOURCE_TYPE, /* highlight-remove-next-line */ permissions: [todoListCreatePermission, todoListUpdatePermission], diff --git a/packages/backend-defaults/package.json b/packages/backend-defaults/package.json index 7ef216af3a..4a2fe1e753 100644 --- a/packages/backend-defaults/package.json +++ b/packages/backend-defaults/package.json @@ -28,7 +28,7 @@ "./httpRouter": "./src/entrypoints/httpRouter/index.ts", "./lifecycle": "./src/entrypoints/lifecycle/index.ts", "./logger": "./src/entrypoints/logger/index.ts", - "./permissionIntegrations": "./src/entrypoints/permissionIntegrations/index.ts", + "./permissionsRegistry": "./src/entrypoints/permissionsRegistry/index.ts", "./permissions": "./src/entrypoints/permissions/index.ts", "./rootConfig": "./src/entrypoints/rootConfig/index.ts", "./rootHealth": "./src/entrypoints/rootHealth/index.ts", @@ -68,8 +68,8 @@ "logger": [ "src/entrypoints/logger/index.ts" ], - "permissionIntegrations": [ - "src/entrypoints/permissionIntegrations/index.ts" + "permissionsRegistry": [ + "src/entrypoints/permissionsRegistry/index.ts" ], "permissions": [ "src/entrypoints/permissions/index.ts" diff --git a/packages/backend-defaults/report-permissionIntegrations.api.md b/packages/backend-defaults/report-permissionsRegistry.api.md similarity index 64% rename from packages/backend-defaults/report-permissionIntegrations.api.md rename to packages/backend-defaults/report-permissionsRegistry.api.md index db24c04d65..c453ca6a1b 100644 --- a/packages/backend-defaults/report-permissionIntegrations.api.md +++ b/packages/backend-defaults/report-permissionsRegistry.api.md @@ -3,12 +3,12 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { PermissionIntegrationsService } from '@backstage/backend-plugin-api'; +import { PermissionsRegistryService } from '@backstage/backend-plugin-api'; import { ServiceFactory } from '@backstage/backend-plugin-api'; // @public -export const permissionIntegrationsServiceFactory: ServiceFactory< - PermissionIntegrationsService, +export const permissionsRegistryServiceFactory: ServiceFactory< + PermissionsRegistryService, 'plugin', 'singleton' >; diff --git a/packages/backend-defaults/src/CreateBackend.ts b/packages/backend-defaults/src/CreateBackend.ts index 3cfb2b24ea..de53409658 100644 --- a/packages/backend-defaults/src/CreateBackend.ts +++ b/packages/backend-defaults/src/CreateBackend.ts @@ -23,8 +23,8 @@ import { httpAuthServiceFactory } from '@backstage/backend-defaults/httpAuth'; import { httpRouterServiceFactory } from '@backstage/backend-defaults/httpRouter'; import { lifecycleServiceFactory } from '@backstage/backend-defaults/lifecycle'; import { loggerServiceFactory } from '@backstage/backend-defaults/logger'; -import { permissionIntegrationsServiceFactory } from '@backstage/backend-defaults/permissionIntegrations'; import { permissionsServiceFactory } from '@backstage/backend-defaults/permissions'; +import { permissionsRegistryServiceFactory } from '@backstage/backend-defaults/permissionsRegistry'; import { rootConfigServiceFactory } from '@backstage/backend-defaults/rootConfig'; import { rootHealthServiceFactory } from '@backstage/backend-defaults/rootHealth'; import { rootHttpRouterServiceFactory } from '@backstage/backend-defaults/rootHttpRouter'; @@ -45,8 +45,8 @@ export const defaultServiceFactories = [ httpRouterServiceFactory, lifecycleServiceFactory, loggerServiceFactory, - permissionIntegrationsServiceFactory, permissionsServiceFactory, + permissionsRegistryServiceFactory, rootHealthServiceFactory, rootHttpRouterServiceFactory, rootLifecycleServiceFactory, diff --git a/packages/backend-defaults/src/entrypoints/permissionIntegrations/index.ts b/packages/backend-defaults/src/entrypoints/permissionsRegistry/index.ts similarity index 86% rename from packages/backend-defaults/src/entrypoints/permissionIntegrations/index.ts rename to packages/backend-defaults/src/entrypoints/permissionsRegistry/index.ts index 8e1daa15a1..abe04d7941 100644 --- a/packages/backend-defaults/src/entrypoints/permissionIntegrations/index.ts +++ b/packages/backend-defaults/src/entrypoints/permissionsRegistry/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { permissionIntegrationsServiceFactory } from './permissionIntegrationsServiceFactory'; +export { permissionsRegistryServiceFactory } from './permissionsRegistryServiceFactory'; diff --git a/packages/backend-defaults/src/entrypoints/permissionIntegrations/permissionIntegrationsServiceFactory.ts b/packages/backend-defaults/src/entrypoints/permissionsRegistry/permissionsRegistryServiceFactory.ts similarity index 91% rename from packages/backend-defaults/src/entrypoints/permissionIntegrations/permissionIntegrationsServiceFactory.ts rename to packages/backend-defaults/src/entrypoints/permissionsRegistry/permissionsRegistryServiceFactory.ts index b083439515..c3bca441ba 100644 --- a/packages/backend-defaults/src/entrypoints/permissionIntegrations/permissionIntegrationsServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/permissionsRegistry/permissionsRegistryServiceFactory.ts @@ -23,14 +23,14 @@ import { createPermissionIntegrationRouter } from '@backstage/plugin-permission- /** * Permission system integration for registering resources and permissions. * - * See {@link @backstage/code-plugin-api#PermissionIntegrationsService} + * See {@link @backstage/code-plugin-api#PermissionsRegistryService} * and {@link https://backstage.io/docs/backend-system/core-services/permission-integrations | the service docs} * for more information. * * @public */ -export const permissionIntegrationsServiceFactory = createServiceFactory({ - service: coreServices.permissionIntegrations, +export const permissionsRegistryServiceFactory = createServiceFactory({ + service: coreServices.permissionsRegistry, deps: { lifecycle: coreServices.lifecycle, httpRouter: coreServices.httpRouter, diff --git a/packages/backend-plugin-api/report.api.md b/packages/backend-plugin-api/report.api.md index 224a057686..82b62b4de0 100644 --- a/packages/backend-plugin-api/report.api.md +++ b/packages/backend-plugin-api/report.api.md @@ -186,8 +186,8 @@ export namespace coreServices { const lifecycle: ServiceRef; const logger: ServiceRef; const permissions: ServiceRef; - const permissionIntegrations: ServiceRef< - PermissionIntegrationsService, + const permissionsRegistry: ServiceRef< + PermissionsRegistryService, 'plugin', 'singleton' >; @@ -433,11 +433,11 @@ export interface LoggerService { } // @public -export interface PermissionIntegrationsService { +export interface PermissionsRegistryService { addPermissionRules(rules: PermissionRule[]): void; addPermissions(permissions: Permission[]): void; addResourceType( - options: PermissionIntegrationsServiceAddResourceTypeOptions< + options: PermissionsRegistryServiceAddResourceTypeOptions< TResourceType, TResource >, @@ -445,7 +445,7 @@ export interface PermissionIntegrationsService { } // @public -export type PermissionIntegrationsServiceAddResourceTypeOptions< +export type PermissionsRegistryServiceAddResourceTypeOptions< TResourceType extends string, TResource, > = { diff --git a/packages/backend-plugin-api/src/services/definitions/PermissionIntegrationsService.ts b/packages/backend-plugin-api/src/services/definitions/PermissionsRegistryService.ts similarity index 94% rename from packages/backend-plugin-api/src/services/definitions/PermissionIntegrationsService.ts rename to packages/backend-plugin-api/src/services/definitions/PermissionsRegistryService.ts index 2d4a2e0ffb..becdb0cddc 100644 --- a/packages/backend-plugin-api/src/services/definitions/PermissionIntegrationsService.ts +++ b/packages/backend-plugin-api/src/services/definitions/PermissionsRegistryService.ts @@ -30,7 +30,7 @@ type NoInfer = T extends infer S ? S : never; * * @public */ -export type PermissionIntegrationsServiceAddResourceTypeOptions< +export type PermissionsRegistryServiceAddResourceTypeOptions< TResourceType extends string, TResource, > = { @@ -71,7 +71,7 @@ export type PermissionIntegrationsServiceAddResourceTypeOptions< * * @public */ -export interface PermissionIntegrationsService { +export interface PermissionsRegistryService { /** * Add permissions for this plugin to the permission system. */ @@ -117,13 +117,13 @@ export interface PermissionIntegrationsService { * The `getResources` argument should load resources based on a reference * identifier. For the catalog, this is an * {@link @backstage/catalog-model#EntityRef}. For other plugins, this can be - * any serialized format. This is used to add a permission integrations API + * any serialized format. This is used to add a permissions registry API * via the HTTP router service. This API will be called by the * `permission-backend` when authorization conditions relating to this plugin * need to be evaluated. */ addResourceType( - options: PermissionIntegrationsServiceAddResourceTypeOptions< + options: PermissionsRegistryServiceAddResourceTypeOptions< TResourceType, TResource >, diff --git a/packages/backend-plugin-api/src/services/definitions/coreServices.ts b/packages/backend-plugin-api/src/services/definitions/coreServices.ts index d65b3b85ea..52adaefd53 100644 --- a/packages/backend-plugin-api/src/services/definitions/coreServices.ts +++ b/packages/backend-plugin-api/src/services/definitions/coreServices.ts @@ -177,15 +177,15 @@ export namespace coreServices { /** * Permission system integration for registering resources and permissions. * - * See {@link PermissionIntegrationsService} + * See {@link PermissionsRegistryService} * and {@link https://backstage.io/docs/backend-system/core-services/permission-integrations | the service docs} * for more information. * * @public */ - export const permissionIntegrations = createServiceRef< - import('./PermissionIntegrationsService').PermissionIntegrationsService - >({ id: 'core.permissionIntegrations' }); + export const permissionsRegistry = createServiceRef< + import('./PermissionsRegistryService').PermissionsRegistryService + >({ id: 'core.permissionsRegistry' }); /** * Built-in service for accessing metadata about the current plugin. diff --git a/packages/backend-plugin-api/src/services/definitions/index.ts b/packages/backend-plugin-api/src/services/definitions/index.ts index 4e82f1e089..95cd37a089 100644 --- a/packages/backend-plugin-api/src/services/definitions/index.ts +++ b/packages/backend-plugin-api/src/services/definitions/index.ts @@ -51,9 +51,9 @@ export type { PermissionsServiceRequestOptions, } from './PermissionsService'; export type { - PermissionIntegrationsService, - PermissionIntegrationsServiceAddResourceTypeOptions, -} from './PermissionIntegrationsService'; + PermissionsRegistryService, + PermissionsRegistryServiceAddResourceTypeOptions, +} from './PermissionsRegistryService'; export type { PluginMetadataService } from './PluginMetadataService'; export type { RootHttpRouterService } from './RootHttpRouterService'; export type { RootLifecycleService } from './RootLifecycleService'; diff --git a/packages/backend-test-utils/report.api.md b/packages/backend-test-utils/report.api.md index 804f06fcbb..e6f0667535 100644 --- a/packages/backend-test-utils/report.api.md +++ b/packages/backend-test-utils/report.api.md @@ -33,7 +33,7 @@ import { LifecycleService } from '@backstage/backend-plugin-api'; import { LoggerService } from '@backstage/backend-plugin-api'; import { ParamsDictionary } from 'express-serve-static-core'; import { ParsedQs } from 'qs'; -import { PermissionIntegrationsService } from '@backstage/backend-plugin-api'; +import { PermissionsRegistryService } from '@backstage/backend-plugin-api'; import { PermissionsService } from '@backstage/backend-plugin-api'; import { RootConfigService } from '@backstage/backend-plugin-api'; import { RootHealthService } from '@backstage/backend-plugin-api'; @@ -256,19 +256,6 @@ export namespace mockServices { ) => ServiceMock; } // (undocumented) - export namespace permissionIntegrations { - const // (undocumented) - factory: () => ServiceFactory< - PermissionIntegrationsService, - 'plugin', - 'singleton' - >; - const // (undocumented) - mock: ( - partialImpl?: Partial | undefined, - ) => ServiceMock; - } - // (undocumented) export namespace permissions { const // (undocumented) factory: () => ServiceFactory; @@ -278,6 +265,19 @@ export namespace mockServices { ) => ServiceMock; } // (undocumented) + export namespace permissionsRegistry { + const // (undocumented) + factory: () => ServiceFactory< + PermissionsRegistryService, + 'plugin', + 'singleton' + >; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; + } + // (undocumented) export function rootConfig(options?: rootConfig.Options): RootConfigService; // (undocumented) export namespace rootConfig { diff --git a/packages/backend-test-utils/src/next/services/mockServices.ts b/packages/backend-test-utils/src/next/services/mockServices.ts index 83c0213296..91b53c4acc 100644 --- a/packages/backend-test-utils/src/next/services/mockServices.ts +++ b/packages/backend-test-utils/src/next/services/mockServices.ts @@ -53,7 +53,7 @@ import { MockRootLoggerService } from './MockRootLoggerService'; import { MockUserInfoService } from './MockUserInfoService'; import { mockCredentials } from './mockCredentials'; import { Knex } from 'knex'; -import { permissionIntegrationsServiceFactory } from '@backstage/backend-defaults/permissionIntegrations'; +import { permissionsRegistryServiceFactory } from '@backstage/backend-defaults/permissionsRegistry'; /** @internal */ function createLoggerMock() { @@ -469,9 +469,9 @@ export namespace mockServices { })); } - export namespace permissionIntegrations { - export const factory = () => permissionIntegrationsServiceFactory; - export const mock = simpleMock(coreServices.permissionIntegrations, () => ({ + export namespace permissionsRegistry { + export const factory = () => permissionsRegistryServiceFactory; + export const mock = simpleMock(coreServices.permissionsRegistry, () => ({ addPermissionRules: jest.fn(), addPermissions: jest.fn(), addResourceType: jest.fn(), diff --git a/packages/backend-test-utils/src/next/wiring/TestBackend.ts b/packages/backend-test-utils/src/next/wiring/TestBackend.ts index 7c0edb069b..0c46c2cd5f 100644 --- a/packages/backend-test-utils/src/next/wiring/TestBackend.ts +++ b/packages/backend-test-utils/src/next/wiring/TestBackend.ts @@ -75,7 +75,7 @@ export const defaultServiceFactories = [ mockServices.lifecycle.factory(), mockServices.logger.factory(), mockServices.permissions.factory(), - mockServices.permissionIntegrations.factory(), + mockServices.permissionsRegistry.factory(), mockServices.rootHealth.factory(), mockServices.rootLifecycle.factory(), mockServices.rootLogger.factory(), diff --git a/plugins/catalog-backend/report.api.md b/plugins/catalog-backend/report.api.md index db7a4818be..c9b7fabbad 100644 --- a/plugins/catalog-backend/report.api.md +++ b/plugins/catalog-backend/report.api.md @@ -49,9 +49,9 @@ import { locationSpecToMetadataName as locationSpecToMetadataName_2 } from '@bac import { LoggerService } from '@backstage/backend-plugin-api'; import { Permission } from '@backstage/plugin-permission-common'; import { PermissionAuthorizer } from '@backstage/plugin-permission-common'; -import { PermissionIntegrationsService } from '@backstage/backend-plugin-api'; import { PermissionRule } from '@backstage/plugin-permission-node'; import { PermissionRuleParams } from '@backstage/plugin-permission-common'; +import { PermissionsRegistryService } from '@backstage/backend-plugin-api'; import { PermissionsService } from '@backstage/backend-plugin-api'; import { PlaceholderResolver as PlaceholderResolver_2 } from '@backstage/plugin-catalog-node'; import { PlaceholderResolverParams as PlaceholderResolverParams_2 } from '@backstage/plugin-catalog-node'; @@ -199,7 +199,7 @@ export type CatalogEnvironment = { config: RootConfigService; reader: UrlReaderService; permissions: PermissionsService | PermissionAuthorizer; - permissionIntegrations?: PermissionIntegrationsService; + permissionsRegistry?: PermissionsRegistryService; scheduler?: SchedulerService; discovery?: DiscoveryService; auth?: AuthService; diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index ab3751eb90..13347edf7c 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -114,7 +114,7 @@ import { RootConfigService, UrlReaderService, SchedulerService, - PermissionIntegrationsService, + PermissionsRegistryService, } from '@backstage/backend-plugin-api'; import { entitiesResponseToObjects } from './response'; @@ -137,7 +137,7 @@ export type CatalogEnvironment = { config: RootConfigService; reader: UrlReaderService; permissions: PermissionsService | PermissionAuthorizer; - permissionIntegrations?: PermissionIntegrationsService; + permissionsRegistry?: PermissionsRegistryService; scheduler?: SchedulerService; discovery?: DiscoveryService; auth?: AuthService; @@ -480,7 +480,7 @@ export class CatalogBuilder { logger, permissions, scheduler, - permissionIntegrations, + permissionsRegistry, discovery = HostDiscovery.fromConfig(config), } = this.env; @@ -593,8 +593,8 @@ export class CatalogBuilder { let permissionIntegrationRouter: | ReturnType | undefined; - if (permissionIntegrations) { - permissionIntegrations.addResourceType(catalogPermissionResource); + if (permissionsRegistry) { + permissionsRegistry.addResourceType(catalogPermissionResource); } else { permissionIntegrationRouter = createPermissionIntegrationRouter( catalogPermissionResource, diff --git a/plugins/catalog-backend/src/service/CatalogPlugin.test.ts b/plugins/catalog-backend/src/service/CatalogPlugin.test.ts index 3390f9c901..9cec22322e 100644 --- a/plugins/catalog-backend/src/service/CatalogPlugin.test.ts +++ b/plugins/catalog-backend/src/service/CatalogPlugin.test.ts @@ -33,10 +33,10 @@ describe('catalogPlugin', () => { register(reg) { reg.registerInit({ deps: { - permissionIntegrations: coreServices.permissionIntegrations, + permissionsRegistry: coreServices.permissionsRegistry, }, - async init({ permissionIntegrations }) { - permissionIntegrations.addPermissionRules([ + async init({ permissionsRegistry }) { + permissionsRegistry.addPermissionRules([ createCatalogPermissionRule({ name: 'test', resourceType: 'catalog-entity', diff --git a/plugins/catalog-backend/src/service/CatalogPlugin.ts b/plugins/catalog-backend/src/service/CatalogPlugin.ts index c076f68980..ffa9a2f1b9 100644 --- a/plugins/catalog-backend/src/service/CatalogPlugin.ts +++ b/plugins/catalog-backend/src/service/CatalogPlugin.ts @@ -227,7 +227,7 @@ export const catalogPlugin = createBackendPlugin({ config: coreServices.rootConfig, reader: coreServices.urlReader, permissions: coreServices.permissions, - permissionIntegrations: coreServices.permissionIntegrations, + permissionsRegistry: coreServices.permissionsRegistry, database: coreServices.database, httpRouter: coreServices.httpRouter, lifecycle: coreServices.rootLifecycle, @@ -243,7 +243,7 @@ export const catalogPlugin = createBackendPlugin({ reader, database, permissions, - permissionIntegrations, + permissionsRegistry, httpRouter, lifecycle, scheduler, @@ -256,7 +256,7 @@ export const catalogPlugin = createBackendPlugin({ config, reader, permissions, - permissionIntegrations, + permissionsRegistry, database, scheduler, logger, diff --git a/plugins/catalog-node/src/extensions.ts b/plugins/catalog-node/src/extensions.ts index 1861dbcea5..b52835b1a9 100644 --- a/plugins/catalog-node/src/extensions.ts +++ b/plugins/catalog-node/src/extensions.ts @@ -138,7 +138,7 @@ export const catalogModelExtensionPoint = /** * @alpha - * @deprecated Use the `coreServices.permissionIntegrations` instead. + * @deprecated Use the `coreServices.permissionsRegistry` instead. */ export type CatalogPermissionRuleInput< TParams extends PermissionRuleParams = PermissionRuleParams, @@ -146,7 +146,7 @@ export type CatalogPermissionRuleInput< /** * @alpha - * @deprecated Use the `coreServices.permissionIntegrations` instead. + * @deprecated Use the `coreServices.permissionsRegistry` instead. */ export interface CatalogPermissionExtensionPoint { addPermissions(...permissions: Array>): void; @@ -159,7 +159,7 @@ export interface CatalogPermissionExtensionPoint { /** * @alpha - * @deprecated Use the `coreServices.permissionIntegrations` instead. + * @deprecated Use the `coreServices.permissionsRegistry` instead. */ export const catalogPermissionExtensionPoint = createExtensionPoint({ From 4e073c72e8e1d1cce4af7d233e98d9513e10c540 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 9 Jan 2025 15:41:28 +0100 Subject: [PATCH 12/15] catalog-backend-module-unprocessed: switch to permissions registry service Signed-off-by: Patrik Oldsberg --- .changeset/green-jokes-provide.md | 5 +++++ plugins/catalog-backend-module-unprocessed/src/module.ts | 9 +++++---- 2 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 .changeset/green-jokes-provide.md diff --git a/.changeset/green-jokes-provide.md b/.changeset/green-jokes-provide.md new file mode 100644 index 0000000000..f9fb78252f --- /dev/null +++ b/.changeset/green-jokes-provide.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-unprocessed': patch +--- + +Use new `PermissionsRegistryService` instead of the deprecated `catalogPermissionExtensionPoint`. diff --git a/plugins/catalog-backend-module-unprocessed/src/module.ts b/plugins/catalog-backend-module-unprocessed/src/module.ts index dabbd60d6f..bf1d50a634 100644 --- a/plugins/catalog-backend-module-unprocessed/src/module.ts +++ b/plugins/catalog-backend-module-unprocessed/src/module.ts @@ -19,7 +19,6 @@ import { createBackendModule, } from '@backstage/backend-plugin-api'; import { UnprocessedEntitiesModule } from './UnprocessedEntitiesModule'; -import { catalogPermissionExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { unprocessedEntitiesDeletePermission } from '@backstage/plugin-catalog-unprocessed-entities-common'; /** @@ -39,7 +38,7 @@ export const catalogModuleUnprocessedEntities = createBackendModule({ httpAuth: coreServices.httpAuth, discovery: coreServices.discovery, permissions: coreServices.permissions, - catalogPermissions: catalogPermissionExtensionPoint, + permissionsRegistry: coreServices.permissionsRegistry, }, async init({ database, @@ -48,7 +47,7 @@ export const catalogModuleUnprocessedEntities = createBackendModule({ permissions, httpAuth, discovery, - catalogPermissions, + permissionsRegistry, }) { const module = UnprocessedEntitiesModule.create({ database: await database.getClient(), @@ -58,7 +57,9 @@ export const catalogModuleUnprocessedEntities = createBackendModule({ httpAuth, }); - catalogPermissions.addPermissions(unprocessedEntitiesDeletePermission); + permissionsRegistry.addPermissions([ + unprocessedEntitiesDeletePermission, + ]); module.registerRoutes(); From 1505fdd60c4795d009a32fe2f2880daf8663f5f0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 14 Jan 2025 17:33:12 +0100 Subject: [PATCH 13/15] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Signed-off-by: Patrik Oldsberg --- .../permissionsRegistry/permissionsRegistryServiceFactory.ts | 2 +- .../src/services/definitions/PermissionsRegistryService.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend-defaults/src/entrypoints/permissionsRegistry/permissionsRegistryServiceFactory.ts b/packages/backend-defaults/src/entrypoints/permissionsRegistry/permissionsRegistryServiceFactory.ts index c3bca441ba..17bcb21091 100644 --- a/packages/backend-defaults/src/entrypoints/permissionsRegistry/permissionsRegistryServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/permissionsRegistry/permissionsRegistryServiceFactory.ts @@ -23,7 +23,7 @@ import { createPermissionIntegrationRouter } from '@backstage/plugin-permission- /** * Permission system integration for registering resources and permissions. * - * See {@link @backstage/code-plugin-api#PermissionsRegistryService} + * See {@link @backstage/core-plugin-api#PermissionsRegistryService} * and {@link https://backstage.io/docs/backend-system/core-services/permission-integrations | the service docs} * for more information. * diff --git a/packages/backend-plugin-api/src/services/definitions/PermissionsRegistryService.ts b/packages/backend-plugin-api/src/services/definitions/PermissionsRegistryService.ts index becdb0cddc..b0212e40ae 100644 --- a/packages/backend-plugin-api/src/services/definitions/PermissionsRegistryService.ts +++ b/packages/backend-plugin-api/src/services/definitions/PermissionsRegistryService.ts @@ -57,7 +57,7 @@ export type PermissionsRegistryServiceAddResourceTypeOptions< * * If this function is not provided the permission system will not be able to * resolve conditional decisions except when requesting resources directly - * form the plugin. + * from the plugin. */ getResources?(resourceRefs: string[]): Promise>; }; From be65d9186bf119f3c9f6c1933608fdc754ba5214 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 15 Jan 2025 09:55:52 +0100 Subject: [PATCH 14/15] backend-plugin-api: update entity ref link in permission registry docs Signed-off-by: Patrik Oldsberg --- .../services/definitions/PermissionsRegistryService.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/backend-plugin-api/src/services/definitions/PermissionsRegistryService.ts b/packages/backend-plugin-api/src/services/definitions/PermissionsRegistryService.ts index b0212e40ae..46236aa6cc 100644 --- a/packages/backend-plugin-api/src/services/definitions/PermissionsRegistryService.ts +++ b/packages/backend-plugin-api/src/services/definitions/PermissionsRegistryService.ts @@ -116,11 +116,11 @@ export interface PermissionsRegistryService { * * The `getResources` argument should load resources based on a reference * identifier. For the catalog, this is an - * {@link @backstage/catalog-model#EntityRef}. For other plugins, this can be - * any serialized format. This is used to add a permissions registry API - * via the HTTP router service. This API will be called by the - * `permission-backend` when authorization conditions relating to this plugin - * need to be evaluated. + * [entity reference](https://backstage.io/docs/features/software-catalog/references#string-references). + * For other plugins, this can be any serialized format. This is used to add a + * permissions registry API via the HTTP router service. This API will be + * called by the `permission-backend` when authorization conditions relating + * to this plugin need to be evaluated. */ addResourceType( options: PermissionsRegistryServiceAddResourceTypeOptions< From 088f2893238aea82aeeb0231c744136a6c28ba08 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 15 Jan 2025 09:57:28 +0100 Subject: [PATCH 15/15] permission-node: update error message for missing permission rules Signed-off-by: Patrik Oldsberg --- .../src/integration/createPermissionIntegrationRouter.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts index f24f4827e7..dbd7e45c74 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts @@ -295,7 +295,9 @@ class PermissionIntegrationMetadataStore { return (name: string): PermissionRule => { const rule = this.#rulesByTypeByName.get(resourceType)?.get(name); if (!rule) { - throw new Error(`Unexpected permission rule: ${name}`); + throw new Error( + `Permission rule '${name}' does not exist for resource type '${resourceType}'`, + ); } return rule; };