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
@@ -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(