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:
Harry Hogg
2023-01-12 12:11:18 +00:00
committed by Vincenzo Scamporlino
parent f8540c1049
commit 277847e064
11 changed files with 153 additions and 178 deletions
+3
View File
@@ -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:
+14 -10
View File
@@ -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;
}
+20 -86
View File
@@ -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);
/**
+4 -1
View File
@@ -26,4 +26,7 @@ export {
isTemplateEntityV1beta3,
} from './TemplateEntityV1beta3';
export * from './permissions';
export type { TemplateEntityV1beta3 } from './TemplateEntityV1beta3';
export type {
TemplateEntityV1beta3,
TemplateEntityStepV1beta3,
} from './TemplateEntityV1beta3';
+34 -6
View File
@@ -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
View File
@@ -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