Merge pull request #7942 from backstage/permission-common
Add permission-common package
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-common
|
||||
|
||||
> NOTE: THIS PACKAGE IS EXPERIMENTAL, HERE BE DRAGONS
|
||||
|
||||
Isomorphic types and client for Backstage permissions and authorization. For
|
||||
more information, see the [authorization
|
||||
PRFC](https://github.com/backstage/backstage/pull/7761).
|
||||
@@ -0,0 +1,95 @@
|
||||
## API Report File for "@backstage/plugin-permission-common"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
// @public
|
||||
export type AuthorizeRequest = {
|
||||
permission: Permission;
|
||||
resourceRef?: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type AuthorizeRequestOptions = {
|
||||
token?: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type AuthorizeResponse =
|
||||
| {
|
||||
result: AuthorizeResult.ALLOW | AuthorizeResult.DENY;
|
||||
}
|
||||
| {
|
||||
result: AuthorizeResult.CONDITIONAL;
|
||||
conditions: PermissionCriteria<PermissionCondition>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export enum AuthorizeResult {
|
||||
ALLOW = 'ALLOW',
|
||||
CONDITIONAL = 'CONDITIONAL',
|
||||
DENY = 'DENY',
|
||||
}
|
||||
|
||||
// @public
|
||||
export type DiscoveryApi = {
|
||||
getBaseUrl(pluginId: string): Promise<string>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type Identified<T> = T & {
|
||||
id: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export function isCreatePermission(permission: Permission): boolean;
|
||||
|
||||
// @public
|
||||
export function isDeletePermission(permission: Permission): boolean;
|
||||
|
||||
// @public
|
||||
export function isReadPermission(permission: Permission): boolean;
|
||||
|
||||
// @public
|
||||
export function isUpdatePermission(permission: Permission): boolean;
|
||||
|
||||
// @public
|
||||
export type Permission = {
|
||||
name: string;
|
||||
attributes: PermissionAttributes;
|
||||
resourceType?: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type PermissionAttributes = {
|
||||
action?: 'create' | 'read' | 'update' | 'delete';
|
||||
};
|
||||
|
||||
// @public
|
||||
export class PermissionClient {
|
||||
constructor(options: { discoveryApi: DiscoveryApi; enabled?: boolean });
|
||||
authorize(
|
||||
requests: AuthorizeRequest[],
|
||||
options?: AuthorizeRequestOptions,
|
||||
): Promise<AuthorizeResponse[]>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type PermissionCondition<TParams extends unknown[] = unknown[]> = {
|
||||
rule: string;
|
||||
params: TParams;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type PermissionCriteria<TQuery> =
|
||||
| {
|
||||
allOf: PermissionCriteria<TQuery>[];
|
||||
}
|
||||
| {
|
||||
anyOf: PermissionCriteria<TQuery>[];
|
||||
}
|
||||
| {
|
||||
not: PermissionCriteria<TQuery>;
|
||||
}
|
||||
| TQuery;
|
||||
```
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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 interface Config {
|
||||
/** Configuration options for Backstage permissions and authorization */
|
||||
permission?: {
|
||||
/**
|
||||
* Whether authorization is enabled in Backstage. Defaults to false, which means authorization
|
||||
* requests will be automatically allowed without invoking the authorization policy.
|
||||
* @visibility frontend
|
||||
*/
|
||||
enabled?: boolean;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "@backstage/plugin-permission-common",
|
||||
"description": "Isomorphic types and client for Backstage permissions and authorization",
|
||||
"version": "0.1.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"module": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"homepage": "https://backstage.io",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/backstage/backstage",
|
||||
"directory": "plugins/permission-common"
|
||||
},
|
||||
"keywords": [
|
||||
"backstage",
|
||||
"permissions"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"files": [
|
||||
"dist",
|
||||
"config.d.ts"
|
||||
],
|
||||
"configSchema": "config.d.ts",
|
||||
"scripts": {
|
||||
"build": "backstage-cli build",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test",
|
||||
"prepack": "backstage-cli prepack",
|
||||
"postpack": "backstage-cli postpack",
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/backstage/backstage/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/errors": "^0.1.2",
|
||||
"cross-fetch": "^3.0.6",
|
||||
"uuid": "^8.0.0",
|
||||
"zod": "^3.11.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.8.0",
|
||||
"@types/jest": "^26.0.7",
|
||||
"msw": "^0.35.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* 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 { RestContext, rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { PermissionClient } from './PermissionClient';
|
||||
import { AuthorizeRequest, AuthorizeResult, Identified } from './types/api';
|
||||
import { DiscoveryApi } from './types/discovery';
|
||||
import { Permission } from './types/permission';
|
||||
|
||||
const server = setupServer();
|
||||
const token = 'fake-token';
|
||||
|
||||
const mockBaseUrl = 'http://backstage:9191/i-am-a-mock-base';
|
||||
const discoveryApi: DiscoveryApi = {
|
||||
async getBaseUrl() {
|
||||
return mockBaseUrl;
|
||||
},
|
||||
};
|
||||
const client: PermissionClient = new PermissionClient({
|
||||
discoveryApi,
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const mockPermission: Permission = {
|
||||
name: 'test.permission',
|
||||
attributes: {},
|
||||
resourceType: 'test-resource',
|
||||
};
|
||||
|
||||
const mockAuthorizeRequest = {
|
||||
permission: mockPermission,
|
||||
resourceRef: 'foo',
|
||||
};
|
||||
|
||||
describe('PermissionClient', () => {
|
||||
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
|
||||
afterAll(() => server.close());
|
||||
afterEach(() => server.resetHandlers());
|
||||
|
||||
describe('authorize', () => {
|
||||
const mockAuthorizeHandler = jest.fn((req, res, { json }: RestContext) => {
|
||||
const responses = req.body.map((a: Identified<AuthorizeRequest>) => ({
|
||||
id: a.id,
|
||||
result: AuthorizeResult.ALLOW,
|
||||
}));
|
||||
|
||||
return res(json(responses));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
server.use(rest.post(`${mockBaseUrl}/authorize`, mockAuthorizeHandler));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should fetch entities from correct endpoint', async () => {
|
||||
await client.authorize([mockAuthorizeRequest]);
|
||||
expect(mockAuthorizeHandler).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should include a request body', async () => {
|
||||
await client.authorize([mockAuthorizeRequest]);
|
||||
|
||||
const request = mockAuthorizeHandler.mock.calls[0][0];
|
||||
expect(request.body[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
permission: mockPermission,
|
||||
resourceRef: 'foo',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return the response from the fetch request', async () => {
|
||||
const response = await client.authorize([mockAuthorizeRequest]);
|
||||
expect(response[0]).toEqual(
|
||||
expect.objectContaining({ result: AuthorizeResult.ALLOW }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not include authorization headers if no token is supplied', async () => {
|
||||
await client.authorize([mockAuthorizeRequest]);
|
||||
|
||||
const request = mockAuthorizeHandler.mock.calls[0][0];
|
||||
expect(request.headers.has('authorization')).toEqual(false);
|
||||
});
|
||||
|
||||
it('should include correctly-constructed authorization header if token is supplied', async () => {
|
||||
await client.authorize([mockAuthorizeRequest], { token });
|
||||
|
||||
const request = mockAuthorizeHandler.mock.calls[0][0];
|
||||
expect(request.headers.get('authorization')).toEqual('Bearer fake-token');
|
||||
});
|
||||
|
||||
it('should forward response errors', async () => {
|
||||
mockAuthorizeHandler.mockImplementationOnce(
|
||||
(_req, res, { status }: RestContext) => {
|
||||
return res(status(401));
|
||||
},
|
||||
);
|
||||
await expect(
|
||||
client.authorize([mockAuthorizeRequest], { token }),
|
||||
).rejects.toThrowError(/request failed with 401/i);
|
||||
});
|
||||
|
||||
it('should reject responses with missing ids', async () => {
|
||||
mockAuthorizeHandler.mockImplementationOnce(
|
||||
(_req, res, { json }: RestContext) => {
|
||||
return res(json([{ id: 'wrong-id', result: AuthorizeResult.ALLOW }]));
|
||||
},
|
||||
);
|
||||
await expect(
|
||||
client.authorize([mockAuthorizeRequest], { token }),
|
||||
).rejects.toThrowError(/Unexpected authorization response/i);
|
||||
});
|
||||
|
||||
it('should reject invalid responses', async () => {
|
||||
mockAuthorizeHandler.mockImplementationOnce(
|
||||
(req, res, { json }: RestContext) => {
|
||||
const responses = req.body.map((a: Identified<AuthorizeRequest>) => ({
|
||||
id: a.id,
|
||||
outcome: AuthorizeResult.ALLOW,
|
||||
}));
|
||||
|
||||
return res(json(responses));
|
||||
},
|
||||
);
|
||||
await expect(
|
||||
client.authorize([mockAuthorizeRequest], { token }),
|
||||
).rejects.toThrowError(/invalid input/i);
|
||||
});
|
||||
|
||||
it('should allow all when authorization is not enabled', async () => {
|
||||
mockAuthorizeHandler.mockImplementationOnce(
|
||||
(req, res, { json }: RestContext) => {
|
||||
const responses = req.body.map((a: Identified<AuthorizeRequest>) => ({
|
||||
id: a.id,
|
||||
outcome: AuthorizeResult.DENY,
|
||||
}));
|
||||
|
||||
return res(json(responses));
|
||||
},
|
||||
);
|
||||
const disabled = new PermissionClient({ discoveryApi, enabled: false });
|
||||
const response = await disabled.authorize([mockAuthorizeRequest]);
|
||||
expect(response[0]).toEqual(
|
||||
expect.objectContaining({ result: AuthorizeResult.ALLOW }),
|
||||
);
|
||||
expect(mockAuthorizeHandler).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* 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 { ResponseError } from '@backstage/errors';
|
||||
import fetch from 'cross-fetch';
|
||||
import * as uuid from 'uuid';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
AuthorizeResult,
|
||||
AuthorizeRequest,
|
||||
AuthorizeResponse,
|
||||
Identified,
|
||||
PermissionCriteria,
|
||||
PermissionCondition,
|
||||
} from './types/api';
|
||||
import { DiscoveryApi } from './types/discovery';
|
||||
|
||||
const permissionCriteriaSchema: z.ZodSchema<
|
||||
PermissionCriteria<PermissionCondition>
|
||||
> = z.lazy(() =>
|
||||
z
|
||||
.object({
|
||||
rule: z.string(),
|
||||
params: z.array(z.unknown()),
|
||||
})
|
||||
.or(z.object({ anyOf: z.array(permissionCriteriaSchema) }))
|
||||
.or(z.object({ allOf: z.array(permissionCriteriaSchema) }))
|
||||
.or(z.object({ not: permissionCriteriaSchema })),
|
||||
);
|
||||
|
||||
const responseSchema = z.array(
|
||||
z
|
||||
.object({
|
||||
id: z.string(),
|
||||
result: z
|
||||
.literal(AuthorizeResult.ALLOW)
|
||||
.or(z.literal(AuthorizeResult.DENY)),
|
||||
})
|
||||
.or(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
result: z.literal(AuthorizeResult.CONDITIONAL),
|
||||
conditions: permissionCriteriaSchema,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Options for authorization requests; currently only an optional auth token.
|
||||
* @public
|
||||
*/
|
||||
export type AuthorizeRequestOptions = {
|
||||
token?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* An isomorphic client for requesting authorization for Backstage permissions.
|
||||
* @public
|
||||
*/
|
||||
export class PermissionClient {
|
||||
private readonly enabled: boolean;
|
||||
private readonly discoveryApi: DiscoveryApi;
|
||||
|
||||
constructor(options: { discoveryApi: DiscoveryApi; enabled?: boolean }) {
|
||||
this.discoveryApi = options.discoveryApi;
|
||||
this.enabled = options.enabled ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request authorization from the permission-backend for the given set of permissions.
|
||||
*
|
||||
* Authorization requests check that a given Backstage user can perform a protected operation,
|
||||
* potentially for a specific resource (such as a catalog entity). The Backstage identity token
|
||||
* should be included in the `options` if available.
|
||||
*
|
||||
* Permissions can be imported from plugins exposing them, such as `catalogEntityReadPermission`.
|
||||
*
|
||||
* The response will be either ALLOW or DENY when either the permission has no resourceType, or a
|
||||
* resourceRef is provided in the request. For permissions with a resourceType, CONDITIONAL may be
|
||||
* returned if no resourceRef is provided in the request. Conditional responses are intended only
|
||||
* for backends which have access to the data source for permissioned resources, so that filters
|
||||
* can be applied when loading collections of resources.
|
||||
* @public
|
||||
*/
|
||||
async authorize(
|
||||
requests: AuthorizeRequest[],
|
||||
options?: AuthorizeRequestOptions,
|
||||
): Promise<AuthorizeResponse[]> {
|
||||
// TODO(permissions): it would be great to provide some kind of typing guarantee that
|
||||
// conditional responses will only ever be returned for requests containing a resourceType
|
||||
// but no resourceRef. That way clients who aren't prepared to handle filtering according
|
||||
// to conditions can be guaranteed that they won't unexpectedly get a CONDITIONAL response.
|
||||
|
||||
if (!this.enabled) {
|
||||
return requests.map(_ => ({ result: AuthorizeResult.ALLOW }));
|
||||
}
|
||||
|
||||
const identifiedRequests: Identified<AuthorizeRequest>[] = requests.map(
|
||||
request => ({
|
||||
id: uuid.v4(),
|
||||
...request,
|
||||
}),
|
||||
);
|
||||
|
||||
const permissionApi = await this.discoveryApi.getBaseUrl('permission');
|
||||
const response = await fetch(`${permissionApi}/authorize`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(identifiedRequests),
|
||||
headers: {
|
||||
...this.getAuthorizationHeader(options?.token),
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw await ResponseError.fromResponse(response);
|
||||
}
|
||||
|
||||
const identifiedResponses = await response.json();
|
||||
this.assertValidResponses(identifiedRequests, identifiedResponses);
|
||||
|
||||
const responsesById = identifiedResponses.reduce((acc, r) => {
|
||||
acc[r.id] = r;
|
||||
return acc;
|
||||
}, {} as Record<string, Identified<AuthorizeResponse>>);
|
||||
|
||||
return identifiedRequests.map(request => responsesById[request.id]);
|
||||
}
|
||||
|
||||
private getAuthorizationHeader(token?: string): Record<string, string> {
|
||||
return token ? { Authorization: `Bearer ${token}` } : {};
|
||||
}
|
||||
|
||||
private assertValidResponses(
|
||||
requests: Identified<AuthorizeRequest>[],
|
||||
json: any,
|
||||
): asserts json is Identified<AuthorizeResponse>[] {
|
||||
const authorizedResponses = responseSchema.parse(json);
|
||||
const responseIds = authorizedResponses.map(r => r.id);
|
||||
const hasAllRequestIds = requests.every(r => responseIds.includes(r.id));
|
||||
if (!hasAllRequestIds) {
|
||||
throw new Error(
|
||||
'Unexpected authorization response from permission-backend',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Isomorphic types and client for Backstage permissions and authorization
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
export * from './types';
|
||||
export * from './permissions';
|
||||
export * from './PermissionClient';
|
||||
@@ -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 * from './util';
|
||||
@@ -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 { Permission } from '../types';
|
||||
|
||||
/**
|
||||
* Check if a given permission is related to a create action.
|
||||
* @public
|
||||
*/
|
||||
export function isCreatePermission(permission: Permission) {
|
||||
return permission.attributes.action === 'create';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given permission is related to a read action.
|
||||
* @public
|
||||
*/
|
||||
export function isReadPermission(permission: Permission) {
|
||||
return permission.attributes.action === 'read';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given permission is related to an update action.
|
||||
* @public
|
||||
*/
|
||||
export function isUpdatePermission(permission: Permission) {
|
||||
return permission.attributes.action === 'update';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given permission is related to a delete action.
|
||||
* @public
|
||||
*/
|
||||
export function isDeletePermission(permission: Permission) {
|
||||
return permission.attributes.action === 'delete';
|
||||
}
|
||||
@@ -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 { Permission } from './permission';
|
||||
|
||||
/**
|
||||
* A request with a UUID identifier, so that batched responses can be matched up with the original
|
||||
* requests.
|
||||
* @public
|
||||
*/
|
||||
export type Identified<T> = T & { id: string };
|
||||
|
||||
/**
|
||||
* The result of an authorization request.
|
||||
* @public
|
||||
*/
|
||||
export enum AuthorizeResult {
|
||||
/**
|
||||
* The authorization request is denied.
|
||||
*/
|
||||
DENY = 'DENY',
|
||||
/**
|
||||
* The authorization request is allowed.
|
||||
*/
|
||||
ALLOW = 'ALLOW',
|
||||
/**
|
||||
* The authorization request is allowed if the provided conditions are met.
|
||||
*/
|
||||
CONDITIONAL = 'CONDITIONAL',
|
||||
}
|
||||
|
||||
/**
|
||||
* An authorization request for {@link PermissionClient#authorize}.
|
||||
* @public
|
||||
*/
|
||||
export type AuthorizeRequest = {
|
||||
permission: Permission;
|
||||
resourceRef?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A condition returned with a CONDITIONAL authorization response.
|
||||
*
|
||||
* Conditions are a reference to a rule defined by a plugin, and parameters to apply the rule. For
|
||||
* example, a rule might be `isOwner` from the catalog-backend, and params may be a list of entity
|
||||
* claims from a identity token.
|
||||
* @public
|
||||
*/
|
||||
export type PermissionCondition<TParams extends unknown[] = unknown[]> = {
|
||||
rule: string;
|
||||
params: TParams;
|
||||
};
|
||||
|
||||
/**
|
||||
* Composes several {@link PermissionCondition}s as criteria with a nested AND/OR structure.
|
||||
* @public
|
||||
*/
|
||||
export type PermissionCriteria<TQuery> =
|
||||
| { allOf: PermissionCriteria<TQuery>[] }
|
||||
| { anyOf: PermissionCriteria<TQuery>[] }
|
||||
| { not: PermissionCriteria<TQuery> }
|
||||
| TQuery;
|
||||
|
||||
/**
|
||||
* An authorization response from {@link PermissionClient#authorize}.
|
||||
* @public
|
||||
*/
|
||||
export type AuthorizeResponse =
|
||||
| { result: AuthorizeResult.ALLOW | AuthorizeResult.DENY }
|
||||
| {
|
||||
result: AuthorizeResult.CONDITIONAL;
|
||||
conditions: PermissionCriteria<PermissionCondition>;
|
||||
};
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This is a copy of the core DiscoveryApi, to avoid importing core.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type DiscoveryApi = {
|
||||
getBaseUrl(pluginId: string): Promise<string>;
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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 { AuthorizeResult } from './api';
|
||||
export type {
|
||||
AuthorizeRequest,
|
||||
AuthorizeResponse,
|
||||
Identified,
|
||||
PermissionCondition,
|
||||
PermissionCriteria,
|
||||
} from './api';
|
||||
export type { DiscoveryApi } from './discovery';
|
||||
export type { PermissionAttributes, Permission } from './permission';
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The attributes related to a given permission; these should be generic and widely applicable to
|
||||
* all permissions in the system.
|
||||
* @public
|
||||
*/
|
||||
export type PermissionAttributes = {
|
||||
action?: 'create' | 'read' | 'update' | 'delete';
|
||||
};
|
||||
|
||||
/**
|
||||
* A permission that can be checked through authorization.
|
||||
*
|
||||
* Permissions are the "what" part of authorization, the action to be performed. This may be reading
|
||||
* an entity from the catalog, executing a software template, or any other action a plugin author
|
||||
* may wish to protect.
|
||||
*
|
||||
* To evaluate authorization, a permission is paired with a Backstage identity (the "who") and
|
||||
* evaluated using an authorization policy.
|
||||
* @public
|
||||
*/
|
||||
export type Permission = {
|
||||
name: string;
|
||||
attributes: PermissionAttributes;
|
||||
resourceType?: string;
|
||||
};
|
||||
@@ -29630,7 +29630,7 @@ zip-stream@^4.1.0:
|
||||
compress-commons "^4.1.0"
|
||||
readable-stream "^3.6.0"
|
||||
|
||||
zod@^3.9.5:
|
||||
zod@^3.11.6, zod@^3.9.5:
|
||||
version "3.11.6"
|
||||
resolved "https://registry.npmjs.org/zod/-/zod-3.11.6.tgz#e43a5e0c213ae2e02aefe7cb2b1a6fa3d7f1f483"
|
||||
integrity sha512-daZ80A81I3/9lIydI44motWe6n59kRBfNzTuS2bfzVh1nAXi667TOTWWtatxyG+fwgNUiagSj/CWZwRRbevJIg==
|
||||
|
||||
Reference in New Issue
Block a user