authz: use zod to validate responses in PermissionClient
Signed-off-by: Mike Lewis <mtlewis@users.noreply.github.com>
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user