catalog-backend: validate cursor

Signed-off-by: Vincenzo Scamporlino <vincenzos@spotify.com>
This commit is contained in:
Vincenzo Scamporlino
2023-02-01 13:30:52 +01:00
parent 0d137f6fa5
commit cf5c58a89f
3 changed files with 54 additions and 10 deletions
@@ -1294,6 +1294,26 @@ describe('DefaultEntitiesCatalog', () => {
expect(response5.totalItems).toBe(6);
},
);
it('should throw in case of malformed cursor', async () => {
const catalog = new DefaultEntitiesCatalog({
database: knex,
logger: getVoidLogger(),
stitcher,
});
const cursor = Buffer.from(
JSON.stringify({ filter: 'notavalidfilter' }),
'utf8',
).toString('base64');
const request: PaginatedEntitiesCursorRequest = {
cursor,
};
await expect(catalog.paginatedEntities(request)).rejects.toThrow(
Error('Malformed cursor, could not be parsed'),
);
});
});
describe('removeEntityByUid', () => {
@@ -23,6 +23,7 @@ import { InputError, NotFoundError } from '@backstage/errors';
import { Knex } from 'knex';
import { isEqual, chunk as lodashChunk } from 'lodash';
import { Logger } from 'winston';
import { z } from 'zod';
import {
EntitiesBatchRequest,
EntitiesBatchResponse,
@@ -658,11 +659,39 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog {
}
}
const entityFilterParser: z.ZodSchema<EntityFilter> = z.lazy(() =>
z
.object({
key: z.string(),
values: z.array(z.string()).optional(),
})
.or(z.object({ not: entityFilterParser }))
.or(z.object({ anyOf: z.array(entityFilterParser) }))
.or(z.object({ allOf: z.array(entityFilterParser) })),
);
export const cursorParser: z.ZodSchema<Cursor> = z.object({
sortFields: z.array(
z.object({ field: z.string(), order: z.enum(['asc', 'desc']) }),
),
sortFieldValues: z.array(z.string().or(z.null())),
filter: entityFilterParser.optional(),
isPrevious: z.boolean(),
query: z.string().optional(),
firstSortFieldValues: z.array(z.string().or(z.null())).optional(),
totalItems: z.number().optional(),
});
function encodeCursor(cursor: Cursor) {
const json = JSON.stringify(cursor);
return Buffer.from(json, 'utf8').toString('base64');
}
function decodeCursor(encodedCursor: string) {
const json = Buffer.from(encodedCursor, 'base64').toString('utf8');
return cursorParser.parse(JSON.parse(json));
}
function parseCursorFromRequest(
request?: PaginatedEntitiesRequest,
): Partial<Cursor> {
@@ -672,10 +701,7 @@ function parseCursorFromRequest(
}
if (isPaginatedEntitiesCursorRequest(request)) {
try {
const json = Buffer.from(request.cursor, 'base64').toString('utf8');
const cursor = JSON.parse(json);
// TODO(vinzscam): validate the shit
return cursor as unknown as Cursor;
return decodeCursor(request.cursor);
} catch {
throw new InputError('Malformed cursor, could not be parsed');
}
+4 -6
View File
@@ -19,9 +19,9 @@ import { Request } from 'express';
import lodash from 'lodash';
import { z } from 'zod';
import {
EntitiesRequest,
PaginatedEntitiesCursorRequest,
PaginatedEntitiesInitialRequest,
PaginatedEntitiesRequest,
} from '../catalog/types';
export async function requireRequestBody(req: Request): Promise<unknown> {
@@ -72,21 +72,19 @@ export function disallowReadonlyMode(readonly: boolean) {
}
export function isPaginatedEntitiesInitialRequest(
input: EntitiesRequest | undefined,
input: PaginatedEntitiesRequest | undefined,
): input is PaginatedEntitiesInitialRequest {
if (!input) {
return false;
}
// TODO(vinzscam) expand this
return !isPaginatedEntitiesCursorRequest(input);
}
export function isPaginatedEntitiesCursorRequest(
input: EntitiesRequest | undefined,
input: PaginatedEntitiesRequest | undefined,
): input is PaginatedEntitiesCursorRequest {
if (!input) {
return false;
}
// TODO(vinzscam) expand this
return input?.hasOwnProperty('cursor') ?? false;
return !!(input as PaginatedEntitiesCursorRequest).cursor;
}