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:
MT Lewis
2022-03-28 11:06:38 +01:00
committed by GitHub
parent ea14b21dc0
commit 8012ac46a0
38 changed files with 676 additions and 193 deletions
+40 -23
View File
@@ -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> => {
@@ -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) {
+2
View File
@@ -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