feat(scaffolder): Migrate scaffolder to use permissions registry (#33740)

* feat(scaffolder-node): add PermissionResourceRef definitions for scaffolder resource types

Signed-off-by: benjdlambert <ben@blam.sh>

* feat(scaffolder-backend): migrate to PermissionsRegistryService with fallback

Signed-off-by: benjdlambert <ben@blam.sh>

* feat(scaffolder-backend): wire permissionsRegistry in ScaffolderPlugin

Signed-off-by: benjdlambert <ben@blam.sh>

* test(scaffolder-backend): verify permissions metadata endpoint returns all scaffolder permissions

Signed-off-by: benjdlambert <ben@blam.sh>

* chore: add changesets for scaffolder permissions registry migration

Signed-off-by: benjdlambert <ben@blam.sh>

* chore: format scaffolder-node alpha exports

Signed-off-by: benjdlambert <ben@blam.sh>

* fix: correct scaffolder-node changeset to patch for sub-1.0 package

Signed-off-by: benjdlambert <ben@blam.sh>

* refactor(scaffolder-backend): simplify by removing fallback path and making permissionsRegistry required

Signed-off-by: benjdlambert <ben@blam.sh>

* chore: update scaffolder-node API report

Signed-off-by: benjdlambert <ben@blam.sh>

---------

Signed-off-by: benjdlambert <ben@blam.sh>
This commit is contained in:
Ben Lambert
2026-04-06 19:10:43 +02:00
committed by GitHub
parent 282c11475f
commit 5af48e77af
10 changed files with 164 additions and 32 deletions
@@ -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.
@@ -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`.
@@ -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],
@@ -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,
@@ -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,
@@ -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<string, WorkspaceProvider>;
permissions?: PermissionsService;
permissionsRegistry: PermissionsRegistryService;
permissionRules?: Array<ScaffolderPermissionRuleInput>;
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<TaskFilters> =
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(
+1
View File
@@ -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",
@@ -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<void>;
// @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<ScaffolderTaskBrokerExtensionPoint>;
// @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)
@@ -153,3 +153,54 @@ export const scaffolderWorkspaceProviderExtensionPoint =
createExtensionPoint<ScaffolderWorkspaceProviderExtensionPoint>({
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<JsonObject, {}>().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,
});
+1
View File
@@ -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"