feat: initial implementation of scaffolder granular permissions
Signed-off-by: Kashish Mittal <kmittal@redhat.com> Co-authored-by: Frank Kong <frkong@redhat.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<TaskBroker>['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<TaskBroker>['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', () => {
|
||||
|
||||
@@ -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<string, WorkspaceProvider>;
|
||||
permissions?: PermissionsService;
|
||||
permissionRules?: Array<
|
||||
TemplatePermissionRuleInput | ActionPermissionRuleInput
|
||||
>;
|
||||
auth: AuthService;
|
||||
httpAuth: HttpAuthService;
|
||||
permissionRules?: Array<ScaffolderPermissionRuleInput>;
|
||||
auth?: AuthService;
|
||||
httpAuth?: HttpAuthService;
|
||||
identity?: IdentityApi;
|
||||
discovery?: DiscoveryService;
|
||||
events?: EventsService;
|
||||
auditor?: AuditorService;
|
||||
autocompleteHandlers?: Record<string, AutocompleteHandler>;
|
||||
@@ -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<TaskFilters> =
|
||||
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;
|
||||
|
||||
@@ -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<TaskFilters>;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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<PermissionCriteria<TaskFilters> | 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;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
+1
-6
@@ -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<HTMLButtonElement>();
|
||||
|
||||
const { allowed: canReadTasks } = usePermission({
|
||||
permission: taskReadPermission,
|
||||
});
|
||||
|
||||
const { allowed: canManageTemplates } = usePermission({
|
||||
permission: templateManagementPermission,
|
||||
});
|
||||
@@ -164,7 +159,7 @@ export function ScaffolderPageContextMenu(
|
||||
/>
|
||||
</MenuItem>
|
||||
)}
|
||||
{onTasksClicked && canReadTasks && (
|
||||
{onTasksClicked && (
|
||||
<MenuItem onClick={onTasksClicked}>
|
||||
<ListItemIcon>
|
||||
<List fontSize="small" />
|
||||
|
||||
@@ -91,14 +91,22 @@ const ListTaskPageContent = (props: MyTaskPageProps) => {
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<>
|
||||
<ErrorPanel error={error} />
|
||||
<EmptyState
|
||||
missing="info"
|
||||
title={t('listTaskPage.content.emptyState.title')}
|
||||
description={t('listTaskPage.content.emptyState.description')}
|
||||
/>
|
||||
</>
|
||||
<CatalogFilterLayout>
|
||||
<CatalogFilterLayout.Filters>
|
||||
<OwnerListPicker
|
||||
filter={ownerFilter}
|
||||
onSelectOwner={id => setOwnerFilter(id)}
|
||||
/>
|
||||
</CatalogFilterLayout.Filters>
|
||||
<CatalogFilterLayout.Content>
|
||||
<ErrorPanel error={error} />
|
||||
<EmptyState
|
||||
missing="info"
|
||||
title={t('listTaskPage.content.emptyState.title')}
|
||||
description={t('listTaskPage.content.emptyState.description')}
|
||||
/>
|
||||
</CatalogFilterLayout.Content>
|
||||
</CatalogFilterLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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 = (
|
||||
<Route
|
||||
path={scaffolderTaskRouteRef.path}
|
||||
element={
|
||||
<RequirePermission permission={taskReadPermission}>
|
||||
<TaskPageComponent
|
||||
TemplateOutputsComponent={TemplateOutputsComponent}
|
||||
/>
|
||||
</RequirePermission>
|
||||
<TaskPageComponent
|
||||
TemplateOutputsComponent={TemplateOutputsComponent}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
@@ -230,11 +225,7 @@ export const InternalRouter = (
|
||||
/>
|
||||
<Route
|
||||
path={scaffolderListTaskRouteRef.path}
|
||||
element={
|
||||
<RequirePermission permission={taskReadPermission}>
|
||||
<ListTasksPage contextMenu={props.contextMenu} />
|
||||
</RequirePermission>
|
||||
}
|
||||
element={<ListTasksPage contextMenu={props.contextMenu} />}
|
||||
/>
|
||||
<Route
|
||||
path={editorRouteRef.path}
|
||||
|
||||
Reference in New Issue
Block a user