added files related to db queries, api-reports and changeset
Signed-off-by: Kashish Mittal <kmittal@redhat.com>
This commit is contained in:
@@ -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.
|
||||
@@ -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<PermissionCondition<'scaffolder-action'>>,
|
||||
) => ConditionalPolicyDecision;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const createScaffolderTaskConditionalDecision: (
|
||||
permission: ResourcePermission<'scaffolder-task'>,
|
||||
conditions: PermissionCriteria<PermissionCondition<'scaffolder-task'>>,
|
||||
) => 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<
|
||||
|
||||
@@ -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<TaskFilters>;
|
||||
}): Promise<{
|
||||
tasks: SerializedTask[];
|
||||
totalTasks?: number;
|
||||
|
||||
@@ -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<TaskFilters> = {
|
||||
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);
|
||||
|
||||
|
||||
@@ -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<TaskFilters>,
|
||||
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<TaskFilters>;
|
||||
}): Promise<{ tasks: SerializedTask[]; totalTasks?: number }> {
|
||||
const { createdBy, status, pagination, order, filters } = options ?? {};
|
||||
const { createdBy, status, pagination, order, filters, permissionFilters } =
|
||||
options ?? {};
|
||||
const queryBuilder = this.db<RawDbTaskRow & { count: number }>('tasks');
|
||||
|
||||
if (createdBy || filters?.createdBy) {
|
||||
const arr: string[] = flattenParams<string>(
|
||||
createdBy,
|
||||
filters?.createdBy,
|
||||
);
|
||||
queryBuilder.whereIn('created_by', [...new Set(arr)]);
|
||||
const createdByValues = flattenParams<string>(
|
||||
createdBy,
|
||||
filters?.createdBy,
|
||||
);
|
||||
|
||||
const combinedPermissionFilters:
|
||||
| PermissionCriteria<TaskFilters>
|
||||
| undefined =
|
||||
createdByValues.length > 0
|
||||
? {
|
||||
allOf: [
|
||||
{ property: 'createdBy', values: createdByValues },
|
||||
...(permissionFilters ? [permissionFilters] : []),
|
||||
],
|
||||
}
|
||||
: permissionFilters;
|
||||
|
||||
if (combinedPermissionFilters) {
|
||||
this.parseFilter(combinedPermissionFilters, queryBuilder, this.db);
|
||||
}
|
||||
|
||||
if (status || filters?.status) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1420,3 +1420,4 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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<string, WorkspaceProvider>;
|
||||
permissions?: PermissionsService;
|
||||
permissionRules?: Array<ScaffolderPermissionRuleInput>;
|
||||
auth?: AuthService;
|
||||
httpAuth?: HttpAuthService;
|
||||
identity?: IdentityApi;
|
||||
discovery?: DiscoveryService;
|
||||
auth: AuthService;
|
||||
httpAuth: HttpAuthService;
|
||||
events?: EventsService;
|
||||
auditor?: AuditorService;
|
||||
autocompleteHandlers?: Record<string, AutocompleteHandler>;
|
||||
|
||||
@@ -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'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<Schema extends z.ZodType<JsonPrimitive>>({
|
||||
});
|
||||
}
|
||||
|
||||
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 };
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<TaskFilters>;
|
||||
}): 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<string> | undefined;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type TaskFilters =
|
||||
| {
|
||||
anyOf: TaskFilter[];
|
||||
}
|
||||
| {
|
||||
allOf: TaskFilter[];
|
||||
}
|
||||
| {
|
||||
not: TaskFilter;
|
||||
}
|
||||
| TaskFilter;
|
||||
|
||||
// @public
|
||||
export type TaskSecrets = Record<string, string> & {
|
||||
backstageToken?: string;
|
||||
|
||||
@@ -18,6 +18,8 @@ export type {
|
||||
TaskSecrets,
|
||||
SerializedTask,
|
||||
SerializedTaskEvent,
|
||||
TaskFilter,
|
||||
TaskFilters,
|
||||
TaskBroker,
|
||||
TaskBrokerDispatchOptions,
|
||||
TaskBrokerDispatchResult,
|
||||
|
||||
@@ -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<string> | 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<TaskFilters>;
|
||||
}): Promise<{ tasks: SerializedTask[]; totalTasks?: number }>;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user