feat(scaffolder): added permissions to backend endpoints

Signed-off-by: Frank Kong <frkong@redhat.com>
This commit is contained in:
Frank Kong
2024-04-22 17:15:22 -04:00
parent 9ee832c5a9
commit a7b500bd08
2 changed files with 184 additions and 14 deletions
+167 -10
View File
@@ -30,7 +30,12 @@ import {
UserEntity,
} from '@backstage/catalog-model';
import { Config, readDurationFromConfig } from '@backstage/config';
import { InputError, NotFoundError, stringifyError } from '@backstage/errors';
import {
InputError,
NotAllowedError,
NotFoundError,
stringifyError,
} from '@backstage/errors';
import { ScmIntegrations } from '@backstage/integration';
import { HumanDuration, JsonObject, JsonValue } from '@backstage/types';
import {
@@ -43,10 +48,16 @@ import {
import {
RESOURCE_TYPE_SCAFFOLDER_ACTION,
RESOURCE_TYPE_SCAFFOLDER_TEMPLATE,
RESOURCE_TYPE_SCAFFOLDER_TASK,
scaffolderActionPermissions,
scaffolderTemplatePermissions,
taskCancelPermission,
taskCreatePermission,
taskReadPermission,
templateParameterReadPermission,
templateStepReadPermission,
scaffolderTaskPermissions,
actionReadPermission,
} from '@backstage/plugin-scaffolder-common/alpha';
import express from 'express';
import Router from 'express-promise-router';
@@ -68,7 +79,10 @@ import { createDryRunner } from '../scaffolder/dryrun';
import { StorageTaskBroker } from '../scaffolder/tasks/StorageTaskBroker';
import { findTemplate, getEntityBaseUrl, getWorkingDirectory } from './helpers';
import { TemplateAction } from '@backstage/plugin-scaffolder-node';
import { PermissionRuleParams } from '@backstage/plugin-permission-common';
import {
AuthorizeResult,
PermissionRuleParams,
} from '@backstage/plugin-permission-common';
import {
createConditionAuthorizer,
createPermissionIntegrationRouter,
@@ -90,6 +104,11 @@ import {
} from '@backstage/plugin-auth-node';
import { InternalTaskSecrets } from '../scaffolder/tasks/types';
type ScaffolderPermissionRuleInput =
| TemplatePermissionRuleInput
| ActionPermissionRuleInput
| TaskPermissionRuleInput;
/**
*
* @public
@@ -103,7 +122,7 @@ export type TemplatePermissionRuleInput<
TParams
>;
function isTemplatePermissionRuleInput(
permissionRule: TemplatePermissionRuleInput | ActionPermissionRuleInput,
permissionRule: ScaffolderPermissionRuleInput,
): permissionRule is TemplatePermissionRuleInput {
return permissionRule.resourceType === RESOURCE_TYPE_SCAFFOLDER_TEMPLATE;
}
@@ -121,11 +140,28 @@ export type ActionPermissionRuleInput<
TParams
>;
function isActionPermissionRuleInput(
permissionRule: TemplatePermissionRuleInput | ActionPermissionRuleInput,
permissionRule: ScaffolderPermissionRuleInput,
): permissionRule is ActionPermissionRuleInput {
return permissionRule.resourceType === RESOURCE_TYPE_SCAFFOLDER_ACTION;
}
/**
*
* @public
*/
export type TaskPermissionRuleInput<
TParams extends PermissionRuleParams = PermissionRuleParams,
> = PermissionRule<
TemplateEntityStepV1beta3 | TemplateParametersV1beta3,
{},
typeof RESOURCE_TYPE_SCAFFOLDER_TASK,
TParams
>;
function isTaskPermissionRuleInput(
permissionRule: ScaffolderPermissionRuleInput,
): permissionRule is TaskPermissionRuleInput {
return permissionRule.resourceType === RESOURCE_TYPE_SCAFFOLDER_TASK;
}
/**
* RouterOptions
*
@@ -154,9 +190,7 @@ export interface RouterOptions {
additionalTemplateFilters?: Record<string, TemplateFilter>;
additionalTemplateGlobals?: Record<string, TemplateGlobal>;
permissions?: PermissionsService;
permissionRules?: Array<
TemplatePermissionRuleInput | ActionPermissionRuleInput
>;
permissionRules?: Array<ScaffolderPermissionRuleInput>;
auth?: AuthService;
httpAuth?: HttpAuthService;
identity?: IdentityApi;
@@ -384,12 +418,14 @@ export async function createRouter(
const actionRules: ActionPermissionRuleInput[] = Object.values(
scaffolderActionRules,
);
const taskRules: TaskPermissionRuleInput[] = [];
if (permissionRules) {
templateRules.push(
...permissionRules.filter(isTemplatePermissionRuleInput),
);
actionRules.push(...permissionRules.filter(isActionPermissionRuleInput));
taskRules.push(...permissionRules.filter(isTaskPermissionRuleInput));
}
const isAuthorized = createConditionAuthorizer(Object.values(templateRules));
@@ -406,6 +442,11 @@ export async function createRouter(
permissions: scaffolderActionPermissions,
rules: actionRules,
},
{
resourceType: RESOURCE_TYPE_SCAFFOLDER_TASK,
permissions: scaffolderTaskPermissions,
rules: taskRules,
},
],
});
@@ -445,7 +486,21 @@ export async function createRouter(
});
},
)
.get('/v2/actions', async (_req, res) => {
.get('/v2/actions', async (req, res) => {
const credentials = await httpAuth.credentials(req);
if (permissions) {
const authorizationResponse = (
await permissions?.authorizeConditional(
[{ permission: actionReadPermission }],
{ credentials: credentials },
)
)[0];
if (authorizationResponse.result === AuthorizeResult.DENY) {
throw new NotAllowedError();
}
}
const actionsList = actionRegistry.list().map(action => {
return {
id: action.id,
@@ -463,6 +518,19 @@ export async function createRouter(
});
const credentials = await httpAuth.credentials(req);
if (permissions) {
const authorizationResponse = (
await permissions?.authorizeConditional(
[{ permission: taskCreatePermission }],
{ credentials: credentials },
)
)[0];
if (authorizationResponse.result === AuthorizeResult.DENY) {
throw new NotAllowedError();
}
}
const { token } = await auth.getPluginRequestToken({
onBehalfOf: credentials,
targetPluginId: 'catalog',
@@ -539,6 +607,21 @@ export async function createRouter(
res.status(201).json({ id: result.taskId });
})
.get('/v2/tasks', async (req, res) => {
const credentials = await httpAuth.credentials(req);
if (permissions) {
const authorizationResponse = (
await permissions?.authorizeConditional(
[{ permission: taskReadPermission }],
{ credentials: credentials },
)
)[0];
if (authorizationResponse.result === AuthorizeResult.DENY) {
throw new NotAllowedError();
}
}
const [userEntityRef] = [req.query.createdBy].flat();
if (
@@ -561,6 +644,21 @@ export async function createRouter(
res.status(200).json(tasks);
})
.get('/v2/tasks/:taskId', async (req, res) => {
const credentials = await httpAuth.credentials(req);
if (permissions) {
const authorizationResponse = (
await permissions?.authorizeConditional(
[{ permission: taskReadPermission }],
{ credentials: credentials },
)
)[0];
if (authorizationResponse.result === AuthorizeResult.DENY) {
throw new NotAllowedError();
}
}
const { taskId } = req.params;
const task = await taskBroker.get(taskId);
if (!task) {
@@ -571,11 +669,43 @@ export async function createRouter(
res.status(200).json(task);
})
.post('/v2/tasks/:taskId/cancel', async (req, res) => {
const credentials = await httpAuth.credentials(req);
if (permissions) {
const authorizationResponses = await permissions?.authorizeConditional(
[
{ permission: taskCancelPermission },
{ permission: taskReadPermission },
],
{ credentials: credentials },
);
// Requires both read and cancel permissions
for (const response of authorizationResponses) {
if (response.result === AuthorizeResult.DENY) {
throw new NotAllowedError();
}
}
}
const { taskId } = req.params;
await taskBroker.cancel?.(taskId);
res.status(200).json({ status: 'cancelled' });
})
.get('/v2/tasks/:taskId/eventstream', async (req, res) => {
const credentials = await httpAuth.credentials(req);
if (permissions) {
const authorizationResponse = (
await permissions?.authorizeConditional(
[{ permission: taskReadPermission }],
{ credentials: credentials },
)
)[0];
if (authorizationResponse.result === AuthorizeResult.DENY) {
throw new NotAllowedError();
}
}
const { taskId } = req.params;
const after =
req.query.after !== undefined ? Number(req.query.after) : undefined;
@@ -624,6 +754,20 @@ export async function createRouter(
});
})
.get('/v2/tasks/:taskId/events', async (req, res) => {
const credentials = await httpAuth.credentials(req);
if (permissions) {
const authorizationResponse = (
await permissions?.authorizeConditional(
[{ permission: taskReadPermission }],
{ credentials: credentials },
)
)[0];
if (authorizationResponse.result === AuthorizeResult.DENY) {
throw new NotAllowedError();
}
}
const { taskId } = req.params;
const after = Number(req.query.after) || undefined;
@@ -654,6 +798,21 @@ export async function createRouter(
});
})
.post('/v2/dry-run', async (req, res) => {
const credentials = await httpAuth.credentials(req);
if (permissions) {
const authorizationResponse = (
await permissions?.authorizeConditional(
[{ permission: taskCreatePermission }],
{ credentials: credentials },
)
)[0];
if (authorizationResponse.result === AuthorizeResult.DENY) {
throw new NotAllowedError();
}
}
const bodySchema = z.object({
template: z.unknown(),
values: z.record(z.unknown()),
@@ -671,8 +830,6 @@ export async function createRouter(
throw new InputError('Input template is not a template');
}
const credentials = await httpAuth.credentials(req);
const { token } = await auth.getPluginRequestToken({
onBehalfOf: credentials,
targetPluginId: 'catalog',
+17 -4
View File
@@ -49,6 +49,18 @@ export const actionExecutePermission = createPermission({
resourceType: RESOURCE_TYPE_SCAFFOLDER_ACTION,
});
/**
* This permission is used to authorize actions that involve access the action registry
*
* @alpha
*/
export const actionReadPermission = createPermission({
name: 'scaffolder.action.read',
attributes: {
action: 'read',
},
resourceType: RESOURCE_TYPE_SCAFFOLDER_ACTION,
});
/**
* This permission is used to authorize actions that involve reading
* one or more parameters from a template.
@@ -123,9 +135,7 @@ export const taskCreatePermission = createPermission({
*/
export const taskCancelPermission = createPermission({
name: 'scaffolder.task.cancel',
attributes: {
action: 'update',
},
attributes: {},
resourceType: RESOURCE_TYPE_SCAFFOLDER_TASK,
});
@@ -152,7 +162,10 @@ export const scaffolderTemplatePermissions = [
* List of the scaffolder permissions that are associated with scaffolder actions.
* @alpha
*/
export const scaffolderActionPermissions = [actionExecutePermission];
export const scaffolderActionPermissions = [
actionExecutePermission,
actionReadPermission,
];
/**
* List of the scaffolder permissions that are associated with scaffolder tasks.