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:
@@ -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>;
|
||||
}
|
||||
@@ -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,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 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 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>;
|
||||
};
|
||||
Reference in New Issue
Block a user