mirror of
https://github.com/datahub-project/datahub.git
synced 2025-07-07 01:00:41 +00:00
428 lines
16 KiB
JavaScript
428 lines
16 KiB
JavaScript
import Ember from 'ember';
|
|
import isTrackingHeaderField from 'wherehows-web/utils/validators/tracking-headers';
|
|
import { defaultFieldDataTypeClassification, classifiers, datasetClassifiers } from 'wherehows-web/constants';
|
|
|
|
const {
|
|
get,
|
|
set,
|
|
isBlank,
|
|
isPresent,
|
|
computed,
|
|
getWithDefault,
|
|
setProperties,
|
|
Component,
|
|
String: { htmlSafe }
|
|
} = Ember;
|
|
|
|
/**
|
|
* String constant identifying the classified fields on the security spec
|
|
* @type {String}
|
|
*/
|
|
const sourceClassificationKey = 'securitySpecification.classification';
|
|
|
|
/**
|
|
* String constant identifying the datasetClassification on the security spec
|
|
* @type {String}
|
|
*/
|
|
const datasetClassificationKey = 'securitySpecification.datasetClassification';
|
|
|
|
/**
|
|
* List of logical types / field level data types
|
|
* https://iwww.corp.linkedin.com/wiki/cf/display/DWH/List+of+Metadata+for+Data+Sets
|
|
* @type {Array}
|
|
*/
|
|
const logicalTypes = Object.keys(defaultFieldDataTypeClassification);
|
|
/**
|
|
* A list of available keys for the datasetClassification map on the security specification
|
|
* @type {Array}
|
|
*/
|
|
const datasetClassifiersKeys = Object.keys(datasetClassifiers);
|
|
|
|
// TODO: DSS-6671 Extract to constants module
|
|
const successUpdating = 'Your changes have been successfully saved!';
|
|
const failedUpdating = 'Oops! We are having trouble updating this dataset at the moment.';
|
|
const missingTypes = 'Looks like some fields are marked as `Confidential` or ' +
|
|
'`Highly Confidential` but do not have a specified `Field Format`?';
|
|
const hiddenTrackingFieldsMsg = htmlSafe(
|
|
'<p>Hey! Just a heads up that some fields in this dataset have been hidden from the table(s) below. ' +
|
|
'These are tracking fields for which we\'ve been able to predetermine the compliance classification.</p>' +
|
|
'<p>For example: <code>header.memberId</code>, <code>requestHeader</code>. ' +
|
|
'Hopefully, this saves you some scrolling!</p>'
|
|
);
|
|
|
|
/**
|
|
* Takes a string, returns a formatted string. Niche , single use case
|
|
* for now, so no need to make into a helper
|
|
* @param {String} string
|
|
*/
|
|
const formatAsCapitalizedStringWithSpaces = string =>
|
|
string.replace(/[A-Z]/g, match => ` ${match}`).capitalize();
|
|
|
|
export default Component.extend({
|
|
sortColumnWithName: 'identifierField',
|
|
filterBy: 'identifierField',
|
|
sortDirection: 'asc',
|
|
searchTerm: '',
|
|
hiddenTrackingFields: hiddenTrackingFieldsMsg,
|
|
|
|
// Map classifiers to options better consumed by drop down
|
|
classifiers: classifiers.map(value => ({
|
|
value,
|
|
label: formatAsCapitalizedStringWithSpaces(value)
|
|
})),
|
|
// Map logicalTypes to options better consumed by drop down
|
|
logicalTypes: ['', ...logicalTypes].map(value => {
|
|
const label = value ?
|
|
value.replace(/_/g, ' ')
|
|
.replace(/([A-Z]{3,})/g, f => f.toLowerCase().capitalize()) :
|
|
'Not Specified';
|
|
|
|
return {
|
|
value,
|
|
label
|
|
};
|
|
}),
|
|
|
|
/**
|
|
* Checks that all tags/ dataset content types have a boolean value
|
|
* @type {Ember.computed}
|
|
*/
|
|
isDatasetFullyClassified: computed('datasetClassification', function() {
|
|
const datasetClassification = get(this, 'datasetClassification');
|
|
|
|
return Object.keys(datasetClassification)
|
|
.map(key => ({ value: datasetClassification[key].value }))
|
|
.every(({ value }) => [true, false].includes(value));
|
|
}),
|
|
|
|
/**
|
|
* Computed property that is dependent on all the keys in the datasetClassification map
|
|
* Returns a new map of datasetClassificationKey: String-> Object.<Boolean|undefined,String>
|
|
* @type {Ember.computed}
|
|
*/
|
|
datasetClassification: computed(`${datasetClassificationKey}.{${datasetClassifiersKeys.join(',')}}`, function() {
|
|
const sourceDatasetClassification = get(this, datasetClassificationKey) || {};
|
|
|
|
return Object.keys(datasetClassifiers).reduce((datasetClassification, classifier) => {
|
|
datasetClassification[classifier] = {
|
|
value: sourceDatasetClassification[classifier],
|
|
label: datasetClassifiers[classifier]
|
|
};
|
|
|
|
return datasetClassification;
|
|
}, {});
|
|
}),
|
|
|
|
/**
|
|
* Creates a lookup table of fieldNames to classification
|
|
* Also, the expectation is that the association from fieldName -> classification
|
|
* is one-to-one hence no check to ensure a fieldName gets clobbered
|
|
* in the lookup assignment
|
|
*/
|
|
fieldNameToClass: computed(
|
|
`${sourceClassificationKey}.{confidential,limitedDistribution,highlyConfidential}.[]`,
|
|
function () {
|
|
const sourceClasses = getWithDefault(this, sourceClassificationKey, []);
|
|
// Creates a lookup table of fieldNames to classification
|
|
// Also, the expectation is that the association from fieldName -> classification
|
|
// is one-to-one hence no check to ensure a fieldName gets clobbered
|
|
// in the lookup assignment
|
|
return Object.keys(sourceClasses)
|
|
.reduce((lookup, classificationKey) =>
|
|
// For the provided classificationKey, iterate over it's fieldNames,
|
|
// and assign the classificationKey to the fieldName in the table
|
|
(sourceClasses[classificationKey] || []).reduce((lookup, field) => {
|
|
const { identifierField } = field;
|
|
// cKey -> 1...fieldNameList => fieldName -> cKey
|
|
lookup[identifierField] = classificationKey;
|
|
return lookup;
|
|
}, lookup),
|
|
{}
|
|
);
|
|
}),
|
|
|
|
/**
|
|
* Lists all the dataset fields found in the `columns` api, and intersects
|
|
* each with the currently classified field names in
|
|
* securitySpecification.classification or null if not found
|
|
*/
|
|
classificationDataFields: computed(
|
|
`${sourceClassificationKey}.{confidential,limitedDistribution,highlyConfidential}.[]`,
|
|
'schemaFieldNamesMappedToDataTypes',
|
|
function () {
|
|
// Set default or if already in policy, retrieve current values from
|
|
// privacyCompliancePolicy.compliancePurgeEntities
|
|
return getWithDefault(
|
|
this, 'schemaFieldNamesMappedToDataTypes', []
|
|
).map(({ fieldName: identifierField, dataType }) => {
|
|
// Get the current classification list
|
|
const currentClassLookup = get(this, 'fieldNameToClass');
|
|
const classification = currentClassLookup[identifierField];
|
|
|
|
// If the classification type exists, then find the identifierField, and
|
|
// assign to field, otherwise null
|
|
// Rather than assigning the default classification here, nulling gives the benefit of allowing
|
|
// subsequent consumer know that this field did not have a previous classification
|
|
const field = classification ?
|
|
get(this, `${sourceClassificationKey}.${classification}`)
|
|
.findBy('identifierField', identifierField) :
|
|
null;
|
|
|
|
// Extract the logicalType from the field
|
|
const logicalType = isPresent(field) ? field.logicalType : null;
|
|
|
|
// Map to a new literal containing these props
|
|
return {
|
|
dataType,
|
|
identifierField,
|
|
classification,
|
|
logicalType
|
|
};
|
|
});
|
|
}
|
|
),
|
|
|
|
/**
|
|
* @type {Boolean} cached boolean flag indicating that fields do contain a `kafka type`
|
|
* tracking header.
|
|
* Used to indicate to viewer that these fields are hidden.
|
|
*/
|
|
containsHiddenTrackingFields: computed(
|
|
'classificationDataFieldsSansHiddenTracking.length',
|
|
function () {
|
|
// If their is a diff in complianceDataFields and complianceDataFieldsSansHiddenTracking,
|
|
// then we have hidden tracking fields
|
|
return get(this, 'classificationDataFieldsSansHiddenTracking.length') !== get(this, 'classificationDataFields.length');
|
|
}),
|
|
|
|
/**
|
|
* @type {Array.<Object>} Filters the mapped confidential data fields without `kafka type`
|
|
* tracking headers
|
|
*/
|
|
classificationDataFieldsSansHiddenTracking: computed('classificationDataFields.[]', function () {
|
|
return get(this, 'classificationDataFields')
|
|
.filter(({ identifierField }) => !isTrackingHeaderField(identifierField));
|
|
}),
|
|
|
|
/**
|
|
* TODO: DSS-6672 Extract to notifications service
|
|
* Helper method to update user when an async server update to the
|
|
* security specification is handled.
|
|
* @param {XMLHttpRequest|Promise|jqXHR|*} request the server request
|
|
* @param {String} [successMessage] optional _message for successful response
|
|
*/
|
|
whenRequestCompletes(request, { successMessage } = {}) {
|
|
Promise.resolve(request)
|
|
.then(({ status = 'error' }) => {
|
|
// The server api currently responds with an object containing
|
|
// a status when complete
|
|
return status === 'ok' ?
|
|
setProperties(this, {
|
|
_message: successMessage || successUpdating,
|
|
_alertType: 'success'
|
|
}) :
|
|
Promise.reject(new Error(`Reason code for this is ${status}`));
|
|
})
|
|
.catch(err => {
|
|
let _message = `${failedUpdating} \n ${err}`;
|
|
let _alertType = 'danger';
|
|
|
|
if (get(this, 'isNewSecuritySpecification')) {
|
|
_message = 'This dataset does not have any ' +
|
|
'previously saved fields with a Security Classification.';
|
|
_alertType = 'info';
|
|
}
|
|
|
|
setProperties(this, {
|
|
_message,
|
|
_alertType
|
|
});
|
|
});
|
|
},
|
|
|
|
/**
|
|
* TODO:DSS-6719 refactor into mixin
|
|
* Clears recently shown user messages
|
|
*/
|
|
clearMessages() {
|
|
return setProperties(this, {
|
|
_message: '',
|
|
_alertType: ''
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Takes an identifierField and a logicalType and updates the field on the
|
|
* classification if it exists. Otherwise this is a no-op
|
|
* @param {String} identifierField
|
|
* @param {String} logicalType the type to be updated
|
|
*/
|
|
changeFieldLogicalType(identifierField, logicalType) {
|
|
const nextProps = { identifierField, logicalType };
|
|
// The current classification name for the candidate identifier
|
|
const currentClassLookup = get(this, 'fieldNameToClass');
|
|
const defaultClassification = getWithDefault(this, `${sourceClassificationKey}.${defaultFieldDataTypeClassification[logicalType]}`, []);
|
|
let currentClassificationName = currentClassLookup[identifierField];
|
|
|
|
/**
|
|
* If the field does not already have a classification, we set it's default
|
|
* @link classificationDataFields
|
|
*/
|
|
if (isBlank(currentClassificationName)) {
|
|
currentClassificationName = defaultFieldDataTypeClassification[logicalType];
|
|
}
|
|
// The current classification list
|
|
const currentClassification = get(this, `${sourceClassificationKey}.${currentClassificationName}`);
|
|
|
|
if (!Array.isArray(currentClassification)) {
|
|
throw new Error(`
|
|
You have specified a classification object that is not a list ${currentClassification}.
|
|
Ensure that the classification for this identifierField (${identifierField}) is
|
|
set before attempting to change the logicalType.
|
|
`);
|
|
}
|
|
|
|
const field = currentClassification.findBy('identifierField', identifierField);
|
|
|
|
// Since this is change on the logical type, if the field already exists in a classification
|
|
// list, we remove it to allow applying the default for the new field type, below
|
|
if (isPresent(field)) {
|
|
// Remove identifierField from list
|
|
currentClassification.setObjects(
|
|
currentClassification.filter(
|
|
({ identifierField: fieldName }) => fieldName !== identifierField)
|
|
);
|
|
}
|
|
|
|
// Update the default classification list
|
|
return defaultClassification.setObjects([nextProps, ...defaultClassification]);
|
|
},
|
|
|
|
/**
|
|
* Checks that each field in a given source classification has a value
|
|
* for the logicalType
|
|
* @param {Array} sourceClassification
|
|
*/
|
|
ensureFieldsContainLogicalType: (sourceClassification = []) =>
|
|
sourceClassification.every(({ logicalType }) => logicalType),
|
|
|
|
actions: {
|
|
/**
|
|
* Updates the logical type for a field with the provided identifierField
|
|
* @param {String} identifierField the name of the field to update
|
|
* @param {String} logicalType the updated logical type
|
|
* @return {*|String|void}
|
|
*/
|
|
updateLogicalType({ identifierField }, { value: logicalType }) {
|
|
// TODO:DSS-6719 refactor into mixin
|
|
this.clearMessages();
|
|
return this.changeFieldLogicalType(identifierField, logicalType);
|
|
},
|
|
/**
|
|
* Toggles the provided identifierField onto a classification list
|
|
* on securitySpecification.classification, identified by the provided
|
|
* classKey.
|
|
* @param {Object} props field attributes for the field to update on the classification list
|
|
* @param {String} classKey the name of the class to add, or potentially
|
|
* remove the identifierField from
|
|
*/
|
|
updateClassification(props, { value: classKey }) {
|
|
const { identifierField } = props;
|
|
// fieldNames can be paths i.e. identifierField.identifierPath.subPath
|
|
// therefore, using Ember's `path lookup` syntax will not work
|
|
const currentClassLookup = get(this, 'fieldNameToClass');
|
|
const currentClass = currentClassLookup[identifierField];
|
|
|
|
// TODO:DSS-6719 refactor into mixin
|
|
this.clearMessages();
|
|
// Since the association from identifierField -> classification is 1-to-1
|
|
// ensure that we do not currently have this identifierField
|
|
// in any other classification lists by checking that the lookup is void
|
|
if (!isBlank(currentClass)) {
|
|
// Get the current classification list
|
|
const currentClassification = get(
|
|
this,
|
|
`${sourceClassificationKey}.${currentClass}`
|
|
);
|
|
|
|
// Remove identifierField from list
|
|
currentClassification.setObjects(
|
|
currentClassification.filter(
|
|
({ identifierField: fieldName }) => fieldName !== identifierField)
|
|
);
|
|
}
|
|
|
|
if (classKey) {
|
|
// Get the candidate list
|
|
let classification = get(
|
|
this,
|
|
`${sourceClassificationKey}.${classKey}`
|
|
);
|
|
// In the case that the list is not pre-populated,
|
|
// the value will be the default null, array ops won't work here
|
|
// ...so make array
|
|
if (!classification) {
|
|
classification = set(this, `${sourceClassificationKey}.${classKey}`, []);
|
|
}
|
|
|
|
// Finally perform operation
|
|
classification.addObject(Object.assign({}, props));
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Updates the source object representing the current datasetClassification map
|
|
* @param {String} classifier the property on the datasetClassification to update
|
|
* @param {Boolean} value flag indicating if this dataset contains member data for the specified classifier
|
|
*/
|
|
didChangeDatasetClassification(classifier, value) {
|
|
let sourceDatasetClassification = getWithDefault(this, datasetClassificationKey, {});
|
|
|
|
// For datasets initially without a datasetClassification, the default value is null
|
|
if (sourceDatasetClassification === null) {
|
|
sourceDatasetClassification = set(this, datasetClassificationKey, {});
|
|
}
|
|
|
|
return set(sourceDatasetClassification, classifier, value);
|
|
},
|
|
|
|
/**
|
|
* Notify controller to propagate changes
|
|
* @return {Boolean}
|
|
*/
|
|
saveSecuritySpecification() {
|
|
/**
|
|
* For Each classifier ensure that the fields on the securitySpec
|
|
* contain a valid `logicalType` attribute
|
|
* @type {Boolean}
|
|
*/
|
|
const classedFieldsHaveLogicalType = classifiers.every(classifier =>
|
|
this.ensureFieldsContainLogicalType(
|
|
getWithDefault(this, `${sourceClassificationKey}.${classifier}`, [])
|
|
));
|
|
|
|
if (classedFieldsHaveLogicalType) {
|
|
this.whenRequestCompletes(get(this, 'onSave')());
|
|
} else {
|
|
setProperties(this, {
|
|
_message: missingTypes,
|
|
_alertType: 'danger'
|
|
});
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Rolls back changes made to the compliance spec to current
|
|
* server state
|
|
*/
|
|
resetSecuritySpecification() {
|
|
const options = {
|
|
successMessage: 'Field classification has been reset to the previously saved state.'
|
|
};
|
|
this.whenRequestCompletes(get(this, 'onReset')(), options);
|
|
}
|
|
}
|
|
});
|