Reworked permission rules and filtering to follow similar pattern to catalog
Signed-off-by: Harry Hogg <hhogg@spotify.com> Co-authored-by: Vincenzo Scamporlino <vincenzos@spotify.com>
This commit is contained in:
committed by
Vincenzo Scamporlino
parent
f8540c1049
commit
277847e064
@@ -223,6 +223,7 @@ catalog:
|
||||
- System
|
||||
- Domain
|
||||
- Location
|
||||
- Template
|
||||
|
||||
processors:
|
||||
ldapOrg:
|
||||
@@ -279,6 +280,8 @@ catalog:
|
||||
# Backstage end-to-end tests of TechDocs
|
||||
- type: file
|
||||
target: ../../cypress/e2e-fixture.catalog.info.yaml
|
||||
- type: file
|
||||
target: ../../template.yaml
|
||||
scaffolder:
|
||||
# Use to customize default commit author info used when new components are created
|
||||
# defaultAuthor:
|
||||
|
||||
@@ -18,7 +18,7 @@ import { BackstageIdentityResponse } from '@backstage/plugin-auth-node';
|
||||
import { createRouter } from '@backstage/plugin-permission-backend';
|
||||
import {
|
||||
AuthorizeResult,
|
||||
isPermission,
|
||||
isResourcePermission,
|
||||
PolicyDecision,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
import {
|
||||
@@ -27,10 +27,10 @@ import {
|
||||
} from '@backstage/plugin-permission-node';
|
||||
|
||||
import {
|
||||
createScaffolderConditionalDecision,
|
||||
scaffolderConditions,
|
||||
createScaffolderStepConditionalDecision,
|
||||
scaffolderStepConditions,
|
||||
} from '@backstage/plugin-scaffolder-backend';
|
||||
import { templateSchemaExecutePermission } from '@backstage/plugin-scaffolder-common';
|
||||
import { RESOURCE_TYPE_SCAFFOLDER_STEP } from '@backstage/plugin-scaffolder-common';
|
||||
import { Router } from 'express';
|
||||
import { PluginEnvironment } from '../types';
|
||||
|
||||
@@ -39,12 +39,16 @@ class ExamplePermissionPolicy implements PermissionPolicy {
|
||||
request: PolicyQuery,
|
||||
_user?: BackstageIdentityResponse,
|
||||
): Promise<PolicyDecision> {
|
||||
if (isPermission(request.permission, templateSchemaExecutePermission)) {
|
||||
return createScaffolderConditionalDecision(request.permission, [
|
||||
scaffolderConditions.allowCapabilities({
|
||||
capabilities: ['example'],
|
||||
}),
|
||||
]);
|
||||
/**
|
||||
* This is an example of how to use the scaffolder step conditions.
|
||||
*/
|
||||
if (
|
||||
isResourcePermission(request.permission, RESOURCE_TYPE_SCAFFOLDER_STEP)
|
||||
) {
|
||||
return createScaffolderStepConditionalDecision(
|
||||
request.permission,
|
||||
scaffolderStepConditions.hasTag({ tag: 'example' }),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -126,7 +126,7 @@ export type MetadataResponse = {
|
||||
rules: MetadataResponseSerializedRule[];
|
||||
};
|
||||
|
||||
const applyConditions = <TResourceType extends string, TResource>(
|
||||
export const applyConditions = <TResourceType extends string, TResource>(
|
||||
criteria: PermissionCriteria<PermissionCondition<TResourceType>>,
|
||||
resource: TResource | undefined,
|
||||
getRule: (name: string) => PermissionRule<TResource, unknown, TResourceType>,
|
||||
|
||||
@@ -19,4 +19,9 @@ export * from './createConditionExports';
|
||||
export * from './createConditionTransformer';
|
||||
export * from './createPermissionIntegrationRouter';
|
||||
export * from './createPermissionRule';
|
||||
export { isAndCriteria, isOrCriteria, isNotCriteria } from './util';
|
||||
export {
|
||||
createGetRule,
|
||||
isAndCriteria,
|
||||
isOrCriteria,
|
||||
isNotCriteria,
|
||||
} from './util';
|
||||
|
||||
@@ -14,34 +14,42 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { RESOURCE_TYPE_SCAFFOLDER_TEMPLATE } from '@backstage/plugin-scaffolder-common';
|
||||
import { RESOURCE_TYPE_SCAFFOLDER_STEP } from '@backstage/plugin-scaffolder-common';
|
||||
import { createConditionExports } from '@backstage/plugin-permission-node';
|
||||
import { scaffolderRules } from './rules';
|
||||
import {
|
||||
AllOfCriteria,
|
||||
ConditionalPolicyDecision,
|
||||
PermissionCondition,
|
||||
PermissionRuleParams,
|
||||
ResourcePermission,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
import { scaffolderStepRules } from './rules';
|
||||
|
||||
const { conditions: scaffolderConditions, createConditionalDecision } =
|
||||
createConditionExports({
|
||||
pluginId: 'scaffolder',
|
||||
resourceType: RESOURCE_TYPE_SCAFFOLDER_TEMPLATE,
|
||||
rules: scaffolderRules,
|
||||
});
|
||||
// const {
|
||||
// conditions: scaffolderTemplateConditions,
|
||||
// createConditionalDecision: createScaffolderTemplateConditionalDecision,
|
||||
// } = createConditionExports({
|
||||
// pluginId: 'scaffolder',
|
||||
// resourceType: RESOURCE_TYPE_SCAFFOLDER_TEMPLATE,
|
||||
// rules: scaffolderRules,
|
||||
// });
|
||||
|
||||
export { scaffolderConditions };
|
||||
// const {
|
||||
// conditions: scaffolderPropertyConditions,
|
||||
// createConditionalDecision: createScaffolderPropertyConditionalDecision,
|
||||
// } = createConditionExports({
|
||||
// pluginId: 'scaffolder',
|
||||
// resourceType: RESOURCE_TYPE_SCAFFOLDER_TEMPLATE,
|
||||
// rules: scaffolderRules,
|
||||
// });
|
||||
|
||||
export function createScaffolderConditionalDecision(
|
||||
permission: ResourcePermission<typeof RESOURCE_TYPE_SCAFFOLDER_TEMPLATE>,
|
||||
conditions: AllOfCriteria<
|
||||
PermissionCondition<
|
||||
typeof RESOURCE_TYPE_SCAFFOLDER_TEMPLATE,
|
||||
PermissionRuleParams
|
||||
>
|
||||
>['allOf'],
|
||||
): ConditionalPolicyDecision {
|
||||
return createConditionalDecision(permission, { allOf: conditions });
|
||||
}
|
||||
const {
|
||||
conditions: scaffolderStepConditions,
|
||||
createConditionalDecision: createScaffolderStepConditionalDecision,
|
||||
} = createConditionExports({
|
||||
pluginId: 'scaffolder',
|
||||
resourceType: RESOURCE_TYPE_SCAFFOLDER_STEP,
|
||||
rules: scaffolderStepRules,
|
||||
});
|
||||
|
||||
export {
|
||||
// scaffolderTemplateConditions,
|
||||
// createScaffolderTemplateConditionalDecision,
|
||||
// scaffolderPropertyConditions,
|
||||
// createScaffolderPropertyConditionalDecision,
|
||||
scaffolderStepConditions,
|
||||
createScaffolderStepConditionalDecision,
|
||||
};
|
||||
|
||||
@@ -25,19 +25,14 @@ import {
|
||||
UserEntity,
|
||||
} from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import {
|
||||
InputError,
|
||||
NotAllowedError,
|
||||
NotFoundError,
|
||||
stringifyError,
|
||||
} from '@backstage/errors';
|
||||
import { InputError, NotFoundError, stringifyError } from '@backstage/errors';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { JsonObject, JsonValue } from '@backstage/types';
|
||||
import {
|
||||
TaskSpec,
|
||||
TemplateEntityV1beta3,
|
||||
templateEntityV1beta3Validator,
|
||||
templateSchemaExecutePermission,
|
||||
templateStepReadPermission,
|
||||
} from '@backstage/plugin-scaffolder-common';
|
||||
import express from 'express';
|
||||
import Router from 'express-promise-router';
|
||||
@@ -54,12 +49,7 @@ import {
|
||||
} from '../scaffolder';
|
||||
import { createDryRunner } from '../scaffolder/dryrun';
|
||||
import { StorageTaskBroker } from '../scaffolder/tasks/StorageTaskBroker';
|
||||
import {
|
||||
findTemplate,
|
||||
getEntityBaseUrl,
|
||||
getWorkingDirectory,
|
||||
TemplateTransform,
|
||||
} from './helpers';
|
||||
import { findTemplate, getEntityBaseUrl, getWorkingDirectory } from './helpers';
|
||||
import {
|
||||
IdentityApi,
|
||||
IdentityApiGetIdentityRequest,
|
||||
@@ -70,11 +60,10 @@ import {
|
||||
PermissionEvaluator,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
import {
|
||||
ConditionTransformer,
|
||||
createConditionTransformer,
|
||||
isAndCriteria,
|
||||
applyConditions,
|
||||
createGetRule,
|
||||
} from '@backstage/plugin-permission-node';
|
||||
import { scaffolderRules } from './rules';
|
||||
import { scaffolderStepRules } from './rules';
|
||||
|
||||
/**
|
||||
* RouterOptions
|
||||
@@ -273,8 +262,7 @@ export async function createRouter(
|
||||
additionalTemplateGlobals,
|
||||
});
|
||||
|
||||
const transformConditions: ConditionTransformer<TemplateTransform> =
|
||||
createConditionTransformer(Object.values(scaffolderRules));
|
||||
const getRule = createGetRule(Object.values(scaffolderStepRules));
|
||||
|
||||
router
|
||||
.get(
|
||||
@@ -576,11 +564,12 @@ export async function createRouter(
|
||||
entityRef: CompoundEntityRef,
|
||||
token: string | undefined,
|
||||
) {
|
||||
let template = await findTemplate({
|
||||
const template = await findTemplate({
|
||||
catalogApi: catalogClient,
|
||||
entityRef,
|
||||
token,
|
||||
});
|
||||
|
||||
if (!isSupportedTemplate(template)) {
|
||||
throw new InputError(
|
||||
`Unsupported apiVersion field in schema entity, ${
|
||||
@@ -588,32 +577,24 @@ export async function createRouter(
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
const authorizeDecision = (
|
||||
await permissionApi.authorizeConditional(
|
||||
[{ permission: templateSchemaExecutePermission }],
|
||||
{
|
||||
token,
|
||||
},
|
||||
[{ permission: templateStepReadPermission }],
|
||||
{ token },
|
||||
)
|
||||
)[0];
|
||||
|
||||
if (authorizeDecision.result === AuthorizeResult.DENY) {
|
||||
throw new NotAllowedError(
|
||||
`Not allowed to execute template ${entityRef.kind}:${entityRef.namespace}/${entityRef.name}`,
|
||||
template.spec.steps = [];
|
||||
} else if (authorizeDecision.result === AuthorizeResult.CONDITIONAL) {
|
||||
template.spec.steps = template.spec.steps.filter(step =>
|
||||
applyConditions(authorizeDecision.conditions, step, getRule),
|
||||
);
|
||||
}
|
||||
if (authorizeDecision.result === AuthorizeResult.CONDITIONAL) {
|
||||
const scaffolderFilter = transformConditions(
|
||||
authorizeDecision.conditions,
|
||||
);
|
||||
if (isAndCriteria(scaffolderFilter)) {
|
||||
template = scaffolderFilter.allOf.reduce(
|
||||
(acc, filter) => (filter as TemplateTransform)(acc),
|
||||
template,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
@@ -14,100 +14,34 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { EntitiesSearchFilter } from '@backstage/plugin-catalog-backend';
|
||||
import { makeCreatePermissionRule } from '@backstage/plugin-permission-node';
|
||||
import {
|
||||
RESOURCE_TYPE_SCAFFOLDER_TEMPLATE,
|
||||
TemplateEntityV1beta3,
|
||||
RESOURCE_TYPE_SCAFFOLDER_STEP,
|
||||
TemplateEntityStepV1beta3,
|
||||
} from '@backstage/plugin-scaffolder-common';
|
||||
import { z } from 'zod';
|
||||
|
||||
import get from 'lodash/get';
|
||||
import unset from 'lodash/unset';
|
||||
import { TemplateTransform } from './helpers';
|
||||
import {
|
||||
SecureTemplater,
|
||||
SecureTemplateRenderer,
|
||||
} from '../lib/templating/SecureTemplater';
|
||||
|
||||
export const createScaffolderPermissionRule = makeCreatePermissionRule<
|
||||
TemplateEntityV1beta3,
|
||||
TemplateTransform,
|
||||
typeof RESOURCE_TYPE_SCAFFOLDER_TEMPLATE
|
||||
export const createScaffolderStepPermissionRule = makeCreatePermissionRule<
|
||||
TemplateEntityStepV1beta3,
|
||||
EntitiesSearchFilter,
|
||||
typeof RESOURCE_TYPE_SCAFFOLDER_STEP
|
||||
>();
|
||||
|
||||
let render: SecureTemplateRenderer;
|
||||
|
||||
export const allowCapabilities = createScaffolderPermissionRule({
|
||||
name: 'ALLOW_CAPABILITIES',
|
||||
resourceType: RESOURCE_TYPE_SCAFFOLDER_TEMPLATE,
|
||||
description: 'Allow capabilities based on permissions',
|
||||
const hasTag = createScaffolderStepPermissionRule({
|
||||
name: 'HAS_TAG',
|
||||
resourceType: RESOURCE_TYPE_SCAFFOLDER_STEP,
|
||||
description: 'Match a scaffolder step with the given tag',
|
||||
paramsSchema: z.object({
|
||||
capabilities: z.array(z.string()),
|
||||
templateRef: z.string().optional(),
|
||||
tag: z.string(),
|
||||
}),
|
||||
apply: (resource, { tag }) => {
|
||||
return resource.metadata?.tags?.includes(tag) ?? false;
|
||||
},
|
||||
toQuery: ({ tag }) => ({
|
||||
key: 'metadata.tags',
|
||||
values: [tag],
|
||||
}),
|
||||
apply: () => true,
|
||||
toQuery:
|
||||
({ capabilities }) =>
|
||||
template => {
|
||||
const capabilitiesObj = capabilities.reduce((acc, c) => {
|
||||
acc[c] = true;
|
||||
return acc;
|
||||
}, {} as Record<string, boolean>);
|
||||
|
||||
const parametersPath = ['spec', 'parameters'];
|
||||
const capabilitiesKey = 'backstage:capabilities';
|
||||
|
||||
if (Array.isArray(template.spec.parameters)) {
|
||||
template.spec.parameters.forEach((parameter, index) => {
|
||||
const parameterPath = [...parametersPath, index.toString()];
|
||||
stripParams(parameterPath);
|
||||
if (parameter.properties) {
|
||||
Object.keys(parameter.properties).forEach(p => {
|
||||
if (stripParams([...parameterPath, 'properties', p])) {
|
||||
const requiredProperties = get(template, [
|
||||
...parameterPath,
|
||||
'required',
|
||||
]);
|
||||
if (Array.isArray(requiredProperties)) {
|
||||
requiredProperties.splice(requiredProperties.indexOf(p), 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
stripParams(parametersPath);
|
||||
}
|
||||
|
||||
template.spec.steps.forEach((_p, index) =>
|
||||
stripParams(['spec', 'steps', index.toString()]),
|
||||
);
|
||||
|
||||
function stripParams(path: string[]) {
|
||||
const jsonObject = get(template, path);
|
||||
|
||||
if (
|
||||
typeof jsonObject[capabilitiesKey] === 'string' &&
|
||||
render(jsonObject[capabilitiesKey], {
|
||||
capabilities: capabilitiesObj,
|
||||
}) !== 'true'
|
||||
) {
|
||||
const parent = get(template, [...path].splice(0, path.length - 1));
|
||||
if (Array.isArray(parent)) {
|
||||
parent.splice(parent.indexOf(jsonObject), 1);
|
||||
} else {
|
||||
unset(template, path);
|
||||
}
|
||||
|
||||
return path[path.length - 1];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return template;
|
||||
},
|
||||
});
|
||||
|
||||
SecureTemplater.loadRenderer().then(r => (render = r));
|
||||
|
||||
export const scaffolderRules = { allowCapabilities };
|
||||
export const scaffolderStepRules = { hasTag };
|
||||
|
||||
@@ -55,13 +55,7 @@ export interface TemplateEntityV1beta3 extends Entity {
|
||||
* A list of steps to be executed in sequence which are defined by the template. These steps are a list of the underlying
|
||||
* javascript action and some optional input parameters that may or may not have been collected from the end user.
|
||||
*/
|
||||
steps: Array<{
|
||||
id?: string;
|
||||
name?: string;
|
||||
action: string;
|
||||
input?: JsonObject;
|
||||
if?: string | boolean;
|
||||
}>;
|
||||
steps: Array<TemplateEntityStepV1beta3>;
|
||||
/**
|
||||
* The output is an object where template authors can pull out information from template actions and return them in a known standard way.
|
||||
*/
|
||||
@@ -73,6 +67,20 @@ export interface TemplateEntityV1beta3 extends Entity {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
export interface TemplateEntityStepV1beta3 extends JsonObject {
|
||||
id?: string;
|
||||
name?: string;
|
||||
action: string;
|
||||
input?: JsonObject;
|
||||
if?: string | boolean;
|
||||
metadata: {
|
||||
tags?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
const validator = entityKindSchemaValidator(schema);
|
||||
|
||||
/**
|
||||
|
||||
@@ -26,4 +26,7 @@ export {
|
||||
isTemplateEntityV1beta3,
|
||||
} from './TemplateEntityV1beta3';
|
||||
export * from './permissions';
|
||||
export type { TemplateEntityV1beta3 } from './TemplateEntityV1beta3';
|
||||
export type {
|
||||
TemplateEntityV1beta3,
|
||||
TemplateEntityStepV1beta3,
|
||||
} from './TemplateEntityV1beta3';
|
||||
|
||||
@@ -16,14 +16,42 @@
|
||||
|
||||
import { createPermission } from '@backstage/plugin-permission-common';
|
||||
|
||||
export const RESOURCE_TYPE_SCAFFOLDER_TEMPLATE = 'scaffolder-field';
|
||||
export const RESOURCE_TYPE_SCAFFOLDER_PARAMETER = 'scaffolder-parameter';
|
||||
export const RESOURCE_TYPE_SCAFFOLDER_PROPERTY = 'scaffolder-property';
|
||||
export const RESOURCE_TYPE_SCAFFOLDER_STEP = 'scaffolder-step';
|
||||
|
||||
export const templateSchemaExecutePermission = createPermission({
|
||||
name: 'scaffolder.template.schema.execute',
|
||||
export const templateParameterReadPermission = createPermission({
|
||||
name: 'scaffolder.template.parameter.read',
|
||||
attributes: {
|
||||
action: 'create',
|
||||
action: 'read',
|
||||
},
|
||||
resourceType: RESOURCE_TYPE_SCAFFOLDER_TEMPLATE,
|
||||
resourceType: RESOURCE_TYPE_SCAFFOLDER_PARAMETER,
|
||||
});
|
||||
|
||||
export const scaffolderPermissions = [templateSchemaExecutePermission];
|
||||
export const templatePropertyReadPermission = createPermission({
|
||||
name: 'scaffolder.template.property.read',
|
||||
attributes: {
|
||||
action: 'read',
|
||||
},
|
||||
resourceType: RESOURCE_TYPE_SCAFFOLDER_PROPERTY,
|
||||
});
|
||||
|
||||
export const templateStepReadPermission = createPermission({
|
||||
name: 'scaffolder.template.step.read',
|
||||
attributes: {
|
||||
action: 'read',
|
||||
},
|
||||
resourceType: RESOURCE_TYPE_SCAFFOLDER_STEP,
|
||||
});
|
||||
|
||||
export const scaffolderParameterPermissions = [templateParameterReadPermission];
|
||||
export const scaffolderPropertyPermissions = [templatePropertyReadPermission];
|
||||
export const scaffolderStepPermissions = [templateStepReadPermission];
|
||||
|
||||
/**
|
||||
* TODOs:
|
||||
* 1. Implement for Parameters & Properties
|
||||
* 2. What metadata should be included in the template?
|
||||
* 3. Write tests
|
||||
* 4. Write documentation
|
||||
*/
|
||||
|
||||
+3
-2
@@ -14,7 +14,6 @@ spec:
|
||||
- owner
|
||||
properties:
|
||||
component_id:
|
||||
backstage:capabilities: ${{ capabilities.cap1 }}
|
||||
title: Name
|
||||
type: string
|
||||
description: Unique name of the component
|
||||
@@ -44,11 +43,13 @@ spec:
|
||||
- github.com
|
||||
steps:
|
||||
- id: one
|
||||
backstage:capabilities: ${{ capabilities.cap1 }}
|
||||
name: First log
|
||||
action: debug:log
|
||||
input:
|
||||
message: hello
|
||||
metadata:
|
||||
tags:
|
||||
- example
|
||||
- id: two
|
||||
name: Second log
|
||||
action: debug:log
|
||||
|
||||
Reference in New Issue
Block a user