authz: update createPermissionIntegration to work with new PermissionCriteria type
Signed-off-by: Mike Lewis <mtlewis@users.noreply.github.com>
This commit is contained in:
@@ -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);
|
||||
};
|
||||
Reference in New Issue
Block a user