Rename permission backend request and response types

Signed-off-by: Joe Porpeglia <josephp@spotify.com>
This commit is contained in:
Joe Porpeglia
2022-03-22 10:24:08 -04:00
committed by Joe Porpeglia
parent 970814ed38
commit e43290ce96
14 changed files with 114 additions and 90 deletions
@@ -16,8 +16,8 @@
import { PermissionApi } from '@backstage/plugin-permission-react';
import {
AuthorizeDecision,
AuthorizeQuery,
EvaluatePermissionResponse,
EvaluatePermissionRequest,
AuthorizeResult,
} from '@backstage/plugin-permission-common';
@@ -31,12 +31,14 @@ import {
export class MockPermissionApi implements PermissionApi {
constructor(
private readonly requestHandler: (
request: AuthorizeQuery,
request: EvaluatePermissionRequest,
) => AuthorizeResult.ALLOW | AuthorizeResult.DENY = () =>
AuthorizeResult.ALLOW,
) {}
async authorize(request: AuthorizeQuery): Promise<AuthorizeDecision> {
async authorize(
request: EvaluatePermissionRequest,
): Promise<EvaluatePermissionResponse> {
return { result: this.requestHandler(request) };
}
}
@@ -30,11 +30,11 @@ import {
} from '@backstage/plugin-auth-node';
import {
AuthorizeResult,
AuthorizeDecision,
AuthorizeQuery,
EvaluatePermissionResponse,
EvaluatePermissionRequest,
IdentifiedPermissionMessage,
AuthorizeRequest,
AuthorizeResponse,
EvaluatePermissionRequestBatch,
EvaluatePermissionResponseBatch,
isResourcePermission,
PermissionAttributes,
} from '@backstage/plugin-permission-common';
@@ -73,17 +73,19 @@ const permissionSchema = z.union([
}),
]);
const querySchema: z.ZodSchema<IdentifiedPermissionMessage<AuthorizeQuery>> =
z.object({
id: z.string(),
resourceRef: z.string().optional(),
permission: permissionSchema,
});
const requestSchema: z.ZodSchema<AuthorizeRequest> = z.object({
items: z.array(querySchema),
const evaluatePermissionRequestSchema: z.ZodSchema<
IdentifiedPermissionMessage<EvaluatePermissionRequest>
> = z.object({
id: z.string(),
resourceRef: z.string().optional(),
permission: permissionSchema,
});
const evaluatePermissionRequestBatchSchema: z.ZodSchema<EvaluatePermissionRequestBatch> =
z.object({
items: z.array(evaluatePermissionRequestSchema),
});
/**
* Options required when constructing a new {@link express#Router} using
* {@link createRouter}.
@@ -99,12 +101,12 @@ export interface RouterOptions {
}
const handleRequest = async (
requests: IdentifiedPermissionMessage<AuthorizeQuery>[],
requests: IdentifiedPermissionMessage<EvaluatePermissionRequest>[],
user: BackstageIdentityResponse | undefined,
policy: PermissionPolicy,
permissionIntegrationClient: PermissionIntegrationClient,
authHeader?: string,
): Promise<IdentifiedPermissionMessage<AuthorizeDecision>[]> => {
): Promise<IdentifiedPermissionMessage<EvaluatePermissionResponse>[]> => {
const applyConditionsLoaderFor = memoize((pluginId: string) => {
return new DataLoader<
ApplyConditionsRequestEntry,
@@ -184,15 +186,17 @@ export async function createRouter(
router.post(
'/authorize',
async (
req: Request<AuthorizeRequest>,
res: Response<AuthorizeResponse>,
req: Request<EvaluatePermissionRequestBatch>,
res: Response<EvaluatePermissionResponseBatch>,
) => {
const token = getBearerTokenFromAuthorizationHeader(
req.header('authorization'),
);
const user = token ? await identity.authenticate(token) : undefined;
const parseResult = requestSchema.safeParse(req.body);
const parseResult = evaluatePermissionRequestBatchSchema.safeParse(
req.body,
);
if (!parseResult.success) {
throw new InputError(parseResult.error.toString());
@@ -19,7 +19,7 @@ import { setupServer } from 'msw/node';
import { ConfigReader } from '@backstage/config';
import { PermissionClient } from './PermissionClient';
import {
AuthorizeQuery,
EvaluatePermissionRequest,
AuthorizeResult,
IdentifiedPermissionMessage,
} from './types/api';
@@ -59,7 +59,7 @@ describe('PermissionClient', () => {
const mockAuthorizeHandler = jest.fn((req, res, { json }: RestContext) => {
const responses = req.body.items.map(
(a: IdentifiedPermissionMessage<AuthorizeQuery>) => ({
(a: IdentifiedPermissionMessage<EvaluatePermissionRequest>) => ({
id: a.id,
result: AuthorizeResult.ALLOW,
}),
@@ -147,7 +147,7 @@ describe('PermissionClient', () => {
mockAuthorizeHandler.mockImplementationOnce(
(req, res, { json }: RestContext) => {
const responses = req.body.items.map(
(a: IdentifiedPermissionMessage<AuthorizeQuery>) => ({
(a: IdentifiedPermissionMessage<EvaluatePermissionRequest>) => ({
id: a.id,
outcome: AuthorizeResult.ALLOW,
}),
@@ -165,7 +165,7 @@ describe('PermissionClient', () => {
mockAuthorizeHandler.mockImplementationOnce(
(req, res, { json }: RestContext) => {
const responses = req.body.map(
(a: IdentifiedPermissionMessage<AuthorizeQuery>) => ({
(a: IdentifiedPermissionMessage<EvaluatePermissionRequest>) => ({
id: a.id,
result: AuthorizeResult.DENY,
}),
@@ -189,7 +189,7 @@ describe('PermissionClient', () => {
mockAuthorizeHandler.mockImplementationOnce(
(req, res, { json }: RestContext) => {
const responses = req.body.map(
(a: IdentifiedPermissionMessage<AuthorizeQuery>) => ({
(a: IdentifiedPermissionMessage<EvaluatePermissionRequest>) => ({
id: a.id,
outcome: AuthorizeResult.DENY,
}),
@@ -21,13 +21,13 @@ import * as uuid from 'uuid';
import { z } from 'zod';
import {
AuthorizeResult,
AuthorizeQuery,
AuthorizeDecision,
EvaluatePermissionRequest,
EvaluatePermissionResponse,
IdentifiedPermissionMessage,
PermissionCriteria,
PermissionCondition,
AuthorizeResponse,
AuthorizeRequest,
EvaluatePermissionResponseBatch,
EvaluatePermissionRequestBatch,
} from './types/api';
import { DiscoveryApi } from './types/discovery';
import {
@@ -107,9 +107,9 @@ export class PermissionClient implements PermissionAuthorizer {
* @public
*/
async authorize(
queries: AuthorizeQuery[],
queries: EvaluatePermissionRequest[],
options?: AuthorizeRequestOptions,
): Promise<AuthorizeDecision[]> {
): Promise<EvaluatePermissionResponse[]> {
// 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
@@ -119,7 +119,7 @@ export class PermissionClient implements PermissionAuthorizer {
return queries.map(_ => ({ result: AuthorizeResult.ALLOW }));
}
const request: AuthorizeRequest = {
const request: EvaluatePermissionRequestBatch = {
items: queries.map(query => ({
id: uuid.v4(),
...query,
@@ -145,7 +145,7 @@ export class PermissionClient implements PermissionAuthorizer {
const responsesById = responseBody.items.reduce((acc, r) => {
acc[r.id] = r;
return acc;
}, {} as Record<string, IdentifiedPermissionMessage<AuthorizeDecision>>);
}, {} as Record<string, IdentifiedPermissionMessage<EvaluatePermissionResponse>>);
return request.items.map(query => responsesById[query.id]);
}
@@ -155,9 +155,9 @@ export class PermissionClient implements PermissionAuthorizer {
}
private assertValidResponse(
request: AuthorizeRequest,
request: EvaluatePermissionRequestBatch,
json: any,
): asserts json is AuthorizeResponse {
): asserts json is EvaluatePermissionResponseBatch {
const authorizedResponses = responseSchema.parse(json);
const responseIds = authorizedResponses.items.map(r => r.id);
const hasAllRequestIds = request.items.every(r =>
+21 -19
View File
@@ -90,21 +90,6 @@ export type PolicyDecision =
| DefinitivePolicyDecision
| ConditionalPolicyDecision;
/**
* An individual authorization request for {@link PermissionClient#authorize}.
* @public
*/
export type AuthorizeQuery = {
permission: Permission;
resourceRef?: string;
};
/**
* A batch of authorization requests from {@link PermissionClient#authorize}.
* @public
*/
export type AuthorizeRequest = PermissionMessageBatch<AuthorizeQuery>;
/**
* A condition returned with a CONDITIONAL authorization response.
*
@@ -159,10 +144,26 @@ export type PermissionCriteria<TQuery> =
| TQuery;
/**
* An individual authorization response from {@link PermissionClient#authorize}.
* An individual request sent to the permission backend.
* @public
*/
export type AuthorizeDecision =
export type EvaluatePermissionRequest = {
permission: Permission;
resourceRef?: string;
};
/**
* A batch of requests sent to the permission backend.
* @public
*/
export type EvaluatePermissionRequestBatch =
PermissionMessageBatch<EvaluatePermissionRequest>;
/**
* An individual response from the permission backend.
* @public
*/
export type EvaluatePermissionResponse =
| { result: AuthorizeResult.ALLOW | AuthorizeResult.DENY }
| {
result: AuthorizeResult.CONDITIONAL;
@@ -170,7 +171,8 @@ export type AuthorizeDecision =
};
/**
* A batch of authorization responses from {@link PermissionClient#authorize}.
* A batch of responses the permission backend.
* @public
*/
export type AuthorizeResponse = PermissionMessageBatch<AuthorizeDecision>;
export type EvaluatePermissionResponseBatch =
PermissionMessageBatch<EvaluatePermissionResponse>;
+4 -4
View File
@@ -16,10 +16,10 @@
export { AuthorizeResult } from './api';
export type {
AuthorizeQuery,
AuthorizeRequest,
AuthorizeDecision,
AuthorizeResponse,
EvaluatePermissionRequest,
EvaluatePermissionRequestBatch,
EvaluatePermissionResponse,
EvaluatePermissionResponseBatch,
IdentifiedPermissionMessage,
PermissionMessageBatch,
ConditionalPolicyDecision,
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { AuthorizeQuery, AuthorizeDecision } from './api';
import { EvaluatePermissionRequest, EvaluatePermissionResponse } from './api';
/**
* The attributes related to a given permission; these should be generic and widely applicable to
@@ -94,9 +94,9 @@ export type ResourcePermission<TResourceType extends string = string> =
*/
export interface PermissionAuthorizer {
authorize(
queries: AuthorizeQuery[],
requests: EvaluatePermissionRequest[],
options?: AuthorizeRequestOptions,
): Promise<AuthorizeDecision[]>;
): Promise<EvaluatePermissionResponse[]>;
}
/**
@@ -17,7 +17,7 @@
import { ServerPermissionClient } from './ServerPermissionClient';
import {
IdentifiedPermissionMessage,
AuthorizeQuery,
EvaluatePermissionRequest,
AuthorizeResult,
createPermission,
} from '@backstage/plugin-permission-common';
@@ -33,7 +33,7 @@ import { RestContext, rest } from 'msw';
const server = setupServer();
const mockAuthorizeHandler = jest.fn((req, res, { json }: RestContext) => {
const responses = req.body.items.map(
(r: IdentifiedPermissionMessage<AuthorizeQuery>) => ({
(r: IdentifiedPermissionMessage<EvaluatePermissionRequest>) => ({
id: r.id,
result: AuthorizeResult.ALLOW,
}),
@@ -20,9 +20,9 @@ import {
} from '@backstage/backend-common';
import { Config } from '@backstage/config';
import {
AuthorizeQuery,
EvaluatePermissionRequest,
AuthorizeRequestOptions,
AuthorizeDecision,
EvaluatePermissionResponse,
AuthorizeResult,
PermissionClient,
PermissionAuthorizer,
@@ -78,9 +78,9 @@ export class ServerPermissionClient implements PermissionAuthorizer {
}
async authorize(
queries: AuthorizeQuery[],
requests: EvaluatePermissionRequest[],
options?: AuthorizeRequestOptions,
): Promise<AuthorizeDecision[]> {
): Promise<EvaluatePermissionResponse[]> {
// Check if permissions are enabled before validating the server token. That
// way when permissions are disabled, the noop token manager can be used
// without fouling up the logic inside the ServerPermissionClient, because
@@ -89,9 +89,9 @@ export class ServerPermissionClient implements PermissionAuthorizer {
!this.permissionEnabled ||
(await this.isValidServerToken(options?.token))
) {
return queries.map(_ => ({ result: AuthorizeResult.ALLOW }));
return requests.map(_ => ({ result: AuthorizeResult.ALLOW }));
}
return this.permissionClient.authorize(queries, options);
return this.permissionClient.authorize(requests, options);
}
private async isValidServerToken(
+5 -2
View File
@@ -15,7 +15,7 @@
*/
import {
AuthorizeQuery,
EvaluatePermissionRequest,
PolicyDecision,
} from '@backstage/plugin-permission-common';
import { BackstageIdentityResponse } from '@backstage/plugin-auth-node';
@@ -31,7 +31,10 @@ import { BackstageIdentityResponse } from '@backstage/plugin-auth-node';
*
* @public
*/
export type PolicyAuthorizeQuery = Omit<AuthorizeQuery, 'resourceRef'>;
export type PolicyAuthorizeQuery = Omit<
EvaluatePermissionRequest,
'resourceRef'
>;
/**
* A policy to evaluate authorization requests for any permissioned action performed in Backstage.
@@ -17,8 +17,8 @@
import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
import { PermissionApi } from './PermissionApi';
import {
AuthorizeQuery,
AuthorizeDecision,
EvaluatePermissionRequest,
EvaluatePermissionResponse,
PermissionClient,
} from '@backstage/plugin-permission-common';
import { Config } from '@backstage/config';
@@ -44,7 +44,9 @@ export class IdentityPermissionApi implements PermissionApi {
return new IdentityPermissionApi(permissionClient, identity);
}
async authorize(request: AuthorizeQuery): Promise<AuthorizeDecision> {
async authorize(
request: EvaluatePermissionRequest,
): Promise<EvaluatePermissionResponse> {
const response = await this.permissionClient.authorize(
[request],
await this.identityApi.getCredentials(),
@@ -15,8 +15,8 @@
*/
import {
AuthorizeQuery,
AuthorizeDecision,
EvaluatePermissionRequest,
EvaluatePermissionResponse,
} from '@backstage/plugin-permission-common';
import { ApiRef, createApiRef } from '@backstage/core-plugin-api';
@@ -27,7 +27,9 @@ import { ApiRef, createApiRef } from '@backstage/core-plugin-api';
* @public
*/
export type PermissionApi = {
authorize(request: AuthorizeQuery): Promise<AuthorizeDecision>;
authorize(
request: EvaluatePermissionRequest,
): Promise<EvaluatePermissionResponse>;
};
/**
@@ -16,7 +16,7 @@
import { ConfigReader } from '@backstage/config';
import {
AuthorizeDecision,
EvaluatePermissionResponse,
AuthorizeResult,
createPermission,
PermissionAuthorizer,
@@ -243,7 +243,7 @@ describe('AuthorizedSearchEngine', () => {
}
return {
result: AuthorizeResult.CONDITIONAL,
} as AuthorizeDecision;
} as EvaluatePermissionResponse;
}
return {
@@ -294,7 +294,7 @@ describe('AuthorizedSearchEngine', () => {
return {
result: AuthorizeResult.CONDITIONAL,
} as AuthorizeDecision;
} as EvaluatePermissionResponse;
}),
);
@@ -336,7 +336,9 @@ describe('AuthorizedSearchEngine', () => {
result: AuthorizeResult.ALLOW,
};
}
return { result: AuthorizeResult.CONDITIONAL } as AuthorizeDecision;
return {
result: AuthorizeResult.CONDITIONAL,
} as EvaluatePermissionResponse;
}),
);
@@ -413,7 +415,9 @@ describe('AuthorizedSearchEngine', () => {
: AuthorizeResult.ALLOW,
};
}
return { result: AuthorizeResult.CONDITIONAL } as AuthorizeDecision;
return {
result: AuthorizeResult.CONDITIONAL,
} as EvaluatePermissionResponse;
}),
);
@@ -495,7 +499,9 @@ describe('AuthorizedSearchEngine', () => {
if (query.resourceRef) {
return { result: AuthorizeResult.ALLOW };
}
return { result: AuthorizeResult.CONDITIONAL } as AuthorizeDecision;
return {
result: AuthorizeResult.CONDITIONAL,
} as EvaluatePermissionResponse;
}),
);
@@ -18,8 +18,8 @@ import { compact, zipObject } from 'lodash';
import qs from 'qs';
import DataLoader from 'dataloader';
import {
AuthorizeDecision,
AuthorizeQuery,
EvaluatePermissionResponse,
EvaluatePermissionRequest,
AuthorizeResult,
isResourcePermission,
PermissionAuthorizer,
@@ -90,7 +90,7 @@ export class AuthorizedSearchEngine implements SearchEngine {
const queryStartTime = Date.now();
const authorizer = new DataLoader(
(requests: readonly AuthorizeQuery[]) =>
(requests: readonly EvaluatePermissionRequest[]) =>
this.permissions.authorize(requests.slice(), options),
{
// Serialize the permission name and resourceRef as
@@ -185,8 +185,11 @@ export class AuthorizedSearchEngine implements SearchEngine {
private async filterResults(
results: IndexableResult[],
typeDecisions: Record<string, AuthorizeDecision>,
authorizer: DataLoader<AuthorizeQuery, AuthorizeDecision>,
typeDecisions: Record<string, EvaluatePermissionResponse>,
authorizer: DataLoader<
EvaluatePermissionRequest,
EvaluatePermissionResponse
>,
) {
return compact(
await Promise.all(