permissions: ensure returned conditions match permission in PermissionPolicy#handle (#10075)
* permission-node: fix signature of permission rule in test suites Signed-off-by: Mike Lewis <mtlewis@users.noreply.github.com> * permission-common: add isPermission helper for comparing permissions Signed-off-by: Mike Lewis <mtlewis@users.noreply.github.com> * permission-node: adjust createConditionExports for more type safety Signed-off-by: Mike Lewis <mtlewis@users.noreply.github.com> * permissions: add resourceType property to PermissionCondition and PermissionRule Signed-off-by: Mike Lewis <mtlewis@users.noreply.github.com> * catalog: handle changes to PermissionCondition and PermissionRule types Signed-off-by: Mike Lewis <mtlewis@users.noreply.github.com> * catalog-backend: avoid re-exporting alpha import cf. https://github.com/backstage/backstage/pull/10128 Signed-off-by: Mike Lewis <mtlewis@users.noreply.github.com> * Update changeset Signed-off-by: Joe Porpeglia <josephp@spotify.com> * Resolve api-report conflict Signed-off-by: Joon Park <joonp@spotify.com> Co-authored-by: Joe Porpeglia <josephp@spotify.com> Co-authored-by: Joon Park <joonp@spotify.com>
This commit is contained in:
@@ -21,6 +21,7 @@ import { PermissionCondition } from '@backstage/plugin-permission-common';
|
||||
import { PermissionCriteria } from '@backstage/plugin-permission-common';
|
||||
import { PluginEndpointDiscovery } from '@backstage/backend-common';
|
||||
import { PolicyDecision } from '@backstage/plugin-permission-common';
|
||||
import { ResourcePermission } from '@backstage/plugin-permission-common';
|
||||
import { TokenManager } from '@backstage/backend-common';
|
||||
|
||||
// @public
|
||||
@@ -48,14 +49,15 @@ export type ApplyConditionsResponseEntry =
|
||||
export type Condition<TRule> = TRule extends PermissionRule<
|
||||
any,
|
||||
any,
|
||||
infer TResourceType,
|
||||
infer TParams
|
||||
>
|
||||
? (...params: TParams) => PermissionCondition<TParams>
|
||||
? (...params: TParams) => PermissionCondition<TResourceType, TParams>
|
||||
: never;
|
||||
|
||||
// @public
|
||||
export type Conditions<
|
||||
TRules extends Record<string, PermissionRule<any, any>>,
|
||||
TRules extends Record<string, PermissionRule<any, any, any>>,
|
||||
> = {
|
||||
[Name in keyof TRules]: Condition<TRules[Name]>;
|
||||
};
|
||||
@@ -67,39 +69,49 @@ export type ConditionTransformer<TQuery> = (
|
||||
|
||||
// @public
|
||||
export const createConditionExports: <
|
||||
TResourceType extends string,
|
||||
TResource,
|
||||
TRules extends Record<string, PermissionRule<TResource, any, unknown[]>>,
|
||||
TRules extends Record<
|
||||
string,
|
||||
PermissionRule<TResource, any, TResourceType, unknown[]>
|
||||
>,
|
||||
>(options: {
|
||||
pluginId: string;
|
||||
resourceType: string;
|
||||
resourceType: TResourceType;
|
||||
rules: TRules;
|
||||
}) => {
|
||||
conditions: Conditions<TRules>;
|
||||
createPolicyDecision: (
|
||||
conditions: PermissionCriteria<PermissionCondition>,
|
||||
createConditionalDecision: (
|
||||
permission: ResourcePermission<TResourceType>,
|
||||
conditions: PermissionCriteria<
|
||||
PermissionCondition<TResourceType, unknown[]>
|
||||
>,
|
||||
) => ConditionalPolicyDecision;
|
||||
};
|
||||
|
||||
// @public
|
||||
export const createConditionFactory: <TParams extends any[]>(
|
||||
rule: PermissionRule<unknown, unknown, TParams>,
|
||||
) => (...params: TParams) => {
|
||||
rule: string;
|
||||
params: TParams;
|
||||
};
|
||||
export const createConditionFactory: <
|
||||
TResourceType extends string,
|
||||
TParams extends any[],
|
||||
>(
|
||||
rule: PermissionRule<unknown, unknown, TResourceType, TParams>,
|
||||
) => (...params: TParams) => PermissionCondition<TResourceType, TParams>;
|
||||
|
||||
// @public
|
||||
export const createConditionTransformer: <
|
||||
TQuery,
|
||||
TRules extends PermissionRule<any, TQuery, unknown[]>[],
|
||||
TRules extends PermissionRule<any, TQuery, string, unknown[]>[],
|
||||
>(
|
||||
permissionRules: [...TRules],
|
||||
) => ConditionTransformer<TQuery>;
|
||||
|
||||
// @public
|
||||
export const createPermissionIntegrationRouter: <TResource>(options: {
|
||||
resourceType: string;
|
||||
rules: PermissionRule<TResource, any, unknown[]>[];
|
||||
export const createPermissionIntegrationRouter: <
|
||||
TResourceType extends string,
|
||||
TResource,
|
||||
>(options: {
|
||||
resourceType: TResourceType;
|
||||
rules: PermissionRule<TResource, any, NoInfer<TResourceType>, unknown[]>[];
|
||||
getResources: (resourceRefs: string[]) => Promise<(TResource | undefined)[]>;
|
||||
}) => express.Router;
|
||||
|
||||
@@ -107,10 +119,11 @@ export const createPermissionIntegrationRouter: <TResource>(options: {
|
||||
export const createPermissionRule: <
|
||||
TResource,
|
||||
TQuery,
|
||||
TResourceType extends string,
|
||||
TParams extends unknown[],
|
||||
>(
|
||||
rule: PermissionRule<TResource, TQuery, TParams>,
|
||||
) => PermissionRule<TResource, TQuery, TParams>;
|
||||
rule: PermissionRule<TResource, TQuery, TResourceType, TParams>,
|
||||
) => PermissionRule<TResource, TQuery, TResourceType, TParams>;
|
||||
|
||||
// @alpha
|
||||
export const isAndCriteria: <T>(
|
||||
@@ -128,11 +141,13 @@ export const isOrCriteria: <T>(
|
||||
) => criteria is AnyOfCriteria<T>;
|
||||
|
||||
// @public
|
||||
export const makeCreatePermissionRule: <TResource, TQuery>() => <
|
||||
TParams extends unknown[],
|
||||
>(
|
||||
rule: PermissionRule<TResource, TQuery, TParams>,
|
||||
) => PermissionRule<TResource, TQuery, TParams>;
|
||||
export const makeCreatePermissionRule: <
|
||||
TResource,
|
||||
TQuery,
|
||||
TResourceType extends string,
|
||||
>() => <TParams extends unknown[]>(
|
||||
rule: PermissionRule<TResource, TQuery, TResourceType, TParams>,
|
||||
) => PermissionRule<TResource, TQuery, TResourceType, TParams>;
|
||||
|
||||
// @public
|
||||
export interface PermissionPolicy {
|
||||
@@ -147,10 +162,12 @@ export interface PermissionPolicy {
|
||||
export type PermissionRule<
|
||||
TResource,
|
||||
TQuery,
|
||||
TResourceType extends string,
|
||||
TParams extends unknown[] = unknown[],
|
||||
> = {
|
||||
name: string;
|
||||
description: string;
|
||||
resourceType: TResourceType;
|
||||
apply(resource: TResource, ...params: TParams): boolean;
|
||||
toQuery(...params: TParams): PermissionCriteria<TQuery>;
|
||||
};
|
||||
|
||||
@@ -14,17 +14,22 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AuthorizeResult } from '@backstage/plugin-permission-common';
|
||||
import {
|
||||
AuthorizeResult,
|
||||
createPermission,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
import { createConditionExports } from './createConditionExports';
|
||||
import { createPermissionRule } from './createPermissionRule';
|
||||
|
||||
const testIntegration = () =>
|
||||
createConditionExports({
|
||||
pluginId: 'test-plugin',
|
||||
resourceType: 'test-resource',
|
||||
rules: {
|
||||
testRule1: {
|
||||
testRule1: createPermissionRule({
|
||||
name: 'testRule1',
|
||||
description: 'Test rule 1',
|
||||
resourceType: 'test-resource',
|
||||
apply: jest.fn(
|
||||
(_resource: any, _firstParam: string, _secondParam: number) => true,
|
||||
),
|
||||
@@ -32,16 +37,17 @@ const testIntegration = () =>
|
||||
query: 'testRule1',
|
||||
params: [firstParam, secondParam],
|
||||
})),
|
||||
},
|
||||
testRule2: {
|
||||
}),
|
||||
testRule2: createPermissionRule({
|
||||
name: 'testRule2',
|
||||
description: 'Test rule 2',
|
||||
apply: jest.fn((_firstParam: object) => false),
|
||||
resourceType: 'test-resource',
|
||||
apply: jest.fn((_resource: any, _firstParam: object) => false),
|
||||
toQuery: jest.fn((firstParam: object) => ({
|
||||
query: 'testRule2',
|
||||
params: [firstParam],
|
||||
})),
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -52,11 +58,13 @@ describe('createConditionExports', () => {
|
||||
|
||||
expect(conditions.testRule1('a', 1)).toEqual({
|
||||
rule: 'testRule1',
|
||||
resourceType: 'test-resource',
|
||||
params: ['a', 1],
|
||||
});
|
||||
|
||||
expect(conditions.testRule2({ baz: 'quux' })).toEqual({
|
||||
rule: 'testRule2',
|
||||
resourceType: 'test-resource',
|
||||
params: [{ baz: 'quux' }],
|
||||
});
|
||||
});
|
||||
@@ -64,18 +72,35 @@ describe('createConditionExports', () => {
|
||||
|
||||
describe('createPolicyDecisions', () => {
|
||||
it('wraps conditions in an object with resourceType and pluginId', () => {
|
||||
const { createPolicyDecision } = testIntegration();
|
||||
const { createConditionalDecision } = testIntegration();
|
||||
const testPermission = createPermission({
|
||||
name: 'test.permission',
|
||||
attributes: {},
|
||||
resourceType: 'test-resource',
|
||||
});
|
||||
|
||||
expect(
|
||||
createPolicyDecision({
|
||||
allOf: [{ rule: 'testRule1', params: ['a', 1] }],
|
||||
createConditionalDecision(testPermission, {
|
||||
allOf: [
|
||||
{
|
||||
rule: 'testRule1',
|
||||
resourceType: 'test-resource',
|
||||
params: ['a', 1],
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toEqual({
|
||||
result: AuthorizeResult.CONDITIONAL,
|
||||
pluginId: 'test-plugin',
|
||||
resourceType: 'test-resource',
|
||||
conditions: {
|
||||
allOf: [{ rule: 'testRule1', params: ['a', 1] }],
|
||||
allOf: [
|
||||
{
|
||||
rule: 'testRule1',
|
||||
resourceType: 'test-resource',
|
||||
params: ['a', 1],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
ConditionalPolicyDecision,
|
||||
PermissionCondition,
|
||||
PermissionCriteria,
|
||||
ResourcePermission,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
import { PermissionRule } from '../types';
|
||||
import { createConditionFactory } from './createConditionFactory';
|
||||
@@ -32,9 +33,10 @@ import { createConditionFactory } from './createConditionFactory';
|
||||
export type Condition<TRule> = TRule extends PermissionRule<
|
||||
any,
|
||||
any,
|
||||
infer TResourceType,
|
||||
infer TParams
|
||||
>
|
||||
? (...params: TParams) => PermissionCondition<TParams>
|
||||
? (...params: TParams) => PermissionCondition<TResourceType, TParams>
|
||||
: never;
|
||||
|
||||
/**
|
||||
@@ -44,39 +46,44 @@ export type Condition<TRule> = TRule extends PermissionRule<
|
||||
* @public
|
||||
*/
|
||||
export type Conditions<
|
||||
TRules extends Record<string, PermissionRule<any, any>>,
|
||||
TRules extends Record<string, PermissionRule<any, any, any>>,
|
||||
> = {
|
||||
[Name in keyof TRules]: Condition<TRules[Name]>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates the recommended condition-related exports for a given plugin based on the built-in
|
||||
* {@link PermissionRule}s it supports.
|
||||
* Creates the recommended condition-related exports for a given plugin based on
|
||||
* the built-in {@link PermissionRule}s it supports.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* The function returns a `conditions` object containing a
|
||||
* {@link @backstage/plugin-permission-common#PermissionCondition} factory for each of the
|
||||
* supplied {@link PermissionRule}s, along with a `createConditions` function which builds the
|
||||
* wrapper object needed to enclose conditions when authoring {@link PermissionPolicy} implementations.
|
||||
* {@link @backstage/plugin-permission-common#PermissionCondition} factory for
|
||||
* each of the supplied {@link PermissionRule}s, along with a
|
||||
* `createConditionalDecision` function which builds the wrapper object needed
|
||||
* to enclose conditions when authoring {@link PermissionPolicy}
|
||||
* implementations.
|
||||
*
|
||||
* Plugin authors should generally call this method with all the built-in {@link PermissionRule}s
|
||||
* the plugin supports, and export the resulting `conditions` object and `createConditions`
|
||||
* function so that they can be used by {@link PermissionPolicy} authors.
|
||||
* Plugin authors should generally call this method with all the built-in
|
||||
* {@link PermissionRule}s the plugin supports, and export the resulting
|
||||
* `conditions` object and `createConditionalDecision` function so that they can
|
||||
* be used by {@link PermissionPolicy} authors.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const createConditionExports = <
|
||||
TResourceType extends string,
|
||||
TResource,
|
||||
TRules extends Record<string, PermissionRule<TResource, any>>,
|
||||
TRules extends Record<string, PermissionRule<TResource, any, TResourceType>>,
|
||||
>(options: {
|
||||
pluginId: string;
|
||||
resourceType: string;
|
||||
resourceType: TResourceType;
|
||||
rules: TRules;
|
||||
}): {
|
||||
conditions: Conditions<TRules>;
|
||||
createPolicyDecision: (
|
||||
conditions: PermissionCriteria<PermissionCondition>,
|
||||
createConditionalDecision: (
|
||||
permission: ResourcePermission<TResourceType>,
|
||||
conditions: PermissionCriteria<PermissionCondition<TResourceType>>,
|
||||
) => ConditionalPolicyDecision;
|
||||
} => {
|
||||
const { pluginId, resourceType, rules } = options;
|
||||
@@ -89,7 +96,8 @@ export const createConditionExports = <
|
||||
}),
|
||||
{} as Conditions<TRules>,
|
||||
),
|
||||
createPolicyDecision: (
|
||||
createConditionalDecision: (
|
||||
_permission: ResourcePermission<TResourceType>,
|
||||
conditions: PermissionCriteria<PermissionCondition>,
|
||||
) => ({
|
||||
result: AuthorizeResult.CONDITIONAL,
|
||||
|
||||
@@ -15,14 +15,16 @@
|
||||
*/
|
||||
|
||||
import { createConditionFactory } from './createConditionFactory';
|
||||
import { createPermissionRule } from './createPermissionRule';
|
||||
|
||||
describe('createConditionFactory', () => {
|
||||
const testRule = {
|
||||
const testRule = createPermissionRule({
|
||||
name: 'test-rule',
|
||||
description: 'test-description',
|
||||
resourceType: 'test-resource',
|
||||
apply: jest.fn(),
|
||||
toQuery: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
it('returns a function', () => {
|
||||
expect(createConditionFactory(testRule)).toEqual(expect.any(Function));
|
||||
@@ -33,6 +35,7 @@ describe('createConditionFactory', () => {
|
||||
const conditionFactory = createConditionFactory(testRule);
|
||||
expect(conditionFactory('a', 'b', 1, 2)).toEqual({
|
||||
rule: 'test-rule',
|
||||
resourceType: 'test-resource',
|
||||
params: ['a', 'b', 1, 2],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { PermissionCondition } from '@backstage/plugin-permission-common';
|
||||
import { PermissionRule } from '../types';
|
||||
|
||||
/**
|
||||
@@ -33,8 +34,11 @@ import { PermissionRule } from '../types';
|
||||
* @public
|
||||
*/
|
||||
export const createConditionFactory =
|
||||
<TParams extends any[]>(rule: PermissionRule<unknown, unknown, TParams>) =>
|
||||
(...params: TParams) => ({
|
||||
<TResourceType extends string, TParams extends any[]>(
|
||||
rule: PermissionRule<unknown, unknown, TResourceType, TParams>,
|
||||
) =>
|
||||
(...params: TParams): PermissionCondition<TResourceType, TParams> => ({
|
||||
rule: rule.name,
|
||||
resourceType: rule.resourceType,
|
||||
params,
|
||||
});
|
||||
|
||||
@@ -19,25 +19,28 @@ import {
|
||||
PermissionCriteria,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
import { createConditionTransformer } from './createConditionTransformer';
|
||||
import { createPermissionRule } from './createPermissionRule';
|
||||
|
||||
const transformConditions = createConditionTransformer([
|
||||
{
|
||||
createPermissionRule({
|
||||
name: 'test-rule-1',
|
||||
description: 'Test rule 1',
|
||||
resourceType: 'test-resource',
|
||||
apply: jest.fn(),
|
||||
toQuery: jest.fn(
|
||||
(firstParam: string, secondParam: number) =>
|
||||
`test-rule-1:${firstParam}/${secondParam}`,
|
||||
),
|
||||
},
|
||||
{
|
||||
}),
|
||||
createPermissionRule({
|
||||
name: 'test-rule-2',
|
||||
description: 'Test rule 2',
|
||||
resourceType: 'test-resource',
|
||||
apply: jest.fn(),
|
||||
toQuery: jest.fn(
|
||||
(firstParam: object) => `test-rule-2:${JSON.stringify(firstParam)}`,
|
||||
),
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
describe('createConditionTransformer', () => {
|
||||
@@ -46,18 +49,30 @@ describe('createConditionTransformer', () => {
|
||||
expectedResult: PermissionCriteria<string>;
|
||||
}[] = [
|
||||
{
|
||||
conditions: { rule: 'test-rule-1', params: ['abc', 123] },
|
||||
conditions: {
|
||||
rule: 'test-rule-1',
|
||||
resourceType: 'test-resource',
|
||||
params: ['abc', 123],
|
||||
},
|
||||
expectedResult: 'test-rule-1:abc/123',
|
||||
},
|
||||
{
|
||||
conditions: { rule: 'test-rule-2', params: [{ foo: 0 }] },
|
||||
conditions: {
|
||||
rule: 'test-rule-2',
|
||||
resourceType: 'test-resource',
|
||||
params: [{ foo: 0 }],
|
||||
},
|
||||
expectedResult: 'test-rule-2:{"foo":0}',
|
||||
},
|
||||
{
|
||||
conditions: {
|
||||
anyOf: [
|
||||
{ rule: 'test-rule-1', params: ['a', 1] },
|
||||
{ rule: 'test-rule-2', params: [{}] },
|
||||
{
|
||||
rule: 'test-rule-1',
|
||||
resourceType: 'test-resource',
|
||||
params: ['a', 1],
|
||||
},
|
||||
{ rule: 'test-rule-2', resourceType: 'test-resource', params: [{}] },
|
||||
],
|
||||
},
|
||||
expectedResult: {
|
||||
@@ -67,8 +82,12 @@ describe('createConditionTransformer', () => {
|
||||
{
|
||||
conditions: {
|
||||
allOf: [
|
||||
{ rule: 'test-rule-1', params: ['a', 1] },
|
||||
{ rule: 'test-rule-2', params: [{}] },
|
||||
{
|
||||
rule: 'test-rule-1',
|
||||
resourceType: 'test-resource',
|
||||
params: ['a', 1],
|
||||
},
|
||||
{ rule: 'test-rule-2', resourceType: 'test-resource', params: [{}] },
|
||||
],
|
||||
},
|
||||
expectedResult: {
|
||||
@@ -77,7 +96,11 @@ describe('createConditionTransformer', () => {
|
||||
},
|
||||
{
|
||||
conditions: {
|
||||
not: { rule: 'test-rule-2', params: [{}] },
|
||||
not: {
|
||||
rule: 'test-rule-2',
|
||||
resourceType: 'test-resource',
|
||||
params: [{}],
|
||||
},
|
||||
},
|
||||
expectedResult: {
|
||||
not: 'test-rule-2:{}',
|
||||
@@ -88,15 +111,31 @@ describe('createConditionTransformer', () => {
|
||||
allOf: [
|
||||
{
|
||||
anyOf: [
|
||||
{ rule: 'test-rule-1', params: ['a', 1] },
|
||||
{ rule: 'test-rule-2', params: [{}] },
|
||||
{
|
||||
rule: 'test-rule-1',
|
||||
resourceType: 'test-resource',
|
||||
params: ['a', 1],
|
||||
},
|
||||
{
|
||||
rule: 'test-rule-2',
|
||||
resourceType: 'test-resource',
|
||||
params: [{}],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
not: {
|
||||
allOf: [
|
||||
{ rule: 'test-rule-1', params: ['b', 2] },
|
||||
{ rule: 'test-rule-2', params: [{ c: 3 }] },
|
||||
{
|
||||
rule: 'test-rule-1',
|
||||
resourceType: 'test-resource',
|
||||
params: ['b', 2],
|
||||
},
|
||||
{
|
||||
rule: 'test-rule-2',
|
||||
resourceType: 'test-resource',
|
||||
params: [{ c: 3 }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -120,15 +159,33 @@ describe('createConditionTransformer', () => {
|
||||
allOf: [
|
||||
{
|
||||
anyOf: [
|
||||
{ rule: 'test-rule-1', params: ['a', 1] },
|
||||
{ rule: 'test-rule-2', params: [{ b: 2 }] },
|
||||
{
|
||||
rule: 'test-rule-1',
|
||||
resourceType: 'test-resource',
|
||||
params: ['a', 1],
|
||||
},
|
||||
{
|
||||
rule: 'test-rule-2',
|
||||
resourceType: 'test-resource',
|
||||
params: [{ b: 2 }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
not: {
|
||||
allOf: [
|
||||
{ rule: 'test-rule-1', params: ['c', 3] },
|
||||
{ not: { rule: 'test-rule-2', params: [{ d: 4 }] } },
|
||||
{
|
||||
rule: 'test-rule-1',
|
||||
resourceType: 'test-resource',
|
||||
params: ['c', 3],
|
||||
},
|
||||
{
|
||||
not: {
|
||||
rule: 'test-rule-2',
|
||||
resourceType: 'test-resource',
|
||||
params: [{ d: 4 }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
|
||||
const mapConditions = <TQuery>(
|
||||
criteria: PermissionCriteria<PermissionCondition>,
|
||||
getRule: (name: string) => PermissionRule<unknown, TQuery>,
|
||||
getRule: (name: string) => PermissionRule<unknown, TQuery, string>,
|
||||
): PermissionCriteria<TQuery> => {
|
||||
if (isAndCriteria(criteria)) {
|
||||
return {
|
||||
@@ -70,7 +70,7 @@ export type ConditionTransformer<TQuery> = (
|
||||
*/
|
||||
export const createConditionTransformer = <
|
||||
TQuery,
|
||||
TRules extends PermissionRule<any, TQuery>[],
|
||||
TRules extends PermissionRule<any, TQuery, string>[],
|
||||
>(
|
||||
permissionRules: [...TRules],
|
||||
): ConditionTransformer<TQuery> => {
|
||||
|
||||
+145
-30
@@ -18,6 +18,7 @@ import { AuthorizeResult } from '@backstage/plugin-permission-common';
|
||||
import express, { Express, Router } from 'express';
|
||||
import request, { Response } from 'supertest';
|
||||
import { createPermissionIntegrationRouter } from './createPermissionIntegrationRouter';
|
||||
import { createPermissionRule } from './createPermissionRule';
|
||||
|
||||
const mockGetResources: jest.MockedFunction<
|
||||
Parameters<typeof createPermissionIntegrationRouter>[0]['getResources']
|
||||
@@ -25,21 +26,23 @@ const mockGetResources: jest.MockedFunction<
|
||||
resourceRefs.map(resourceRef => ({ id: resourceRef })),
|
||||
);
|
||||
|
||||
const testRule1 = {
|
||||
const testRule1 = createPermissionRule({
|
||||
name: 'test-rule-1',
|
||||
description: 'Test rule 1',
|
||||
resourceType: 'test-resource',
|
||||
apply: jest.fn(
|
||||
(_resource: any, _firstParam: string, _secondParam: number) => true,
|
||||
),
|
||||
toQuery: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const testRule2 = {
|
||||
const testRule2 = createPermissionRule({
|
||||
name: 'test-rule-2',
|
||||
description: 'Test rule 2',
|
||||
apply: jest.fn((_firstParam: object) => false),
|
||||
resourceType: 'test-resource',
|
||||
apply: jest.fn((_resource: any, _firstParam: object) => false),
|
||||
toQuery: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('createPermissionIntegrationRouter', () => {
|
||||
let app: Express;
|
||||
@@ -65,29 +68,57 @@ describe('createPermissionIntegrationRouter', () => {
|
||||
|
||||
describe('POST /.well-known/backstage/permissions/apply-conditions', () => {
|
||||
it.each([
|
||||
{ rule: 'test-rule-1', params: ['abc', 123] },
|
||||
{
|
||||
rule: 'test-rule-1',
|
||||
resourceType: 'test-resource',
|
||||
params: ['abc', 123],
|
||||
},
|
||||
{
|
||||
anyOf: [
|
||||
{ rule: 'test-rule-1', params: ['a', 1] },
|
||||
{ rule: 'test-rule-2', params: [{}] },
|
||||
{
|
||||
rule: 'test-rule-1',
|
||||
resourceType: 'test-resource',
|
||||
params: ['a', 1],
|
||||
},
|
||||
{ rule: 'test-rule-2', resourceType: 'test-resource', params: [{}] },
|
||||
],
|
||||
},
|
||||
{
|
||||
not: { rule: 'test-rule-2', params: [{}] },
|
||||
not: {
|
||||
rule: 'test-rule-2',
|
||||
resourceType: 'test-resource',
|
||||
params: [{}],
|
||||
},
|
||||
},
|
||||
{
|
||||
allOf: [
|
||||
{
|
||||
anyOf: [
|
||||
{ rule: 'test-rule-1', params: ['a', 1] },
|
||||
{ rule: 'test-rule-2', params: [{}] },
|
||||
{
|
||||
rule: 'test-rule-1',
|
||||
resourceType: 'test-resource',
|
||||
params: ['a', 1],
|
||||
},
|
||||
{
|
||||
rule: 'test-rule-2',
|
||||
resourceType: 'test-resource',
|
||||
params: [{}],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
not: {
|
||||
allOf: [
|
||||
{ rule: 'test-rule-1', params: ['b', 2] },
|
||||
{ rule: 'test-rule-2', params: [{ c: 3 }] },
|
||||
{
|
||||
rule: 'test-rule-1',
|
||||
resourceType: 'test-resource',
|
||||
params: ['b', 2],
|
||||
},
|
||||
{
|
||||
rule: 'test-rule-2',
|
||||
resourceType: 'test-resource',
|
||||
params: [{ c: 3 }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -119,26 +150,52 @@ describe('createPermissionIntegrationRouter', () => {
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ rule: 'test-rule-2', params: [{ foo: 0 }] },
|
||||
{
|
||||
rule: 'test-rule-2',
|
||||
resourceType: 'test-resource',
|
||||
params: [{ foo: 0 }],
|
||||
},
|
||||
{
|
||||
allOf: [
|
||||
{ rule: 'test-rule-1', params: ['a', 1] },
|
||||
{ rule: 'test-rule-2', params: [{}] },
|
||||
{
|
||||
rule: 'test-rule-1',
|
||||
resourceType: 'test-resource',
|
||||
params: ['a', 1],
|
||||
},
|
||||
{ rule: 'test-rule-2', resourceType: 'test-resource', params: [{}] },
|
||||
],
|
||||
},
|
||||
{
|
||||
allOf: [
|
||||
{
|
||||
anyOf: [
|
||||
{ rule: 'test-rule-1', params: ['a', 1] },
|
||||
{ rule: 'test-rule-2', params: [{ b: 2 }] },
|
||||
{
|
||||
rule: 'test-rule-1',
|
||||
resourceType: 'test-resource',
|
||||
params: ['a', 1],
|
||||
},
|
||||
{
|
||||
rule: 'test-rule-2',
|
||||
resourceType: 'test-resource',
|
||||
params: [{ b: 2 }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
not: {
|
||||
allOf: [
|
||||
{ rule: 'test-rule-1', params: ['c', 3] },
|
||||
{ not: { rule: 'test-rule-2', params: [{ d: 4 }] } },
|
||||
{
|
||||
rule: 'test-rule-1',
|
||||
resourceType: 'test-resource',
|
||||
params: ['c', 3],
|
||||
},
|
||||
{
|
||||
not: {
|
||||
rule: 'test-rule-2',
|
||||
resourceType: 'test-resource',
|
||||
params: [{ d: 4 }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -179,25 +236,45 @@ describe('createPermissionIntegrationRouter', () => {
|
||||
id: '123',
|
||||
resourceRef: 'default:test/resource-1',
|
||||
resourceType: 'test-resource',
|
||||
conditions: { rule: 'test-rule-1', params: [] },
|
||||
conditions: {
|
||||
rule: 'test-rule-1',
|
||||
resourceType: 'test-resource',
|
||||
params: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '234',
|
||||
resourceRef: 'default:test/resource-1',
|
||||
resourceType: 'test-resource',
|
||||
conditions: { rule: 'test-rule-2', params: [] },
|
||||
conditions: {
|
||||
rule: 'test-rule-2',
|
||||
resourceType: 'test-resource',
|
||||
params: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '345',
|
||||
resourceRef: 'default:test/resource-2',
|
||||
resourceType: 'test-resource',
|
||||
conditions: { not: { rule: 'test-rule-1', params: [] } },
|
||||
conditions: {
|
||||
not: {
|
||||
rule: 'test-rule-1',
|
||||
resourceType: 'test-resource',
|
||||
params: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '456',
|
||||
resourceRef: 'default:test/resource-3',
|
||||
resourceType: 'test-resource',
|
||||
conditions: { not: { rule: 'test-rule-2', params: [] } },
|
||||
conditions: {
|
||||
not: {
|
||||
rule: 'test-rule-2',
|
||||
resourceType: 'test-resource',
|
||||
params: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '567',
|
||||
@@ -205,8 +282,16 @@ describe('createPermissionIntegrationRouter', () => {
|
||||
resourceType: 'test-resource',
|
||||
conditions: {
|
||||
anyOf: [
|
||||
{ rule: 'test-rule-1', params: [] },
|
||||
{ rule: 'test-rule-2', params: [] },
|
||||
{
|
||||
rule: 'test-rule-1',
|
||||
resourceType: 'test-resource',
|
||||
params: [],
|
||||
},
|
||||
{
|
||||
rule: 'test-rule-2',
|
||||
resourceType: 'test-resource',
|
||||
params: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -248,6 +333,7 @@ describe('createPermissionIntegrationRouter', () => {
|
||||
resourceType: 'test-incorrect-resource-1',
|
||||
conditions: {
|
||||
rule: 'test-rule-1',
|
||||
resourceType: 'test-incorrect-resource-1',
|
||||
params: [{}],
|
||||
},
|
||||
},
|
||||
@@ -257,6 +343,7 @@ describe('createPermissionIntegrationRouter', () => {
|
||||
resourceType: 'test-resource',
|
||||
conditions: {
|
||||
rule: 'test-rule-1',
|
||||
resourceType: 'test-resource',
|
||||
params: [{}],
|
||||
},
|
||||
},
|
||||
@@ -266,6 +353,7 @@ describe('createPermissionIntegrationRouter', () => {
|
||||
resourceType: 'test-incorrect-resource-2',
|
||||
conditions: {
|
||||
rule: 'test-rule-1',
|
||||
resourceType: 'test-incorrect-resource-2',
|
||||
params: [{}],
|
||||
},
|
||||
},
|
||||
@@ -291,7 +379,11 @@ describe('createPermissionIntegrationRouter', () => {
|
||||
id: '123',
|
||||
resourceRef: 'default:test/resource',
|
||||
resourceType: 'test-resource',
|
||||
conditions: { rule: 'test-rule-1', params: [] },
|
||||
conditions: {
|
||||
rule: 'test-rule-1',
|
||||
resourceType: 'test-resource',
|
||||
params: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -324,19 +416,31 @@ describe('createPermissionIntegrationRouter', () => {
|
||||
id: '123',
|
||||
resourceRef: 'default:test/resource-1',
|
||||
resourceType: 'test-resource',
|
||||
conditions: { rule: 'test-rule-1', params: [] },
|
||||
conditions: {
|
||||
rule: 'test-rule-1',
|
||||
resourceType: 'test-resource',
|
||||
params: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '234',
|
||||
resourceRef: 'default:test/missing-resource',
|
||||
resourceType: 'test-resource',
|
||||
conditions: { rule: 'test-rule-1', params: [] },
|
||||
conditions: {
|
||||
rule: 'test-rule-1',
|
||||
resourceType: 'test-resource',
|
||||
params: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '345',
|
||||
resourceRef: 'default:test/resource-2',
|
||||
resourceType: 'test-resource',
|
||||
conditions: { rule: 'test-rule-1', params: [] },
|
||||
conditions: {
|
||||
rule: 'test-rule-1',
|
||||
resourceType: 'test-resource',
|
||||
params: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -377,6 +481,17 @@ describe('createPermissionIntegrationRouter', () => {
|
||||
],
|
||||
},
|
||||
{ items: [{ conditions: { anyOf: [] } }] },
|
||||
{
|
||||
items: [
|
||||
{ conditions: { rule: 'TEST_RULE', resourceType: 'test-resource' } },
|
||||
],
|
||||
},
|
||||
{ items: [{ conditions: { rule: 'TEST_RULE', params: ['foo'] } }] },
|
||||
{
|
||||
items: [
|
||||
{ conditions: { resourceType: 'test-resource', params: ['foo'] } },
|
||||
],
|
||||
},
|
||||
])(`returns 400 for invalid input %#`, async input => {
|
||||
const response = await request(app)
|
||||
.post('/.well-known/backstage/permissions/apply-conditions')
|
||||
|
||||
@@ -44,6 +44,7 @@ const permissionCriteriaSchema: z.ZodSchema<
|
||||
z
|
||||
.object({
|
||||
rule: z.string(),
|
||||
resourceType: z.string(),
|
||||
params: z.array(z.unknown()),
|
||||
})
|
||||
.strict(),
|
||||
@@ -100,10 +101,10 @@ export type ApplyConditionsResponse = {
|
||||
items: ApplyConditionsResponseEntry[];
|
||||
};
|
||||
|
||||
const applyConditions = <TResource>(
|
||||
criteria: PermissionCriteria<PermissionCondition>,
|
||||
const applyConditions = <TResourceType extends string, TResource>(
|
||||
criteria: PermissionCriteria<PermissionCondition<TResourceType>>,
|
||||
resource: TResource | undefined,
|
||||
getRule: (name: string) => PermissionRule<TResource, unknown>,
|
||||
getRule: (name: string) => PermissionRule<TResource, unknown, TResourceType>,
|
||||
): boolean => {
|
||||
// If resource was not found, deny. This avoids leaking information from the
|
||||
// apply-conditions API which would allow a user to differentiate between
|
||||
@@ -127,6 +128,14 @@ const applyConditions = <TResource>(
|
||||
return getRule(criteria.rule).apply(resource, ...criteria.params);
|
||||
};
|
||||
|
||||
/**
|
||||
* Prevent use of type parameter from contributing to type inference.
|
||||
*
|
||||
* https://github.com/Microsoft/TypeScript/issues/14829#issuecomment-980401795
|
||||
* @ignore
|
||||
*/
|
||||
type NoInfer<T> = T extends infer S ? S : never;
|
||||
|
||||
/**
|
||||
* Create an express Router which provides an authorization route to allow
|
||||
* integration between the permission backend and other Backstage backend
|
||||
@@ -162,9 +171,16 @@ const applyConditions = <TResource>(
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const createPermissionIntegrationRouter = <TResource>(options: {
|
||||
resourceType: string;
|
||||
rules: PermissionRule<TResource, any>[];
|
||||
export const createPermissionIntegrationRouter = <
|
||||
TResourceType extends string,
|
||||
TResource,
|
||||
>(options: {
|
||||
resourceType: TResourceType;
|
||||
// Do not infer value of TResourceType from supplied rules.
|
||||
// instead only consider the resourceType parameter, and
|
||||
// consider any rules whose resource type does not match
|
||||
// to be an error.
|
||||
rules: PermissionRule<TResource, any, NoInfer<TResourceType>>[];
|
||||
getResources: (
|
||||
resourceRefs: string[],
|
||||
) => Promise<Array<TResource | undefined>>;
|
||||
|
||||
@@ -24,9 +24,10 @@ import { PermissionRule } from '../types';
|
||||
export const createPermissionRule = <
|
||||
TResource,
|
||||
TQuery,
|
||||
TResourceType extends string,
|
||||
TParams extends unknown[],
|
||||
>(
|
||||
rule: PermissionRule<TResource, TQuery, TParams>,
|
||||
rule: PermissionRule<TResource, TQuery, TResourceType, TParams>,
|
||||
) => rule;
|
||||
|
||||
/**
|
||||
@@ -38,8 +39,8 @@ export const createPermissionRule = <
|
||||
* @public
|
||||
*/
|
||||
export const makeCreatePermissionRule =
|
||||
<TResource, TQuery>() =>
|
||||
<TResource, TQuery, TResourceType extends string>() =>
|
||||
<TParams extends unknown[]>(
|
||||
rule: PermissionRule<TResource, TQuery, TParams>,
|
||||
rule: PermissionRule<TResource, TQuery, TResourceType, TParams>,
|
||||
) =>
|
||||
createPermissionRule(rule);
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createPermissionRule } from './createPermissionRule';
|
||||
import {
|
||||
createGetRule,
|
||||
isAndCriteria,
|
||||
@@ -25,19 +26,21 @@ describe('permission integration utils', () => {
|
||||
describe('createGetRule', () => {
|
||||
let getRule: ReturnType<typeof createGetRule>;
|
||||
|
||||
const testRule1 = {
|
||||
const testRule1 = createPermissionRule({
|
||||
name: 'test-rule-1',
|
||||
description: 'Test rule 1',
|
||||
resourceType: 'test-resource',
|
||||
apply: jest.fn(),
|
||||
toQuery: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const testRule2 = {
|
||||
const testRule2 = createPermissionRule({
|
||||
name: 'test-rule-2',
|
||||
description: 'Test rule 2',
|
||||
resourceType: 'test-resource',
|
||||
apply: jest.fn(),
|
||||
toQuery: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
getRule = createGetRule([testRule1, testRule2]);
|
||||
|
||||
@@ -62,11 +62,11 @@ export const isNotCriteria = <T>(
|
||||
Object.prototype.hasOwnProperty.call(criteria, 'not');
|
||||
|
||||
export const createGetRule = <TResource, TQuery>(
|
||||
rules: PermissionRule<TResource, TQuery>[],
|
||||
rules: PermissionRule<TResource, TQuery, string>[],
|
||||
) => {
|
||||
const rulesMap = new Map(Object.values(rules).map(rule => [rule.name, rule]));
|
||||
|
||||
return (name: string): PermissionRule<TResource, TQuery> => {
|
||||
return (name: string): PermissionRule<TResource, TQuery, string> => {
|
||||
const rule = rulesMap.get(name);
|
||||
|
||||
if (!rule) {
|
||||
|
||||
@@ -35,10 +35,12 @@ import type { PermissionCriteria } from '@backstage/plugin-permission-common';
|
||||
export type PermissionRule<
|
||||
TResource,
|
||||
TQuery,
|
||||
TResourceType extends string,
|
||||
TParams extends unknown[] = unknown[],
|
||||
> = {
|
||||
name: string;
|
||||
description: string;
|
||||
resourceType: TResourceType;
|
||||
|
||||
/**
|
||||
* Apply this rule to a resource already loaded from a backing data source. The params are
|
||||
|
||||
Reference in New Issue
Block a user