From ef79ccc954a22071e11448d4a076eb1e688bc373 Mon Sep 17 00:00:00 2001 From: tada5hi Date: Sun, 7 Aug 2022 14:55:01 +0200 Subject: [PATCH] only permit query operations for permitted fields+relations by allowed or default option --- src/parameter/fields/parse.ts | 6 +- src/parameter/fields/utils/domain.ts | 6 +- src/parameter/filters/parse.ts | 5 +- src/parameter/filters/type.ts | 2 - src/parameter/filters/utils.ts | 6 +- src/parameter/relations/parse.ts | 15 ++-- src/parse/module.ts | 110 ++++++++++++++++----------- src/parse/parameter/module.ts | 2 +- src/parse/type.ts | 2 +- test/unit/fields.spec.ts | 4 +- test/unit/filters.spec.ts | 4 +- test/unit/parse.spec.ts | 51 +++++++++---- test/unit/relations.spec.ts | 4 +- test/unit/sort.spec.ts | 2 +- 14 files changed, 131 insertions(+), 88 deletions(-) diff --git a/src/parameter/fields/parse.ts b/src/parameter/fields/parse.ts index 20af87b9..51c5d2a4 100644 --- a/src/parameter/fields/parse.ts +++ b/src/parameter/fields/parse.ts @@ -50,11 +50,11 @@ export function parseQueryFields( options ??= {}; options.defaultAlias ??= DEFAULT_ALIAS_ID; - const defaultDomainFields = buildFieldDomainRecords(options.default ?? []); + const defaultDomainFields = buildFieldDomainRecords(options.default); const defaultDomainKeys = Object.keys(defaultDomainFields); const allowedDomainFields = mergeFieldsDomainRecords( - buildFieldDomainRecords(options.allowed ?? []), + buildFieldDomainRecords(options.allowed), { ...defaultDomainFields }, options, ); @@ -131,7 +131,7 @@ export function parseQueryFields( } if (fieldsInputTransformed.default.length > 0) { - fieldsInputTransformed.default = [...fieldsInputTransformed.default, ...fieldsInputTransformed.included]; + fieldsInputTransformed.default = Array.from(new Set([...fieldsInputTransformed.default, ...fieldsInputTransformed.included])); for (let j = 0; j < fieldsInputTransformed.excluded.length; j++) { const index = fieldsInputTransformed.default.indexOf(fieldsInputTransformed.excluded[j]); if (index !== -1) { diff --git a/src/parameter/fields/utils/domain.ts b/src/parameter/fields/utils/domain.ts index 43e6f7cd..b5a46437 100644 --- a/src/parameter/fields/utils/domain.ts +++ b/src/parameter/fields/utils/domain.ts @@ -10,9 +10,13 @@ import { DEFAULT_ALIAS_ID } from '../constants'; import { hasOwnProperty } from '../../../utils'; export function buildFieldDomainRecords( - data: Record | string[], + data?: Record | string[], options?: FieldsParseOptions, ): Record { + if (typeof data === 'undefined') { + return {}; + } + options = options ?? { defaultAlias: DEFAULT_ALIAS_ID }; let domainFields: Record = {}; diff --git a/src/parameter/filters/parse.ts b/src/parameter/filters/parse.ts index d4674e9e..a2882f60 100644 --- a/src/parameter/filters/parse.ts +++ b/src/parameter/filters/parse.ts @@ -40,10 +40,9 @@ export function parseQueryFilters( options = options ?? {}; // If it is an empty array nothing is allowed - if ( - typeof options.allowed !== 'undefined' && - Object.keys(options.allowed).length === 0 + typeof options.allowed === 'undefined' || + options.allowed.length === 0 ) { return []; } diff --git a/src/parameter/filters/type.ts b/src/parameter/filters/type.ts index 4a070cac..25473ad7 100644 --- a/src/parameter/filters/type.ts +++ b/src/parameter/filters/type.ts @@ -20,8 +20,6 @@ export type FilterOperatorConfig = V extends string | number | boolean ? (V | V[]) : never; type FilterValueWithOperator = V extends string | number | boolean ? (FilterValue | FilterValueOperator | Array>) : diff --git a/src/parameter/filters/utils.ts b/src/parameter/filters/utils.ts index 7811c090..71e78d87 100644 --- a/src/parameter/filters/utils.ts +++ b/src/parameter/filters/utils.ts @@ -6,7 +6,7 @@ */ import { - FilterOperatorConfig, FilterOperatorLabelType, + FilterOperatorConfig, } from './type'; import { hasOwnProperty, isSimpleValue } from '../../utils'; import { FilterOperator, FilterOperatorLabel } from './constants'; @@ -25,12 +25,12 @@ for (let i = 0; i < operatorKeys.length; i++) { } export function determineFilterOperatorLabelsByValue(input: string) : { - operators: FilterOperatorLabelType[], + operators: (`${FilterOperatorLabel}`)[], value: string | string[] } { let value : string[] | string = input; - const operators : FilterOperatorLabelType[] = []; + const operators : (`${FilterOperatorLabel}`)[] = []; for (let i = 0; i < config.length; i++) { if (typeof value !== 'string') { diff --git a/src/parameter/relations/parse.ts b/src/parameter/relations/parse.ts index 3695c9c7..5cdf6792 100644 --- a/src/parameter/relations/parse.ts +++ b/src/parameter/relations/parse.ts @@ -65,7 +65,7 @@ export function parseQueryRelations( // If it is an empty array nothing is allowed if ( - Array.isArray(options.allowed) && + typeof options.allowed === 'undefined' || options.allowed.length === 0 ) { return []; @@ -101,14 +101,11 @@ export function parseQueryRelations( return []; } - items = items - .map((item) => { - if (hasOwnProperty(options.aliasMapping, item)) { - item = options.aliasMapping[item]; - } - - return item; - }); + for (let i = 0; i < items.length; i++) { + if (hasOwnProperty(options.aliasMapping, items[i])) { + items[i] = options.aliasMapping[items[i]]; + } + } if (options.allowed) { items = items diff --git a/src/parse/module.ts b/src/parse/module.ts index 01ce711e..6f80f0dc 100644 --- a/src/parse/module.ts +++ b/src/parse/module.ts @@ -6,7 +6,11 @@ */ import { - FieldsParseOutput, FiltersParseOutput, PaginationParseOutput, RelationsParseOutput, SortParseOutput, + FieldsParseOutput, + FiltersParseOutput, + PaginationParseOutput, + RelationsParseOutput, + SortParseOutput, } from '../parameter'; import { Parameter, URLParameter } from '../constants'; import { parseQueryParameter } from './parameter'; @@ -20,20 +24,12 @@ export function parseQuery( const output : ParseOutput = {}; - const nonEnabled : boolean = Object.keys(options).length === 0; - let relations : RelationsParseOutput | undefined; - if (!!options[Parameter.RELATIONS] || nonEnabled) { - relations = parseQueryParameter( - Parameter.RELATIONS, - input[Parameter.RELATIONS] ?? input[URLParameter.RELATIONS], - options[Parameter.RELATIONS], - ); - - output[Parameter.RELATIONS] = relations; - } const keys : Parameter[] = [ + // relations must be first parameter + Parameter.RELATIONS, + Parameter.FIELDS, Parameter.FILTERS, Parameter.PAGINATION, @@ -41,46 +37,70 @@ export function parseQuery( ]; for (let i = 0; i < keys.length; i++) { - const enabled = !!options[keys[i]] || - nonEnabled; - - if (!enabled) continue; - const key : Parameter = keys[i]; switch (key) { - case Parameter.FIELDS: - output[Parameter.FIELDS] = parseQueryParameter( - keys[i], - input[Parameter.FIELDS] ?? input[URLParameter.FIELDS], - options[Parameter.FIELDS], - relations, - ) as FieldsParseOutput; + case Parameter.RELATIONS: { + const value = input[Parameter.RELATIONS] ?? input[URLParameter.RELATIONS]; + if (value || options[Parameter.RELATIONS]) { + relations = parseQueryParameter( + Parameter.RELATIONS, + value, + options[Parameter.RELATIONS], + ); + + output[Parameter.RELATIONS] = relations; + } + break; + } + case Parameter.FIELDS: { + const value = input[Parameter.FIELDS] ?? input[URLParameter.FIELDS]; + if (value || options[Parameter.FIELDS]) { + output[Parameter.FIELDS] = parseQueryParameter( + keys[i], + value, + options[Parameter.FIELDS], + relations, + ) as FieldsParseOutput; + } break; - case Parameter.FILTERS: - output[Parameter.FILTERS] = parseQueryParameter( - keys[i], - input[Parameter.FILTERS] ?? input[URLParameter.FILTERS], - options[Parameter.FILTERS], - relations, - ) as FiltersParseOutput; + } + case Parameter.FILTERS: { + const value = input[Parameter.FILTERS] ?? input[URLParameter.FILTERS]; + if (value || options[Parameter.FILTERS]) { + output[Parameter.FILTERS] = parseQueryParameter( + keys[i], + value, + options[Parameter.FILTERS], + relations, + ) as FiltersParseOutput; + } break; - case Parameter.PAGINATION: - output[Parameter.PAGINATION] = parseQueryParameter( - keys[i], - input[Parameter.PAGINATION] ?? input[URLParameter.PAGINATION], - options[Parameter.PAGINATION], - relations, - ) as PaginationParseOutput; + } + case Parameter.PAGINATION: { + const value = input[Parameter.PAGINATION] ?? input[URLParameter.PAGINATION]; + if (value || options[Parameter.PAGINATION]) { + output[Parameter.PAGINATION] = parseQueryParameter( + keys[i], + value, + options[Parameter.PAGINATION], + relations, + ) as PaginationParseOutput; + } break; - case Parameter.SORT: - output[Parameter.SORT] = parseQueryParameter( - keys[i], - input[Parameter.SORT] ?? input[URLParameter.SORT], - options[Parameter.SORT], - relations, - ) as SortParseOutput; + } + case Parameter.SORT: { + const value = input[Parameter.SORT] ?? input[URLParameter.SORT]; + if (value || options[Parameter.SORT]) { + output[Parameter.SORT] = parseQueryParameter( + keys[i], + value, + options[Parameter.SORT], + relations, + ) as SortParseOutput; + } break; + } } } diff --git a/src/parse/parameter/module.ts b/src/parse/parameter/module.ts index 6c5931c2..1b8b47ca 100644 --- a/src/parse/parameter/module.ts +++ b/src/parse/parameter/module.ts @@ -16,7 +16,7 @@ import { ParseParameterOptions, ParseParameterOutput } from './type'; export function parseQueryParameter( key: K, data: unknown, - options?: ParseParameterOptions | boolean, + options?: ParseParameterOptions, relations?: RelationsParseOutput, ): ParseParameterOutput { switch (key) { diff --git a/src/parse/type.ts b/src/parse/type.ts index 74d87b04..6570fe73 100644 --- a/src/parse/type.ts +++ b/src/parse/type.ts @@ -21,7 +21,7 @@ export type ParseOptions = { /** * On default all query keys are enabled. */ - [K in `${Parameter}`]?: ParseParameterOptions | boolean + [K in `${Parameter}`]?: ParseParameterOptions }; //------------------------------------------------ diff --git a/test/unit/fields.spec.ts b/test/unit/fields.spec.ts index bd8e332f..68711915 100644 --- a/test/unit/fields.spec.ts +++ b/test/unit/fields.spec.ts @@ -66,9 +66,9 @@ describe('src/fields/index.ts', () => { data = parseQueryFields('id', { ...options, allowed: [] }); expect(data).toEqual([] as FieldsParseOutput); - // undefined allowed -> allows everything + // undefined allowed -> allows nothing data = parseQueryFields('id', { ...options, allowed: undefined }); - expect(data).toEqual([{ key: 'id' }] as FieldsParseOutput); + expect(data).toEqual([] as FieldsParseOutput); // field not allowed data = parseQueryFields('avatar', options); diff --git a/test/unit/filters.spec.ts b/test/unit/filters.spec.ts index 3ea4f1c9..789d3e34 100644 --- a/test/unit/filters.spec.ts +++ b/test/unit/filters.spec.ts @@ -12,7 +12,7 @@ import { describe('src/filter/index.ts', () => { it('should transform request filters', () => { // filter id - let allowedFilter = parseQueryFilters({ id: 1 }); + let allowedFilter = parseQueryFilters({ id: 1 }, {allowed: ['id']}); expect(allowedFilter).toEqual([{ key: 'id', value: 1, @@ -192,7 +192,7 @@ describe('src/filter/index.ts', () => { }); it('should transform filters with includes', () => { - const include = parseQueryRelations(['profile', 'user_roles.role']); + const include = parseQueryRelations(['profile', 'user_roles.role'], {allowed: ['profile', 'user_roles.role']}); const options : FiltersParseOptions = { allowed: ['id', 'profile.id', 'role.id'], diff --git a/test/unit/parse.spec.ts b/test/unit/parse.spec.ts index 94128101..9ba45548 100644 --- a/test/unit/parse.spec.ts +++ b/test/unit/parse.spec.ts @@ -6,7 +6,13 @@ */ import { - FieldsParseOutput, Parameter, ParseOutput, parseQuery, parseQueryParameter, + FieldsParseOutput, + FiltersParseOutput, + PaginationParseOutput, + Parameter, + ParseOutput, + parseQuery, + parseQueryParameter, RelationsParseOutput, SortDirection, SortParseOutput, } from '../../src'; describe('src/parse.ts', () => { @@ -14,13 +20,14 @@ describe('src/parse.ts', () => { let value = parseQuery({ fields: ['id', 'name'], }, { - [Parameter.FIELDS]: true, + fields: { + allowed: ['id'] + } }); expect(value).toEqual({ fields: [ { key: 'id' }, - { key: 'name' }, ], } as ParseOutput); @@ -29,22 +36,40 @@ describe('src/parse.ts', () => { }); expect(value).toEqual({ - [Parameter.FIELDS]: [ - { key: 'id' }, - { key: 'name' }, - ], - [Parameter.FILTERS]: [], - [Parameter.RELATIONS]: [], - [Parameter.PAGINATION]: {}, - [Parameter.SORT]: [], + fields: [] } as ParseOutput); }); - it('should parse single query parameter', () => { - const value = parseQueryParameter(Parameter.FIELDS, ['id', 'name']); + it('should parse field query parameter', () => { + let value = parseQueryParameter(Parameter.FIELDS, ['id', 'name'], {allowed: ['id', 'name']}); expect(value).toEqual([ { key: 'id' }, { key: 'name' }, ] as FieldsParseOutput); }); + + it('should parse filter query parameter', () => { + let value = parseQueryParameter(Parameter.FILTERS, { name: 'tada5hi' }, {allowed: ['name']}); + expect(value).toEqual([{ + key: 'name', + value: 'tada5hi', + }] as FiltersParseOutput); + }); + + it('should parse pagination query parameter', () => { + let value = parseQueryParameter(Parameter.PAGINATION, { offset: 20, limit: 20 }, { maxLimit: 50 }); + expect(value).toEqual({ offset: 20, limit: 20 } as PaginationParseOutput); + }); + + it('should parse relation query parameter', () => { + let value = parseQueryParameter(Parameter.RELATIONS, 'profile', { allowed: ['profile'] }); + expect(value).toEqual([ + { key: 'profile', value: 'profile' }, + ] as RelationsParseOutput); + }); + + it('should parse sort query parameter', () => { + let value = parseQueryParameter(Parameter.SORT, '-id', { allowed: ['id'] }); + expect(value).toEqual([{ key: 'id', value: SortDirection.DESC }] as SortParseOutput); + }); }); diff --git a/test/unit/relations.spec.ts b/test/unit/relations.spec.ts index 215cba0f..2b7ee141 100644 --- a/test/unit/relations.spec.ts +++ b/test/unit/relations.spec.ts @@ -67,9 +67,9 @@ describe('src/relations/index.ts', () => { allowed = parseQueryRelations(['profile'], { allowed: [] }); expect(allowed).toEqual([] as RelationsParseOutput); - // all allowed + // no allowed allowed = parseQueryRelations(['profile'], { allowed: undefined }); - expect(allowed).toEqual([{ key: 'profile', value: 'profile' }] as RelationsParseOutput); + expect(allowed).toEqual([] as RelationsParseOutput); // nested data with alias allowed = parseQueryRelations(['profile.photos', 'profile.photos.abc', 'profile.abc'], { allowed: ['profile.photos'], defaultAlias: 'user' }); diff --git a/test/unit/sort.spec.ts b/test/unit/sort.spec.ts index eecc866f..17dce96b 100644 --- a/test/unit/sort.spec.ts +++ b/test/unit/sort.spec.ts @@ -98,7 +98,7 @@ describe('src/sort/index.ts', () => { }); it('should transform sort data with includes', () => { - const includes = parseQueryRelations(['profile', 'user_roles.role']); + const includes = parseQueryRelations(['profile', 'user_roles.role'], {allowed: ['profile', 'user_roles.role']}); const options : SortParseOptions = { allowed: ['id', 'profile.id', 'user_roles.role.id'],