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
+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>;
};