UNPKG

passbolt-styleguide

Version:

Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.

692 lines (643 loc) 22.4 kB
/** * Passbolt ~ Open source password manager for teams * Copyright (c) Passbolt SA (https://www.passbolt.com) * * Licensed under GNU Affero General Public License version 3 of the or any later version. * For full copyright and license information, please see the LICENSE.txt * Redistributions of files must retain the above copyright notice. * * @copyright Copyright (c) Passbolt SA (https://www.passbolt.com) * @license https://opensource.org/licenses/AGPL-3.0 AGPL License * @link https://www.passbolt.com Passbolt(tm) * @since 2.13.0 */ import EntityValidationError from "./entityValidationError"; import Validator from "validator"; import CollectionValidationError from "./collectionValidationError"; class EntitySchema { /** * Baseline schema validation * TODO use json-schema validation tools * * @param {object} schema * @param {string} name * @throws TypeError if schema is invalid */ static validateSchema(name, schema) { if (!schema) { throw new TypeError(`Could not validate entity ${name}. No schema for entity ${name}.`); } if (!schema.type) { throw new TypeError(`Could not validate entity ${name}. Type missing.`); } if (schema.type === "array") { if (!schema.items) { throw new TypeError(`Could not validate entity ${name}. Schema error: missing item definition.`); } return; } if (schema.type === "object") { if (!schema.required || !Array.isArray(schema.required)) { throw new TypeError(`Could not validate entity ${name}. Schema error: no required properties.`); } if (!schema.properties || !Object.keys(schema).length) { throw new TypeError(`Could not validate entity ${name}. Schema error: no properties.`); } const schemaProps = schema.properties; for (const propName in schemaProps) { // Check type is defined if ( !Object.prototype.hasOwnProperty.call(schemaProps, propName) || (!schemaProps[propName].type && !schemaProps[propName].anyOf) ) { throw TypeError(`Invalid schema. Type missing for ${propName}...`); } // In case there is multiple types if (schemaProps[propName].anyOf) { if (!Array.isArray(schemaProps[propName].anyOf) || !schemaProps[propName].anyOf.length) { throw new TypeError(`Invalid schema, prop ${propName} anyOf should be an array`); } // TODO subcheck anyOf items } } } } /** * Validate * TODO use json-schema validation tools * * @param {string} name of entity * @param {object} dto data transfer object * @param {object} schema json-schema "like" data transfer object definition * @return {object} properties that are listed in the schema * @throws ValidationError */ static validate(name, dto, schema) { if (!name || !dto || !schema) { throw new TypeError(`Could not validate entity ${name}. No data provided.`); } switch (schema.type) { case "object": return EntitySchema.validateObject(name, dto, schema); case "array": return EntitySchema.validateArray(name, dto, schema); default: throw new TypeError(`Could not validate entity ${name}. Unsupported type.`); } } /** * Validate a given array against a given schema * * @param {string} name of entity * @param {object} dto data transfer object * @param {object} schema json-schema "like" data transfer object definition * @return {object} properties that are listed in the schema * @throws ValidationError */ static validateArray(name, dto, schema) { let validationError; const parsedItems = EntitySchema.validateProp("items", dto, schema); if (typeof schema.minItems === "number") { if (!EntitySchema.isGreaterThanOrEqual(dto.length, schema.minItems)) { validationError = EntitySchema.handleCollectionValidationError( "minItems", `The items array should contain at least ${schema.minItems} item(s).`, validationError, ); } } if (typeof schema.maxItems === "number") { if (!EntitySchema.isLessThanOrEqual(dto.length, schema.maxItems)) { validationError = EntitySchema.handleCollectionValidationError( "maxItems", `The items array should contain at maximum ${schema.maxItems} item(s).`, validationError, ); } } if (validationError) { throw validationError; } return parsedItems; } /** * Validate a given object against a given schema * * @param {string} name of entity * @param {object} dto data transfer object * @param {object} schema json-schema "like" data transfer object definition * @return {object} properties that are listed in the schema * @throws ValidationError */ static validateObject(name, dto, schema) { const requiredProps = schema.required; const schemaProps = schema.properties; const result = {}; let validationError; for (const propName in schemaProps) { if (!Object.prototype.hasOwnProperty.call(schemaProps, propName)) { continue; } // check if property is null if (dto?.[propName] === null) { // the prop is explicitly null, is it explicitly nullable? if (schemaProps[propName]?.nullable === true) { result[propName] = null; continue; } /* * else: * the property is null but not marked as nullable. However, it could still be valid if an `anyOf` rule is set * with a type `null`. So, we cannot consider for the moment this data as invalid. */ } // Check if property is required if (requiredProps.includes(propName)) { if (!Object.prototype.hasOwnProperty.call(dto, propName)) { validationError = EntitySchema.getOrInitEntityValidationError(name, validationError); validationError.addError(propName, "required", `The ${propName} is required.`); continue; } } else { // if it's not required and not present proceed if (!Object.prototype.hasOwnProperty.call(dto, propName)) { continue; } } try { result[propName] = EntitySchema.validateProp(propName, dto[propName], schemaProps[propName]); } catch (error) { if (error instanceof EntityValidationError) { validationError = EntitySchema.getOrInitEntityValidationError(name, validationError); validationError.details[propName] = error.details[propName]; } else { throw error; } } } // Throw error if some issues were gathered if (validationError) { throw validationError; } return result; } /** * Get or init entity validation error. * @param {string} name The name of the entity. * @param {EntityValidationError|null} [validationError] The entity validation error to get or init if does not exist. * @returns {EntityValidationError} */ static getOrInitEntityValidationError(name, validationError) { return validationError || new EntityValidationError(`Could not validate entity ${name}.`); } /** * Validate a given property against a given schema * * @param {string} propName example: name * @param {*} prop example 'my folder' * @param {object} propSchema example {type:string, maxLength: 64} * @throw {EntityValidationError} * @returns {*} prop */ static validateProp(propName, prop, propSchema) { // Check for props that can be of multiple types if (propSchema.anyOf) { EntitySchema.validateAnyOf(propName, prop, propSchema.anyOf); return prop; } // check if prop is null if (propSchema.nullable === true && prop === null) { return prop; } // Check if prop validates based on type EntitySchema.validatePropType(propName, prop, propSchema); // Check if the value is the enumerated list if (propSchema.enum) { EntitySchema.validatePropEnum(propName, prop, propSchema); return prop; } // Additional rules by types switch (propSchema.type) { case "string": // maxLength, minLength, length, regex, etc. EntitySchema.validatePropTypeString(propName, prop, propSchema); break; /* * Note on 'array' - unchecked as not in use beyond array of objects in passbolt * Currently it must be done manually when bootstrapping collections * example: foldersCollection, permissionsCollection, etc. * * Note on 'object' - we do not check if property of type 'object' validate (or array of objects, see above) * Currently it must be done manually in the entities when bootstrapping associations * * Note on 'integer' and 'number' - Min / max supported, not needed in passbolt */ case "integer": case "number": EntitySchema.validatePropTypeNumber(propName, prop, propSchema); break; case "array": EntitySchema.validatePropTypeArray(propName, prop, propSchema); break; case "object": case "boolean": case "blob": case "null": // No additional checks break; case "x-custom": EntitySchema.validatePropCustom(propName, prop, propSchema); break; default: throw new TypeError(`Could not validate property ${propName}. Unsupported prop type ${propSchema.type}`); } return prop; } /** * Validate a prop of type string * Throw an error with the validation details if validation fails * * @param {string} propName example: name * @param {*} prop example 'my folder' * @param {object} propSchema example {type:string} * @throw {EntityValidationError} * @returns void */ static validatePropType(propName, prop, propSchema) { if (!EntitySchema.isValidPropType(prop, propSchema.type)) { throw EntitySchema.handlePropertyValidationError( propName, "type", `The ${propName} is not a valid ${propSchema.type}.`, ); } } /** * Validate a prop with a custom validator * Throw an error with the validation details if validation fails * * @param {string} propName example: name * @param {*} prop the value to validate * @param {object} propSchema example {type:string} * @throw {EntityValidationError} * @returns void */ static validatePropCustom(propName, prop, propSchema) { try { propSchema.validationCallback(prop); } catch (e) { throw EntitySchema.handlePropertyValidationError( propName, "custom", `The ${propName} is not valid: ${e.message}`, ); } } /** * Validate a prop of type string * Throw an error with the validation details if validation fails * * @param {string} propName example: name * @param {*} prop example 'my folder' * @param {object} propSchema example {type:string, maxLength: 64} * @throw {EntityValidationError} * @returns void */ static validatePropTypeString(propName, prop, propSchema) { let validationError; if (propSchema.format) { if (!EntitySchema.isValidStringFormat(prop, propSchema.format)) { validationError = EntitySchema.handlePropertyValidationError( propName, "format", `The ${propName} is not a valid ${propSchema.format}.`, validationError, ); } } if (propSchema.length) { if (!EntitySchema.isValidStringLength(prop, propSchema.length, propSchema.length)) { validationError = EntitySchema.handlePropertyValidationError( propName, "length", `The ${propName} should be ${propSchema.length} character in length.`, validationError, ); } } if (propSchema.minLength) { if (!EntitySchema.isValidStringLength(prop, propSchema.minLength)) { validationError = EntitySchema.handlePropertyValidationError( propName, "minLength", `The ${propName} should be ${propSchema.minLength} character in length minimum.`, validationError, ); } } if (propSchema.maxLength) { if (!EntitySchema.isValidStringLength(prop, 0, propSchema.maxLength)) { validationError = EntitySchema.handlePropertyValidationError( propName, "maxLength", `The ${propName} should be ${propSchema.maxLength} character in length maximum.`, validationError, ); } } if (propSchema.pattern) { if (!Validator.matches(prop, propSchema.pattern)) { validationError = EntitySchema.handlePropertyValidationError( propName, "pattern", `The ${propName} is not valid.`, validationError, ); } } if (propSchema.custom) { if (!propSchema.custom(prop)) { validationError = EntitySchema.handlePropertyValidationError( propName, "custom", `The ${propName} is not valid.`, validationError, ); } } if (validationError) { throw validationError; } } /** * Handle property validation error. * instantiate it if it does not exist yet. * @param {string} [propName] The failing property. * @param {string} [rule] The failing rule. * @param {string} [message] The error message. * @param {EntityValidationError|null} [validationError=null] The entity validation error to add the error to, * instantiate it if it does not exist yet. * @returns {EntityValidationError} */ static handlePropertyValidationError(propName, rule, message, validationError = null) { validationError = validationError || new EntityValidationError(`Could not validate property ${propName}.`); validationError.addError(propName, rule, message); return validationError; } /** * Handle collection validation error. * instantiate it if it does not exist yet. * @param {string} [rule] The failing rule. * @param {string} [message] The error message. * @param {CollectionValidationError|null} [validationError=null] The collection validation error to add the error to, * instantiate it if it does not exist yet. * @returns {CollectionValidationError} */ static handleCollectionValidationError(rule, message, validationError = null) { validationError = validationError || new CollectionValidationError(`Could not validate collection.`); validationError.addCollectionValidationError(rule, message); return validationError; } /** * Validate a prop of type number * Throw an error with the validation details if validation fails * * @param {string} propName example: name * @param {*} prop example 42 * @param {object} propSchema example {type: number, minimum: 64, maximum: 128} * @throw {EntityValidationError} * @returns void */ static validatePropTypeNumber(propName, prop, propSchema) { let validationError; if (typeof propSchema.minimum === "number") { if (!EntitySchema.isGreaterThanOrEqual(prop, propSchema.minimum)) { validationError = EntitySchema.handlePropertyValidationError( propName, "minimum", `The ${propName} should be greater or equal to ${propSchema.minimum}.`, validationError, ); } } if (typeof propSchema.maximum === "number") { if (!EntitySchema.isLesserThanOrEqual(prop, propSchema.maximum)) { validationError = EntitySchema.handlePropertyValidationError( propName, "maximum", `The ${propName} should be lesser or equal to ${propSchema.maximum}.`, validationError, ); } } if (validationError) { throw validationError; } } /** * Validate a prop of type array * Throw an error with the validation details if validation fails * * @param {string} propName example: name * @param {[*]} prop example [*] * @param {object} propSchema example {type: array, items: {type:string, maxLength: 64}} * @throw {EntityValidationError} * @returns void */ static validatePropTypeArray(propName, prop, propSchema) { let validationError; // Do not validate array items if no schema items schema defined. if (!propSchema?.items || !(typeof propSchema.items === "object")) { return; } for (let index = 0; index < prop.length; index++) { const propItemName = `${propName}.${index}`; try { this.validateProp(propItemName, prop[index], propSchema.items); } catch (error) { if (error instanceof EntityValidationError) { validationError = EntitySchema.getOrInitEntityValidationError(propName, validationError); const errorDetails = error.details[propItemName]; validationError.details[propName] = { ...validationError.details[propName], [index]: errorDetails }; } else { throw error; } } } if (validationError) { throw validationError; } } /** * Validate a prop of any type with possible values define in enum * Throw an error with the validation details if validation fails * * @param {string} propName example: role * @param {*} prop example 'admin' * @param {object} propSchema example {type: string, enum: ['admin', 'user']} * @throw {EntityValidationError} * @returns void */ static validatePropEnum(propName, prop, propSchema) { if (!EntitySchema.isPropInEnum(prop, propSchema.enum)) { const validationError = new EntityValidationError(`Could not validate property ${propName}.`); validationError.addError(propName, "enum", `The ${propName} value is not included in the supported list.`); throw validationError; } } /** * Validate a given property against multiple possible types * * @param {string} propName example: name * @param {*} prop example 'my folder' * @param {array} anyOf example [{type:string, maxLength: 64}, {type:null}] * @throw {EntityValidationError} * @returns {*} prop */ static validateAnyOf(propName, prop, anyOf) { for (let i = 0; i < anyOf.length; i++) { try { EntitySchema.validateProp(propName, prop, anyOf[i]); return; } catch { // All must fail... } } const validationError = new EntityValidationError(`Could not validate property ${propName}.`); validationError.addError(propName, "type", `The ${propName} does not match any of the supported types.`); throw validationError; } /** * Check if prop validates based on type * * @param {*} prop * @param {string} type * @returns {boolean} * @throws TypeError if type is not supported */ static isValidPropType(prop, type) { if (Array.isArray(type)) { throw new TypeError("EntitySchema isValidPropType multiple types are not supported."); } if (typeof type !== "string") { throw new TypeError("EntitySchema isValidPropType type is invalid."); } switch (type) { case "null": return prop === null; case "boolean": return typeof prop === "boolean"; case "string": return typeof prop === "string"; case "integer": return Number.isInteger(prop); case "number": return typeof prop === "number"; case "object": return typeof prop === "object"; case "array": return Array.isArray(prop); case "blob": return prop instanceof Blob; case "x-custom": return true; default: throw new TypeError("EntitySchema validation type not supported."); } } /** * Check if prop validates based on format * * @param {*} prop * @param {string} format * @returns {boolean} * @throws TypeError if format is not supported */ static isValidStringFormat(prop, format) { if (typeof format !== "string") { throw new TypeError("EntitySchema validPropFormat format is invalid."); } switch (format) { case "uuid": return Validator.isUUID(prop); case "email": case "idn-email": return Validator.isEmail(prop); case "date-time": return Validator.isISO8601(prop); /* * case 'ipv4': * return Validator.isIP(prop, '4'); * case 'ipv6': * return Validator.isIP(prop, '6'); */ /* * Not in json-schema but needed by passbolt * cowboy style section 🤠 */ case "x-hex-color": return Validator.isHexColor(prop); case "x-base64": return Validator.isBase64(prop); // Not supported - Not needed default: throw new TypeError(`EntitySchema string validation format ${format} is not supported.`); } } /** * Validate if a string is of a given length * @param {string} str * @param {int} min * @param {int} max * @returns {boolean|*} */ static isValidStringLength(str, min, max) { min = min || 0; return Validator.isLength(str, min, max); } /** * Check if the value is the enumerated list * * @param {*} prop * @param {array<string>} enumList * @returns {boolean} * @throws TypeError if format is not supported */ static isPropInEnum(prop, enumList) { if (!enumList || !Array.isArray(enumList) || !enumList.length) { throw new TypeError(`EntitySchema enum schema cannot be empty.`); } return enumList.includes(prop); } /** * Check if the value is greater than the given value * * @param {number} prop * @param {number} gte * @returns {boolean} */ static isGreaterThanOrEqual(prop, gte) { return prop >= gte; } /** * Check if the value is less than the given value * * @param {number} prop * @param {number} lte * @returns {boolean} */ static isLessThanOrEqual(prop, lte) { return prop <= lte; } /** * Check if the value is lesser than the given value * * @param {number} prop * @param {number} lte * @returns {boolean} */ static isLesserThanOrEqual(prop, lte) { return prop <= lte; } } export default EntitySchema;