catalog-backend: validate cursor
Signed-off-by: Vincenzo Scamporlino <vincenzos@spotify.com>
This commit is contained in:
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user