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:
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user