diff --git a/.changeset/weak-bags-behave.md b/.changeset/weak-bags-behave.md new file mode 100644 index 0000000000..2c7bc1d37c --- /dev/null +++ b/.changeset/weak-bags-behave.md @@ -0,0 +1,11 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +'@backstage/plugin-scaffolder-common': minor +'@backstage/plugin-scaffolder-react': minor +'@backstage/plugin-scaffolder-node': minor +'@backstage/plugin-scaffolder': minor +--- + +BREAKING : Added two new scaffolder rules for `scaffolder.task.read` and `scaffolder.task.cancel` to allow for conditional permission policies such as restricting access to tasks and task events based on creators (`hasCreatedBy`) and granting template owners visibility into all runs of their templates (`hasTemplateEntityRefs`). + +BREAKING: Removed requirement to have both `scaffolder.task.read` and `scaffolder.task.cancel` permissions to cancel tasks. diff --git a/plugins/scaffolder-backend/report-alpha.api.md b/plugins/scaffolder-backend/report-alpha.api.md index 958d68fc25..cd9ca2eb00 100644 --- a/plugins/scaffolder-backend/report-alpha.api.md +++ b/plugins/scaffolder-backend/report-alpha.api.md @@ -10,6 +10,8 @@ import { PermissionCondition } from '@backstage/plugin-permission-common'; import { PermissionCriteria } from '@backstage/plugin-permission-common'; import { PermissionRule } from '@backstage/plugin-permission-node'; import { ResourcePermission } from '@backstage/plugin-permission-common'; +import { SerializedTask } from '@backstage/plugin-scaffolder-node'; +import { TaskFilter } from '@backstage/plugin-scaffolder-node'; import { TemplateEntityStepV1beta3 } from '@backstage/plugin-scaffolder-common'; import { TemplateParametersV1beta3 } from '@backstage/plugin-scaffolder-common'; @@ -19,6 +21,12 @@ export const createScaffolderActionConditionalDecision: ( conditions: PermissionCriteria>, ) => ConditionalPolicyDecision; +// @alpha (undocumented) +export const createScaffolderTaskConditionalDecision: ( + permission: ResourcePermission<'scaffolder-task'>, + conditions: PermissionCriteria>, +) => ConditionalPolicyDecision; + // @alpha export const createScaffolderTemplateConditionalDecision: ( permission: ResourcePermission<'scaffolder-template'>, @@ -76,6 +84,32 @@ export const scaffolderActionConditions: Conditions<{ >; }>; +// @alpha +export const scaffolderTaskConditions: Conditions<{ + hasCreatedBy: PermissionRule< + SerializedTask, + { + property: TaskFilter['property']; + values: any; + }, + 'scaffolder-task', + { + createdBy: string[]; + } + >; + hasTemplateEntityRefs: PermissionRule< + SerializedTask, + { + property: TaskFilter['property']; + values: any; + }, + 'scaffolder-task', + { + templateEntityRefs: string[]; + } + >; +}>; + // @alpha export const scaffolderTemplateConditions: Conditions<{ hasTag: PermissionRule< diff --git a/plugins/scaffolder-backend/report.api.md b/plugins/scaffolder-backend/report.api.md index 1f2a17a384..9da363e872 100644 --- a/plugins/scaffolder-backend/report.api.md +++ b/plugins/scaffolder-backend/report.api.md @@ -17,6 +17,7 @@ import { JsonObject } from '@backstage/types'; import { JsonValue } from '@backstage/types'; import { Knex } from 'knex'; import { LoggerService } from '@backstage/backend-plugin-api'; +import { PermissionCriteria } from '@backstage/plugin-permission-common'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; import { PermissionRule } from '@backstage/plugin-permission-node'; import { PermissionRuleParams } from '@backstage/plugin-permission-common'; @@ -28,6 +29,7 @@ import { SerializedTaskEvent } from '@backstage/plugin-scaffolder-node'; import { TaskBroker } from '@backstage/plugin-scaffolder-node'; import { TaskCompletionState } from '@backstage/plugin-scaffolder-node'; import { TaskContext } from '@backstage/plugin-scaffolder-node'; +import { TaskFilters } from '@backstage/plugin-scaffolder-node'; import { TaskRecovery } from '@backstage/plugin-scaffolder-common'; import { TaskSecrets } from '@backstage/plugin-scaffolder-node'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; @@ -351,6 +353,7 @@ export class DatabaseTaskStore implements TaskStore { order: 'asc' | 'desc'; field: string; }[]; + permissionFilters?: PermissionCriteria; }): Promise<{ tasks: SerializedTask[]; totalTasks?: number; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.test.ts index eb0418b5ed..2d9b4db124 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.test.ts @@ -22,6 +22,8 @@ import { ConflictError } from '@backstage/errors'; import { createMockDirectory } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; import { EventsService } from '@backstage/plugin-events-node'; +import { PermissionCriteria } from '@backstage/plugin-permission-common'; +import { TaskFilters } from '@backstage/plugin-scaffolder-node'; const createStore = async (events?: EventsService) => { const manager = DatabaseManager.fromConfig( @@ -231,6 +233,73 @@ describe('DatabaseTaskStore', () => { expect(tasks[0].id).toBeDefined(); }); + it('should filter tasks based on permissionFilters', async () => { + const { store } = await createStore(); + + await store.createTask({ + spec: { + templateInfo: { entityRef: 'template:default/three' }, + } as TaskSpec, + createdBy: 'user:default/one', + }); + + await store.createTask({ + spec: { + templateInfo: { entityRef: 'template:default/four' }, + } as TaskSpec, + createdBy: 'user:default/two', + }); + + await store.createTask({ + spec: { + templateInfo: { entityRef: 'template:default/one' }, + } as TaskSpec, + createdBy: 'user:default/three', + }); + + await store.createTask({ + spec: { + templateInfo: { entityRef: 'template:default/two' }, + } as TaskSpec, + createdBy: 'user:default/three', + }); + + await store.createTask({ + spec: { + templateInfo: { entityRef: 'template:default/three' }, + } as TaskSpec, + createdBy: 'user:default/three', + }); + + const permissionFilters: PermissionCriteria = { + anyOf: [ + { + property: 'createdBy', + values: ['user:default/one', 'user:default/two'], + }, + { + property: 'templateEntityRefs', + values: ['template:default/one', 'template:default/two'], + }, + ], + }; + + const { tasks, totalTasks } = await store.list({ + permissionFilters: permissionFilters, + }); + + expect(totalTasks).toBe(4); + + expect(tasks).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + createdBy: 'user:default/three', + spec: { templateInfo: { entityRef: 'template:default/three' } }, + }), + ]), + ); + }); + it('should sent an event to start cancelling the task', async () => { const { store } = await createStore(eventsService); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts index f81a79a5a0..b9b5fd0d7c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts @@ -35,6 +35,7 @@ import { SerializedTask, SerializedTaskEvent, TaskEventType, + TaskFilter, TaskSecrets, TaskStatus, } from '@backstage/plugin-scaffolder-node'; @@ -48,6 +49,14 @@ import { } from '@backstage/plugin-scaffolder-node/alpha'; import { flattenParams } from '../../service/helpers'; import { EventsService } from '@backstage/plugin-events-node'; +import { PermissionCriteria } from '@backstage/plugin-permission-common'; +import { + isAndCriteria, + isNotCriteria, + isOrCriteria, +} from '@backstage/plugin-permission-node'; +import { TaskFilters } from '@backstage/plugin-scaffolder-node'; +import { compact } from 'lodash'; const migrationsDir = resolvePackagePath( '@backstage/plugin-scaffolder-backend', @@ -195,6 +204,63 @@ export class DatabaseTaskStore implements TaskStore { } } + private isTaskFilter(filter: any): filter is TaskFilter { + return filter.hasOwnProperty('property'); + } + + private parseFilter( + filter: PermissionCriteria, + query: Knex.QueryBuilder, + db: Knex, + negate: boolean = false, + ): Knex.QueryBuilder { + // handle not criteria + if (isNotCriteria(filter)) { + return this.parseFilter(filter.not, query, db, !negate); + } + + if (this.isTaskFilter(filter)) { + const values: string[] = compact(filter.values) ?? []; + + if (filter.property === 'createdBy') { + query.whereIn('created_by', [...new Set(values)]); + } + + if (filter.property === 'templateEntityRefs' && values.length > 0) { + const dbClient = this.db.client.config.client; + const placeholders = values.map(() => '?').join(', '); + if (dbClient === 'pg') { + query.whereRaw( + `spec::jsonb->'templateInfo'->>'entityRef' IN (${placeholders})`, + values, + ); + } else if (dbClient === 'better-sqlite3') { + query.whereRaw( + `json_extract(spec, '$.templateInfo.entityRef') IN (${placeholders})`, + values, + ); + } + } + return query; + } + + return query[negate ? 'andWhereNot' : 'andWhere'](subQuery => { + if (isOrCriteria(filter)) { + for (const subFilter of filter.anyOf ?? []) { + subQuery.orWhere(subQueryInner => + this.parseFilter(subFilter, subQueryInner, db, false), + ); + } + } else if (isAndCriteria(filter)) { + for (const subFilter of filter.allOf ?? []) { + subQuery.andWhere(subQueryInner => + this.parseFilter(subFilter, subQueryInner, db, false), + ); + } + } + }); + } + async list(options: { createdBy?: string; status?: TaskStatus; @@ -207,16 +273,31 @@ export class DatabaseTaskStore implements TaskStore { offset?: number; }; order?: { order: 'asc' | 'desc'; field: string }[]; + permissionFilters?: PermissionCriteria; }): Promise<{ tasks: SerializedTask[]; totalTasks?: number }> { - const { createdBy, status, pagination, order, filters } = options ?? {}; + const { createdBy, status, pagination, order, filters, permissionFilters } = + options ?? {}; const queryBuilder = this.db('tasks'); - if (createdBy || filters?.createdBy) { - const arr: string[] = flattenParams( - createdBy, - filters?.createdBy, - ); - queryBuilder.whereIn('created_by', [...new Set(arr)]); + const createdByValues = flattenParams( + createdBy, + filters?.createdBy, + ); + + const combinedPermissionFilters: + | PermissionCriteria + | undefined = + createdByValues.length > 0 + ? { + allOf: [ + { property: 'createdBy', values: createdByValues }, + ...(permissionFilters ? [permissionFilters] : []), + ], + } + : permissionFilters; + + if (combinedPermissionFilters) { + this.parseFilter(combinedPermissionFilters, queryBuilder, this.db); } if (status || filters?.status) { diff --git a/plugins/scaffolder-backend/src/service/permissions.ts b/plugins/scaffolder-backend/src/service/permissions.ts index 4843fcb51e..cd10cabde0 100644 --- a/plugins/scaffolder-backend/src/service/permissions.ts +++ b/plugins/scaffolder-backend/src/service/permissions.ts @@ -28,7 +28,6 @@ import { PermissionRuleParams } from '@backstage/plugin-permission-common'; import { SerializedTask, TaskFilter, - TaskFilters, } from '@backstage/plugin-scaffolder-node'; /** @@ -52,7 +51,7 @@ export type TemplatePermissionRuleInput< TParams >; export function isTemplatePermissionRuleInput( - permissionRule: TemplatePermissionRuleInput | ActionPermissionRuleInput, + permissionRule: ScaffolderPermissionRuleInput, ): permissionRule is TemplatePermissionRuleInput { return permissionRule.resourceType === RESOURCE_TYPE_SCAFFOLDER_TEMPLATE; } @@ -70,7 +69,7 @@ export type ActionPermissionRuleInput< TParams >; export function isActionPermissionRuleInput( - permissionRule: TemplatePermissionRuleInput | ActionPermissionRuleInput, + permissionRule: ScaffolderPermissionRuleInput, ): permissionRule is ActionPermissionRuleInput { return permissionRule.resourceType === RESOURCE_TYPE_SCAFFOLDER_ACTION; } diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index 661939eaf2..baad34457a 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -1420,3 +1420,4 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{ }); }); }); + diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index b97626b5f3..72a22333b9 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -42,7 +42,6 @@ import { EventsService } from '@backstage/plugin-events-node'; import { createConditionAuthorizer, createPermissionIntegrationRouter, - PermissionRule, createConditionTransformer, ConditionTransformer, } from '@backstage/plugin-permission-node'; @@ -132,6 +131,10 @@ import { scaffolderTaskRules, } from './rules'; +import { + TaskFilters, +} from '@backstage/plugin-scaffolder-node'; + /** * RouterOptions */ @@ -158,10 +161,8 @@ export interface RouterOptions { additionalWorkspaceProviders?: Record; permissions?: PermissionsService; permissionRules?: Array; - auth?: AuthService; - httpAuth?: HttpAuthService; - identity?: IdentityApi; - discovery?: DiscoveryService; + auth: AuthService; + httpAuth: HttpAuthService; events?: EventsService; auditor?: AuditorService; autocompleteHandlers?: Record; diff --git a/plugins/scaffolder-backend/src/service/rules.test.ts b/plugins/scaffolder-backend/src/service/rules.test.ts index 8e24cacb32..f50a015a2e 100644 --- a/plugins/scaffolder-backend/src/service/rules.test.ts +++ b/plugins/scaffolder-backend/src/service/rules.test.ts @@ -22,10 +22,14 @@ import { hasProperty, hasStringProperty, hasTag, + hasCreatedBy, + hasTemplateEntityRefs, } from './rules'; import { createConditionAuthorizer } from '@backstage/plugin-permission-node'; import { RESOURCE_TYPE_SCAFFOLDER_ACTION } from '@backstage/plugin-scaffolder-common/alpha'; import { AuthorizeResult } from '@backstage/plugin-permission-common'; +import { SerializedTask } from '@backstage/plugin-scaffolder-node'; +import { TaskSpec } from '@backstage/plugin-scaffolder-common'; describe('hasTag', () => { describe('apply', () => { @@ -523,3 +527,158 @@ describe('hasStringProperty', () => { ); }); }); + +describe('hasCreatedBy', () => { + describe('apply', () => { + const task: SerializedTask = { + id: 'a-random-id', + spec: {} as TaskSpec, + status: 'completed', + createdAt: '', + createdBy: 'user:default/user-1', + }; + it('returns false when createdBy is an empty array', () => { + expect( + hasCreatedBy.apply(task, { + createdBy: [], + }), + ).toEqual(false); + }); + it('returns false when createdBy is not matched (single user in createdBy)', () => { + expect( + hasCreatedBy.apply(task, { + createdBy: ['not-matched'], + }), + ).toEqual(false); + }); + it('returns true when createdBy matches (single user in createdBy)', () => { + expect( + hasCreatedBy.apply(task, { + createdBy: ['user:default/user-1'], + }), + ).toEqual(true); + }); + it('returns false when createdBy is not matched (multiple users in createdBy)', () => { + expect( + hasCreatedBy.apply(task, { + createdBy: [ + 'user:default/user-2', + 'user:default/user-3', + 'user:default/user-4', + ], + }), + ).toEqual(false); + }); + it('returns true when createdBy matches (multiple users in createdBy)', () => { + expect( + hasCreatedBy.apply(task, { + createdBy: [ + 'user:default/user-1', + 'user:default/user-2', + 'user:default/user-3', + ], + }), + ).toEqual(true); + }); + }); + describe('toQuery', () => { + it('returns the correct query filter with values (single user in createdBy)', () => { + expect( + hasCreatedBy.toQuery({ + createdBy: ['user:default/user-1'], + }), + ).toEqual({ property: 'createdBy', values: ['user:default/user-1'] }); + }); + }); + it('returns the correct query filter with values (multiple users in createdBy)', () => { + expect( + hasCreatedBy.toQuery({ + createdBy: ['user:default/user-1', 'user:default/user-2'], + }), + ).toEqual({ + property: 'createdBy', + values: ['user:default/user-1', 'user:default/user-2'], + }); + }); +}); + +describe('hasTemplateEntityRefs', () => { + describe('apply', () => { + const task: SerializedTask = { + id: 'a-random-id', + spec: { + templateInfo: { entityRef: 'template:default/test-1' }, + } as TaskSpec, + status: 'completed', + createdAt: '', + }; + it('returns false when templateEntityRefs is an empty array', () => { + expect( + hasTemplateEntityRefs.apply(task, { + templateEntityRefs: [], + }), + ).toEqual(false); + }); + it('returns false when templateEntityRef is not matched (single entityRef in templateEntityRefs)', () => { + expect( + hasTemplateEntityRefs.apply(task, { + templateEntityRefs: ['template:default/not-matched'], + }), + ).toEqual(false); + }); + it('returns true when templateEntityRef matches (single entityRef in templateEntityRefs)', () => { + expect( + hasTemplateEntityRefs.apply(task, { + templateEntityRefs: ['template:default/test-1'], + }), + ).toEqual(true); + }); + it('returns false when templateEntityRefs is not matched (multiple entitRefs in templateEntityRefs)', () => { + expect( + hasTemplateEntityRefs.apply(task, { + templateEntityRefs: [ + 'template:default/test-2', + 'template:default/test-3', + 'template:default/test-4', + ], + }), + ).toEqual(false); + }); + it('returns true when templateEntityRefs matches (multiple entityRefs in templateEntityRefs)', () => { + expect( + hasTemplateEntityRefs.apply(task, { + templateEntityRefs: [ + 'template:default/test-2', + 'template:default/test-1', + 'template:default/test-3', + ], + }), + ).toEqual(true); + }); + }); + describe('toQuery', () => { + it('returns the correct query filter with values (single entityRef in templateEntityRefs)', () => { + expect( + hasTemplateEntityRefs.toQuery({ + templateEntityRefs: ['template:default/test-1'], + }), + ).toEqual({ + property: 'templateEntityRefs', + values: ['template:default/test-1'], + }); + }); + }); + it('returns the correct query filter with values (multiple entityRefs in templateEntityRefs)', () => { + expect( + hasTemplateEntityRefs.toQuery({ + templateEntityRefs: [ + 'template:default/test-1', + 'template:default/test-2', + ], + }), + ).toEqual({ + property: 'templateEntityRefs', + values: ['template:default/test-1', 'template:default/test-2'], + }); + }); +}); diff --git a/plugins/scaffolder-backend/src/service/rules.ts b/plugins/scaffolder-backend/src/service/rules.ts index b197810757..316e8e65dc 100644 --- a/plugins/scaffolder-backend/src/service/rules.ts +++ b/plugins/scaffolder-backend/src/service/rules.ts @@ -15,9 +15,11 @@ */ import { makeCreatePermissionRule } 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 { @@ -25,6 +27,8 @@ import { TemplateParametersV1beta3, } from '@backstage/plugin-scaffolder-common'; +import { SerializedTask, TaskFilter } from '@backstage/plugin-scaffolder-node'; + import { z } from 'zod'; import { JsonObject, JsonPrimitive } from '@backstage/types'; import { get } from 'lodash'; @@ -129,6 +133,65 @@ function buildHasProperty>({ }); } +export const createTaskPermissionRule = makeCreatePermissionRule< + SerializedTask, + { + property: TaskFilter['property']; + values: any; + }, + typeof RESOURCE_TYPE_SCAFFOLDER_TASK +>(); + +export const hasCreatedBy = createTaskPermissionRule({ + name: 'HAS_CREATED_BY', + description: 'Allows tasks created by certain users to be accessible', + resourceType: RESOURCE_TYPE_SCAFFOLDER_TASK, + paramsSchema: z.object({ + createdBy: z + .array(z.string()) + .describe( + 'List of creater entity refs; only tasks created by these users will be viewable', + ), + }), + apply: (resource, { createdBy }) => { + if (!resource.createdBy) { + return false; + } + return createdBy.includes(resource.createdBy); + }, + toQuery: ({ createdBy }) => { + return { + property: 'createdBy' as TaskFilter['property'], + values: createdBy, + }; + }, +}); + +export const hasTemplateEntityRefs = createTaskPermissionRule({ + name: 'HAS_TEMPLATE_ENTITY_REFS', + description: 'Match tasks with the given template entity refs', + resourceType: RESOURCE_TYPE_SCAFFOLDER_TASK, + paramsSchema: z.object({ + templateEntityRefs: z + .array(z.string()) + .describe( + 'List of template entity refs; only tasks related to these templates will be viewable', + ), + }), + apply: (resource, { templateEntityRefs }) => { + if (!resource.spec.templateInfo) { + return false; + } + return templateEntityRefs.includes(resource.spec.templateInfo.entityRef); + }, + toQuery: ({ templateEntityRefs }) => { + return { + property: 'templateEntityRefs' as TaskFilter['property'], + values: templateEntityRefs, + }; + }, +}); + export const scaffolderTemplateRules = { hasTag }; export const scaffolderActionRules = { hasActionId, @@ -136,3 +199,4 @@ export const scaffolderActionRules = { hasNumberProperty, hasStringProperty, }; +export const scaffolderTaskRules = { hasCreatedBy, hasTemplateEntityRefs }; diff --git a/plugins/scaffolder-common/report-alpha.api.md b/plugins/scaffolder-common/report-alpha.api.md index 97a3de63c5..0bed459cc8 100644 --- a/plugins/scaffolder-common/report-alpha.api.md +++ b/plugins/scaffolder-common/report-alpha.api.md @@ -12,6 +12,9 @@ export const actionExecutePermission: ResourcePermission<'scaffolder-action'>; // @alpha export const RESOURCE_TYPE_SCAFFOLDER_ACTION = 'scaffolder-action'; +// @alpha +export const RESOURCE_TYPE_SCAFFOLDER_TASK = 'scaffolder-task'; + // @alpha export const RESOURCE_TYPE_SCAFFOLDER_TEMPLATE = 'scaffolder-template'; @@ -23,22 +26,26 @@ export const scaffolderPermissions: ( | BasicPermission | ResourcePermission<'scaffolder-action'> | ResourcePermission<'scaffolder-template'> + | ResourcePermission<'scaffolder-task'> )[]; // @alpha -export const scaffolderTaskPermissions: BasicPermission[]; +export const scaffolderTaskPermissions: ( + | BasicPermission + | ResourcePermission<'scaffolder-task'> +)[]; // @alpha export const scaffolderTemplatePermissions: ResourcePermission<'scaffolder-template'>[]; // @alpha -export const taskCancelPermission: BasicPermission; +export const taskCancelPermission: ResourcePermission<'scaffolder-task'>; // @alpha export const taskCreatePermission: BasicPermission; // @alpha -export const taskReadPermission: BasicPermission; +export const taskReadPermission: ResourcePermission<'scaffolder-task'>; // @alpha export const templateManagementPermission: BasicPermission; diff --git a/plugins/scaffolder-node/package.json b/plugins/scaffolder-node/package.json index cd03ead977..9efecdfbdc 100644 --- a/plugins/scaffolder-node/package.json +++ b/plugins/scaffolder-node/package.json @@ -58,6 +58,7 @@ "@backstage/catalog-model": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/integration": "workspace:^", + "@backstage/plugin-permission-common": "workspace:^", "@backstage/plugin-scaffolder-common": "workspace:^", "@backstage/types": "workspace:^", "@isomorphic-git/pgp-plugin": "^0.0.7", diff --git a/plugins/scaffolder-node/report.api.md b/plugins/scaffolder-node/report.api.md index a23a9137b1..4674ee8563 100644 --- a/plugins/scaffolder-node/report.api.md +++ b/plugins/scaffolder-node/report.api.md @@ -9,6 +9,7 @@ import { JsonObject } from '@backstage/types'; import { JsonValue } from '@backstage/types'; import { LoggerService } from '@backstage/backend-plugin-api'; import { Observable } from '@backstage/types'; +import { PermissionCriteria } from '@backstage/plugin-permission-common'; import { Schema } from 'jsonschema'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { ScmIntegrations } from '@backstage/integration'; @@ -373,6 +374,7 @@ export interface TaskBroker { order: 'asc' | 'desc'; field: string; }[]; + permissionFilters?: PermissionCriteria; }): Promise<{ tasks: SerializedTask[]; totalTasks?: number; @@ -464,6 +466,25 @@ export interface TaskContext { // @public export type TaskEventType = 'completion' | 'log' | 'cancelled' | 'recovered'; +// @public +export type TaskFilter = { + property: 'createdBy' | 'templateEntityRefs'; + values: Array | undefined; +}; + +// @public +export type TaskFilters = + | { + anyOf: TaskFilter[]; + } + | { + allOf: TaskFilter[]; + } + | { + not: TaskFilter; + } + | TaskFilter; + // @public export type TaskSecrets = Record & { backstageToken?: string; diff --git a/plugins/scaffolder-node/src/tasks/index.ts b/plugins/scaffolder-node/src/tasks/index.ts index 930de95237..7e93e84cc0 100644 --- a/plugins/scaffolder-node/src/tasks/index.ts +++ b/plugins/scaffolder-node/src/tasks/index.ts @@ -18,6 +18,8 @@ export type { TaskSecrets, SerializedTask, SerializedTaskEvent, + TaskFilter, + TaskFilters, TaskBroker, TaskBrokerDispatchOptions, TaskBrokerDispatchResult, diff --git a/plugins/scaffolder-node/src/tasks/types.ts b/plugins/scaffolder-node/src/tasks/types.ts index d8cfd24530..8101080dae 100644 --- a/plugins/scaffolder-node/src/tasks/types.ts +++ b/plugins/scaffolder-node/src/tasks/types.ts @@ -15,6 +15,7 @@ */ import { BackstageCredentials } from '@backstage/backend-plugin-api'; +import { PermissionCriteria } from '@backstage/plugin-permission-common'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; import { JsonObject, JsonValue, Observable } from '@backstage/types'; @@ -104,6 +105,25 @@ export type TaskBrokerDispatchOptions = { createdBy?: string; }; +/** + * TaskFilter + * @public + */ +export type TaskFilter = { + property: 'createdBy' | 'templateEntityRefs'; + values: Array | undefined; +}; + +/** + * TaskFilters + * @public + */ +export type TaskFilters = + | { anyOf: TaskFilter[] } + | { allOf: TaskFilter[] } + | { not: TaskFilter } + | TaskFilter; + /** * Task * @@ -194,6 +214,7 @@ export interface TaskBroker { offset?: number; }; order?: { order: 'asc' | 'desc'; field: string }[]; + permissionFilters?: PermissionCriteria; }): Promise<{ tasks: SerializedTask[]; totalTasks?: number }>; /** diff --git a/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx b/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx index c01aa79441..b3c82dca19 100644 --- a/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx +++ b/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx @@ -139,7 +139,6 @@ function OngoingTaskContent(props: { const [logsVisible, setLogVisibleState] = useState(false); const [buttonBarVisible, setButtonBarVisibleState] = useState(true); - // 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, diff --git a/yarn.lock b/yarn.lock index c3e37b0ac0..ccceb14d2c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7615,6 +7615,7 @@ __metadata: "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" + "@backstage/plugin-permission-common": "workspace:^" "@backstage/plugin-scaffolder-common": "workspace:^" "@backstage/types": "workspace:^" "@isomorphic-git/pgp-plugin": "npm:^0.0.7"