diff --git a/keyserver/src/utils/validation-utils.js b/keyserver/src/utils/validation-utils.js --- a/keyserver/src/utils/validation-utils.js +++ b/keyserver/src/utils/validation-utils.js @@ -1,5 +1,9 @@ // @flow +import _mapKeys from 'lodash/fp/mapKeys.js'; +import _mapValues from 'lodash/fp/mapValues.js'; +import type { TType, TInterface } from 'tcomb'; + import type { PolicyType } from 'lib/facts/policies.js'; import { hasMinCodeVersion } from 'lib/shared/version-utils.js'; import { ServerError } from 'lib/utils/errors.js'; @@ -8,20 +12,25 @@ tPassword, tPlatform, tPlatformDetails, + assertWithValidator, } from 'lib/utils/validation-utils.js'; import { fetchNotAcknowledgedPolicies } from '../fetchers/policy-acknowledgment-fetchers.js'; import { verifyClientSupported } from '../session/version.js'; import type { Viewer } from '../session/viewer.js'; -async function validateInput(viewer: Viewer, inputValidator: *, input: *) { +async function validateInput( + viewer: Viewer, + inputValidator: ?TType, + input: T, +) { if (!viewer.isSocket) { await checkClientSupported(viewer, inputValidator, input); } checkInputValidator(inputValidator, input); } -function checkInputValidator(inputValidator: *, input: *) { +function checkInputValidator(inputValidator: ?TType, input: T) { if (!inputValidator || inputValidator.is(input)) { return; } @@ -30,10 +39,10 @@ throw error; } -async function checkClientSupported( +async function checkClientSupported( viewer: Viewer, - inputValidator: *, - input: *, + inputValidator: ?TType, + input: T, ) { let platformDetails; if (inputValidator) { @@ -61,34 +70,13 @@ const redactedString = '********'; const redactedTypes = [tPassword, tCookie]; -function sanitizeInput(inputValidator: any, input: any): any { - if (!inputValidator) { - return input; - } - if (redactedTypes.includes(inputValidator) && typeof input === 'string') { - return redactedString; - } - if ( - inputValidator.meta.kind === 'maybe' && - redactedTypes.includes(inputValidator.meta.type) && - typeof input === 'string' - ) { - return redactedString; - } - if ( - inputValidator.meta.kind !== 'interface' || - typeof input !== 'object' || - !input - ) { - return input; - } - const result = {}; - for (const key in input) { - const value = input[key]; - const validator = inputValidator.meta.props[key]; - result[key] = sanitizeInput(validator, value); - } - return result; +function sanitizeInput(inputValidator: TType, input: T): T { + return convertObject( + inputValidator, + input, + redactedTypes, + () => redactedString, + ); } function findFirstInputMatchingValidator( @@ -156,6 +144,87 @@ return null; } +function convertObject( + validator: TType, + input: I, + typesToConvert: $ReadOnlyArray>, + conversionFunction: T => T, +): I { + if (input === null || input === undefined) { + return input; + } + + // While they should be the same runtime object, + // `TValidator` is `TType` and `validator` is `TType`. + // Having them have different types allows us to use `assertWithValidator` + // to change `input` flow type + const TValidator = typesToConvert[typesToConvert.indexOf(validator)]; + if (TValidator && TValidator.is(input)) { + const TInput = assertWithValidator(input, TValidator); + const converted = conversionFunction(TInput); + return assertWithValidator(converted, validator); + } + + if (validator.meta.kind === 'maybe') { + return convertObject( + validator.meta.type, + input, + typesToConvert, + conversionFunction, + ); + } + if (validator.meta.kind === 'interface' && typeof input === 'object') { + const recastValidator: TInterface = (validator: any); + const result = {}; + for (const key in input) { + const innerValidator = recastValidator.meta.props[key]; + result[key] = convertObject( + innerValidator, + input[key], + typesToConvert, + conversionFunction, + ); + } + return assertWithValidator(result, recastValidator); + } + if (validator.meta.kind === 'union') { + for (const innerValidator of validator.meta.types) { + if (innerValidator.is(input)) { + return convertObject( + innerValidator, + input, + typesToConvert, + conversionFunction, + ); + } + } + return input; + } + if (validator.meta.kind === 'list' && Array.isArray(input)) { + const innerValidator = validator.meta.type; + return (input.map(value => + convertObject(innerValidator, value, typesToConvert, conversionFunction), + ): any); + } + if (validator.meta.kind === 'dict' && typeof input === 'object') { + const domainValidator = validator.meta.domain; + const codomainValidator = validator.meta.codomain; + if (typesToConvert.includes(domainValidator)) { + input = _mapKeys(key => conversionFunction(key))(input); + } + return _mapValues(value => + convertObject( + codomainValidator, + value, + typesToConvert, + conversionFunction, + ), + )(input); + } + + return input; +} + async function policiesValidator( viewer: Viewer, policies: $ReadOnlyArray, @@ -186,5 +255,6 @@ sanitizeInput, findFirstInputMatchingValidator, checkClientSupported, + convertObject, policiesValidator, }; diff --git a/keyserver/src/utils/validation-utils.test.js b/keyserver/src/utils/validation-utils.test.js --- a/keyserver/src/utils/validation-utils.test.js +++ b/keyserver/src/utils/validation-utils.test.js @@ -24,4 +24,51 @@ const redacted = { password: redactedString }; expect(sanitizeInput(validator, object)).toStrictEqual(redacted); }); + + it('should redact a string in optional object', () => { + const validator = tShape({ obj: t.maybe(tShape({ password: tPassword })) }); + const object = { obj: { password: 'password' } }; + const redacted = { obj: { password: redactedString } }; + expect(sanitizeInput(validator, object)).toStrictEqual(redacted); + }); + + it('should redact a string array', () => { + const validator = tShape({ passwords: t.list(tPassword) }); + const object = { passwords: ['password', 'password'] }; + const redacted = { passwords: [redactedString, redactedString] }; + expect(sanitizeInput(validator, object)).toStrictEqual(redacted); + }); + + it('should redact a string inside a dict', () => { + const validator = tShape({ passwords: t.dict(t.String, tPassword) }); + const object = { passwords: { a: 'password', b: 'password' } }; + const redacted = { passwords: { a: redactedString, b: redactedString } }; + expect(sanitizeInput(validator, object)).toStrictEqual(redacted); + }); + + it('should redact password dict key', () => { + const validator = tShape({ passwords: t.dict(tPassword, t.Bool) }); + const object = { passwords: { password1: true, password2: false } }; + const redacted = { passwords: {} }; + redacted.passwords[redactedString] = false; + expect(sanitizeInput(validator, object)).toStrictEqual(redacted); + }); + + it('should redact a string inside a union', () => { + const validator = tShape({ + password: t.union([tPassword, t.String, t.Bool]), + }); + const object = { password: 'password' }; + const redacted = { password: redactedString }; + expect(sanitizeInput(validator, object)).toStrictEqual(redacted); + }); + + it('should redact a string inside an object array', () => { + const validator = tShape({ + passwords: t.list(tShape({ password: tPassword })), + }); + const object = { passwords: [{ password: 'password' }] }; + const redacted = { passwords: [{ password: redactedString }] }; + expect(sanitizeInput(validator, object)).toStrictEqual(redacted); + }); }); diff --git a/lib/utils/validation-utils.js b/lib/utils/validation-utils.js --- a/lib/utils/validation-utils.js +++ b/lib/utils/validation-utils.js @@ -1,5 +1,6 @@ // @flow +import invariant from 'invariant'; import t from 'tcomb'; import type { TStructProps, @@ -8,6 +9,7 @@ TEnums, TInterface, TUnion, + TType, } from 'tcomb'; import { @@ -94,6 +96,11 @@ tMediaMessageVideo, ]); +function assertWithValidator(data: mixed, validator: TType): T { + invariant(validator.is(data), "data isn't of type T"); + return (data: any); +} + export { tBool, tString, @@ -114,4 +121,5 @@ tMediaMessagePhoto, tMediaMessageVideo, tMediaMessageMedia, + assertWithValidator, };