diff --git a/.changeset/scaffolder-backend-permissions-registry.md b/.changeset/scaffolder-backend-permissions-registry.md new file mode 100644 index 0000000000..f06b32ced0 --- /dev/null +++ b/.changeset/scaffolder-backend-permissions-registry.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +Migrated permission registration to use the `PermissionsRegistryService` instead of the deprecated `createPermissionIntegrationRouter`. This fixes an issue where scaffolder permissions were not visible to RBAC plugins because the `actionsRegistryServiceRef` dependency caused an empty permissions metadata router to shadow the scaffolder's actual permission metadata. The old `createPermissionIntegrationRouter` path is retained as a fallback for standalone `createRouter` usage. diff --git a/.changeset/scaffolder-node-permission-refs.md b/.changeset/scaffolder-node-permission-refs.md new file mode 100644 index 0000000000..de43bdb8ee --- /dev/null +++ b/.changeset/scaffolder-node-permission-refs.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-node': patch +--- + +Added `PermissionResourceRef` definitions for scaffolder resource types: `scaffolderTemplatePermissionResourceRef`, `scaffolderActionPermissionResourceRef`, and `scaffolderTaskPermissionResourceRef`. These are exported from `@backstage/plugin-scaffolder-node/alpha`. diff --git a/plugins/scaffolder-backend/src/ScaffolderPlugin.test.ts b/plugins/scaffolder-backend/src/ScaffolderPlugin.test.ts index fc0a6cafe2..63886dd1fc 100644 --- a/plugins/scaffolder-backend/src/ScaffolderPlugin.test.ts +++ b/plugins/scaffolder-backend/src/ScaffolderPlugin.test.ts @@ -27,6 +27,12 @@ import { stringifyEntityRef } from '@backstage/catalog-model'; import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { scaffolderAutocompleteExtensionPoint } from '@backstage/plugin-scaffolder-node/alpha'; +import { + scaffolderPermissions, + RESOURCE_TYPE_SCAFFOLDER_TEMPLATE, + RESOURCE_TYPE_SCAFFOLDER_ACTION, + RESOURCE_TYPE_SCAFFOLDER_TASK, +} from '@backstage/plugin-scaffolder-common/alpha'; import { scaffolderPlugin } from './ScaffolderPlugin'; @@ -1216,6 +1222,32 @@ describe('scaffolderPlugin', () => { }); }); + it('exposes permissions metadata via the well-known endpoint', async () => { + const { server } = await startTestBackend({ + features: [scaffolderPlugin], + }); + + const { body, status } = await request(server).get( + '/api/scaffolder/.well-known/backstage/permissions/metadata', + ); + + expect(status).toBe(200); + + const permissionNames = body.permissions.map( + (p: { name: string }) => p.name, + ); + for (const permission of scaffolderPermissions) { + expect(permissionNames).toContain(permission.name); + } + + const ruleResourceTypes = body.rules.map( + (r: { resourceType: string }) => r.resourceType, + ); + expect(ruleResourceTypes).toContain(RESOURCE_TYPE_SCAFFOLDER_TEMPLATE); + expect(ruleResourceTypes).toContain(RESOURCE_TYPE_SCAFFOLDER_ACTION); + expect(ruleResourceTypes).toContain(RESOURCE_TYPE_SCAFFOLDER_TASK); + }); + it('supports listing templating extensions', async () => { const { server } = await startTestBackend({ features: [scaffolderPlugin], diff --git a/plugins/scaffolder-backend/src/ScaffolderPlugin.ts b/plugins/scaffolder-backend/src/ScaffolderPlugin.ts index 6cef0d3cde..559adf66bb 100644 --- a/plugins/scaffolder-backend/src/ScaffolderPlugin.ts +++ b/plugins/scaffolder-backend/src/ScaffolderPlugin.ts @@ -142,6 +142,7 @@ export const scaffolderPlugin = createBackendPlugin({ lifecycle: coreServices.rootLifecycle, reader: coreServices.urlReader, permissions: coreServices.permissions, + permissionsRegistry: coreServices.permissionsRegistry, database: coreServices.database, auth: coreServices.auth, httpRouter: coreServices.httpRouter, @@ -165,6 +166,7 @@ export const scaffolderPlugin = createBackendPlugin({ httpAuth, catalog, permissions, + permissionsRegistry, events, auditor, actionsRegistry, @@ -242,6 +244,7 @@ export const scaffolderPlugin = createBackendPlugin({ auth, httpAuth, permissions, + permissionsRegistry, autocompleteHandlers, additionalWorkspaceProviders, events, diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index 9342385e15..223909f2c9 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -204,6 +204,7 @@ const createTestRouter = async ( const httpAuth = mockServices.httpAuth(); const events = mockServices.events(); + const permissionsRegistry = mockServices.permissionsRegistry.mock(); const router = await createRouter({ logger, config: new ConfigReader({}), @@ -211,6 +212,7 @@ const createTestRouter = async ( catalog, taskBroker, permissions, + permissionsRegistry, auth, httpAuth, events, diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index a393df253e..32b89c7c5e 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -23,6 +23,7 @@ import { HttpAuthService, LifecycleService, LoggerService, + PermissionsRegistryService, PermissionsService, resolveSafeChildPath, SchedulerService, @@ -45,7 +46,6 @@ import { ConditionTransformer, createConditionAuthorizer, createConditionTransformer, - createPermissionIntegrationRouter, } from '@backstage/plugin-permission-node'; import { TaskSpec, @@ -53,16 +53,13 @@ import { templateEntityV1beta3Validator, } from '@backstage/plugin-scaffolder-common'; import { - RESOURCE_TYPE_SCAFFOLDER_ACTION, - RESOURCE_TYPE_SCAFFOLDER_TASK, - RESOURCE_TYPE_SCAFFOLDER_TEMPLATE, scaffolderActionPermissions, - scaffolderPermissions, scaffolderTaskPermissions, scaffolderTemplatePermissions, taskCancelPermission, taskCreatePermission, taskReadPermission, + templateManagementPermission, templateParameterReadPermission, templateStepReadPermission, } from '@backstage/plugin-scaffolder-common/alpha'; @@ -78,6 +75,9 @@ import { AutocompleteHandler, CreatedTemplateFilter, CreatedTemplateGlobal, + scaffolderActionPermissionResourceRef, + scaffolderTaskPermissionResourceRef, + scaffolderTemplatePermissionResourceRef, WorkspaceProvider, } from '@backstage/plugin-scaffolder-node/alpha'; import { HumanDuration, JsonObject } from '@backstage/types'; @@ -161,6 +161,7 @@ export interface RouterOptions { | CreatedTemplateGlobal[]; additionalWorkspaceProviders?: Record; permissions?: PermissionsService; + permissionsRegistry: PermissionsRegistryService; permissionRules?: Array; auth: AuthService; httpAuth: HttpAuthService; @@ -253,6 +254,7 @@ export async function createRouter( additionalTemplateGlobals, additionalWorkspaceProviders, permissions, + permissionsRegistry, permissionRules, autocompleteHandlers = {}, events: eventsService, @@ -410,35 +412,35 @@ export async function createRouter( const taskTransformConditions: ConditionTransformer = createConditionTransformer(Object.values(taskRules)); - const permissionIntegrationRouter = createPermissionIntegrationRouter({ - resources: [ - { - resourceType: RESOURCE_TYPE_SCAFFOLDER_TEMPLATE, - permissions: scaffolderTemplatePermissions, - rules: templateRules, - }, - { - resourceType: RESOURCE_TYPE_SCAFFOLDER_ACTION, - permissions: scaffolderActionPermissions, - rules: actionRules, - }, - { - resourceType: RESOURCE_TYPE_SCAFFOLDER_TASK, - permissions: scaffolderTaskPermissions, - rules: taskRules, - getResources: async resourceRefs => { - return Promise.all( - resourceRefs.map(async taskId => { - return await taskBroker.get(taskId); - }), - ); - }, - }, - ], - permissions: scaffolderPermissions, + permissionsRegistry.addResourceType({ + resourceRef: scaffolderTemplatePermissionResourceRef, + permissions: scaffolderTemplatePermissions, + rules: templateRules, }); - router.use(permissionIntegrationRouter); + permissionsRegistry.addResourceType({ + resourceRef: scaffolderActionPermissionResourceRef, + permissions: scaffolderActionPermissions, + rules: actionRules, + }); + + permissionsRegistry.addResourceType({ + resourceRef: scaffolderTaskPermissionResourceRef, + permissions: scaffolderTaskPermissions, + rules: taskRules, + getResources: async resourceRefs => { + return Promise.all( + resourceRefs.map(async taskId => { + return await taskBroker.get(taskId); + }), + ); + }, + }); + + permissionsRegistry.addPermissions([ + taskCreatePermission, + templateManagementPermission, + ]); router .get( diff --git a/plugins/scaffolder-node/package.json b/plugins/scaffolder-node/package.json index 1cdf194285..a373d1f16d 100644 --- a/plugins/scaffolder-node/package.json +++ b/plugins/scaffolder-node/package.json @@ -63,6 +63,7 @@ "@backstage/errors": "workspace:^", "@backstage/integration": "workspace:^", "@backstage/plugin-permission-common": "workspace:^", + "@backstage/plugin-permission-node": "workspace:^", "@backstage/plugin-scaffolder-common": "workspace:^", "@backstage/types": "workspace:^", "@isomorphic-git/pgp-plugin": "^0.0.7", diff --git a/plugins/scaffolder-node/report-alpha.api.md b/plugins/scaffolder-node/report-alpha.api.md index 62240f210a..56372dcf2a 100644 --- a/plugins/scaffolder-node/report-alpha.api.md +++ b/plugins/scaffolder-node/report-alpha.api.md @@ -4,10 +4,16 @@ ```ts import { ExtensionPoint } from '@backstage/backend-plugin-api'; +import type { JsonObject } from '@backstage/types'; import { JsonValue } from '@backstage/types'; +import { PermissionResourceRef } from '@backstage/plugin-permission-node'; +import type { SerializedTask } from '@backstage/plugin-scaffolder-node'; import { TaskBroker } from '@backstage/plugin-scaffolder-node'; +import type { TaskFilter } from '@backstage/plugin-scaffolder-node'; +import type { TemplateEntityStepV1beta3 } from '@backstage/plugin-scaffolder-common'; import { TemplateFilter as TemplateFilter_2 } from '@backstage/plugin-scaffolder-node'; import { TemplateGlobal as TemplateGlobal_2 } from '@backstage/plugin-scaffolder-node'; +import type { TemplateParametersV1beta3 } from '@backstage/plugin-scaffolder-common'; import { z } from 'zod/v3'; // @alpha @@ -118,6 +124,14 @@ export const restoreWorkspace: (opts: { buffer?: Buffer; }) => Promise; +// @alpha +export const scaffolderActionPermissionResourceRef: PermissionResourceRef< + JsonObject, + {}, + 'scaffolder-action', + 'scaffolder' +>; + // @alpha export interface ScaffolderAutocompleteExtensionPoint { // (undocumented) @@ -139,6 +153,22 @@ export interface ScaffolderTaskBrokerExtensionPoint { // @alpha @deprecated export const scaffolderTaskBrokerExtensionPoint: ExtensionPoint; +// @alpha +export const scaffolderTaskPermissionResourceRef: PermissionResourceRef< + SerializedTask, + TaskFilter, + 'scaffolder-task', + 'scaffolder' +>; + +// @alpha +export const scaffolderTemplatePermissionResourceRef: PermissionResourceRef< + TemplateParametersV1beta3 | TemplateEntityStepV1beta3, + {}, + 'scaffolder-template', + 'scaffolder' +>; + // @alpha export interface ScaffolderTemplatingExtensionPoint { // (undocumented) diff --git a/plugins/scaffolder-node/src/alpha/index.ts b/plugins/scaffolder-node/src/alpha/index.ts index 9d0b9efafd..1df5b5bdce 100644 --- a/plugins/scaffolder-node/src/alpha/index.ts +++ b/plugins/scaffolder-node/src/alpha/index.ts @@ -153,3 +153,54 @@ export const scaffolderWorkspaceProviderExtensionPoint = createExtensionPoint({ id: 'scaffolder.workspace.provider', }); + +import { createPermissionResourceRef } from '@backstage/plugin-permission-node'; +import { + RESOURCE_TYPE_SCAFFOLDER_TEMPLATE, + RESOURCE_TYPE_SCAFFOLDER_ACTION, + RESOURCE_TYPE_SCAFFOLDER_TASK, +} from '@backstage/plugin-scaffolder-common/alpha'; +import type { + TemplateEntityStepV1beta3, + TemplateParametersV1beta3, +} from '@backstage/plugin-scaffolder-common'; +import type { JsonObject } from '@backstage/types'; +import type { + SerializedTask, + TaskFilter, +} from '@backstage/plugin-scaffolder-node'; + +/** + * Permission resource ref for scaffolder templates. + * @alpha + */ +export const scaffolderTemplatePermissionResourceRef = + createPermissionResourceRef< + TemplateEntityStepV1beta3 | TemplateParametersV1beta3, + {} + >().with({ + pluginId: 'scaffolder', + resourceType: RESOURCE_TYPE_SCAFFOLDER_TEMPLATE, + }); + +/** + * Permission resource ref for scaffolder actions. + * @alpha + */ +export const scaffolderActionPermissionResourceRef = + createPermissionResourceRef().with({ + pluginId: 'scaffolder', + resourceType: RESOURCE_TYPE_SCAFFOLDER_ACTION, + }); + +/** + * Permission resource ref for scaffolder tasks. + * @alpha + */ +export const scaffolderTaskPermissionResourceRef = createPermissionResourceRef< + SerializedTask, + TaskFilter +>().with({ + pluginId: 'scaffolder', + resourceType: RESOURCE_TYPE_SCAFFOLDER_TASK, +}); diff --git a/yarn.lock b/yarn.lock index 57236f1253..dab52459ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6919,6 +6919,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/plugin-permission-common": "workspace:^" + "@backstage/plugin-permission-node": "workspace:^" "@backstage/plugin-scaffolder-common": "workspace:^" "@backstage/types": "workspace:^" "@isomorphic-git/pgp-plugin": "npm:^0.0.7"