From 96a80cfae7af65330c58a7ac4cd644dc10e8f813 Mon Sep 17 00:00:00 2001 From: Seyi Adebajo Date: Sun, 5 Aug 2018 18:13:45 -0700 Subject: [PATCH] validates the identifierField values against the expected schema field names when a user manually edits the values --- .../app/components/dataset-compliance.ts | 61 +++++++++++++------ wherehows-web/app/utils/array.ts | 11 +++- .../datasets/compliance/metadata-schema.ts | 33 +++++++++- 3 files changed, 82 insertions(+), 23 deletions(-) diff --git a/wherehows-web/app/components/dataset-compliance.ts b/wherehows-web/app/components/dataset-compliance.ts index 9b375aa792..9d75888ac5 100644 --- a/wherehows-web/app/components/dataset-compliance.ts +++ b/wherehows-web/app/components/dataset-compliance.ts @@ -66,13 +66,11 @@ import { import { uniqBy } from 'lodash'; import { IColumnFieldProps } from 'wherehows-web/typings/app/dataset-columns'; import { NonIdLogicalType } from 'wherehows-web/constants/datasets/compliance'; -import { pick } from 'lodash'; import { trackableEvent, TrackableEventCategory } from 'wherehows-web/constants/analytics/event-tracking'; import { notificationDialogActionFactory } from 'wherehows-web/utils/notifications/notifications'; -import validateMetadataObject, { - complianceEntitiesTaxonomy -} from 'wherehows-web/utils/datasets/compliance/metadata-schema'; +import { isMetadataObject, jsonValuesMatch } from 'wherehows-web/utils/datasets/compliance/metadata-schema'; import { typeOf } from '@ember/utils'; +import { pick } from 'wherehows-web/utils/object'; const { complianceDataException, @@ -154,7 +152,14 @@ export default class DatasetCompliance extends Component { jsonComplianceEntities: ComputedProperty = computed('columnIdFieldsToCurrentPrivacyPolicy', function( this: DatasetCompliance ): string { - const entityAttrs = ['identifierField', 'identifierType', 'logicalType', 'nonOwner', 'valuePattern', 'readonly']; + const entityAttrs: Array = [ + 'identifierField', + 'identifierType', + 'logicalType', + 'nonOwner', + 'valuePattern', + 'readonly' + ]; const entityMap: ISchemaFieldsToPolicy = get(this, 'columnIdFieldsToCurrentPrivacyPolicy'); const entitiesWithModifiableKeys = arrayMap((tag: IComplianceEntityWithMetadata) => pick(tag, entityAttrs))( (>[]).concat(...Object.values(entityMap)) @@ -472,12 +477,14 @@ export default class DatasetCompliance extends Component { // invoking the previousStep action to go back in the sequence // batch previousStep invocation in a afterRender queue due to editStepIndex update previousAction = noop; - run((): void => { - if (this.isDestroyed || this.isDestroying) { - return; + run( + (): void => { + if (this.isDestroyed || this.isDestroying) { + return; + } + schedule('afterRender', this, this.actions.previousStep); } - schedule('afterRender', this, this.actions.previousStep); - }); + ); } } }).enqueue(); @@ -979,7 +986,7 @@ export default class DatasetCompliance extends Component { ]); const { complianceEntities } = complianceInfo!; // All changeSet attrs that can be on policy, excluding changeSet metadata - const entityAttrs = [ + const entityAttrs: Array = [ 'identifierField', 'identifierType', 'logicalType', @@ -989,7 +996,7 @@ export default class DatasetCompliance extends Component { 'valuePattern' ]; const updatingComplianceEntities = arrayMap( - (tag: IComplianceChangeSet): IComplianceEntity => pick(tag, entityAttrs) + (tag: IComplianceChangeSet): IComplianceEntity => pick(tag, entityAttrs) )(workingCopy); return complianceEntities.setObjects(updatingComplianceEntities); @@ -1095,12 +1102,24 @@ export default class DatasetCompliance extends Component { */ onManualComplianceUpdate(this: DatasetCompliance, updatedEntities: string): void { try { - // check if the string is parseable as a JSON object + // check if the string is parse-able as a JSON object const entities = JSON.parse(updatedEntities); const metadataObject = { complianceEntities: entities }; - const isValid = validateMetadataObject(metadataObject, complianceEntitiesTaxonomy); + // Check that metadataObject has a valid property matching complianceEntitiesTaxonomy + let isValid = isMetadataObject(metadataObject); + + // Lists the fieldNames / identifierField property values on the edit compliance policy + const updatedIdentifierFieldValues = arrayMap(({ identifierField }: IComplianceEntity) => identifierField)( + entities + ); + // Lists the expected fieldNames / identifierField property values from the schemaFieldNamesMappedToDataTypes + const expectedIdentifierFieldValues = arrayMap( + ({ fieldName }: Pick) => fieldName + )(get(this, 'schemaFieldNamesMappedToDataTypes')); + + isValid = isValid && jsonValuesMatch(updatedIdentifierFieldValues, expectedIdentifierFieldValues); setProperties(this, { isManualApplyDisabled: !isValid, manualParseError: '' }); @@ -1119,7 +1138,7 @@ export default class DatasetCompliance extends Component { async onApplyComplianceJson(this: DatasetCompliance) { try { await get(this, 'onComplianceJsonUpdate')(JSON.stringify(get(this, 'manuallyEnteredComplianceEntities'))); - // Proceed to next step if application of entites is successful + // Proceed to next step if application of entities is successful this.actions.nextStep.call(this); } catch { noop(); @@ -1263,11 +1282,13 @@ export default class DatasetCompliance extends Component { } if (willMarkAllAsNo) { - return setProperties( - this.getDatasetClassificationRef(), - datasetClassifiersKeys.reduce( - (classification, classifier): {} => ({ ...classification, ...{ [classifier]: false } }), - {} + return ( + setProperties( + this.getDatasetClassificationRef(), + datasetClassifiersKeys.reduce( + (classification, classifier): {} => ({ ...classification, ...{ [classifier]: false } }), + {} + ) ) ); } diff --git a/wherehows-web/app/utils/array.ts b/wherehows-web/app/utils/array.ts index 8edbb89596..e5fa75ccb9 100644 --- a/wherehows-web/app/utils/array.ts +++ b/wherehows-web/app/utils/array.ts @@ -13,6 +13,14 @@ type Iteratee = (a: A) => R; */ type Many = T | Array; +/** + * Serializes the values in an array as a sorted string + * @template S the type of values in the array that are assignable to type string + * @param {Array} stringArray array of values assignable to S + * @returns {string} + */ +const serializeStringArray = (stringArray: Array): string => String([...stringArray].sort()); + /** * Takes a number of elements in the list from the start up to the length of the list * @template T type of elements in array @@ -253,5 +261,6 @@ export { arrayEvery, arraySome, iterateArrayAsync, - reduceArrayAsync + reduceArrayAsync, + serializeStringArray }; diff --git a/wherehows-web/app/utils/datasets/compliance/metadata-schema.ts b/wherehows-web/app/utils/datasets/compliance/metadata-schema.ts index 13e6cc9b3f..3ddc7541f9 100644 --- a/wherehows-web/app/utils/datasets/compliance/metadata-schema.ts +++ b/wherehows-web/app/utils/datasets/compliance/metadata-schema.ts @@ -1,9 +1,10 @@ import { typeOf } from '@ember/utils'; import { DatasetClassifiers } from 'wherehows-web/constants'; -import { arrayEvery, arrayMap, arrayReduce } from 'wherehows-web/utils/array'; +import { arrayEvery, arrayMap, arrayReduce, serializeStringArray } from 'wherehows-web/utils/array'; import { IObject } from 'wherehows-web/typings/generic'; import { isObject } from 'wherehows-web/utils/object'; import { difference } from 'lodash'; +import { IComplianceEntity } from 'wherehows-web/typings/api/datasets/compliance'; /** * Defines the interface for an IDL that specifies the data types for properties on @@ -185,6 +186,34 @@ const keysMatchNames = (object: IObject, typeMaps: Array): t return match; }; +/** + * Validates that an array of json object string values match + * @param {Array} values the received list of strings + * @param {Array} expectedValues the expected list of strings + * @returns {true} + */ +const jsonValuesMatch = (values: Array, expectedValues: Array): true => { + const sValues = serializeStringArray(values); + const sExpectedValues = serializeStringArray(expectedValues); + const match = sValues === sExpectedValues; + + if (!match) { + throw new Error( + ` Found ${difference(values, expectedValues).join(', ')}. Expected only ${expectedValues.join(', ')}` + ); + } + + return match; +}; + +/** + * Type guard asserts that object is assignable to { complianceEntities: Array } + * @param {*} object object to be tested against complianceEntitiesTaxonomy + * @returns {(object is { complianceEntities: Array })} + */ +const isMetadataObject = (object: any): object is { complianceEntities: Array } => + keysEquiv(object, complianceEntitiesTaxonomy); + /** * Checks each key on an object matches the expected types in the typeMap * @param {IObject} object the object with keys to check @@ -196,4 +225,4 @@ const keysEquiv = (object: IObject, typeMaps: Array): boolea export default keysEquiv; -export { complianceMetadataTaxonomy, complianceEntitiesTaxonomy }; +export { complianceMetadataTaxonomy, complianceEntitiesTaxonomy, jsonValuesMatch, isMetadataObject };