permissions: rename authorize request and response types to avoid envelope suffix

Signed-off-by: MT Lewis <mtlewis@users.noreply.github.com>
This commit is contained in:
MT Lewis
2022-01-13 13:39:15 +00:00
parent b768259244
commit 0ae4f4cc82
19 changed files with 145 additions and 134 deletions
@@ -18,7 +18,7 @@ import { RestContext, rest } from 'msw';
import { setupServer } from 'msw/node';
import { ConfigReader } from '@backstage/config';
import { PermissionClient } from './PermissionClient';
import { AuthorizeRequest, AuthorizeResult, Identified } from './types/api';
import { AuthorizeQuery, AuthorizeResult, Identified } from './types/api';
import { DiscoveryApi } from './types/discovery';
import { Permission } from './types/permission';
@@ -42,7 +42,7 @@ const mockPermission: Permission = {
resourceType: 'test-resource',
};
const mockAuthorizeRequest = {
const mockAuthorizeQuery = {
permission: mockPermission,
resourceRef: 'foo',
};
@@ -54,12 +54,10 @@ describe('PermissionClient', () => {
describe('authorize', () => {
const mockAuthorizeHandler = jest.fn((req, res, { json }: RestContext) => {
const responses = req.body.items.map(
(a: Identified<AuthorizeRequest>) => ({
id: a.id,
result: AuthorizeResult.ALLOW,
}),
);
const responses = req.body.items.map((a: Identified<AuthorizeQuery>) => ({
id: a.id,
result: AuthorizeResult.ALLOW,
}));
return res(json({ items: responses }));
});
@@ -73,12 +71,12 @@ describe('PermissionClient', () => {
});
it('should fetch entities from correct endpoint', async () => {
await client.authorize([mockAuthorizeRequest]);
await client.authorize([mockAuthorizeQuery]);
expect(mockAuthorizeHandler).toHaveBeenCalled();
});
it('should include a request body', async () => {
await client.authorize([mockAuthorizeRequest]);
await client.authorize([mockAuthorizeQuery]);
const request = mockAuthorizeHandler.mock.calls[0][0];
@@ -93,21 +91,21 @@ describe('PermissionClient', () => {
});
it('should return the response from the fetch request', async () => {
const response = await client.authorize([mockAuthorizeRequest]);
const response = await client.authorize([mockAuthorizeQuery]);
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]);
await client.authorize([mockAuthorizeQuery]);
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 });
await client.authorize([mockAuthorizeQuery], { token });
const request = mockAuthorizeHandler.mock.calls[0][0];
expect(request.headers.get('authorization')).toEqual('Bearer fake-token');
@@ -120,7 +118,7 @@ describe('PermissionClient', () => {
},
);
await expect(
client.authorize([mockAuthorizeRequest], { token }),
client.authorize([mockAuthorizeQuery], { token }),
).rejects.toThrowError(/request failed with 401/i);
});
@@ -135,7 +133,7 @@ describe('PermissionClient', () => {
},
);
await expect(
client.authorize([mockAuthorizeRequest], { token }),
client.authorize([mockAuthorizeQuery], { token }),
).rejects.toThrowError(/Unexpected authorization response/i);
});
@@ -143,7 +141,7 @@ describe('PermissionClient', () => {
mockAuthorizeHandler.mockImplementationOnce(
(req, res, { json }: RestContext) => {
const responses = req.body.items.map(
(a: Identified<AuthorizeRequest>) => ({
(a: Identified<AuthorizeQuery>) => ({
id: a.id,
outcome: AuthorizeResult.ALLOW,
}),
@@ -153,14 +151,14 @@ describe('PermissionClient', () => {
},
);
await expect(
client.authorize([mockAuthorizeRequest], { token }),
client.authorize([mockAuthorizeQuery], { token }),
).rejects.toThrowError(/invalid input/i);
});
it('should allow all when permission.enabled is false', async () => {
mockAuthorizeHandler.mockImplementationOnce(
(req, res, { json }: RestContext) => {
const responses = req.body.map((a: Identified<AuthorizeRequest>) => ({
const responses = req.body.map((a: Identified<AuthorizeQuery>) => ({
id: a.id,
result: AuthorizeResult.DENY,
}));
@@ -172,7 +170,7 @@ describe('PermissionClient', () => {
discovery,
config: new ConfigReader({ permission: { enabled: false } }),
});
const response = await disabled.authorize([mockAuthorizeRequest]);
const response = await disabled.authorize([mockAuthorizeQuery]);
expect(response[0]).toEqual(
expect.objectContaining({ result: AuthorizeResult.ALLOW }),
);
@@ -182,7 +180,7 @@ describe('PermissionClient', () => {
it('should allow all when permission.enabled is not configured', async () => {
mockAuthorizeHandler.mockImplementationOnce(
(req, res, { json }: RestContext) => {
const responses = req.body.map((a: Identified<AuthorizeRequest>) => ({
const responses = req.body.map((a: Identified<AuthorizeQuery>) => ({
id: a.id,
outcome: AuthorizeResult.DENY,
}));
@@ -194,7 +192,7 @@ describe('PermissionClient', () => {
discovery,
config: new ConfigReader({}),
});
const response = await disabled.authorize([mockAuthorizeRequest]);
const response = await disabled.authorize([mockAuthorizeQuery]);
expect(response[0]).toEqual(
expect.objectContaining({ result: AuthorizeResult.ALLOW }),
);
@@ -21,13 +21,13 @@ import * as uuid from 'uuid';
import { z } from 'zod';
import {
AuthorizeResult,
AuthorizeRequest,
AuthorizeResponse,
AuthorizeQuery,
AuthorizeDecision,
Identified,
PermissionCriteria,
PermissionCondition,
AuthorizeResponseEnvelope,
AuthorizeRequestEnvelope,
AuthorizeResponse,
AuthorizeRequest,
} from './types/api';
import { DiscoveryApi } from './types/discovery';
import {
@@ -98,29 +98,29 @@ export class PermissionClient implements PermissionAuthorizer {
* @public
*/
async authorize(
requests: AuthorizeRequest[],
queries: AuthorizeQuery[],
options?: AuthorizeRequestOptions,
): Promise<AuthorizeResponse[]> {
): Promise<AuthorizeDecision[]> {
// 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 }));
return queries.map(_ => ({ result: AuthorizeResult.ALLOW }));
}
const requestEnvelope: AuthorizeRequestEnvelope = {
items: requests.map(request => ({
const request: AuthorizeRequest = {
items: queries.map(query => ({
id: uuid.v4(),
...request,
...query,
})),
};
const permissionApi = await this.discovery.getBaseUrl('permission');
const response = await fetch(`${permissionApi}/authorize`, {
method: 'POST',
body: JSON.stringify(requestEnvelope),
body: JSON.stringify(request),
headers: {
...this.getAuthorizationHeader(options?.token),
'content-type': 'application/json',
@@ -130,28 +130,28 @@ export class PermissionClient implements PermissionAuthorizer {
throw await ResponseError.fromResponse(response);
}
const responseEnvelope = await response.json();
this.assertValidResponses(requestEnvelope, responseEnvelope);
const responseBody = await response.json();
this.assertValidResponse(request, responseBody);
const responsesById = responseEnvelope.items.reduce((acc, r) => {
const responsesById = responseBody.items.reduce((acc, r) => {
acc[r.id] = r;
return acc;
}, {} as Record<string, Identified<AuthorizeResponse>>);
}, {} as Record<string, Identified<AuthorizeDecision>>);
return requestEnvelope.items.map(request => responsesById[request.id]);
return request.items.map(query => responsesById[query.id]);
}
private getAuthorizationHeader(token?: string): Record<string, string> {
return token ? { Authorization: `Bearer ${token}` } : {};
}
private assertValidResponses(
requestEnvelope: AuthorizeRequestEnvelope,
private assertValidResponse(
request: AuthorizeRequest,
json: any,
): asserts json is AuthorizeResponseEnvelope {
): asserts json is AuthorizeResponse {
const authorizedResponses = responseSchema.parse(json);
const responseIds = authorizedResponses.items.map(r => r.id);
const hasAllRequestIds = requestEnvelope.items.every(r =>
const hasAllRequestIds = request.items.every(r =>
responseIds.includes(r.id),
);
if (!hasAllRequestIds) {
+6 -6
View File
@@ -46,7 +46,7 @@ export enum AuthorizeResult {
* An individual authorization request for {@link PermissionClient#authorize}.
* @public
*/
export type AuthorizeRequest = {
export type AuthorizeQuery = {
permission: Permission;
resourceRef?: string;
};
@@ -55,8 +55,8 @@ export type AuthorizeRequest = {
* A batch of authorization requests from {@link PermissionClient#authorize}.
* @public
*/
export type AuthorizeRequestEnvelope = {
items: Identified<AuthorizeRequest>[];
export type AuthorizeRequest = {
items: Identified<AuthorizeQuery>[];
};
/**
@@ -86,7 +86,7 @@ export type PermissionCriteria<TQuery> =
* An individual authorization response from {@link PermissionClient#authorize}.
* @public
*/
export type AuthorizeResponse =
export type AuthorizeDecision =
| { result: AuthorizeResult.ALLOW | AuthorizeResult.DENY }
| {
result: AuthorizeResult.CONDITIONAL;
@@ -97,6 +97,6 @@ export type AuthorizeResponse =
* A batch of authorization responses from {@link PermissionClient#authorize}.
* @public
*/
export type AuthorizeResponseEnvelope = {
items: Identified<AuthorizeResponse>[];
export type AuthorizeResponse = {
items: Identified<AuthorizeDecision>[];
};
+2 -2
View File
@@ -16,10 +16,10 @@
export { AuthorizeResult } from './api';
export type {
AuthorizeQuery,
AuthorizeRequest,
AuthorizeRequestEnvelope,
AuthorizeDecision,
AuthorizeResponse,
AuthorizeResponseEnvelope,
Identified,
PermissionCondition,
PermissionCriteria,
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { AuthorizeRequest, AuthorizeResponse } from './api';
import { AuthorizeQuery, AuthorizeDecision } from './api';
/**
* The attributes related to a given permission; these should be generic and widely applicable to
@@ -48,9 +48,9 @@ export type Permission = {
*/
export interface PermissionAuthorizer {
authorize(
requests: AuthorizeRequest[],
queries: AuthorizeQuery[],
options?: AuthorizeRequestOptions,
): Promise<AuthorizeResponse[]>;
): Promise<AuthorizeDecision[]>;
}
/**