From 152ae1e3c55e76833403b8d0f70425092b152a3a Mon Sep 17 00:00:00 2001 From: Kashish Mittal Date: Wed, 12 Mar 2025 18:35:41 -0400 Subject: [PATCH] feat: initial implementation of scaffolder granular permissions Signed-off-by: Kashish Mittal Co-authored-by: Frank Kong --- .../scaffolder-backend/src/service/alpha.ts | 27 +++- .../src/service/permissions.ts | 35 +++++ .../src/service/router.test.ts | 124 ++++++++++++++++++ .../scaffolder-backend/src/service/router.ts | 124 ++++++++++++++---- .../src/util/checkPermissions.ts | 66 ++++++++++ plugins/scaffolder-common/src/permissions.ts | 9 ++ .../ScaffolderPageContextMenu.tsx | 7 +- .../ListTasksPage/ListTasksPage.tsx | 24 ++-- .../components/OngoingTask/ContextMenu.tsx | 3 + .../components/OngoingTask/OngoingTask.tsx | 5 +- .../src/components/Router/Router.tsx | 19 +-- 11 files changed, 384 insertions(+), 59 deletions(-) diff --git a/plugins/scaffolder-backend/src/service/alpha.ts b/plugins/scaffolder-backend/src/service/alpha.ts index 7d22c9d262..ac14f0b1ba 100644 --- a/plugins/scaffolder-backend/src/service/alpha.ts +++ b/plugins/scaffolder-backend/src/service/alpha.ts @@ -17,9 +17,14 @@ import { RESOURCE_TYPE_SCAFFOLDER_TEMPLATE, RESOURCE_TYPE_SCAFFOLDER_ACTION, + RESOURCE_TYPE_SCAFFOLDER_TASK, } from '@backstage/plugin-scaffolder-common/alpha'; import { createConditionExports } from '@backstage/plugin-permission-node'; -import { scaffolderTemplateRules, scaffolderActionRules } from './rules'; +import { + scaffolderTemplateRules, + scaffolderActionRules, + scaffolderTaskRules, +} from './rules'; const templateConditionExports = createConditionExports({ pluginId: 'scaffolder', @@ -33,6 +38,12 @@ const actionsConditionExports = createConditionExports({ rules: scaffolderActionRules, }); +const taskConditionExports = createConditionExports({ + pluginId: 'scaffolder', + resourceType: RESOURCE_TYPE_SCAFFOLDER_TASK, + rules: scaffolderTaskRules, +}); + /** * `createScaffolderTemplateConditionalDecision` can be used when authoring policies to * create conditional decisions. It requires a permission of type @@ -90,3 +101,17 @@ export const createScaffolderActionConditionalDecision = * @alpha */ export const scaffolderActionConditions = actionsConditionExports.conditions; + +/** + * @alpha + */ +export const createScaffolderTaskConditionalDecision = + taskConditionExports.createConditionalDecision; + +/** + * These conditions are used when creating conditional decisions for scaffolder + * tasks that are returned by authorization policies. + * + * @alpha + */ +export const scaffolderTaskConditions = taskConditionExports.conditions; diff --git a/plugins/scaffolder-backend/src/service/permissions.ts b/plugins/scaffolder-backend/src/service/permissions.ts index 9b12e115b9..4843fcb51e 100644 --- a/plugins/scaffolder-backend/src/service/permissions.ts +++ b/plugins/scaffolder-backend/src/service/permissions.ts @@ -21,9 +21,24 @@ import { } from '@backstage/plugin-scaffolder-common'; import { RESOURCE_TYPE_SCAFFOLDER_ACTION, + RESOURCE_TYPE_SCAFFOLDER_TASK, RESOURCE_TYPE_SCAFFOLDER_TEMPLATE, } from '@backstage/plugin-scaffolder-common/alpha'; import { PermissionRuleParams } from '@backstage/plugin-permission-common'; +import { + SerializedTask, + TaskFilter, + TaskFilters, +} from '@backstage/plugin-scaffolder-node'; + +/** + * + * @public + */ +export type ScaffolderPermissionRuleInput = + | TemplatePermissionRuleInput + | ActionPermissionRuleInput + | TaskPermissionRuleInput; /** * @public @@ -59,3 +74,23 @@ export function isActionPermissionRuleInput( ): permissionRule is ActionPermissionRuleInput { return permissionRule.resourceType === RESOURCE_TYPE_SCAFFOLDER_ACTION; } + +/** + * @public + */ +export type TaskPermissionRuleInput< + TParams extends PermissionRuleParams = PermissionRuleParams, +> = PermissionRule< + SerializedTask, + { + property: TaskFilter['property']; + values: any; + }, + typeof RESOURCE_TYPE_SCAFFOLDER_TASK, + TParams +>; +export function isTaskPermissionRuleInput( + permissionRule: ScaffolderPermissionRuleInput, +): permissionRule is TaskPermissionRuleInput { + return permissionRule.resourceType === RESOURCE_TYPE_SCAFFOLDER_TASK; +} \ No newline at end of file diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index 679ac44640..661939eaf2 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -945,6 +945,36 @@ describe('scaffolder router', () => { order: [{ order: 'desc', field: 'created_at' }], }); }); + + it('disallows users from seeing tasks they do not own', async () => { + const { router, taskBroker, permissions } = await createTestRouter(); + jest + .spyOn(permissions, 'authorizeConditional') + .mockImplementationOnce(async () => [ + { + conditions: { + resourceType: 'scaffolder-task', + rule: 'IS_TASK_OWNER', + params: { createdBy: ['user'] }, + }, + pluginId: 'scaffolder', + resourceType: 'scaffolder-task', + result: AuthorizeResult.CONDITIONAL, + }, + ]); + const response = await request(router).get( + `/v2/tasks?createdBy=not-user`, + ); + expect(taskBroker.list).toHaveBeenCalledWith({ + filters: { createdBy: ['not-user'], status: undefined }, + order: undefined, + pagination: { limit: undefined, offset: undefined }, + permissionFilters: { key: 'created_by', values: ['user'] }, + }); + expect(response.status).toBe(200); + expect(response.body.totalTasks).toBe(0); + expect(response.body.tasks).toEqual([]); + }); }); describe('GET /v2/tasks/:taskId', () => { @@ -966,6 +996,37 @@ describe('scaffolder router', () => { expect(response.body.status).toBe('completed'); expect(response.body.secrets).toBeUndefined(); }); + it('disallows users from seeing tasks they do not own', async () => { + const { router, permissions, taskBroker } = await createTestRouter(); + jest + .spyOn(permissions, 'authorizeConditional') + .mockImplementationOnce(async () => [ + { + conditions: { + resourceType: 'scaffolder-task', + rule: 'IS_TASK_OWNER', + params: { createdBy: ['user'] }, + }, + pluginId: 'scaffolder', + resourceType: 'scaffolder-task', + result: AuthorizeResult.CONDITIONAL, + }, + ]); + (taskBroker.get as jest.Mocked['get']).mockResolvedValue({ + id: 'a-random-id', + spec: {} as any, + status: 'completed', + createdAt: '', + secrets: { + __initiatorCredentials: JSON.stringify(credentials), + }, + createdBy: 'not-user', + }); + + const response = await request(router).get(`/v2/tasks/a-random-id`); + expect(taskBroker.get).toHaveBeenCalledWith('a-random-id'); + expect(response.error).not.toBeFalsy(); + }); }); describe('GET /v2/tasks/:taskId/eventstream', () => { @@ -1206,6 +1267,40 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{ }); expect(subscriber!.closed).toBe(true); }); + it('disallows users from seeing events for tasks they do not own', async () => { + const { permissions, router, taskBroker } = await createTestRouter(); + + jest + .spyOn(permissions, 'authorizeConditional') + .mockImplementationOnce(async () => [ + { + conditions: { + resourceType: 'scaffolder-task', + rule: 'IS_TASK_OWNER', + params: { createdBy: ['user'] }, + }, + pluginId: 'scaffolder', + resourceType: 'scaffolder-task', + result: AuthorizeResult.CONDITIONAL, + }, + ]); + (taskBroker.get as jest.Mocked['get']).mockResolvedValue({ + id: 'a-random-id', + spec: {} as any, + status: 'completed', + createdAt: '', + secrets: { + __initiatorCredentials: JSON.stringify(credentials), + }, + createdBy: 'not-user', + }); + + const response = await request(router).get( + `/v2/tasks/a-random-id/events`, + ); + expect(taskBroker.get).toHaveBeenCalledWith('a-random-id'); + expect(response.error).not.toBeFalsy(); + }); }); describe('POST /v2/dry-run', () => { @@ -1233,6 +1328,35 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{ expect.anything(), ); }); + it('disallows users from seeing tasks they do not own', async () => { + const { permissions, router, taskBroker } = await createTestRouter(); + jest + .spyOn(permissions, 'authorizeConditional') + .mockImplementationOnce(async () => [ + { + conditions: { + resourceType: 'scaffolder-task', + rule: 'IS_TASK_OWNER', + params: { createdBy: ['user'] }, + }, + pluginId: 'scaffolder', + resourceType: 'scaffolder-task', + result: AuthorizeResult.CONDITIONAL, + }, + ]); + const response = await request(router).get( + `/v2/tasks?createdBy=not-user`, + ); + expect(taskBroker.list).toHaveBeenCalledWith({ + filters: { createdBy: ['not-user'], status: undefined }, + order: undefined, + pagination: { limit: undefined, offset: undefined }, + permissionFilters: { key: 'created_by', values: ['user'] }, + }); + expect(response.status).toBe(200); + expect(response.body.totalTasks).toBe(0); + expect(response.body.tasks).toEqual([]); + }); }); describe('GET /v2/autocomplete/:provider/:resource', () => { diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 0ea6293f36..b97626b5f3 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -42,6 +42,9 @@ import { EventsService } from '@backstage/plugin-events-node'; import { createConditionAuthorizer, createPermissionIntegrationRouter, + PermissionRule, + createConditionTransformer, + ConditionTransformer, } from '@backstage/plugin-permission-node'; import { TaskSpec, @@ -51,7 +54,9 @@ import { import { RESOURCE_TYPE_SCAFFOLDER_ACTION, RESOURCE_TYPE_SCAFFOLDER_TEMPLATE, + RESOURCE_TYPE_SCAFFOLDER_TASK, scaffolderActionPermissions, + scaffolderTaskPermissions, scaffolderPermissions, scaffolderTemplatePermissions, taskCancelPermission, @@ -89,7 +94,11 @@ import { import { createDryRunner } from '../scaffolder/dryrun'; import { StorageTaskBroker } from '../scaffolder/tasks/StorageTaskBroker'; import { InternalTaskSecrets } from '../scaffolder/tasks/types'; -import { checkPermission } from '../util/checkPermissions'; +import { + checkPermission, + checkTaskPermission, + getAuthorizeConditions, +} from '../util/checkPermissions'; import { findTemplate, getEntityBaseUrl, @@ -97,7 +106,7 @@ import { parseNumberParam, parseStringsParam, } from './helpers'; -import { scaffolderActionRules, scaffolderTemplateRules } from './rules'; + import { convertFiltersToRecord, convertGlobalsToRecord, @@ -107,6 +116,9 @@ import { } from '../util/templating'; import { createDefaultFilters } from '../lib/templating/filters/createDefaultFilters'; import { + ScaffolderPermissionRuleInput, + TaskPermissionRuleInput, + isTaskPermissionRuleInput, ActionPermissionRuleInput, isActionPermissionRuleInput, isTemplatePermissionRuleInput, @@ -114,6 +126,12 @@ import { } from './permissions'; import { CatalogService } from '@backstage/plugin-catalog-node'; +import { + scaffolderActionRules, + scaffolderTemplateRules, + scaffolderTaskRules, +} from './rules'; + /** * RouterOptions */ @@ -139,11 +157,11 @@ export interface RouterOptions { | CreatedTemplateGlobal[]; additionalWorkspaceProviders?: Record; permissions?: PermissionsService; - permissionRules?: Array< - TemplatePermissionRuleInput | ActionPermissionRuleInput - >; - auth: AuthService; - httpAuth: HttpAuthService; + permissionRules?: Array; + auth?: AuthService; + httpAuth?: HttpAuthService; + identity?: IdentityApi; + discovery?: DiscoveryService; events?: EventsService; auditor?: AuditorService; autocompleteHandlers?: Record; @@ -312,15 +330,24 @@ export async function createRouter( const actionRules: ActionPermissionRuleInput[] = Object.values( scaffolderActionRules, ); + const taskRules: TaskPermissionRuleInput[] = + Object.values(scaffolderTaskRules); if (permissionRules) { templateRules.push( ...permissionRules.filter(isTemplatePermissionRuleInput), ); actionRules.push(...permissionRules.filter(isActionPermissionRuleInput)); + taskRules.push(...permissionRules.filter(isTaskPermissionRuleInput)); } - const isAuthorized = createConditionAuthorizer(Object.values(templateRules)); + const isTemplateAuthorized = createConditionAuthorizer( + Object.values(templateRules), + ); + const isTaskAuthorized = createConditionAuthorizer(Object.values(taskRules)); + + const taskTransformConditions: ConditionTransformer = + createConditionTransformer(Object.values(taskRules)); const permissionIntegrationRouter = createPermissionIntegrationRouter({ resources: [ @@ -334,6 +361,18 @@ export async function createRouter( 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, }); @@ -532,11 +571,6 @@ export async function createRouter( try { const credentials = await httpAuth.credentials(req); - await checkPermission({ - credentials, - permissions: [taskReadPermission], - permissionService: permissions, - }); if (!taskBroker.list) { throw new Error( @@ -564,6 +598,13 @@ export async function createRouter( const limit = parseNumberParam(req.query.limit, 'limit'); const offset = parseNumberParam(req.query.offset, 'offset'); + const taskPermissionFilters = await getAuthorizeConditions({ + credentials: credentials, + permission: taskReadPermission, + permissionService: permissions, + transformConditions: taskTransformConditions, + }); + const tasks = await taskBroker.list({ filters: { createdBy, @@ -574,6 +615,7 @@ export async function createRouter( limit: limit ? limit[0] : undefined, offset: offset ? offset[0] : undefined, }, + permissionFilters: taskPermissionFilters, }); await auditorEvent?.success(); @@ -598,13 +640,17 @@ export async function createRouter( try { const credentials = await httpAuth.credentials(req); - await checkPermission({ - credentials, - permissions: [taskReadPermission], - permissionService: permissions, - }); const task = await taskBroker.get(taskId); + + await checkTaskPermission({ + credentials, + permission: taskReadPermission, + permissionService: permissions, + task: task, + isTaskAuthorized, + }); + if (!task) { throw new NotFoundError(`Task with id ${taskId} does not exist`); } @@ -634,11 +680,13 @@ export async function createRouter( try { const credentials = await httpAuth.credentials(req); - // Requires both read and cancel permissions - await checkPermission({ + const task = await taskBroker.get(taskId); + await checkTaskPermission({ credentials, - permissions: [taskCancelPermission, taskReadPermission], + permission: taskCancelPermission, permissionService: permissions, + task: task, + isTaskAuthorized, }); await taskBroker.cancel?.(taskId); @@ -666,13 +714,23 @@ export async function createRouter( try { const credentials = await httpAuth.credentials(req); + const task = await taskBroker.get(taskId); + // Requires both read and cancel permissions await checkPermission({ credentials, - permissions: [taskCreatePermission, taskReadPermission], + permissions: [taskCreatePermission], permissionService: permissions, }); + await checkTaskPermission({ + credentials, + permission: taskReadPermission, + permissionService: permissions, + task: task, + isTaskAuthorized, + }); + await auditorEvent?.success(); const { token } = await auth.getPluginRequestToken({ @@ -711,10 +769,14 @@ export async function createRouter( try { const credentials = await httpAuth.credentials(req); - await checkPermission({ + const task = await taskBroker.get(taskId); + + await checkTaskPermission({ credentials, - permissions: [taskReadPermission], + permission: taskReadPermission, permissionService: permissions, + task: task, + isTaskAuthorized, }); const after = @@ -783,10 +845,14 @@ export async function createRouter( try { const credentials = await httpAuth.credentials(req); - await checkPermission({ + const task = await taskBroker.get(taskId); + + await checkTaskPermission({ credentials, - permissions: [taskReadPermission], + permission: taskReadPermission, permissionService: permissions, + task: task, + isTaskAuthorized, }); const after = Number(req.query.after) || undefined; @@ -1023,18 +1089,18 @@ export async function createRouter( // Authorize parameters if (Array.isArray(template.spec.parameters)) { template.spec.parameters = template.spec.parameters.filter(step => - isAuthorized(parameterDecision, step), + isTemplateAuthorized(parameterDecision, step), ); } else if ( template.spec.parameters && - !isAuthorized(parameterDecision, template.spec.parameters) + !isTemplateAuthorized(parameterDecision, template.spec.parameters) ) { template.spec.parameters = undefined; } // Authorize steps template.spec.steps = template.spec.steps.filter(step => - isAuthorized(stepDecision, step), + isTemplateAuthorized(stepDecision, step), ); return template; diff --git a/plugins/scaffolder-backend/src/util/checkPermissions.ts b/plugins/scaffolder-backend/src/util/checkPermissions.ts index b6c0395fbb..950f506e55 100644 --- a/plugins/scaffolder-backend/src/util/checkPermissions.ts +++ b/plugins/scaffolder-backend/src/util/checkPermissions.ts @@ -21,7 +21,13 @@ import { NotAllowedError } from '@backstage/errors'; import { AuthorizeResult, BasicPermission, + PermissionCriteria, + PolicyDecision, + ResourcePermission, } from '@backstage/plugin-permission-common'; +import { ConditionTransformer } from '@backstage/plugin-permission-node'; +import { SerializedTask } from '@backstage/plugin-scaffolder-node'; +import { TaskFilters } from '@backstage/plugin-scaffolder-node'; export type checkPermissionOptions = { credentials: BackstageCredentials; @@ -29,6 +35,24 @@ export type checkPermissionOptions = { permissionService?: PermissionsService; }; +export type checkTaskPermissionOptions = { + credentials: BackstageCredentials; + permission: ResourcePermission; + permissionService?: PermissionsService; + task: SerializedTask; + isTaskAuthorized: ( + decision: PolicyDecision, + resource: SerializedTask | undefined, + ) => boolean; +}; + +export type authorizeConditionsOptions = { + credentials: BackstageCredentials; + permission: ResourcePermission; + permissionService?: PermissionsService; + transformConditions: ConditionTransformer; +}; + /** * Does a basic check on permissions. Throws 403 error if any permission responds with AuthorizeResult.DENY * @public @@ -51,3 +75,45 @@ export async function checkPermission(options: checkPermissionOptions) { } } } + +/** + * Does a conditional permission check for scaffolder task reading and cancellation. + * Throws 403 error if permission responds with AuthorizeResult.DENY, or does not resolve to true during the conditional rule check + * @public + */ +export async function checkTaskPermission(options: checkTaskPermissionOptions) { + const { permission, permissionService, credentials, task, isTaskAuthorized } = + options; + if (permissionService) { + const [taskDecision] = await permissionService.authorizeConditional( + [{ permission: permission }], + { credentials }, + ); + if ( + taskDecision.result === AuthorizeResult.DENY || + !isTaskAuthorized(taskDecision, task) + ) { + throw new NotAllowedError(); + } + } +} + +/** Fetches and transforms authorization conditions into filters, or returns `undefined` if the decision is not conditional. + * @public + */ +export const getAuthorizeConditions = async ( + options: authorizeConditionsOptions, +): Promise | undefined> => { + const { permission, permissionService, credentials, transformConditions } = + options; + if (permissionService) { + const [taskDecision] = await permissionService.authorizeConditional( + [{ permission: permission }], + { credentials }, + ); + if (taskDecision.result === AuthorizeResult.CONDITIONAL) { + return transformConditions(taskDecision.conditions); + } + } + return undefined; +}; diff --git a/plugins/scaffolder-common/src/permissions.ts b/plugins/scaffolder-common/src/permissions.ts index e758291e75..36c72072e9 100644 --- a/plugins/scaffolder-common/src/permissions.ts +++ b/plugins/scaffolder-common/src/permissions.ts @@ -30,6 +30,13 @@ export const RESOURCE_TYPE_SCAFFOLDER_TEMPLATE = 'scaffolder-template'; */ export const RESOURCE_TYPE_SCAFFOLDER_ACTION = 'scaffolder-action'; +/** + * Permission resource type which corresponds to scaffolder tasks + * + * @alpha + */ +export const RESOURCE_TYPE_SCAFFOLDER_TASK = 'scaffolder-task'; + /** * This permission is used to authorize actions that involve executing * an action from a template. @@ -89,6 +96,7 @@ export const taskReadPermission = createPermission({ attributes: { action: 'read', }, + resourceType: RESOURCE_TYPE_SCAFFOLDER_TASK, }); /** @@ -111,6 +119,7 @@ export const taskCreatePermission = createPermission({ export const taskCancelPermission = createPermission({ name: 'scaffolder.task.cancel', attributes: {}, + resourceType: RESOURCE_TYPE_SCAFFOLDER_TASK, }); /** diff --git a/plugins/scaffolder-react/src/next/components/ScaffolderPageContextMenu/ScaffolderPageContextMenu.tsx b/plugins/scaffolder-react/src/next/components/ScaffolderPageContextMenu/ScaffolderPageContextMenu.tsx index 9546792983..2df4ff3b71 100644 --- a/plugins/scaffolder-react/src/next/components/ScaffolderPageContextMenu/ScaffolderPageContextMenu.tsx +++ b/plugins/scaffolder-react/src/next/components/ScaffolderPageContextMenu/ScaffolderPageContextMenu.tsx @@ -30,7 +30,6 @@ import Functions from '@material-ui/icons/Functions'; import MoreVert from '@material-ui/icons/MoreVert'; import { SyntheticEvent, useState } from 'react'; import { usePermission } from '@backstage/plugin-permission-react'; -import { taskReadPermission } from '@backstage/plugin-scaffolder-common/alpha'; import { templateManagementPermission } from '@backstage/plugin-scaffolder-common/alpha'; import { scaffolderReactTranslationRef } from '../../../translation'; @@ -69,10 +68,6 @@ export function ScaffolderPageContextMenu( const classes = useStyles(); const [anchorEl, setAnchorEl] = useState(); - const { allowed: canReadTasks } = usePermission({ - permission: taskReadPermission, - }); - const { allowed: canManageTemplates } = usePermission({ permission: templateManagementPermission, }); @@ -164,7 +159,7 @@ export function ScaffolderPageContextMenu( /> )} - {onTasksClicked && canReadTasks && ( + {onTasksClicked && ( diff --git a/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx b/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx index 6d48cec125..d6b5b7116e 100644 --- a/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx +++ b/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx @@ -91,14 +91,22 @@ const ListTaskPageContent = (props: MyTaskPageProps) => { if (error) { return ( - <> - - - + + + setOwnerFilter(id)} + /> + + + + + + ); } diff --git a/plugins/scaffolder/src/components/OngoingTask/ContextMenu.tsx b/plugins/scaffolder/src/components/OngoingTask/ContextMenu.tsx index e1a5f37ddc..ee27053a99 100644 --- a/plugins/scaffolder/src/components/OngoingTask/ContextMenu.tsx +++ b/plugins/scaffolder/src/components/OngoingTask/ContextMenu.tsx @@ -46,6 +46,7 @@ type ContextMenuProps = { onStartOver?: () => void; onToggleLogs?: (state: boolean) => void; onToggleButtonBar?: (state: boolean) => void; + taskId?: string; isCancelButtonDisabled: boolean; onCancel: () => void; }; @@ -67,6 +68,7 @@ export const ContextMenu = (props: ContextMenuProps) => { onStartOver, onToggleLogs, onToggleButtonBar, + taskId } = props; const { getPageTheme } = useTheme(); const pageTheme = getPageTheme({ themeId: 'website' }); @@ -76,6 +78,7 @@ export const ContextMenu = (props: ContextMenuProps) => { const { allowed: canReadTask } = usePermission({ permission: taskReadPermission, + resourceRef: taskId }); const { allowed: canCreateTask } = usePermission({ diff --git a/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx b/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx index c9a119e4a1..c01aa79441 100644 --- a/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx +++ b/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx @@ -46,7 +46,7 @@ import { TaskSteps, } from '@backstage/plugin-scaffolder-react/alpha'; import { useAsync } from '@react-hookz/web'; -import { usePermission } from '@backstage/plugin-permission-react'; +import { usePermission} from '@backstage/plugin-permission-react'; import { taskCancelPermission, taskCreatePermission, @@ -142,10 +142,12 @@ function OngoingTaskContent(props: { // Used dummy string value for `resourceRef` since `allowed` field will always return `false` if `resourceRef` is `undefined` const { allowed: canCancelTask } = usePermission({ permission: taskCancelPermission, + resourceRef: taskId, }); const { allowed: canReadTask } = usePermission({ permission: taskReadPermission, + resourceRef: taskId, }); const { allowed: canCreateTask } = usePermission({ @@ -269,6 +271,7 @@ function OngoingTaskContent(props: { onRetry={triggerRetry} onToggleLogs={setLogVisibleState} onToggleButtonBar={setButtonBarVisibleState} + taskId={taskId} onCancel={triggerCancel} isCancelButtonDisabled={isCancelButtonDisabled} /> diff --git a/plugins/scaffolder/src/components/Router/Router.tsx b/plugins/scaffolder/src/components/Router/Router.tsx index 91567c1ddd..d8dee73849 100644 --- a/plugins/scaffolder/src/components/Router/Router.tsx +++ b/plugins/scaffolder/src/components/Router/Router.tsx @@ -60,10 +60,7 @@ import { CustomFieldsPage, } from '../../alpha/components/TemplateEditorPage'; import { RequirePermission } from '@backstage/plugin-permission-react'; -import { - taskReadPermission, - templateManagementPermission, -} from '@backstage/plugin-scaffolder-common/alpha'; +import { templateManagementPermission } from '@backstage/plugin-scaffolder-common/alpha'; import { useApp } from '@backstage/core-plugin-api'; import { FormField, OpaqueFormField } from '@internal/scaffolder'; import { useAsync, useMountEffect } from '@react-hookz/web'; @@ -182,11 +179,9 @@ export const InternalRouter = ( - - + } /> - - - } + element={} />