diff --git a/keyserver/src/utils/validation-utils.js b/keyserver/src/utils/validation-utils.js index df33ac7a4..6e618289b 100644 --- a/keyserver/src/utils/validation-utils.js +++ b/keyserver/src/utils/validation-utils.js @@ -1,260 +1,329 @@ // @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 { isWebPlatform } from 'lib/types/device-types.js'; import { ServerError } from 'lib/utils/errors.js'; import { tCookie, tPassword, tPlatform, tPlatformDetails, assertWithValidator, + tID, } 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: ?TType, input: T, ) { if (!viewer.isSocket) { await checkClientSupported(viewer, inputValidator, input); } checkInputValidator(inputValidator, input); } +const convertToNewIDSchema = false; +const keyserverPrefixID = '256'; + +function validateOutput( + viewer: Viewer, + outputValidator: TType, + data: T, +): T { + if (!outputValidator.is(data)) { + console.trace( + 'Output validation failed, validator is:', + outputValidator.displayName, + ); + return data; + } + + if ( + hasMinCodeVersion(viewer.platformDetails, 1000) && + !isWebPlatform(viewer.platformDetails?.platform) && + convertToNewIDSchema + ) { + return convertServerIDsToClientIDs( + keyserverPrefixID, + outputValidator, + data, + ); + } + + return data; +} + +function convertServerIDsToClientIDs( + serverPrefixID: string, + outputValidator: TType, + data: T, +): T { + const conversionFunction = id => { + if (id.indexOf('|') !== -1) { + console.warn(`Server id '${id}' already has a prefix`); + return id; + } + return `${serverPrefixID}|${id}`; + }; + + return convertObject(outputValidator, data, [tID], conversionFunction); +} + +function convertClientIDsToServerIDs( + serverPrefixID: string, + outputValidator: TType, + data: T, +): T { + const prefix = serverPrefixID + '|'; + const conversionFunction = id => { + if (id.startsWith(prefix)) { + return id.substr(prefix.length); + } + + throw new ServerError('invalid_client_id_prefix'); + }; + + return convertObject(outputValidator, data, [tID], conversionFunction); +} + function checkInputValidator(inputValidator: ?TType, input: T) { if (!inputValidator || inputValidator.is(input)) { return; } const error = new ServerError('invalid_parameters'); error.sanitizedInput = input ? sanitizeInput(inputValidator, input) : null; throw error; } async function checkClientSupported( viewer: Viewer, inputValidator: ?TType, input: T, ) { let platformDetails; if (inputValidator) { platformDetails = findFirstInputMatchingValidator( inputValidator, tPlatformDetails, input, ); } if (!platformDetails && inputValidator) { const platform = findFirstInputMatchingValidator( inputValidator, tPlatform, input, ); if (platform) { platformDetails = { platform }; } } if (!platformDetails) { ({ platformDetails } = viewer); } await verifyClientSupported(viewer, platformDetails); } const redactedString = '********'; const redactedTypes = [tPassword, tCookie]; function sanitizeInput(inputValidator: TType, input: T): T { return convertObject( inputValidator, input, redactedTypes, () => redactedString, ); } function findFirstInputMatchingValidator( wholeInputValidator: *, inputValidatorToMatch: *, input: *, ): any { if (!wholeInputValidator || input === null || input === undefined) { return null; } if ( wholeInputValidator === inputValidatorToMatch && wholeInputValidator.is(input) ) { return input; } if (wholeInputValidator.meta.kind === 'maybe') { return findFirstInputMatchingValidator( wholeInputValidator.meta.type, inputValidatorToMatch, input, ); } if ( wholeInputValidator.meta.kind === 'interface' && typeof input === 'object' ) { for (const key in input) { const value = input[key]; const validator = wholeInputValidator.meta.props[key]; const innerResult = findFirstInputMatchingValidator( validator, inputValidatorToMatch, value, ); if (innerResult) { return innerResult; } } } if (wholeInputValidator.meta.kind === 'union') { for (const validator of wholeInputValidator.meta.types) { if (validator.is(input)) { return findFirstInputMatchingValidator( validator, inputValidatorToMatch, input, ); } } } if (wholeInputValidator.meta.kind === 'list' && Array.isArray(input)) { const validator = wholeInputValidator.meta.type; for (const value of input) { const innerResult = findFirstInputMatchingValidator( validator, inputValidatorToMatch, value, ); if (innerResult) { return innerResult; } } } 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, ) { if (!policies.length) { return; } if (!hasMinCodeVersion(viewer.platformDetails, 181)) { return; } const notAcknowledgedPolicies = await fetchNotAcknowledgedPolicies( viewer.id, policies, ); if (notAcknowledgedPolicies.length) { throw new ServerError('policies_not_accepted', { notAcknowledgedPolicies, }); } } export { validateInput, + validateOutput, checkInputValidator, redactedString, sanitizeInput, findFirstInputMatchingValidator, checkClientSupported, + convertServerIDsToClientIDs, + convertClientIDsToServerIDs, convertObject, policiesValidator, }; diff --git a/keyserver/src/utils/validation-utils.test.js b/keyserver/src/utils/validation-utils.test.js index 244e5c6e2..5967f20f0 100644 --- a/keyserver/src/utils/validation-utils.test.js +++ b/keyserver/src/utils/validation-utils.test.js @@ -1,74 +1,109 @@ // @flow import t from 'tcomb'; -import { tPassword, tShape } from 'lib/utils/validation-utils.js'; +import { tPassword, tShape, tID } from 'lib/utils/validation-utils.js'; -import { sanitizeInput, redactedString } from './validation-utils.js'; +import { + convertServerIDsToClientIDs, + sanitizeInput, + redactedString, + convertClientIDsToServerIDs, +} from './validation-utils.js'; describe('sanitization', () => { it('should redact a string', () => { expect(sanitizeInput(tPassword, 'password')).toStrictEqual(redactedString); }); it('should redact a string inside an object', () => { const validator = tShape({ password: tPassword }); const object = { password: 'password' }; const redacted = { password: redactedString }; expect(sanitizeInput(validator, object)).toStrictEqual(redacted); }); it('should redact an optional string', () => { const validator = tShape({ password: t.maybe(tPassword) }); const object = { password: 'password' }; 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); }); }); + +describe('id conversion', () => { + it('should convert string id', () => { + const validator = tShape({ id: tID }); + const serverData = { id: '1' }; + const clientData = { id: '0|1' }; + + expect( + convertServerIDsToClientIDs('0', validator, serverData), + ).toStrictEqual(clientData); + expect( + convertClientIDsToServerIDs('0', validator, clientData), + ).toStrictEqual(serverData); + }); + + it('should convert a complex type', () => { + const validator = tShape({ ids: t.dict(tID, t.list(tID)) }); + const serverData = { ids: { '1': ['11', '12'], '2': [], '3': ['13'] } }; + const clientData = { + ids: { '0|1': ['0|11', '0|12'], '0|2': [], '0|3': ['0|13'] }, + }; + + expect( + convertServerIDsToClientIDs('0', validator, serverData), + ).toStrictEqual(clientData); + expect( + convertClientIDsToServerIDs('0', validator, clientData), + ).toStrictEqual(serverData); + }); +});