Require at least one item in allOf/anyOf criteria

Signed-off-by: Joe Porpeglia <josephp@spotify.com>
This commit is contained in:
Joe Porpeglia
2022-02-03 18:58:24 -05:00
committed by Joe Porpeglia
parent de103e5fc1
commit e66bb84798
8 changed files with 66 additions and 18 deletions
@@ -20,7 +20,11 @@ import express, { Router, RequestHandler } from 'express';
import { RestContext, rest } from 'msw';
import { setupServer, SetupServerApi } from 'msw/node';
import { PluginEndpointDiscovery } from '@backstage/backend-common';
import { AuthorizeResult } from '@backstage/plugin-permission-common';
import {
AuthorizeResult,
PermissionCondition,
PermissionCriteria,
} from '@backstage/plugin-permission-common';
import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node';
import { PermissionIntegrationClient } from './PermissionIntegrationClient';
@@ -28,7 +32,7 @@ describe('PermissionIntegrationClient', () => {
describe('applyConditions', () => {
let server: SetupServerApi;
const mockConditions = {
const mockConditions: PermissionCriteria<PermissionCondition> = {
not: {
allOf: [
{ rule: 'RULE_1', params: [] },
@@ -43,8 +43,8 @@ const permissionCriteriaSchema: z.ZodSchema<
rule: z.string(),
params: z.array(z.unknown()),
})
.or(z.object({ anyOf: z.array(permissionCriteriaSchema) }))
.or(z.object({ allOf: z.array(permissionCriteriaSchema) }))
.or(z.object({ anyOf: z.array(permissionCriteriaSchema).nonempty() }))
.or(z.object({ allOf: z.array(permissionCriteriaSchema).nonempty() }))
.or(z.object({ not: permissionCriteriaSchema })),
);
+22 -3
View File
@@ -72,14 +72,33 @@ export type PermissionCondition<TParams extends unknown[] = unknown[]> = {
params: TParams;
};
/**
* Utility type to represent an array with 1 or more elements.
*
* @private
*/
export type NonEmptyArray<T> = [T, ...T[]];
export type AllOfCriteria<TQuery> = {
allOf: NonEmptyArray<PermissionCriteria<TQuery>>;
};
export type AnyOfCriteria<TQuery> = {
anyOf: NonEmptyArray<PermissionCriteria<TQuery>>;
};
export type NotCriteria<TQuery> = {
not: PermissionCriteria<TQuery>;
};
/**
* Composes several {@link PermissionCondition}s as criteria with a nested AND/OR structure.
* @public
*/
export type PermissionCriteria<TQuery> =
| { allOf: PermissionCriteria<TQuery>[] }
| { anyOf: PermissionCriteria<TQuery>[] }
| { not: PermissionCriteria<TQuery> }
| AllOfCriteria<TQuery>
| AnyOfCriteria<TQuery>
| NotCriteria<TQuery>
| TQuery;
/**
@@ -23,6 +23,9 @@ export type {
Identified,
PermissionCondition,
PermissionCriteria,
AllOfCriteria,
AnyOfCriteria,
NotCriteria,
} from './api';
export type { DiscoveryApi } from './discovery';
export type {
@@ -25,17 +25,31 @@ import {
isOrCriteria,
} from './util';
const nonEmpty = <T>(list: T[]): [T, ...T[]] => {
if (list.length === 0) {
throw new Error(
'Invalid conditions provided. Expected at least one item for `allOf`/`anyOf` criteria but received none.',
);
}
return list as [T, ...T[]];
};
const mapConditions = <TQuery>(
criteria: PermissionCriteria<PermissionCondition>,
getRule: (name: string) => PermissionRule<unknown, TQuery>,
): PermissionCriteria<TQuery> => {
if (isAndCriteria(criteria)) {
return {
allOf: criteria.allOf.map(child => mapConditions(child, getRule)),
allOf: nonEmpty(
criteria.allOf.map(child => mapConditions(child, getRule)),
),
};
} else if (isOrCriteria(criteria)) {
return {
anyOf: criteria.anyOf.map(child => mapConditions(child, getRule)),
anyOf: nonEmpty(
criteria.anyOf.map(child => mapConditions(child, getRule)),
),
};
} else if (isNotCriteria(criteria)) {
return {
@@ -247,7 +247,8 @@ describe('createPermissionIntegrationRouter', () => {
resourceRef: 'default:test/resource-1',
resourceType: 'test-incorrect-resource-1',
conditions: {
anyOf: [],
rule: 'test-rule-1',
params: [{}],
},
},
{
@@ -255,7 +256,8 @@ describe('createPermissionIntegrationRouter', () => {
resourceRef: 'default:test/resource-2',
resourceType: 'test-resource',
conditions: {
anyOf: [],
rule: 'test-rule-1',
params: [{}],
},
},
{
@@ -263,7 +265,8 @@ describe('createPermissionIntegrationRouter', () => {
resourceRef: 'default:test/resource-3',
resourceType: 'test-incorrect-resource-2',
conditions: {
anyOf: [],
rule: 'test-rule-1',
params: [{}],
},
},
],
@@ -38,8 +38,8 @@ const permissionCriteriaSchema: z.ZodSchema<
PermissionCriteria<PermissionCondition>
> = z.lazy(() =>
z.union([
z.object({ anyOf: z.array(permissionCriteriaSchema) }),
z.object({ allOf: z.array(permissionCriteriaSchema) }),
z.object({ anyOf: z.array(permissionCriteriaSchema).nonempty() }),
z.object({ allOf: z.array(permissionCriteriaSchema).nonempty() }),
z.object({ not: permissionCriteriaSchema }),
z.object({
rule: z.string(),
@@ -14,22 +14,27 @@
* limitations under the License.
*/
import { PermissionCriteria } from '@backstage/plugin-permission-common';
import {
AllOfCriteria,
AnyOfCriteria,
NotCriteria,
PermissionCriteria,
} from '@backstage/plugin-permission-common';
import { PermissionRule } from '../types';
export const isAndCriteria = (
filter: PermissionCriteria<unknown>,
): filter is { allOf: PermissionCriteria<unknown>[] } =>
): filter is AllOfCriteria<unknown> =>
Object.prototype.hasOwnProperty.call(filter, 'allOf');
export const isOrCriteria = (
filter: PermissionCriteria<unknown>,
): filter is { anyOf: PermissionCriteria<unknown>[] } =>
): filter is AnyOfCriteria<unknown> =>
Object.prototype.hasOwnProperty.call(filter, 'anyOf');
export const isNotCriteria = (
filter: PermissionCriteria<unknown>,
): filter is { not: PermissionCriteria<unknown> } =>
): filter is NotCriteria<unknown> =>
Object.prototype.hasOwnProperty.call(filter, 'not');
export const createGetRule = <TResource, TQuery>(