diff --git a/packages/backend/src/plugins/permission.ts b/packages/backend/src/plugins/permission.ts index c5f92c17d0..e59a53549b 100644 --- a/packages/backend/src/plugins/permission.ts +++ b/packages/backend/src/plugins/permission.ts @@ -57,10 +57,15 @@ class ExamplePermissionPolicy implements PermissionPolicy { if (isPermission(request.permission, actionExecutePermission)) { return createScaffolderActionConditionalDecision( request.permission, - scaffolderActionConditions.matchesInput({ + scaffolderActionConditions.hasInputProperty({ action: 'debug:log', - schema: inputSchema, + key: 'message', + value: 'Test', }), + // scaffolderActionConditions.matchesInput({ + // action: 'debug:log', + // schema: inputSchema, + // }), ); } diff --git a/plugins/scaffolder-backend/src/scaffolder/dryrun/createDryRunner.ts b/plugins/scaffolder-backend/src/scaffolder/dryrun/createDryRunner.ts index d9302f4d74..d28d16f4cf 100644 --- a/plugins/scaffolder-backend/src/scaffolder/dryrun/createDryRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/dryrun/createDryRunner.ts @@ -35,6 +35,7 @@ import { createTemplateAction, TaskSecrets, } from '@backstage/plugin-scaffolder-node'; +import { PermissionEvaluator } from '@backstage/plugin-permission-common'; interface DryRunInput { spec: TaskSpec; @@ -53,6 +54,7 @@ export type TemplateTesterCreateOptions = { logger: Logger; integrations: ScmIntegrations; actionRegistry: TemplateActionRegistry; + permissionApi: PermissionEvaluator; workingDirectory: string; additionalTemplateFilters?: Record; additionalTemplateGlobals?: Record; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index b31def1739..203c3cd32d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -42,10 +42,15 @@ import { TaskSpecV1beta3, TaskStep, } from '@backstage/plugin-scaffolder-common'; + import { TemplateAction } from '@backstage/plugin-scaffolder-node'; +import { createConditionAuthorizer } from '@backstage/plugin-permission-node'; import { UserEntity } from '@backstage/catalog-model'; import { createCounterMetric, createHistogramMetric } from '../../util/metrics'; import { createDefaultFilters } from '../../lib/templating/filters'; +import { PermissionEvaluator } from '@backstage/plugin-permission-common'; +import { scaffolderActionRules } from '../../service/rules'; +import { actionExecutePermission } from '@backstage/plugin-scaffolder-common/alpha'; type NunjucksWorkflowRunnerOptions = { workingDirectory: string; @@ -54,6 +59,7 @@ type NunjucksWorkflowRunnerOptions = { logger: winston.Logger; additionalTemplateFilters?: Record; additionalTemplateGlobals?: Record; + permissionApi: PermissionEvaluator; }; type TemplateContext = { @@ -103,6 +109,10 @@ const createStepLogger = ({ return { taskLogger, streamLogger }; }; +const isActionAuthorized = createConditionAuthorizer( + Object.values(scaffolderActionRules), +); + export class NunjucksWorkflowRunner implements WorkflowRunner { private readonly defaultTemplateFilters: Record; constructor(private readonly options: NunjucksWorkflowRunnerOptions) { @@ -282,6 +292,23 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { } } + const [decision] = await this.options.permissionApi.authorizeConditional( + [{ permission: actionExecutePermission }], + { token: task.secrets?.backstageToken }, + ); + + if (!isActionAuthorized(decision, { action: action.id, input })) { + throw new InputError( + `Unauthorized action: ${ + action.id + }. The input is not allowed. Input: ${JSON.stringify( + input, + null, + 2, + )}`, + ); + } + const tmpDirs = new Array(); const stepOutput: { [outputName: string]: JsonValue } = {}; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts index 5723e7136f..5c7a35303c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts @@ -22,7 +22,7 @@ import { TemplateActionRegistry } from '../actions'; import { ScmIntegrations } from '@backstage/integration'; import { assertError } from '@backstage/errors'; import { TemplateFilter, TemplateGlobal } from '../../lib'; - +import { PermissionEvaluator } from '@backstage/plugin-permission-common'; /** * TaskWorkerOptions * @@ -30,6 +30,7 @@ import { TemplateFilter, TemplateGlobal } from '../../lib'; */ export type TaskWorkerOptions = { taskBroker: TaskBroker; + permissionApi: PermissionEvaluator; runners: { workflowRunner: WorkflowRunner; }; @@ -43,6 +44,7 @@ export type TaskWorkerOptions = { */ export type CreateWorkerOptions = { taskBroker: TaskBroker; + permissionApi: PermissionEvaluator; actionRegistry: TemplateActionRegistry; integrations: ScmIntegrations; workingDirectory: string; @@ -86,6 +88,7 @@ export class TaskWorker { additionalTemplateFilters, concurrentTasksLimit = 10, // from 1 to Infinity additionalTemplateGlobals, + permissionApi, } = options; const workflowRunner = new NunjucksWorkflowRunner({ @@ -95,12 +98,14 @@ export class TaskWorker { workingDirectory, additionalTemplateFilters, additionalTemplateGlobals, + permissionApi, }); return new TaskWorker({ taskBroker: taskBroker, runners: { workflowRunner }, concurrentTasksLimit, + permissionApi, }); } diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 19207a7b75..41d7a67c89 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -263,6 +263,7 @@ export async function createRouter( additionalTemplateFilters, additionalTemplateGlobals, concurrentTasksLimit, + permissionApi, }); workers.push(worker); } @@ -288,6 +289,7 @@ export async function createRouter( workingDirectory, additionalTemplateFilters, additionalTemplateGlobals, + permissionApi, }); const templateRules: TemplatePermissionRuleInput[] = Object.values( @@ -381,16 +383,27 @@ export async function createRouter( } } - const taskSpec = await authorizeActions( - { - parameters: values, - template, - templateRef: { kind, name, namespace }, - user: userEntity as UserEntity, - userEntityRef, + const taskSpec: TaskSpec = { + apiVersion: template.apiVersion, + steps: template.spec.steps.map((step, index) => ({ + ...step, + id: step.id ?? `step-${index + 1}`, + name: step.name ?? step.action, + })), + output: template.spec.output ?? {}, + parameters: values, + user: { + entity: userEntity as UserEntity, + ref: userEntityRef, }, - { token }, - ); + templateInfo: { + entityRef: stringifyEntityRef({ kind, name, namespace }), + baseUrl: getEntityBaseUrl(template), + entity: { + metadata: template.metadata, + }, + }, + }; const result = await taskBroker.dispatch({ spec: taskSpec, diff --git a/plugins/scaffolder-backend/src/service/rules.ts b/plugins/scaffolder-backend/src/service/rules.ts index 7f71693478..379f95db10 100644 --- a/plugins/scaffolder-backend/src/service/rules.ts +++ b/plugins/scaffolder-backend/src/service/rules.ts @@ -28,6 +28,7 @@ import { import { z } from 'zod'; import { JsonObject } from '@backstage/types'; +import { get } from 'lodash'; export const createTemplatePermissionRule = makeCreatePermissionRule< TemplateEntityStepV1beta3 | TemplateParametersV1beta3, @@ -91,6 +92,36 @@ export const matchesInput = createActionPermissionRule({ toQuery: () => ({}), }); +export const hasInputProperty = createActionPermissionRule({ + name: 'HAS_INPUT', + resourceType: RESOURCE_TYPE_SCAFFOLDER_ACTION, + description: `Matches the key and value of the input of an action`, + paramsSchema: z.object({ + action: z.string().describe('Name of the actionId to match on'), + key: z.string().describe('Name of the property to match on'), + value: z.string().describe('Value of the property to match on').optional(), + }), + apply: (resource, { action, key, value }) => { + if (resource.action !== action) { + return true; + } + + const foundValue = get(resource.input, key); + + if (Array.isArray(foundValue)) { + if (value !== undefined) { + return foundValue.includes(value); + } + return foundValue.length > 0; + } + if (value !== undefined) { + return value === foundValue; + } + return !!foundValue; + }, + toQuery: () => ({}), +}); + export const hasTag = createTemplatePermissionRule({ name: 'HAS_TAG', resourceType: RESOURCE_TYPE_SCAFFOLDER_TEMPLATE, @@ -105,4 +136,4 @@ export const hasTag = createTemplatePermissionRule({ }); export const scaffolderTemplateRules = { hasTag }; -export const scaffolderActionRules = { matchesInput }; +export const scaffolderActionRules = { matchesInput, hasInputProperty }; diff --git a/template.yaml b/template.yaml index 7773a04315..fef90dbcd8 100644 --- a/template.yaml +++ b/template.yaml @@ -2,29 +2,33 @@ apiVersion: scaffolder.backstage.io/v1beta3 kind: Template metadata: name: my-custom-template +spec: + type: service parameters: - - title: Provide some simple information - metadata: - tags: - - bang + - title: Basic information properties: description: title: Description type: string - metadata: - tags: - - bar + - title: Extra information + backstage:accessControl: + tags: + - foo + properties: + name: + title: Your name + type: string steps: - id: step-one name: First log action: debug:log input: - message: hello - metadata: - tags: - - foo + message: Test - id: step-two name: Second log action: debug:log input: - message: world + message: Hello ${{ parameters.name }} + backstage:accessControl: + tags: + - foo