authz: use zod to validate responses in PermissionClient

Signed-off-by: Mike Lewis <mtlewis@users.noreply.github.com>
This commit is contained in:
Mike Lewis
2021-11-12 11:44:24 +00:00
parent 0c0cf0ddaf
commit f0124d29a9
2 changed files with 51 additions and 9 deletions
@@ -115,7 +115,7 @@ describe('PermissionClient', () => {
).rejects.toThrowError(/request failed with 401/i);
});
it('should reject invalid responses', async () => {
it('should reject responses with missing ids', async () => {
mockAuthorizeHandler.mockImplementationOnce(
(_req, res, { json }: RestContext) => {
return res(json([{ id: 'wrong-id', result: AuthorizeResult.ALLOW }]));
@@ -125,5 +125,21 @@ describe('PermissionClient', () => {
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);
});
});
});
@@ -17,14 +17,47 @@
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
@@ -103,14 +136,7 @@ export class PermissionClient {
requests: Identified<AuthorizeRequest>[],
json: any,
): asserts json is Identified<AuthorizeResponse>[] {
const responses = Array.isArray(json) ? json : [];
const authorizedResponses: Identified<AuthorizeResponse>[] =
responses.filter(
(r: any): r is Identified<AuthorizeResponse> =>
typeof r === 'object' &&
typeof r.id === 'string' &&
r.result in AuthorizeResult,
);
const authorizedResponses = responseSchema.parse(json);
const responseIds = authorizedResponses.map(r => r.id);
const hasAllRequestIds = requests.every(r => responseIds.includes(r.id));
if (!hasAllRequestIds) {