Add permission-node package

Signed-off-by: Tim Hansen <timbonicus@gmail.com>
Co-authored-by: Mike Lewis <mtlewis@users.noreply.github.com>
Co-authored-by: Himanshu Mishra <himanshu@orkohunter.net>
Co-authored-by: Joe Porpeglia <joeporpeglia@users.noreply.github.com>
Co-authored-by: Vincenzo Scamporlino <vinzscam@users.noreply.github.com>
This commit is contained in:
Tim Hansen
2021-11-10 16:00:21 -07:00
committed by Mike Lewis
parent a2e1a008bf
commit a1e2972dda
12 changed files with 607 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
};
+7
View File
@@ -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).
+126
View File
@@ -0,0 +1,126 @@
## 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 { AuthorizeResult } from '@backstage/permission-common';
import { BackstageIdentity } from '@backstage/plugin-auth-backend';
import { DefinitiveAuthorizeResult } from '@backstage/permission-common';
import { OpaqueAuthorizeRequest } from '@backstage/permission-common';
import { PermissionCondition } from '@backstage/permission-common';
import { PermissionCriteria } from '@backstage/permission-common';
import { Router } from 'express';
// Warning: (ae-missing-release-tag) "AllowAllPermissionPolicy" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export class AllowAllPermissionPolicy implements PermissionPolicy {
// (undocumented)
handle(
_request: OpaqueAuthorizeRequest,
_user?: BackstageIdentity,
): Promise<PolicyResult>;
}
// Warning: (ae-missing-release-tag) "ApplyConditionsRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type ApplyConditionsRequest = {
resourceRef: string;
resourceType: string;
conditions: PermissionCriteria<PermissionCondition<unknown[]>>;
};
// Warning: (ae-missing-release-tag) "ConditionalPolicyResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type ConditionalPolicyResult = {
result: AuthorizeResult.MAYBE;
conditions: {
pluginId: string;
resourceType: string;
conditions: PermissionCriteria<PermissionCondition>;
};
};
// Warning: (ae-missing-release-tag) "conditionFor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const conditionFor: <TParams extends any[]>(
rule: PermissionRule<any, any, TParams>,
) => (...params: TParams) => {
rule: string;
params: TParams;
};
// Warning: (ae-missing-release-tag) "createPermissionIntegration" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const createPermissionIntegration: <
TResource,
TRules extends {
[key: string]: PermissionRule<TResource, any, any>;
},
TGetResourceParams extends any[] = [],
>({
pluginId,
resourceType,
rules,
getResource,
}: {
pluginId: string;
resourceType: string;
rules: TRules;
getResource: (
resourceRef: string,
...params: TGetResourceParams
) => Promise<TResource | undefined>;
}) => {
createPermissionIntegrationRouter: (...params: TGetResourceParams) => Router;
toQuery: (
conditions: PermissionCriteria<PermissionCondition>,
) => PermissionCriteria<QueryType<TRules>>;
conditions: Conditions<TRules>;
createConditions: (conditions: PermissionCriteria<PermissionCondition>) => {
pluginId: string;
resourceType: string;
conditions: PermissionCriteria<PermissionCondition>;
};
registerPermissionRule: (
rule: PermissionRule<TResource, QueryType<TRules>, any>,
) => void;
};
// Warning: (ae-missing-release-tag) "PermissionPolicy" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface PermissionPolicy {
// (undocumented)
handle(
request: OpaqueAuthorizeRequest,
user?: BackstageIdentity,
): Promise<PolicyResult>;
}
// Warning: (ae-missing-release-tag) "PermissionRule" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type PermissionRule<TResource, TQuery, TParams extends any[] = any> = {
name: string;
description: string;
apply(resource: TResource, ...params: TParams): boolean;
toQuery(...params: TParams): TQuery | PermissionCriteria<TQuery>;
};
// Warning: (ae-missing-release-tag) "PolicyResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type PolicyResult = DefinitiveAuthorizeResult | ConditionalPolicyResult;
// Warnings were encountered during analysis:
//
// src/integration/createPermissionIntegration.d.ts:23:5 - (ae-forgotten-export) The symbol "QueryType" needs to be exported by the entry point index.d.ts
// src/integration/createPermissionIntegration.d.ts:24:5 - (ae-forgotten-export) The symbol "Conditions" needs to be exported by the entry point index.d.ts
// (No @packageDocumentation comment for this package)
```
+33
View File
@@ -0,0 +1,33 @@
{
"name": "@backstage/plugin-permission-node",
"description": "Common permission and authorization utilities for backend plugins",
"version": "0.1.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"
},
"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/permission-common": "^0.1.0",
"@backstage/plugin-auth-backend": "^0.4.6",
"@types/express": "*",
"express": "^4.17.1"
},
"devDependencies": {
"@backstage/cli": "^0.8.1"
},
"files": [
"dist"
]
}
+25
View File
@@ -0,0 +1,25 @@
/*
* 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,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.
*/
import { PermissionRule } from '../types';
export const conditionFor =
<TParams extends any[]>(rule: PermissionRule<any, any, TParams>) =>
(...params: TParams) => ({
rule: rule.name,
params,
});
@@ -0,0 +1,203 @@
/*
* 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,
PermissionCondition,
PermissionCriteria,
} from '@backstage/permission-common';
import express, { Response, Router } from 'express';
import { PermissionRule } from '../types';
import { conditionFor } from './conditionFor';
/**
* A request to load the referenced resource and apply conditions, to finalize a conditional
* authorization response.
* @public
*/
export type ApplyConditionsRequest = {
resourceRef: string;
resourceType: string;
conditions: PermissionCriteria<PermissionCondition<unknown>>;
};
type Condition<TRule> = TRule extends PermissionRule<any, any, infer TParams>
? (...params: TParams) => PermissionCondition<TParams>
: never;
type Conditions<TRules extends Record<string, PermissionRule<any, any>>> = {
[Name in keyof TRules]: Condition<TRules[Name]>;
};
type QueryType<TRules> = TRules extends Record<
string,
PermissionRule<any, infer TQuery, any>
>
? TQuery
: never;
// TODO(permission-node): this is no longer correct
function isPermissionCriteria(
criteria: unknown,
): criteria is PermissionCriteria<unknown> {
return Object.prototype.hasOwnProperty.call(criteria, 'anyOf');
}
export const createPermissionIntegration = <
TResource,
TRules extends { [key: string]: PermissionRule<TResource, any> },
TGetResourceParams extends any[] = [],
>({
pluginId,
resourceType,
rules,
getResource,
}: {
pluginId: string;
resourceType: string;
rules: TRules;
getResource: (
resourceRef: string,
...params: TGetResourceParams
) => Promise<TResource | undefined>;
}): {
createPermissionIntegrationRouter: (...params: TGetResourceParams) => Router;
toQuery: (
conditions: PermissionCriteria<PermissionCondition<QueryType<TRules>>>,
) => PermissionCriteria<QueryType<TRules>>;
conditions: Conditions<TRules>;
createConditions: (
conditions: PermissionCriteria<PermissionCondition<QueryType<TRules>>>,
) => {
pluginId: string;
resourceType: string;
conditions: PermissionCriteria<PermissionCondition<QueryType<TRules>>>;
};
registerPermissionRule: (
rule: PermissionRule<TResource, QueryType<TRules>>,
) => void;
} => {
const rulesMap = new Map(Object.values(rules).map(rule => [rule.name, rule]));
const getRule = (
name: string,
): PermissionRule<TResource, QueryType<TRules>> => {
const rule = rulesMap.get(name);
if (!rule) {
throw new Error(`Unexpected permission rule: ${name}`);
}
return rule;
};
return {
createPermissionIntegrationRouter: (
...getResourceParams: TGetResourceParams
) => {
const router = Router();
router.use('/permissions/', express.json());
router.post(
'/permissions/apply-conditions',
async (
req,
res: Response<Omit<AuthorizeResult, AuthorizeResult.CONDITIONAL>>,
) => {
// TODO(authorization-framework): validate input
const body = req.body as ApplyConditionsRequest;
if (body.resourceType !== resourceType) {
throw new Error(`Unexpected resource type: ${body.resourceType}`);
}
const resource = await getResource(
body.resourceRef,
...getResourceParams,
);
if (!resource) {
return res.status(400).end();
}
const resolveCriteria = (
criteria: PermissionCriteria<
PermissionCondition<QueryType<TRules>>
>,
): boolean => {
return criteria.anyOf.some(({ allOf }) =>
allOf.every(child =>
isPermissionCriteria(child)
? resolveCriteria(child)
: getRule(child.rule).apply(resource, ...child.params),
),
);
};
return res.status(200).json({
result: resolveCriteria(body.conditions)
? AuthorizeResult.ALLOW
: AuthorizeResult.DENY,
});
},
);
return router;
},
toQuery: (
conditions: PermissionCriteria<PermissionCondition<QueryType<TRules>>>,
): PermissionCriteria<QueryType<TRules>> => {
const mapCriteria = (
criteria: PermissionCriteria<PermissionCondition<QueryType<TRules>>>,
mapFn: (
condition: PermissionCondition<QueryType<TRules>>,
) => QueryType<TRules> | PermissionCriteria<QueryType<TRules>>,
): PermissionCriteria<QueryType<TRules>> => {
return {
anyOf: criteria.anyOf.map(({ allOf }) => ({
allOf: allOf.map(child =>
isPermissionCriteria(child)
? mapCriteria(child, mapFn)
: mapFn(child),
),
})),
};
};
return mapCriteria(conditions, condition =>
getRule(condition.rule).toQuery(...condition.params),
);
},
conditions: Object.entries(rules).reduce(
(acc, [key, rule]) => ({
...acc,
[key]: conditionFor(rule),
}),
{} as Conditions<TRules>,
),
createConditions: (
conditions: PermissionCriteria<PermissionCondition<QueryType<TRules>>>,
) => ({
pluginId,
resourceType,
conditions,
}),
registerPermissionRule: rule => {
rulesMap.set(rule.name, rule);
},
};
};
@@ -0,0 +1,18 @@
/*
* 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 './conditionFor';
export * from './createPermissionIntegration';
@@ -0,0 +1,21 @@
/*
* 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,
PolicyResult,
} from './types';
@@ -0,0 +1,81 @@
/*
* 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/permission-common';
import { BackstageIdentity } from '@backstage/plugin-auth-backend';
/**
* An authorization request to be evaluated by the {@link PermissionPolicy}.
*
* This differs from {@link 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}.
*
* 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 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<unknown>>;
};
};
/**
* 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.
*
* 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>;
}
+17
View File
@@ -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 {};
+49
View File
@@ -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 type { PermissionCriteria } from '@backstage/permission-common';
/**
* A conditional rule that can be provided in an {@link AuthorizeResult} response to an
* authorization request.
*
* 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 dont, 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 any[] = any> = {
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): TQuery | PermissionCriteria<TQuery>;
};