Merge pull request #8119 from backstage/permission-node
Add @backstage/plugin-permission-node
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
# @backstage/plugin-permission-node
|
||||
|
||||
> NOTE: THIS PACKAGE IS EXPERIMENTAL, HERE BE DRAGONS
|
||||
|
||||
Common permission and authorization utilities for backend plugins. For more
|
||||
information, see the [authorization
|
||||
PRFC](https://github.com/backstage/backstage/pull/7761).
|
||||
@@ -0,0 +1,130 @@
|
||||
## API Report File for "@backstage/plugin-permission-node"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { AuthorizeRequest } from '@backstage/plugin-permission-common';
|
||||
import { AuthorizeResult } from '@backstage/plugin-permission-common';
|
||||
import { BackstageIdentity } from '@backstage/plugin-auth-backend';
|
||||
import { PermissionCondition } from '@backstage/plugin-permission-common';
|
||||
import { PermissionCriteria } from '@backstage/plugin-permission-common';
|
||||
import { Router } from 'express';
|
||||
|
||||
// @public
|
||||
export type ApplyConditionsRequest = {
|
||||
resourceRef: string;
|
||||
resourceType: string;
|
||||
conditions: PermissionCriteria<PermissionCondition>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type ApplyConditionsResponse = {
|
||||
result: AuthorizeResult.ALLOW | AuthorizeResult.DENY;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type Condition<TRule> = TRule extends PermissionRule<
|
||||
any,
|
||||
any,
|
||||
infer TParams
|
||||
>
|
||||
? (...params: TParams) => PermissionCondition<TParams>
|
||||
: never;
|
||||
|
||||
// @public
|
||||
export type ConditionalPolicyResult = {
|
||||
result: AuthorizeResult.CONDITIONAL;
|
||||
conditions: {
|
||||
pluginId: string;
|
||||
resourceType: string;
|
||||
conditions: PermissionCriteria<PermissionCondition>;
|
||||
};
|
||||
};
|
||||
|
||||
// @public
|
||||
export type Conditions<
|
||||
TRules extends Record<string, PermissionRule<any, any>>,
|
||||
> = {
|
||||
[Name in keyof TRules]: Condition<TRules[Name]>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type ConditionTransformer<TQuery> = (
|
||||
conditions: PermissionCriteria<PermissionCondition>,
|
||||
) => PermissionCriteria<TQuery>;
|
||||
|
||||
// @public
|
||||
export const createConditionExports: <
|
||||
TResource,
|
||||
TRules extends Record<string, PermissionRule<TResource, any, unknown[]>>,
|
||||
>(options: {
|
||||
pluginId: string;
|
||||
resourceType: string;
|
||||
rules: TRules;
|
||||
}) => {
|
||||
conditions: Conditions<TRules>;
|
||||
createConditions: (conditions: PermissionCriteria<PermissionCondition>) => {
|
||||
pluginId: string;
|
||||
resourceType: string;
|
||||
conditions: PermissionCriteria<PermissionCondition>;
|
||||
};
|
||||
};
|
||||
|
||||
// @public
|
||||
export const createConditionFactory: <TParams extends any[]>(
|
||||
rule: PermissionRule<unknown, unknown, TParams>,
|
||||
) => (...params: TParams) => {
|
||||
rule: string;
|
||||
params: TParams;
|
||||
};
|
||||
|
||||
// @public
|
||||
export const createConditionTransformer: <
|
||||
TQuery,
|
||||
TRules extends PermissionRule<any, TQuery, unknown[]>[],
|
||||
>(
|
||||
permissionRules: [...TRules],
|
||||
) => ConditionTransformer<TQuery>;
|
||||
|
||||
// @public
|
||||
export const createPermissionIntegrationRouter: <TResource>({
|
||||
resourceType,
|
||||
rules,
|
||||
getResource,
|
||||
}: {
|
||||
resourceType: string;
|
||||
rules: PermissionRule<TResource, any, unknown[]>[];
|
||||
getResource: (resourceRef: string) => Promise<TResource | undefined>;
|
||||
}) => Router;
|
||||
|
||||
// @public
|
||||
export interface PermissionPolicy {
|
||||
// (undocumented)
|
||||
handle(
|
||||
request: PolicyAuthorizeRequest,
|
||||
user?: BackstageIdentity,
|
||||
): Promise<PolicyResult>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type PermissionRule<
|
||||
TResource,
|
||||
TQuery,
|
||||
TParams extends unknown[] = unknown[],
|
||||
> = {
|
||||
name: string;
|
||||
description: string;
|
||||
apply(resource: TResource, ...params: TParams): boolean;
|
||||
toQuery(...params: TParams): PermissionCriteria<TQuery>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type PolicyAuthorizeRequest = Omit<AuthorizeRequest, 'resourceRef'>;
|
||||
|
||||
// @public
|
||||
export type PolicyResult =
|
||||
| {
|
||||
result: AuthorizeResult.ALLOW | AuthorizeResult.DENY;
|
||||
}
|
||||
| ConditionalPolicyResult;
|
||||
```
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "@backstage/plugin-permission-node",
|
||||
"description": "Common permission and authorization utilities for backend plugins",
|
||||
"version": "0.0.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"homepage": "https://backstage.io",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/backstage/backstage",
|
||||
"directory": "plugins/permission-node"
|
||||
},
|
||||
"keywords": [
|
||||
"backstage",
|
||||
"permissions"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "backstage-cli backend:build",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test",
|
||||
"prepack": "backstage-cli prepack",
|
||||
"postpack": "backstage-cli postpack",
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/plugin-auth-backend": "^0.4.6",
|
||||
"@backstage/plugin-permission-common": "^0.1.0",
|
||||
"@types/express": "^4.17.6",
|
||||
"express": "^4.17.1",
|
||||
"zod": "^3.11.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.9.0",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"supertest": "^6.1.3"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Common permission and authorization utilities for backend plugins
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
export * from './integration';
|
||||
export * from './policy';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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 { createConditionExports } from './createConditionExports';
|
||||
|
||||
const testIntegration = () =>
|
||||
createConditionExports({
|
||||
pluginId: 'test-plugin',
|
||||
resourceType: 'test-resource',
|
||||
rules: {
|
||||
testRule1: {
|
||||
name: 'testRule1',
|
||||
description: 'Test rule 1',
|
||||
apply: jest.fn(
|
||||
(_resource: any, _firstParam: string, _secondParam: number) => true,
|
||||
),
|
||||
toQuery: jest.fn((firstParam: string, secondParam: number) => ({
|
||||
query: 'testRule1',
|
||||
params: [firstParam, secondParam],
|
||||
})),
|
||||
},
|
||||
testRule2: {
|
||||
name: 'testRule2',
|
||||
description: 'Test rule 2',
|
||||
apply: jest.fn((_firstParam: object) => false),
|
||||
toQuery: jest.fn((firstParam: object) => ({
|
||||
query: 'testRule2',
|
||||
params: [firstParam],
|
||||
})),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe('createConditionExports', () => {
|
||||
describe('conditions', () => {
|
||||
it('creates condition factories for the supplied rules', () => {
|
||||
const { conditions } = testIntegration();
|
||||
|
||||
expect(conditions.testRule1('a', 1)).toEqual({
|
||||
rule: 'testRule1',
|
||||
params: ['a', 1],
|
||||
});
|
||||
|
||||
expect(conditions.testRule2({ baz: 'quux' })).toEqual({
|
||||
rule: 'testRule2',
|
||||
params: [{ baz: 'quux' }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createConditions', () => {
|
||||
it('wraps conditions in an object with resourceType and pluginId', () => {
|
||||
const { createConditions } = testIntegration();
|
||||
|
||||
expect(
|
||||
createConditions({ allOf: [{ rule: 'testRule1', params: ['a', 1] }] }),
|
||||
).toEqual({
|
||||
pluginId: 'test-plugin',
|
||||
resourceType: 'test-resource',
|
||||
conditions: {
|
||||
allOf: [{ rule: 'testRule1', params: ['a', 1] }],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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/plugin-permission-common';
|
||||
import { PermissionRule } from '../types';
|
||||
import { createConditionFactory } from './createConditionFactory';
|
||||
|
||||
/**
|
||||
* A utility type for mapping a single {@link PermissionRule} to its
|
||||
* corresponding {@link @backstage/plugin-permission-common#PermissionCondition}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type Condition<TRule> = TRule extends PermissionRule<
|
||||
any,
|
||||
any,
|
||||
infer TParams
|
||||
>
|
||||
? (...params: TParams) => PermissionCondition<TParams>
|
||||
: never;
|
||||
|
||||
/**
|
||||
* A utility type for mapping {@link PermissionRule}s to their corresponding
|
||||
* {@link @backstage/plugin-permission-common#PermissionCondition}s.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type Conditions<
|
||||
TRules extends Record<string, PermissionRule<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.
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const createConditionExports = <
|
||||
TResource,
|
||||
TRules extends Record<string, PermissionRule<TResource, any>>,
|
||||
>(options: {
|
||||
pluginId: string;
|
||||
resourceType: string;
|
||||
rules: TRules;
|
||||
}): {
|
||||
conditions: Conditions<TRules>;
|
||||
createConditions: (conditions: PermissionCriteria<PermissionCondition>) => {
|
||||
pluginId: string;
|
||||
resourceType: string;
|
||||
conditions: PermissionCriteria<PermissionCondition>;
|
||||
};
|
||||
} => {
|
||||
const { pluginId, resourceType, rules } = options;
|
||||
|
||||
return {
|
||||
conditions: Object.entries(rules).reduce(
|
||||
(acc, [key, rule]) => ({
|
||||
...acc,
|
||||
[key]: createConditionFactory(rule),
|
||||
}),
|
||||
{} as Conditions<TRules>,
|
||||
),
|
||||
createConditions: (
|
||||
conditions: PermissionCriteria<PermissionCondition>,
|
||||
) => ({
|
||||
pluginId,
|
||||
resourceType,
|
||||
conditions,
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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 { createConditionFactory } from './createConditionFactory';
|
||||
|
||||
describe('createConditionFactory', () => {
|
||||
const testRule = {
|
||||
name: 'test-rule',
|
||||
description: 'test-description',
|
||||
apply: jest.fn(),
|
||||
toQuery: jest.fn(),
|
||||
};
|
||||
|
||||
it('returns a function', () => {
|
||||
expect(createConditionFactory(testRule)).toEqual(expect.any(Function));
|
||||
});
|
||||
|
||||
describe('return value', () => {
|
||||
it('constructs a condition with the rule name and supplied params', () => {
|
||||
const conditionFactory = createConditionFactory(testRule);
|
||||
expect(conditionFactory('a', 'b', 1, 2)).toEqual({
|
||||
rule: 'test-rule',
|
||||
params: ['a', 'b', 1, 2],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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 { PermissionRule } from '../types';
|
||||
|
||||
/**
|
||||
* Creates a condition factory function for a given authorization rule and parameter types.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* For example, an isEntityOwner rule for catalog entities might take an array of entityRef strings.
|
||||
* The rule itself defines _how_ to check a given resource, whereas a condition also includes _what_
|
||||
* to verify.
|
||||
*
|
||||
* Plugin authors should generally use the {@link createConditionExports} in order to efficiently
|
||||
* create multiple condition factories. This helper should generally only be used to construct
|
||||
* condition factories for third-party rules that aren't part of the backend plugin with which
|
||||
* they're intended to integrate.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const createConditionFactory =
|
||||
<TParams extends any[]>(rule: PermissionRule<unknown, unknown, TParams>) =>
|
||||
(...params: TParams) => ({
|
||||
rule: rule.name,
|
||||
params,
|
||||
});
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* 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/plugin-permission-common';
|
||||
import { createConditionTransformer } from './createConditionTransformer';
|
||||
|
||||
const transformConditions = createConditionTransformer([
|
||||
{
|
||||
name: 'test-rule-1',
|
||||
description: 'Test rule 1',
|
||||
apply: jest.fn(),
|
||||
toQuery: jest.fn(
|
||||
(firstParam: string, secondParam: number) =>
|
||||
`test-rule-1:${firstParam}/${secondParam}`,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'test-rule-2',
|
||||
description: 'Test rule 2',
|
||||
apply: jest.fn(),
|
||||
toQuery: jest.fn(
|
||||
(firstParam: object) => `test-rule-2:${JSON.stringify(firstParam)}`,
|
||||
),
|
||||
},
|
||||
]);
|
||||
|
||||
describe('createConditionTransformer', () => {
|
||||
const testCases: {
|
||||
conditions: PermissionCriteria<PermissionCondition>;
|
||||
expectedResult: PermissionCriteria<string>;
|
||||
}[] = [
|
||||
{
|
||||
conditions: { rule: 'test-rule-1', params: ['abc', 123] },
|
||||
expectedResult: 'test-rule-1:abc/123',
|
||||
},
|
||||
{
|
||||
conditions: { rule: 'test-rule-2', params: [{ foo: 0 }] },
|
||||
expectedResult: 'test-rule-2:{"foo":0}',
|
||||
},
|
||||
{
|
||||
conditions: {
|
||||
anyOf: [
|
||||
{ rule: 'test-rule-1', params: ['a', 1] },
|
||||
{ rule: 'test-rule-2', params: [{}] },
|
||||
],
|
||||
},
|
||||
expectedResult: {
|
||||
anyOf: ['test-rule-1:a/1', 'test-rule-2:{}'],
|
||||
},
|
||||
},
|
||||
{
|
||||
conditions: {
|
||||
allOf: [
|
||||
{ rule: 'test-rule-1', params: ['a', 1] },
|
||||
{ rule: 'test-rule-2', params: [{}] },
|
||||
],
|
||||
},
|
||||
expectedResult: {
|
||||
allOf: ['test-rule-1:a/1', 'test-rule-2:{}'],
|
||||
},
|
||||
},
|
||||
{
|
||||
conditions: {
|
||||
not: { rule: 'test-rule-2', params: [{}] },
|
||||
},
|
||||
expectedResult: {
|
||||
not: 'test-rule-2:{}',
|
||||
},
|
||||
},
|
||||
{
|
||||
conditions: {
|
||||
allOf: [
|
||||
{
|
||||
anyOf: [
|
||||
{ rule: 'test-rule-1', params: ['a', 1] },
|
||||
{ rule: 'test-rule-2', params: [{}] },
|
||||
],
|
||||
},
|
||||
{
|
||||
not: {
|
||||
allOf: [
|
||||
{ rule: 'test-rule-1', params: ['b', 2] },
|
||||
{ rule: 'test-rule-2', params: [{ c: 3 }] },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
expectedResult: {
|
||||
allOf: [
|
||||
{
|
||||
anyOf: ['test-rule-1:a/1', 'test-rule-2:{}'],
|
||||
},
|
||||
{
|
||||
not: {
|
||||
allOf: ['test-rule-1:b/2', 'test-rule-2:{"c":3}'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
conditions: {
|
||||
allOf: [
|
||||
{
|
||||
anyOf: [
|
||||
{ rule: 'test-rule-1', params: ['a', 1] },
|
||||
{ rule: 'test-rule-2', params: [{ b: 2 }] },
|
||||
],
|
||||
},
|
||||
{
|
||||
not: {
|
||||
allOf: [
|
||||
{ rule: 'test-rule-1', params: ['c', 3] },
|
||||
{ not: { rule: 'test-rule-2', params: [{ d: 4 }] } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
expectedResult: {
|
||||
allOf: [
|
||||
{
|
||||
anyOf: ['test-rule-1:a/1', 'test-rule-2:{"b":2}'],
|
||||
},
|
||||
{
|
||||
not: {
|
||||
allOf: ['test-rule-1:c/3', { not: 'test-rule-2:{"d":4}' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
it.each(testCases)(
|
||||
'works with criteria %#',
|
||||
({ conditions, expectedResult }) => {
|
||||
expect(transformConditions(conditions)).toEqual(expectedResult);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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/plugin-permission-common';
|
||||
import { PermissionRule } from '../types';
|
||||
import {
|
||||
createGetRule,
|
||||
isAndCriteria,
|
||||
isNotCriteria,
|
||||
isOrCriteria,
|
||||
} from './util';
|
||||
|
||||
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)),
|
||||
};
|
||||
} else if (isOrCriteria(criteria)) {
|
||||
return {
|
||||
anyOf: criteria.anyOf.map(child => mapConditions(child, getRule)),
|
||||
};
|
||||
} else if (isNotCriteria(criteria)) {
|
||||
return {
|
||||
not: mapConditions(criteria.not, getRule),
|
||||
};
|
||||
}
|
||||
|
||||
return getRule(criteria.rule).toQuery(...criteria.params);
|
||||
};
|
||||
|
||||
/**
|
||||
* A function which accepts {@link @backstage/plugin-permission-common#PermissionCondition}s
|
||||
* logically grouped in a {@link @backstage/plugin-permission-common#PermissionCriteria}
|
||||
* object, and transforms the {@link @backstage/plugin-permission-common#PermissionCondition}s
|
||||
* into plugin specific query fragments while retaining the enclosing criteria shape.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type ConditionTransformer<TQuery> = (
|
||||
conditions: PermissionCriteria<PermissionCondition>,
|
||||
) => PermissionCriteria<TQuery>;
|
||||
|
||||
/**
|
||||
* A higher-order helper function which accepts an array of
|
||||
* {@link PermissionRule}s, and returns a {@link ConditionTransformer}
|
||||
* which transforms input conditions into equivalent plugin-specific
|
||||
* query fragments using the supplied rules.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const createConditionTransformer = <
|
||||
TQuery,
|
||||
TRules extends PermissionRule<any, TQuery>[],
|
||||
>(
|
||||
permissionRules: [...TRules],
|
||||
): ConditionTransformer<TQuery> => {
|
||||
const getRule = createGetRule(permissionRules);
|
||||
|
||||
return conditions => mapConditions(conditions, getRule);
|
||||
};
|
||||
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
* 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 { AuthorizeResult } from '@backstage/plugin-permission-common';
|
||||
import express, { Express, Router } from 'express';
|
||||
import request from 'supertest';
|
||||
import { createPermissionIntegrationRouter } from './createPermissionIntegrationRouter';
|
||||
|
||||
const mockGetResource: jest.MockedFunction<
|
||||
(resourceRef: string) => Promise<any>
|
||||
> = jest.fn((resourceRef: string) =>
|
||||
Promise.resolve({
|
||||
resourceRef,
|
||||
}),
|
||||
);
|
||||
|
||||
const testRule1 = {
|
||||
name: 'test-rule-1',
|
||||
description: 'Test rule 1',
|
||||
apply: jest.fn(
|
||||
(_resource: any, _firstParam: string, _secondParam: number) => true,
|
||||
),
|
||||
toQuery: jest.fn(),
|
||||
};
|
||||
|
||||
const testRule2 = {
|
||||
name: 'test-rule-2',
|
||||
description: 'Test rule 2',
|
||||
apply: jest.fn((_firstParam: object) => false),
|
||||
toQuery: jest.fn(),
|
||||
};
|
||||
|
||||
describe('createPermissionIntegrationRouter', () => {
|
||||
let app: Express;
|
||||
let router: Router;
|
||||
|
||||
beforeEach(() => {
|
||||
router = createPermissionIntegrationRouter({
|
||||
resourceType: 'test-resource',
|
||||
getResource: mockGetResource,
|
||||
rules: [testRule1, testRule2],
|
||||
});
|
||||
|
||||
app = express().use(router);
|
||||
});
|
||||
|
||||
it('works', async () => {
|
||||
expect(router).toBeDefined();
|
||||
});
|
||||
|
||||
describe('POST /permissions/apply-conditions', () => {
|
||||
it.each([
|
||||
{ rule: 'test-rule-1', params: ['abc', 123] },
|
||||
{
|
||||
anyOf: [
|
||||
{ rule: 'test-rule-1', params: ['a', 1] },
|
||||
{ rule: 'test-rule-2', params: [{}] },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
not: { rule: 'test-rule-2', params: [{}] },
|
||||
},
|
||||
{
|
||||
allOf: [
|
||||
{
|
||||
anyOf: [
|
||||
{ rule: 'test-rule-1', params: ['a', 1] },
|
||||
{ rule: 'test-rule-2', params: [{}] },
|
||||
],
|
||||
},
|
||||
{
|
||||
not: {
|
||||
allOf: [
|
||||
{ rule: 'test-rule-1', params: ['b', 2] },
|
||||
{ rule: 'test-rule-2', params: [{ c: 3 }] },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
])('returns 200/ALLOW when criteria match (case %#)', async conditions => {
|
||||
const response = await request(app)
|
||||
.post('/permissions/apply-conditions')
|
||||
.send({
|
||||
resourceRef: 'default:test/resource',
|
||||
resourceType: 'test-resource',
|
||||
conditions,
|
||||
});
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual({ result: AuthorizeResult.ALLOW });
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ rule: 'test-rule-2', params: [{ foo: 0 }] },
|
||||
{
|
||||
allOf: [
|
||||
{ rule: 'test-rule-1', params: ['a', 1] },
|
||||
{ rule: 'test-rule-2', params: [{}] },
|
||||
],
|
||||
},
|
||||
{
|
||||
allOf: [
|
||||
{
|
||||
anyOf: [
|
||||
{ rule: 'test-rule-1', params: ['a', 1] },
|
||||
{ rule: 'test-rule-2', params: [{ b: 2 }] },
|
||||
],
|
||||
},
|
||||
{
|
||||
not: {
|
||||
allOf: [
|
||||
{ rule: 'test-rule-1', params: ['c', 3] },
|
||||
{ not: { rule: 'test-rule-2', params: [{ d: 4 }] } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
])(
|
||||
'returns 200/DENY when criteria do not match (case %#)',
|
||||
async conditions => {
|
||||
const response = await request(app)
|
||||
.post('/permissions/apply-conditions')
|
||||
.send({
|
||||
resourceRef: 'default:test/resource',
|
||||
resourceType: 'test-resource',
|
||||
conditions,
|
||||
});
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual({ result: AuthorizeResult.DENY });
|
||||
},
|
||||
);
|
||||
|
||||
it('returns 400 when called with incorrect resource type', async () => {
|
||||
const response = await request(app)
|
||||
.post('/permissions/apply-conditions')
|
||||
.send({
|
||||
resourceRef: 'default:test/resource',
|
||||
resourceType: 'test-incorrect-resource',
|
||||
conditions: {
|
||||
anyOf: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).toEqual(400);
|
||||
expect(response.error && response.error.text).toMatch(
|
||||
/unexpected resource type: test-incorrect-resource/i,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns 400 when resource is not found', async () => {
|
||||
mockGetResource.mockReturnValueOnce(Promise.resolve(undefined));
|
||||
|
||||
const response = await request(app)
|
||||
.post('/permissions/apply-conditions')
|
||||
.send({
|
||||
resourceRef: 'default:test/resource',
|
||||
resourceType: 'test-resource',
|
||||
conditions: {
|
||||
not: {
|
||||
rule: 'testRule1',
|
||||
params: ['a', 1],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).toEqual(400);
|
||||
expect(response.error && response.error.text).toMatch(
|
||||
/resource for ref default:test\/resource not found/i,
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
undefined,
|
||||
{},
|
||||
{ resourceType: 'test-resource-type' },
|
||||
{ resourceRef: 'test/resource-ref' },
|
||||
{
|
||||
resourceType: 'test-resource-type',
|
||||
resourceRef: 'test/resource-ref',
|
||||
},
|
||||
{ conditions: { anyOf: [] } },
|
||||
])(`returns 400 for invalid input %#`, async input => {
|
||||
const response = await request(app)
|
||||
.post('/permissions/apply-conditions')
|
||||
.send(input);
|
||||
|
||||
expect(response.status).toEqual(400);
|
||||
expect(response.error && response.error.text).toMatch(
|
||||
/invalid request body/i,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* 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 express, { Response, Router } from 'express';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
AuthorizeResult,
|
||||
PermissionCondition,
|
||||
PermissionCriteria,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
import { PermissionRule } from '../types';
|
||||
import {
|
||||
createGetRule,
|
||||
isAndCriteria,
|
||||
isNotCriteria,
|
||||
isOrCriteria,
|
||||
} from './util';
|
||||
|
||||
const permissionCriteriaSchema: z.ZodSchema<
|
||||
PermissionCriteria<PermissionCondition>
|
||||
> = z.lazy(() =>
|
||||
z.union([
|
||||
z.object({ anyOf: z.array(permissionCriteriaSchema) }),
|
||||
z.object({ allOf: z.array(permissionCriteriaSchema) }),
|
||||
z.object({ not: permissionCriteriaSchema }),
|
||||
z.object({
|
||||
rule: z.string(),
|
||||
params: z.array(z.unknown()),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
const applyConditionsRequestSchema = z.object({
|
||||
resourceRef: z.string(),
|
||||
resourceType: z.string(),
|
||||
conditions: permissionCriteriaSchema,
|
||||
});
|
||||
|
||||
/**
|
||||
* A request to load the referenced resource and apply conditions in order to
|
||||
* finalize a conditional authorization response.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type ApplyConditionsRequest = {
|
||||
resourceRef: string;
|
||||
resourceType: string;
|
||||
conditions: PermissionCriteria<PermissionCondition>;
|
||||
};
|
||||
|
||||
/**
|
||||
* The result of applying the conditions, expressed as a definitive authorize
|
||||
* result of ALLOW or DENY.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type ApplyConditionsResponse = {
|
||||
result: AuthorizeResult.ALLOW | AuthorizeResult.DENY;
|
||||
};
|
||||
|
||||
const applyConditions = <TResource>(
|
||||
criteria: PermissionCriteria<PermissionCondition>,
|
||||
resource: TResource,
|
||||
getRule: (name: string) => PermissionRule<TResource, unknown>,
|
||||
): boolean => {
|
||||
if (isAndCriteria(criteria)) {
|
||||
return criteria.allOf.every(child =>
|
||||
applyConditions(child, resource, getRule),
|
||||
);
|
||||
} else if (isOrCriteria(criteria)) {
|
||||
return criteria.anyOf.some(child =>
|
||||
applyConditions(child, resource, getRule),
|
||||
);
|
||||
} else if (isNotCriteria(criteria)) {
|
||||
return !applyConditions(criteria.not, resource, getRule);
|
||||
}
|
||||
|
||||
return getRule(criteria.rule).apply(resource, ...criteria.params);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create an express Router which provides an authorization route to allow integration between the
|
||||
* permission backend and other Backstage backend plugins. Plugin owners that wish to support
|
||||
* conditional authorization for their resources should add the router created by this function
|
||||
* to their express app inside their `createRouter` implementation.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* To make this concrete, we can use the Backstage software catalog as an example. The catalog has
|
||||
* conditional rules around access to specific _entities_ in the catalog. The _type_ of resource is
|
||||
* captured here as `resourceType`, a string identifier (`catalog-entity` in this example) that can
|
||||
* be provided with permission definitions. This is merely a _type_ to verify that conditions in an
|
||||
* authorization policy are constructed correctly, not a reference to a specific resource.
|
||||
*
|
||||
* The `rules` parameter is an array of {@link PermissionRule}s that introduce conditional
|
||||
* filtering logic for resources; for the catalog, these are things like `isEntityOwner` or
|
||||
* `hasAnnotation`. Rules describe how to filter a list of resources, and the `conditions` returned
|
||||
* allow these rules to be applied with specific parameters (such as 'group:default/team-a', or
|
||||
* 'backstage.io/edit-url').
|
||||
*
|
||||
* The `getResource` argument should load a resource by reference. For the catalog, this is an
|
||||
* {@link @backstage/catalog-model#EntityRef}. For other plugins, this can be any serialized format.
|
||||
* This is used to construct the `createPermissionIntegrationRouter`, a function to add an
|
||||
* authorization route to your backend plugin. This route will be called by the `permission-backend`
|
||||
* when authorization conditions relating to this plugin need to be evaluated.
|
||||
* @public
|
||||
*/
|
||||
export const createPermissionIntegrationRouter = <TResource>({
|
||||
resourceType,
|
||||
rules,
|
||||
getResource,
|
||||
}: {
|
||||
resourceType: string;
|
||||
rules: PermissionRule<TResource, any>[];
|
||||
getResource: (resourceRef: string) => Promise<TResource | undefined>;
|
||||
}): Router => {
|
||||
const router = Router();
|
||||
|
||||
const getRule = createGetRule(rules);
|
||||
|
||||
router.post(
|
||||
'/permissions/apply-conditions',
|
||||
express.json(),
|
||||
async (
|
||||
req,
|
||||
res: Response<
|
||||
| {
|
||||
result: Omit<AuthorizeResult, AuthorizeResult.CONDITIONAL>;
|
||||
}
|
||||
| string
|
||||
>,
|
||||
) => {
|
||||
const parseResult = applyConditionsRequestSchema.safeParse(req.body);
|
||||
|
||||
if (!parseResult.success) {
|
||||
return res.status(400).send(`Invalid request body.`);
|
||||
}
|
||||
|
||||
const { data: body } = parseResult;
|
||||
|
||||
if (body.resourceType !== resourceType) {
|
||||
return res
|
||||
.status(400)
|
||||
.send(`Unexpected resource type: ${body.resourceType}.`);
|
||||
}
|
||||
|
||||
const resource = await getResource(body.resourceRef);
|
||||
|
||||
if (!resource) {
|
||||
return res
|
||||
.status(400)
|
||||
.send(`Resource for ref ${body.resourceRef} not found.`);
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
result: applyConditions(body.conditions, resource, getRule)
|
||||
? AuthorizeResult.ALLOW
|
||||
: AuthorizeResult.DENY,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
return router;
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './createConditionFactory';
|
||||
export * from './createConditionExports';
|
||||
export * from './createConditionTransformer';
|
||||
export * from './createPermissionIntegrationRouter';
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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 {
|
||||
createGetRule,
|
||||
isAndCriteria,
|
||||
isNotCriteria,
|
||||
isOrCriteria,
|
||||
} from './util';
|
||||
|
||||
describe('permission integration utils', () => {
|
||||
describe('createGetRule', () => {
|
||||
let getRule: ReturnType<typeof createGetRule>;
|
||||
|
||||
const testRule1 = {
|
||||
name: 'test-rule-1',
|
||||
description: 'Test rule 1',
|
||||
apply: jest.fn(),
|
||||
toQuery: jest.fn(),
|
||||
};
|
||||
|
||||
const testRule2 = {
|
||||
name: 'test-rule-2',
|
||||
description: 'Test rule 2',
|
||||
apply: jest.fn(),
|
||||
toQuery: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
getRule = createGetRule([testRule1, testRule2]);
|
||||
});
|
||||
|
||||
it('returns the rule matching the supplied name', () => {
|
||||
expect(getRule('test-rule-1')).toBe(testRule1);
|
||||
});
|
||||
|
||||
it('throws if there is no rule for the supplied name', () => {
|
||||
expect(() => getRule('test-rule-3')).toThrowError(
|
||||
/unexpected permission rule/i,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isOrCriteria', () => {
|
||||
it('returns true if input has a top-level "anyOf" property', () => {
|
||||
expect(isOrCriteria({ anyOf: { not: { allOf: [] } } })).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns false if input does not have a top-level "anyOf" property', () => {
|
||||
expect(isOrCriteria({ allOf: { not: { anyOf: [] } } })).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAndCriteria', () => {
|
||||
it('returns true if input has a top-level "allOf" property', () => {
|
||||
expect(isAndCriteria({ allOf: { not: { anyOf: [] } } })).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns false if input does not have a top-level "allOf" property', () => {
|
||||
expect(isAndCriteria({ anyOf: { not: { allOf: [] } } })).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isNotCriteria', () => {
|
||||
it('returns true if input has a top-level "not" property', () => {
|
||||
expect(isNotCriteria({ not: { allOf: [{ anyOf: [] }] } })).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns false if input does not have a top-level "not" property', () => {
|
||||
expect(isNotCriteria({ anyOf: { not: { allOf: [] } } })).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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/plugin-permission-common';
|
||||
import { PermissionRule } from '../types';
|
||||
|
||||
export const isAndCriteria = (
|
||||
filter: PermissionCriteria<unknown>,
|
||||
): filter is { allOf: PermissionCriteria<unknown>[] } =>
|
||||
Object.prototype.hasOwnProperty.call(filter, 'allOf');
|
||||
|
||||
export const isOrCriteria = (
|
||||
filter: PermissionCriteria<unknown>,
|
||||
): filter is { anyOf: PermissionCriteria<unknown>[] } =>
|
||||
Object.prototype.hasOwnProperty.call(filter, 'anyOf');
|
||||
|
||||
export const isNotCriteria = (
|
||||
filter: PermissionCriteria<unknown>,
|
||||
): filter is { not: PermissionCriteria<unknown> } =>
|
||||
Object.prototype.hasOwnProperty.call(filter, 'not');
|
||||
|
||||
export const createGetRule = <TResource, TQuery>(
|
||||
rules: PermissionRule<TResource, TQuery>[],
|
||||
) => {
|
||||
const rulesMap = new Map(Object.values(rules).map(rule => [rule.name, rule]));
|
||||
|
||||
return (name: string): PermissionRule<TResource, TQuery> => {
|
||||
const rule = rulesMap.get(name);
|
||||
|
||||
if (!rule) {
|
||||
throw new Error(`Unexpected permission rule: ${name}`);
|
||||
}
|
||||
|
||||
return rule;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export type {
|
||||
ConditionalPolicyResult,
|
||||
PermissionPolicy,
|
||||
PolicyAuthorizeRequest,
|
||||
PolicyResult,
|
||||
} from './types';
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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 {
|
||||
AuthorizeRequest,
|
||||
AuthorizeResult,
|
||||
PermissionCondition,
|
||||
PermissionCriteria,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
import { BackstageIdentity } from '@backstage/plugin-auth-backend';
|
||||
|
||||
/**
|
||||
* An authorization request to be evaluated by the {@link PermissionPolicy}.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* This differs from {@link @backstage/permission-common#AuthorizeRequest} in that `resourceRef`
|
||||
* should never be provided. This forces policies to be written in a way that's compatible with
|
||||
* filtering collections of resources at data load time.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type PolicyAuthorizeRequest = Omit<AuthorizeRequest, 'resourceRef'>;
|
||||
|
||||
/**
|
||||
* A conditional result to an authorization request, returned by the {@link PermissionPolicy}.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* This indicates that the policy allows authorization for the request, given that the returned
|
||||
* conditions hold when evaluated. The conditions will be evaluated by the corresponding plugin
|
||||
* which knows about the referenced permission rules.
|
||||
*
|
||||
* Similar to {@link @backstage/permission-common#AuthorizeResult}, but with the plugin and resource
|
||||
* identifiers needed to evaluate the returned conditions.
|
||||
* @public
|
||||
*/
|
||||
export type ConditionalPolicyResult = {
|
||||
result: AuthorizeResult.CONDITIONAL;
|
||||
conditions: {
|
||||
pluginId: string;
|
||||
resourceType: string;
|
||||
conditions: PermissionCriteria<PermissionCondition>;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* The result of evaluating an authorization request with a {@link PermissionPolicy}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type PolicyResult =
|
||||
| { result: AuthorizeResult.ALLOW | AuthorizeResult.DENY }
|
||||
| ConditionalPolicyResult;
|
||||
|
||||
/**
|
||||
* A policy to evaluate authorization requests for any permissioned action performed in Backstage.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* This takes as input a permission and an optional Backstage identity, and should return ALLOW if
|
||||
* the user is permitted to execute that action; otherwise DENY. For permissions relating to
|
||||
* resources, such a catalog entities, a conditional response can also be returned. This states
|
||||
* that the action is allowed if the conditions provided hold true.
|
||||
*
|
||||
* Conditions are a rule, and parameters to evaluate against that rule. For example, the rule might
|
||||
* be `isOwner` and the parameters a collection of entityRefs; if one of the entityRefs matches
|
||||
* the `owner` field on a catalog entity, this would resolve to ALLOW.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface PermissionPolicy {
|
||||
handle(
|
||||
request: PolicyAuthorizeRequest,
|
||||
user?: BackstageIdentity,
|
||||
): Promise<PolicyResult>;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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 type { PermissionCriteria } from '@backstage/plugin-permission-common';
|
||||
|
||||
/**
|
||||
* A conditional rule that can be provided in an
|
||||
* {@link @backstage/permission-common#AuthorizeResult} response to an authorization request.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* Rules can either be evaluated against a resource loaded in memory, or used as filters when
|
||||
* loading a collection of resources from a data source. The `apply` and `toQuery` methods implement
|
||||
* these two concepts.
|
||||
*
|
||||
* The two operations should always have the same logical result. If they don’t, the effective
|
||||
* outcome of an authorization operation will sometimes differ depending on how the authorization
|
||||
* check was performed.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type PermissionRule<
|
||||
TResource,
|
||||
TQuery,
|
||||
TParams extends unknown[] = unknown[],
|
||||
> = {
|
||||
name: string;
|
||||
description: string;
|
||||
|
||||
/**
|
||||
* Apply this rule to a resource already loaded from a backing data source. The params are
|
||||
* arguments supplied for the rule; for example, a rule could be `isOwner` with entityRefs as the
|
||||
* params.
|
||||
*/
|
||||
apply(resource: TResource, ...params: TParams): boolean;
|
||||
|
||||
/**
|
||||
* Translate this rule to criteria suitable for use in querying a backing data store. The criteria
|
||||
* can be used for loading a collection of resources efficiently with conditional criteria already
|
||||
* applied.
|
||||
*/
|
||||
toQuery(...params: TParams): PermissionCriteria<TQuery>;
|
||||
};
|
||||
Reference in New Issue
Block a user