diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 43618ed5a7..7b39290cfc 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -30,7 +30,12 @@ import { UserEntity, } from '@backstage/catalog-model'; import { Config, readDurationFromConfig } from '@backstage/config'; -import { InputError, NotFoundError, stringifyError } from '@backstage/errors'; +import { + InputError, + NotAllowedError, + NotFoundError, + stringifyError, +} from '@backstage/errors'; import { ScmIntegrations } from '@backstage/integration'; import { HumanDuration, JsonObject, JsonValue } from '@backstage/types'; import { @@ -43,10 +48,16 @@ import { import { RESOURCE_TYPE_SCAFFOLDER_ACTION, RESOURCE_TYPE_SCAFFOLDER_TEMPLATE, + RESOURCE_TYPE_SCAFFOLDER_TASK, scaffolderActionPermissions, scaffolderTemplatePermissions, + taskCancelPermission, + taskCreatePermission, + taskReadPermission, templateParameterReadPermission, templateStepReadPermission, + scaffolderTaskPermissions, + actionReadPermission, } from '@backstage/plugin-scaffolder-common/alpha'; import express from 'express'; import Router from 'express-promise-router'; @@ -68,7 +79,10 @@ import { createDryRunner } from '../scaffolder/dryrun'; import { StorageTaskBroker } from '../scaffolder/tasks/StorageTaskBroker'; import { findTemplate, getEntityBaseUrl, getWorkingDirectory } from './helpers'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { PermissionRuleParams } from '@backstage/plugin-permission-common'; +import { + AuthorizeResult, + PermissionRuleParams, +} from '@backstage/plugin-permission-common'; import { createConditionAuthorizer, createPermissionIntegrationRouter, @@ -90,6 +104,11 @@ import { } from '@backstage/plugin-auth-node'; import { InternalTaskSecrets } from '../scaffolder/tasks/types'; +type ScaffolderPermissionRuleInput = + | TemplatePermissionRuleInput + | ActionPermissionRuleInput + | TaskPermissionRuleInput; + /** * * @public @@ -103,7 +122,7 @@ export type TemplatePermissionRuleInput< TParams >; function isTemplatePermissionRuleInput( - permissionRule: TemplatePermissionRuleInput | ActionPermissionRuleInput, + permissionRule: ScaffolderPermissionRuleInput, ): permissionRule is TemplatePermissionRuleInput { return permissionRule.resourceType === RESOURCE_TYPE_SCAFFOLDER_TEMPLATE; } @@ -121,11 +140,28 @@ export type ActionPermissionRuleInput< TParams >; function isActionPermissionRuleInput( - permissionRule: TemplatePermissionRuleInput | ActionPermissionRuleInput, + permissionRule: ScaffolderPermissionRuleInput, ): permissionRule is ActionPermissionRuleInput { return permissionRule.resourceType === RESOURCE_TYPE_SCAFFOLDER_ACTION; } +/** + * + * @public + */ +export type TaskPermissionRuleInput< + TParams extends PermissionRuleParams = PermissionRuleParams, +> = PermissionRule< + TemplateEntityStepV1beta3 | TemplateParametersV1beta3, + {}, + typeof RESOURCE_TYPE_SCAFFOLDER_TASK, + TParams +>; +function isTaskPermissionRuleInput( + permissionRule: ScaffolderPermissionRuleInput, +): permissionRule is TaskPermissionRuleInput { + return permissionRule.resourceType === RESOURCE_TYPE_SCAFFOLDER_TASK; +} /** * RouterOptions * @@ -154,9 +190,7 @@ export interface RouterOptions { additionalTemplateFilters?: Record; additionalTemplateGlobals?: Record; permissions?: PermissionsService; - permissionRules?: Array< - TemplatePermissionRuleInput | ActionPermissionRuleInput - >; + permissionRules?: Array; auth?: AuthService; httpAuth?: HttpAuthService; identity?: IdentityApi; @@ -384,12 +418,14 @@ export async function createRouter( const actionRules: ActionPermissionRuleInput[] = Object.values( scaffolderActionRules, ); + const taskRules: TaskPermissionRuleInput[] = []; if (permissionRules) { templateRules.push( ...permissionRules.filter(isTemplatePermissionRuleInput), ); actionRules.push(...permissionRules.filter(isActionPermissionRuleInput)); + taskRules.push(...permissionRules.filter(isTaskPermissionRuleInput)); } const isAuthorized = createConditionAuthorizer(Object.values(templateRules)); @@ -406,6 +442,11 @@ export async function createRouter( permissions: scaffolderActionPermissions, rules: actionRules, }, + { + resourceType: RESOURCE_TYPE_SCAFFOLDER_TASK, + permissions: scaffolderTaskPermissions, + rules: taskRules, + }, ], }); @@ -445,7 +486,21 @@ export async function createRouter( }); }, ) - .get('/v2/actions', async (_req, res) => { + .get('/v2/actions', async (req, res) => { + const credentials = await httpAuth.credentials(req); + + if (permissions) { + const authorizationResponse = ( + await permissions?.authorizeConditional( + [{ permission: actionReadPermission }], + { credentials: credentials }, + ) + )[0]; + + if (authorizationResponse.result === AuthorizeResult.DENY) { + throw new NotAllowedError(); + } + } const actionsList = actionRegistry.list().map(action => { return { id: action.id, @@ -463,6 +518,19 @@ export async function createRouter( }); const credentials = await httpAuth.credentials(req); + if (permissions) { + const authorizationResponse = ( + await permissions?.authorizeConditional( + [{ permission: taskCreatePermission }], + { credentials: credentials }, + ) + )[0]; + + if (authorizationResponse.result === AuthorizeResult.DENY) { + throw new NotAllowedError(); + } + } + const { token } = await auth.getPluginRequestToken({ onBehalfOf: credentials, targetPluginId: 'catalog', @@ -539,6 +607,21 @@ export async function createRouter( res.status(201).json({ id: result.taskId }); }) .get('/v2/tasks', async (req, res) => { + const credentials = await httpAuth.credentials(req); + + if (permissions) { + const authorizationResponse = ( + await permissions?.authorizeConditional( + [{ permission: taskReadPermission }], + { credentials: credentials }, + ) + )[0]; + + if (authorizationResponse.result === AuthorizeResult.DENY) { + throw new NotAllowedError(); + } + } + const [userEntityRef] = [req.query.createdBy].flat(); if ( @@ -561,6 +644,21 @@ export async function createRouter( res.status(200).json(tasks); }) .get('/v2/tasks/:taskId', async (req, res) => { + const credentials = await httpAuth.credentials(req); + + if (permissions) { + const authorizationResponse = ( + await permissions?.authorizeConditional( + [{ permission: taskReadPermission }], + { credentials: credentials }, + ) + )[0]; + + if (authorizationResponse.result === AuthorizeResult.DENY) { + throw new NotAllowedError(); + } + } + const { taskId } = req.params; const task = await taskBroker.get(taskId); if (!task) { @@ -571,11 +669,43 @@ export async function createRouter( res.status(200).json(task); }) .post('/v2/tasks/:taskId/cancel', async (req, res) => { + const credentials = await httpAuth.credentials(req); + + if (permissions) { + const authorizationResponses = await permissions?.authorizeConditional( + [ + { permission: taskCancelPermission }, + { permission: taskReadPermission }, + ], + { credentials: credentials }, + ); + // Requires both read and cancel permissions + for (const response of authorizationResponses) { + if (response.result === AuthorizeResult.DENY) { + throw new NotAllowedError(); + } + } + } + const { taskId } = req.params; await taskBroker.cancel?.(taskId); res.status(200).json({ status: 'cancelled' }); }) .get('/v2/tasks/:taskId/eventstream', async (req, res) => { + const credentials = await httpAuth.credentials(req); + + if (permissions) { + const authorizationResponse = ( + await permissions?.authorizeConditional( + [{ permission: taskReadPermission }], + { credentials: credentials }, + ) + )[0]; + + if (authorizationResponse.result === AuthorizeResult.DENY) { + throw new NotAllowedError(); + } + } const { taskId } = req.params; const after = req.query.after !== undefined ? Number(req.query.after) : undefined; @@ -624,6 +754,20 @@ export async function createRouter( }); }) .get('/v2/tasks/:taskId/events', async (req, res) => { + const credentials = await httpAuth.credentials(req); + + if (permissions) { + const authorizationResponse = ( + await permissions?.authorizeConditional( + [{ permission: taskReadPermission }], + { credentials: credentials }, + ) + )[0]; + + if (authorizationResponse.result === AuthorizeResult.DENY) { + throw new NotAllowedError(); + } + } const { taskId } = req.params; const after = Number(req.query.after) || undefined; @@ -654,6 +798,21 @@ export async function createRouter( }); }) .post('/v2/dry-run', async (req, res) => { + const credentials = await httpAuth.credentials(req); + + if (permissions) { + const authorizationResponse = ( + await permissions?.authorizeConditional( + [{ permission: taskCreatePermission }], + { credentials: credentials }, + ) + )[0]; + + if (authorizationResponse.result === AuthorizeResult.DENY) { + throw new NotAllowedError(); + } + } + const bodySchema = z.object({ template: z.unknown(), values: z.record(z.unknown()), @@ -671,8 +830,6 @@ export async function createRouter( throw new InputError('Input template is not a template'); } - const credentials = await httpAuth.credentials(req); - const { token } = await auth.getPluginRequestToken({ onBehalfOf: credentials, targetPluginId: 'catalog', diff --git a/plugins/scaffolder-common/src/permissions.ts b/plugins/scaffolder-common/src/permissions.ts index bd548df950..4a4137875a 100644 --- a/plugins/scaffolder-common/src/permissions.ts +++ b/plugins/scaffolder-common/src/permissions.ts @@ -49,6 +49,18 @@ export const actionExecutePermission = createPermission({ resourceType: RESOURCE_TYPE_SCAFFOLDER_ACTION, }); +/** + * This permission is used to authorize actions that involve access the action registry + * + * @alpha + */ +export const actionReadPermission = createPermission({ + name: 'scaffolder.action.read', + attributes: { + action: 'read', + }, + resourceType: RESOURCE_TYPE_SCAFFOLDER_ACTION, +}); /** * This permission is used to authorize actions that involve reading * one or more parameters from a template. @@ -123,9 +135,7 @@ export const taskCreatePermission = createPermission({ */ export const taskCancelPermission = createPermission({ name: 'scaffolder.task.cancel', - attributes: { - action: 'update', - }, + attributes: {}, resourceType: RESOURCE_TYPE_SCAFFOLDER_TASK, }); @@ -152,7 +162,10 @@ export const scaffolderTemplatePermissions = [ * List of the scaffolder permissions that are associated with scaffolder actions. * @alpha */ -export const scaffolderActionPermissions = [actionExecutePermission]; +export const scaffolderActionPermissions = [ + actionExecutePermission, + actionReadPermission, +]; /** * List of the scaffolder permissions that are associated with scaffolder tasks.