UNPKG

passbolt-styleguide

Version:

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

726 lines (681 loc) 26.2 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 5.0.0 */ import assertString from "validator/es/lib/util/assertString"; import { ResourceEditCreateFormEnumerationTypes } from "../../resource/ResourceEditCreateFormEnumerationTypes"; import EntityV2 from "../abstract/entityV2"; import EntityValidationError from "../abstract/entityValidationError"; import { RESOURCE_TYPE_PASSWORD_AND_DESCRIPTION_SLUG, RESOURCE_TYPE_PASSWORD_DESCRIPTION_TOTP_SLUG, RESOURCE_TYPE_PASSWORD_STRING_SLUG, RESOURCE_TYPE_TOTP_SLUG, RESOURCE_TYPE_V5_CUSTOM_FIELDS_SLUG, RESOURCE_TYPE_V5_DEFAULT_SLUG, RESOURCE_TYPE_V5_DEFAULT_TOTP_SLUG, RESOURCE_TYPE_V5_PASSWORD_STRING_SLUG, RESOURCE_TYPE_V5_TOTP_SLUG, RESOURCE_TYPE_V5_STANDALONE_NOTE_SLUG, V4_TO_V5_RESOURCE_TYPE_MAPPING, } from "../resourceType/resourceTypeSchemasDefinition"; import ResourceTypesCollection from "../resourceType/resourceTypesCollection"; import SecretDataEntity from "../secretData/secretDataEntity"; import SecretDataV4DefaultEntity from "../secretData/secretDataV4DefaultEntity"; import SecretDataV4DefaultTotpEntity from "../secretData/secretDataV4DefaultTotpEntity"; import SecretDataV4PasswordStringEntity from "../secretData/secretDataV4PasswordStringEntity"; import SecretDataV4StandaloneTotpEntity from "../secretData/secretDataV4StandaloneTotpEntity"; import SecretDataV5DefaultEntity from "../secretData/secretDataV5DefaultEntity"; import SecretDataV5DefaultTotpEntity from "../secretData/secretDataV5DefaultTotpEntity"; import SecretDataV5PasswordStringEntity from "../secretData/secretDataV5PasswordStringEntity"; import SecretDataV5StandaloneCustomFieldsCollection from "../secretData/secretDataV5StandaloneCustomFieldsCollection"; import SecretDataV5StandaloneTotpEntity from "../secretData/secretDataV5StandaloneTotpEntity"; import ResourceMetadataEntity from "./metadata/resourceMetadataEntity"; import { CUSTOM_FIELD_KEY_MAX_LENGTH, CUSTOM_FIELD_TEXT_MAX_LENGTH } from "../customField/customFieldEntity"; import SecretDataV5StandaloneNoteEntity from "../secretData/secretDataV5StandaloneNoteEntity"; class ResourceFormEntity extends EntityV2 { /** * @inheritDoc */ constructor(dtos = {}, options = {}) { super(dtos, options); } /** * @inheritDoc * @returns {{metadata: ResourceMetadataEntity, secret: SecretDataEntity}} */ static get associations() { return { metadata: ResourceMetadataEntity, secret: SecretDataEntity, }; } /** * Get resource entity schema * @returns {Object} schema */ static getSchema() { return { type: "object", required: ["metadata", "resource_type_id", "secret"], properties: { id: { type: "string", format: "uuid", }, resource_type_id: { type: "string", format: "uuid", }, folder_parent_id: { type: "string", format: "uuid", nullable: true, }, expired: { type: "string", format: "date-time", nullable: true, }, // Associated models metadata: ResourceMetadataEntity.getSchema(), secret: { anyOf: [ SecretDataV5DefaultEntity.getSchema(), SecretDataV5DefaultTotpEntity.getSchema(), SecretDataV5StandaloneTotpEntity.getSchema(), SecretDataV5PasswordStringEntity.getSchema(), SecretDataV4DefaultEntity.getSchema(), SecretDataV4DefaultTotpEntity.getSchema(), SecretDataV4StandaloneTotpEntity.getSchema(), SecretDataV4PasswordStringEntity.getSchema(), SecretDataV5StandaloneCustomFieldsCollection.getSchema(), ], }, }, }; } /** * Create the association entity: its schema and its build rules. * Only for secret association get the entity class according the resource type * * Note: * - This function sets secret association properties if a secret dto is present. (In that case it's possible to have null or undefined property. This is useful for edition and validation) * - This function will create default secret if there is no secret (This is useful for the creation) * * @param {object} [options] Options * * @throws {Error} If no secret entity class has been found. */ createAssociations(options = {}) { const validationErrors = new EntityValidationError(); if (this._props.resource_type_id && options.resourceTypes instanceof ResourceTypesCollection) { this.resourceTypes = options.resourceTypes; const resourceType = this.resourceTypes.getFirstById(this._props.resource_type_id); const secretEntityClass = this.getSecretEntityClassByResourceType(resourceType.slug); if (!secretEntityClass) { throw new Error("No secret association class has been found in resource types."); } try { // For editing and validation, it's important to keep null or undefined property and not set with default value if (this._props.secret) { this._secret = new secretEntityClass(this._props.secret, options); } else { this._secret = secretEntityClass.createFromDefault(this._props.secret, options); } } catch (error) { if (error instanceof EntityValidationError) { validationErrors.addAssociationError("secret", error); } else { throw error; } } delete this._props.secret; } try { super.createAssociations(options); } catch (error) { Object.assign(validationErrors.details, error.details); } // Throw error if some issues were gathered if (validationErrors.hasErrors()) { throw validationErrors; } } /** * Get the secret entity class by resource type id * @param {string} resourceTypeSlug * @private */ getSecretEntityClassByResourceType(resourceTypeSlug) { assertString(resourceTypeSlug); switch (resourceTypeSlug) { case RESOURCE_TYPE_V5_DEFAULT_SLUG: return SecretDataV5DefaultEntity; case RESOURCE_TYPE_V5_DEFAULT_TOTP_SLUG: return SecretDataV5DefaultTotpEntity; case RESOURCE_TYPE_V5_TOTP_SLUG: return SecretDataV5StandaloneTotpEntity; case RESOURCE_TYPE_V5_PASSWORD_STRING_SLUG: return SecretDataV5PasswordStringEntity; case RESOURCE_TYPE_PASSWORD_AND_DESCRIPTION_SLUG: return SecretDataV4DefaultEntity; case RESOURCE_TYPE_PASSWORD_DESCRIPTION_TOTP_SLUG: return SecretDataV4DefaultTotpEntity; case RESOURCE_TYPE_TOTP_SLUG: return SecretDataV4StandaloneTotpEntity; case RESOURCE_TYPE_PASSWORD_STRING_SLUG: return SecretDataV4PasswordStringEntity; case RESOURCE_TYPE_V5_CUSTOM_FIELDS_SLUG: return SecretDataV5StandaloneCustomFieldsCollection; case RESOURCE_TYPE_V5_STANDALONE_NOTE_SLUG: return SecretDataV5StandaloneNoteEntity; default: return null; } } /** * Add secret property to the entity. * If secret should mutate get the entity class according the resource type * - Set the new secret association * - Set the resource type id * Finally * - Set the property in the secret association * * Note: This function set secret property. * @param {string} secret The secret to add * @param {object} [options] Options * * @throws {Error} If no secret entity class has been found. * @throws {Error} If secret is not a string. */ addSecret(secret, options) { assertString(secret); // If unknown secret do nothing if (!Object.values(ResourceEditCreateFormEnumerationTypes).includes(secret)) { return; } // Get the current resource type const currentResourceType = this.resourceTypes.getFirstById(this.resourceTypeId); // Get the string behind "secret." (secret.password.test => password.test) const secretPropName = secret.match(/secret\.(.+)$/)?.[1]; // Verify if the current resource type has the secret property if (!this.isResourceTypeHasSecretProperty(currentResourceType, secret)) { const resourceDto = this.toDto(); // Set an empty value to have the property defined and find the matching resource type (Will be set with a default value later) resourceDto.secret[secretPropName] = ""; // Get the resource type slug to mutate when adding secret const mutateResourceType = this.resourceTypes.getResourceTypeMatchingResource( resourceDto, currentResourceType.version, ); // Get the secret entity class associate const secretEntityClass = this.getSecretEntityClassByResourceType(mutateResourceType.slug); if (!secretEntityClass) { throw new Error("No secret association class has been found in resource types."); } // Set the secret with the actual secret data this.set("secret", new secretEntityClass(this.secret.toDto(), { validate: false })); // If current resource type is V4 and password string if (currentResourceType.isV4() && currentResourceType.isPasswordString()) { // Set the secret description with the metadata description this.set("secret.description", this.metadata?.description || "", options); // Set the metadata description with empty value this.set("metadata.description", null, options); } // Set the resource type id this.set("resource_type_id", mutateResourceType.id); this.set("metadata.resource_type_id", mutateResourceType.id); } // Set the secret to add this.set(secret, this.secret.constructor.getDefaultProp(secretPropName), options); } /** * Delete secret property to the entity. * - Delete the property in the secret association * If secret should mutate get the entity class according the resource type * - Set the new secret association * - Set the resource type id * * Note: This function set secret property. * @param {string} secret The secret to delete * @param {object} [options] Options * * @throws {Error} If no secret entity class has been found. * @throws {Error} If secret is not a string. */ deleteSecret(secret, options) { assertString(secret); // If unknown secret do nothing if (!Object.values(ResourceEditCreateFormEnumerationTypes).includes(secret)) { return; } // Get the string behind a "secret." (secret.password => password and secret.password.test => password.test) const secretPropName = secret.match(/secret\.(.+)$/)?.[1]; // Delete the secret property const resourceDto = this.toDto(); delete resourceDto.secret[secretPropName]; // Get the current resource type const currentResourceType = this.resourceTypes.getFirstById(this.resourceTypeId); // Get the resource type slug to mutate when deleting secret const mutateResourceType = this.resourceTypes.getResourceTypeMatchingResource( resourceDto, currentResourceType.version, ); if (currentResourceType.id !== mutateResourceType.id) { // Get the secret entity class associate const secretEntityClass = this.getSecretEntityClassByResourceType(mutateResourceType.slug); if (!secretEntityClass) { throw new Error("No secret association class has been found in resource types."); } // Set the secret with the new secret data this.set("secret", new secretEntityClass(resourceDto.secret, options)); // Set the resource type id this.set("resource_type_id", mutateResourceType.id); this.set("metadata.resource_type_id", mutateResourceType.id); } else { // Set the secret with the new secret data this.set("secret", new this._secret.constructor(resourceDto.secret, options)); } } /** * Convert metadata description to note and resource type to V4 password and description encrypted * @param {object} [options] Options * * @throws {Error} If no secret entity class has been found. */ convertToNote(options) { const resourceType = this.resourceTypes.getFirstBySlug(RESOURCE_TYPE_PASSWORD_AND_DESCRIPTION_SLUG); const secretEntityClass = this.getSecretEntityClassByResourceType(resourceType?.slug); if (!secretEntityClass) { throw new Error("No secret association class has been found in resource types."); } // Set the secret with the new secret data this.set("secret", new secretEntityClass(this.secret.toDto(), options)); // Set the secret description with the metadata description this.set("secret.description", this.metadata?.description || "", options); // Set the metadata description with empty value this.set("metadata.description", null, options); // Set the resource type id this.set("resource_type_id", resourceType.id); this.set("metadata.resource_type_id", resourceType.id); } /** * Convert note to metadata description and resource type to V4 password string * @param {object} [options] Options * * @throws {Error} If no secret entity class has been found. */ convertToMetadataDescription(options) { const resourceType = this.resourceTypes.getFirstBySlug(RESOURCE_TYPE_PASSWORD_STRING_SLUG); const secretEntityClass = this.getSecretEntityClassByResourceType(resourceType?.slug); if (!secretEntityClass) { throw new Error("No secret association class has been found in resource types."); } // Set the metadata description with the secret description this.set("metadata.description", this.secret?.description || "", options); // Set the secret description with empty value this.set("secret.description", null, options); // Set the secret with the new secret data this.set("secret", new secretEntityClass(this.secret.toDto(), options)); // Set the resource type id this.set("resource_type_id", resourceType.id); this.set("metadata.resource_type_id", resourceType.id); } /** * Is resource type has secret property * @param {ResourceTypeEntity} resourceType The resource type * @param {string} secretProp The secret property * @returns {boolean} */ isResourceTypeHasSecretProperty(resourceType, secretProp) { assertString(secretProp); switch (secretProp) { case ResourceEditCreateFormEnumerationTypes.PASSWORD: return resourceType.hasPassword(); case ResourceEditCreateFormEnumerationTypes.TOTP: return resourceType.hasTotp(); case ResourceEditCreateFormEnumerationTypes.NOTE: return resourceType.hasSecretDescription(); case ResourceEditCreateFormEnumerationTypes.CUSTOM_FIELDS: return resourceType.hasCustomFields(); default: return false; } } /* * ================================================== * Dynamic properties getters * ================================================== */ /** * Get resource type id * @returns {string} resource type id */ get resourceTypeId() { return this._props.resource_type_id; } /** * Get resource form metadata * @returns {ResourceMetadataEntity} metadata */ get metadata() { return this._metadata; } /** * Get resource form secret * @returns {SecretDataEntity} secret */ get secret() { return this._secret; } /* * ================================================== * Serialization * ================================================== */ /** * @inheritDoc */ toDto() { const result = Object.assign({}, this._props); if (this._metadata) { result.metadata = this.metadata.toDto(ResourceMetadataEntity.DEFAULT_CONTAIN); } if (this._secret) { result.secret = this.secret.toDto(); } return result; } /** * To resource DTO * @returns {*} */ toResourceDto() { const result = Object.assign({}, this._props); if (this._metadata) { result.metadata = this.metadata.toDto(ResourceMetadataEntity.DEFAULT_CONTAIN); // Add manually custom fields in metadata if any in secret property if (this.secret._customFields) { result.metadata.custom_fields = this.secret._customFields.toMetadataDto(); } } return result; } /** * To secret DTO * @returns {*|null} */ toSecretDto() { if (this._secret) { const result = this.secret.toDto(); // Set manually custom fields in secret if any in secret property if (this.secret._customFields) { result.custom_fields = this.secret._customFields.toSecretDto(); } return result; } return null; } /** * Verifies data integrity to inform users if fields exceed their maximum allowed size * @return {EntityValidationError|null} Validation errors or null if no errors are detected */ verifyHealth() { let validationError = null; // Verify secret data if (this.secret) { validationError = this.validateMaxLengthAgainstSchema( this.secret.toDto(), this.secret, validationError, "secret", ); // Verify secret totp association if (this.secret.totp) { validationError = this.validateMaxLengthAgainstSchema( this.secret.totp.toDto(), this.secret.totp, validationError, "secret.totp", ); } if (this.secret.customFields) { validationError = this.validateCustomFields(this.secret.customFields, validationError); } } // Verify metadata if (this.metadata) { validationError = this.validateMaxLengthAgainstSchema( this.metadata.toDto(ResourceMetadataEntity.DEFAULT_CONTAIN), this.metadata, validationError, "metadata", ); } return validationError; } /** * Validates maxLength fields against a given schema * @param {Object} dataObject - The association or properties to validate * @param {Entity} entity - The entity * @param {EntityValidationError|null} currentError - The existing error object or null * @param {string} associationName - The name of the association * @return {EntityValidationError|null} The updated or unchanged error object * @private */ validateMaxLengthAgainstSchema(dataObject, entity, currentError, associationName = "") { let error = currentError; Object.entries(dataObject).forEach(([fieldName, fieldValue]) => { const fieldSchema = entity.constructor.getSchema().properties[fieldName]; if (!fieldSchema) { return; } if (fieldSchema.type === "array" && Array.isArray(fieldValue)) { // Validate array elements const maxItemLength = fieldSchema.items?.maxLength; if (typeof maxItemLength !== "undefined") { fieldValue.forEach((value, index) => { if (value?.length >= maxItemLength) { error = error || new EntityValidationError(); error.addError( `${associationName}.${fieldName}.${index}`, "maxLength", `${associationName}.${fieldName} at index ${index} exceeds maximum length limit`, ); } }); } } else { // Validate simple fields const maxLength = fieldSchema.maxLength; if (typeof maxLength !== "undefined" && fieldValue?.length >= maxLength) { error = error || new EntityValidationError(); error.addError( `${associationName}.${fieldName}`, "maxLength", `${associationName}.${fieldName} exceeds maximum length limit`, ); } } }); return error; } /** * Validates custom fields * - Validates each key maxLength * - Validates each value maxLength * @param {CustomFieldsCollection} customFields - the custom fields dto to validate * @param {EntityValidationError|null} currentError - The existing error object or null * @private */ validateCustomFields(customFields, currentError) { let error = currentError; // Use an object to collect unique key property values as key and store the index of the custom field const uniqueKeys = {}; for (let i = 0; i < customFields.length; i++) { const customField = customFields.items[i]; const isKeyTooLong = customField.key.length >= CUSTOM_FIELD_KEY_MAX_LENGTH; const isValueTooLong = customField.value.length >= CUSTOM_FIELD_TEXT_MAX_LENGTH; if (customField.key.length > 0 && uniqueKeys[customField.key] != null) { error = error || new EntityValidationError(); error.addError(`custom_fields.${uniqueKeys[customField.key]}.key`, "unique", `The key name is already used`); error.addError(`custom_fields.${i}.key`, "unique", `The key name is already used`); } else { uniqueKeys[customField.key] = i; } if (isKeyTooLong || isValueTooLong) { error = error || new EntityValidationError(); if (isKeyTooLong) { error.addError( `custom_fields.${i}.key`, "maxLength", `The custom field key at index ${i} reached the maximum length`, ); } if (isValueTooLong) { error.addError( `custom_fields.${i}.value`, "maxLength", `The custom field value at index ${i} reached the maximum length`, ); } } } return error; } /** * @inheritdoc */ validate(options = {}) { /* * options.skipSchemaAssociationValidation remove the schema required validation on the associations * Required association is not part of the props after the entity is created */ const validationErrors = super.validate( Object.assign(options, { skipSchemaAssociationValidation: true, }), ); return validationErrors; } /** * Remove empty secret * @param {object} [options] Options * * This function removes: * * - empty Totp on: * - V4: password, description and Totp * - V5: DefaultTotp * * - empty custom fields on: * - V5: Default and defaultTotp * * Not supported: * - Other resource type */ removeEmptySecret(options) { //If totp has no value then remove it if (this.secret instanceof SecretDataV4DefaultTotpEntity || this.secret instanceof SecretDataV5DefaultTotpEntity) { if (!this.secret.totp.hasSecretKey) { this.deleteSecret(ResourceEditCreateFormEnumerationTypes.TOTP, options); } } //If custom_fields has no value then remove it if (this.secret instanceof SecretDataV5DefaultEntity || this.secret instanceof SecretDataV5DefaultTotpEntity) { if (this.secret.customFields && this.secret.customFields.isEmpty()) { this.deleteSecret(ResourceEditCreateFormEnumerationTypes.CUSTOM_FIELDS, options); } } } /** * Remove metadata that are set but not in use anymore. * i.e. a username is set on password form but password secret has been removed. * * This function removes: * * - username if password secret is not set */ removeUnusedNonEmptyMetadata() { const currentResourceType = this.resourceTypes.getFirstById(this.resourceTypeId); if (!currentResourceType.hasPassword()) { this.metadata.unset("username"); } } /** * Add required secret * @param {object} [options] Options * * Note: This function add password on: * V4: * - password and description if not set add empty password * V5: * - Default if password is not set add null password * * Not supported: * - Other resource type */ addRequiredSecret(options) { const resourceType = this.resourceTypes.getFirstById(this._props.resource_type_id); if (resourceType.hasPassword() && this.secret.password == null) { if (resourceType.isV5()) { this.secret.set("password", null, options); } else if (resourceType.isV4()) { this.secret.set("password", "", options); } } } /** * Upgrade resource v4 to resource v5 * @returns {void} * @throws {Error} If no secret entity class has been found. */ upgradeToV5() { const resourceType = this.resourceTypes.getFirstById(this.resourceTypeId); let v5ResourceTypeSlug = null; if (resourceType.slug === RESOURCE_TYPE_PASSWORD_STRING_SLUG) { v5ResourceTypeSlug = RESOURCE_TYPE_V5_DEFAULT_SLUG; } else if (resourceType.slug in V4_TO_V5_RESOURCE_TYPE_MAPPING) { v5ResourceTypeSlug = V4_TO_V5_RESOURCE_TYPE_MAPPING[resourceType.slug]; } //Do nothing if slug cannot be found if (v5ResourceTypeSlug) { const v5ResourceType = this.resourceTypes.getFirstBySlug(v5ResourceTypeSlug); const secretEntityClass = this.getSecretEntityClassByResourceType(v5ResourceTypeSlug); if (!secretEntityClass) { throw new Error("No secret association class has been found in resource types."); } this.set("resource_type_id", v5ResourceType.id); this.set("metadata.resource_type_id", v5ResourceType.id); this.set("metadata.object_type", ResourceMetadataEntity.METADATA_OBJECT_TYPE); /* * Set the secret with the secret data v5 */ this.set("secret", new secretEntityClass(this.secret.toDto())); } } /** * @inheritdoc */ marshall() { if (!this._props.metadata) { this._props.metadata = { resource_type_id: this._props.resource_type_id, name: "", uris: [], object_type: ResourceMetadataEntity.METADATA_OBJECT_TYPE, }; } } } export default ResourceFormEntity;