authz: update createPermissionIntegration to work with new PermissionCriteria type

Signed-off-by: Mike Lewis <mtlewis@users.noreply.github.com>
This commit is contained in:
Mike Lewis
2021-11-11 14:13:26 +00:00
parent d0a99e3219
commit 22f5de9840
4 changed files with 304 additions and 61 deletions
@@ -14,14 +14,15 @@
* limitations under the License.
*/
import express, { Response, Router } from 'express';
import {
AuthorizeResult,
PermissionCondition,
PermissionCriteria,
} from '@backstage/permission-common';
import express, { Response, Router } from 'express';
import { PermissionRule } from '../types';
import { conditionFor } from './conditionFor';
import { applyConditions, mapConditions } from './util';
/**
* A request to load the referenced resource and apply conditions, to finalize a conditional
@@ -49,13 +50,6 @@ type QueryType<TRules> = TRules extends Record<
? TQuery
: never;
// TODO(permission-node): this is no longer correct
function isPermissionCriteria(
criteria: unknown,
): criteria is PermissionCriteria<unknown> {
return Object.prototype.hasOwnProperty.call(criteria, 'anyOf');
}
export const createPermissionIntegration = <
TResource,
TRules extends { [key: string]: PermissionRule<TResource, any> },
@@ -76,15 +70,13 @@ export const createPermissionIntegration = <
}): {
createPermissionIntegrationRouter: (...params: TGetResourceParams) => Router;
toQuery: (
conditions: PermissionCriteria<PermissionCondition<QueryType<TRules>>>,
conditions: PermissionCriteria<PermissionCondition>,
) => PermissionCriteria<QueryType<TRules>>;
conditions: Conditions<TRules>;
createConditions: (
conditions: PermissionCriteria<PermissionCondition<QueryType<TRules>>>,
) => {
createConditions: (conditions: PermissionCriteria<PermissionCondition>) => {
pluginId: string;
resourceType: string;
conditions: PermissionCriteria<PermissionCondition<QueryType<TRules>>>;
conditions: PermissionCriteria<PermissionCondition>;
};
registerPermissionRule: (
rule: PermissionRule<TResource, QueryType<TRules>>,
@@ -92,9 +84,7 @@ export const createPermissionIntegration = <
} => {
const rulesMap = new Map(Object.values(rules).map(rule => [rule.name, rule]));
const getRule = (
name: string,
): PermissionRule<TResource, QueryType<TRules>> => {
const getRule = (name: string) => {
const rule = rulesMap.get(name);
if (!rule) {
@@ -110,10 +100,9 @@ export const createPermissionIntegration = <
) => {
const router = Router();
router.use('/permissions/', express.json());
router.post(
'/permissions/apply-conditions',
express.json(),
async (
req,
res: Response<{
@@ -136,22 +125,10 @@ export const createPermissionIntegration = <
return res.status(400).end();
}
const resolveCriteria = (
criteria: PermissionCriteria<
PermissionCondition<QueryType<TRules>>
>,
): boolean => {
return criteria.anyOf.some(({ allOf }) =>
allOf.every(child =>
isPermissionCriteria(child)
? resolveCriteria(child)
: getRule(child.rule).apply(resource, ...child.params),
),
);
};
return res.status(200).json({
result: resolveCriteria(body.conditions)
result: applyConditions(body.conditions, ({ rule, params }) =>
getRule(rule).apply(resource, ...params),
)
? AuthorizeResult.ALLOW
: AuthorizeResult.DENY,
});
@@ -161,27 +138,10 @@ export const createPermissionIntegration = <
return router;
},
toQuery: (
conditions: PermissionCriteria<PermissionCondition<QueryType<TRules>>>,
conditions: PermissionCriteria<PermissionCondition>,
): PermissionCriteria<QueryType<TRules>> => {
const mapCriteria = (
criteria: PermissionCriteria<PermissionCondition<QueryType<TRules>>>,
mapFn: (
condition: PermissionCondition<QueryType<TRules>>,
) => QueryType<TRules> | PermissionCriteria<QueryType<TRules>>,
): PermissionCriteria<QueryType<TRules>> => {
return {
anyOf: criteria.anyOf.map(({ allOf }) => ({
allOf: allOf.map(child =>
isPermissionCriteria(child)
? mapCriteria(child, mapFn)
: mapFn(child),
),
})),
};
};
return mapCriteria(conditions, condition =>
getRule(condition.rule).toQuery(...condition.params),
return mapConditions(conditions, ({ rule, params }) =>
getRule(rule).toQuery(...params),
);
},
conditions: Object.entries(rules).reduce(
@@ -192,7 +152,7 @@ export const createPermissionIntegration = <
{} as Conditions<TRules>,
),
createConditions: (
conditions: PermissionCriteria<PermissionCondition<QueryType<TRules>>>,
conditions: PermissionCriteria<PermissionCondition>,
) => ({
pluginId,
resourceType,
@@ -0,0 +1,217 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
PermissionCondition,
PermissionCriteria,
} from '@backstage/permission-common';
import { applyConditions, mapConditions } from './util';
describe('integration utils', () => {
const testCases: {
criteria: PermissionCriteria<PermissionCondition>;
expectedResult: {
applyConditions: boolean;
mapConditions: PermissionCriteria<{ query: string; params: unknown[] }>;
};
}[] = [
{
criteria: { rule: 'test-rule-1', params: [] },
expectedResult: {
applyConditions: true,
mapConditions: { query: 'test-rule-1', params: [] },
},
},
{
criteria: { rule: 'test-rule-2', params: [] },
expectedResult: {
applyConditions: false,
mapConditions: { query: 'test-rule-2', params: [] },
},
},
{
criteria: {
anyOf: [
{ rule: 'test-rule-1', params: ['a', 'b'] },
{ rule: 'test-rule-2', params: ['c', 'd'] },
],
},
expectedResult: {
applyConditions: true,
mapConditions: {
anyOf: [
{ query: 'test-rule-1', params: ['a', 'b'] },
{ query: 'test-rule-2', params: ['c', 'd'] },
],
},
},
},
{
criteria: {
allOf: [
{ rule: 'test-rule-1', params: ['e', 'f'] },
{ rule: 'test-rule-2', params: ['g', 'h'] },
],
},
expectedResult: {
applyConditions: false,
mapConditions: {
allOf: [
{ query: 'test-rule-1', params: ['e', 'f'] },
{ query: 'test-rule-2', params: ['g', 'h'] },
],
},
},
},
{
criteria: {
not: { rule: 'test-rule-2', params: ['i'] },
},
expectedResult: {
applyConditions: true,
mapConditions: {
not: { query: 'test-rule-2', params: ['i'] },
},
},
},
{
criteria: {
allOf: [
{
anyOf: [
{ rule: 'test-rule-1', params: ['j'] },
{ rule: 'test-rule-2', params: ['k'] },
],
},
{
not: {
allOf: [
{ rule: 'test-rule-1', params: ['l'] },
{ rule: 'test-rule-2', params: ['m'] },
],
},
},
],
},
expectedResult: {
applyConditions: true,
mapConditions: {
allOf: [
{
anyOf: [
{ query: 'test-rule-1', params: ['j'] },
{ query: 'test-rule-2', params: ['k'] },
],
},
{
not: {
allOf: [
{ query: 'test-rule-1', params: ['l'] },
{ query: 'test-rule-2', params: ['m'] },
],
},
},
],
},
},
},
{
criteria: {
allOf: [
{
anyOf: [
{ rule: 'test-rule-1', params: ['j'] },
{ rule: 'test-rule-2', params: ['k'] },
],
},
{
not: {
allOf: [
{ rule: 'test-rule-1', params: ['l'] },
{ not: { rule: 'test-rule-2', params: ['m'] } },
],
},
},
],
},
expectedResult: {
applyConditions: false,
mapConditions: {
allOf: [
{
anyOf: [
{ query: 'test-rule-1', params: ['j'] },
{ query: 'test-rule-2', params: ['k'] },
],
},
{
not: {
allOf: [
{ query: 'test-rule-1', params: ['l'] },
{ not: { query: 'test-rule-2', params: ['m'] } },
],
},
},
],
},
},
},
];
describe('applyConditions', () => {
const applyFn = jest.fn(condition => condition.rule === 'test-rule-1');
it('invokes applyFn with rules to determine result', () => {
applyConditions({ rule: 'test-rule-1', params: ['foo', 'bar'] }, applyFn);
expect(applyFn).toHaveBeenCalledWith({
rule: 'test-rule-1',
params: ['foo', 'bar'],
});
});
it.each(testCases)(
'works with criteria %#',
({ criteria, expectedResult }) => {
expect(applyConditions(criteria, applyFn)).toEqual(
expectedResult.applyConditions,
);
},
);
});
describe('mapConditions', () => {
const mapFn = jest.fn(({ rule, params }) => ({ query: rule, params }));
it('invokes mapFn to transform conditions', () => {
mapConditions({ rule: 'test-rule-1', params: ['foo', 'bar'] }, mapFn);
expect(mapFn).toHaveBeenCalledWith({
rule: 'test-rule-1',
params: ['foo', 'bar'],
});
});
it.each(testCases)(
'works with criteria %#',
({ criteria, expectedResult }) => {
expect(mapConditions(criteria, mapFn)).toEqual(
expectedResult.mapConditions,
);
},
);
});
});
@@ -0,0 +1,68 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { PermissionCriteria } from '@backstage/permission-common';
const isAndCriteria = (
filter: PermissionCriteria<unknown>,
): filter is { allOf: PermissionCriteria<unknown>[] } =>
Object.prototype.hasOwnProperty.call(filter, 'allOf');
const isOrCriteria = (
filter: PermissionCriteria<unknown>,
): filter is { anyOf: PermissionCriteria<unknown>[] } =>
Object.prototype.hasOwnProperty.call(filter, 'anyOf');
const isNotCriteria = (
filter: PermissionCriteria<unknown>,
): filter is { not: PermissionCriteria<unknown> } =>
Object.prototype.hasOwnProperty.call(filter, 'not');
export const mapConditions = <TQueryIn, TQueryOut>(
criteria: PermissionCriteria<TQueryIn>,
mapFn: (condition: TQueryIn) => TQueryOut,
): PermissionCriteria<TQueryOut> => {
if (isAndCriteria(criteria)) {
return {
allOf: criteria.allOf.map(child => mapConditions(child, mapFn)),
};
} else if (isOrCriteria(criteria)) {
return {
anyOf: criteria.anyOf.map(child => mapConditions(child, mapFn)),
};
} else if (isNotCriteria(criteria)) {
return {
not: mapConditions(criteria.not, mapFn),
};
}
return mapFn(criteria);
};
export const applyConditions = <TQuery>(
criteria: PermissionCriteria<TQuery>,
applyFn: (condition: TQuery) => boolean,
): boolean => {
if (isAndCriteria(criteria)) {
return criteria.allOf.every(child => applyConditions(child, applyFn));
} else if (isOrCriteria(criteria)) {
return criteria.anyOf.some(child => applyConditions(child, applyFn));
} else if (isNotCriteria(criteria)) {
return !applyConditions(criteria.not, applyFn);
}
return applyFn(criteria);
};