mirror of
https://github.com/datahub-project/datahub.git
synced 2025-10-13 09:54:10 +00:00
adds feature to download a compliance policy as a json document. upload as json and apply to policy. performs attribute type assertions on values in uploaded policy
This commit is contained in:
parent
30f9bf28b2
commit
c3de4a5ff7
@ -1,9 +1,7 @@
|
||||
import Base from 'ember-simple-auth/authenticators/base';
|
||||
import Ember from 'ember';
|
||||
|
||||
const {
|
||||
$: { post }
|
||||
} = Ember;
|
||||
const { $: { post } } = Ember;
|
||||
|
||||
export default Base.extend({
|
||||
/**
|
||||
@ -11,7 +9,7 @@ export default Base.extend({
|
||||
* Resolves with data object returned from successful request.
|
||||
* @param {String} username username to authenticate with
|
||||
* @param {String} password matching candidate password for username
|
||||
* @return {Promise<{Object, String}>}
|
||||
* @return {Promise}
|
||||
*/
|
||||
authenticate: (username, password) =>
|
||||
Promise.resolve(
|
||||
|
@ -8,8 +8,19 @@ import {
|
||||
nonIdFieldLogicalTypes,
|
||||
defaultFieldDataTypeClassification
|
||||
} from 'wherehows-web/constants';
|
||||
import { isPolicyExpectedShape } from 'wherehows-web/utils/datasets/functions';
|
||||
|
||||
const { Component, computed, set, get, setProperties, getWithDefault, isEmpty, String: { htmlSafe } } = Ember;
|
||||
const {
|
||||
Component,
|
||||
computed,
|
||||
set,
|
||||
get,
|
||||
setProperties,
|
||||
getProperties,
|
||||
getWithDefault,
|
||||
isEmpty,
|
||||
String: { htmlSafe }
|
||||
} = Ember;
|
||||
|
||||
// TODO: DSS-6671 Extract to constants module
|
||||
const missingTypes = 'Looks like some fields may contain privacy data but do not have a specified `Field Format`?';
|
||||
@ -153,6 +164,13 @@ export default Component.extend({
|
||||
label: value ? formatAsCapitalizedStringWithSpaces(value) : '...'
|
||||
})),
|
||||
|
||||
/**
|
||||
* Caches the policy's modification time in milliseconds
|
||||
*/
|
||||
policyModificationTimeInEpoch: computed('complianceInfo', function() {
|
||||
return getWithDefault(this, 'complianceInfo.modifiedTime', 0) * 1000;
|
||||
}),
|
||||
|
||||
/**
|
||||
* @type {Boolean} cached boolean flag indicating that fields do contain a `kafka type`
|
||||
* tracking header.
|
||||
@ -405,6 +423,38 @@ export default Component.extend({
|
||||
},
|
||||
|
||||
actions: {
|
||||
/**
|
||||
* Receives the json representation for compliance and applies each key to the policy
|
||||
* @param {String} textString string representation for the JSON file
|
||||
*/
|
||||
onComplianceJsonUpload(textString) {
|
||||
const policy = JSON.parse(textString);
|
||||
if (isPolicyExpectedShape(policy)) {
|
||||
const currentPolicy = get(this, 'complianceInfo');
|
||||
return set(this, 'complianceInfo', Object.assign({}, currentPolicy, policy));
|
||||
}
|
||||
|
||||
alert('Received policy in an unexpected format! Please check the provided attributes and try again.');
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles the compliance policy download action
|
||||
*/
|
||||
onComplianceDownloadJson() {
|
||||
const currentPolicy = get(this, 'complianceInfo');
|
||||
const policyProps = [
|
||||
datasetClassificationKey,
|
||||
policyFieldClassificationKey,
|
||||
policyComplianceEntitiesKey
|
||||
].map(name => name.split('.').pop());
|
||||
const policy = Object.assign({}, getProperties(currentPolicy, policyProps));
|
||||
const href = `data:text/json;charset=utf-8,${encodeURIComponent(JSON.stringify(policy))}`;
|
||||
const download = `${get(this, 'datasetName')}_policy.json`;
|
||||
const anchor = document.createElement('a');
|
||||
Object.assign(anchor, { download, href });
|
||||
anchor.click();
|
||||
},
|
||||
|
||||
/**
|
||||
* When a user updates the identifierFieldType in the DOM, update the backing store
|
||||
* @param {String} identifierField
|
||||
|
51
wherehows-web/app/components/json-upload.js
Normal file
51
wherehows-web/app/components/json-upload.js
Normal file
@ -0,0 +1,51 @@
|
||||
import Ember from 'ember';
|
||||
|
||||
const { get, computed, Component } = Ember;
|
||||
|
||||
export default Component.extend({
|
||||
classNames: ['nacho-uploader'],
|
||||
|
||||
/**
|
||||
* Handles the change event on the json-upload DOM element
|
||||
* @param {FileList} files extracts a FileList object from the event object
|
||||
* @return {void|null}
|
||||
*/
|
||||
change({ target: { files = [] } }) {
|
||||
const hasFiles = !!files.length;
|
||||
if (hasFiles) {
|
||||
return this.sendFileAsText(Array.from(files).shift());
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Caches a unique id for the HTML Element for file upload
|
||||
*/
|
||||
uploadInputId: computed(function() {
|
||||
return `${get(this, 'elementId')}-uploader`;
|
||||
}),
|
||||
|
||||
/**
|
||||
* Reads a file as a text string and passes the output to the closure action
|
||||
* @param {Blob} fileBlob
|
||||
*/
|
||||
sendFileAsText(fileBlob) {
|
||||
const closureAction = get(this, 'attrs.receiveJsonFile');
|
||||
const reader = new FileReader();
|
||||
if (typeof closureAction === 'function') {
|
||||
reader.onload = ({ target: { result } }) => closureAction(result);
|
||||
reader.readAsText(fileBlob);
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
/**
|
||||
* Proxies the user's click interaction on the styled upload button to the file input element
|
||||
*/
|
||||
didSelectUpload() {
|
||||
const uploadInput = this.$(`#${get(this, 'uploadInputId')}`);
|
||||
return uploadInput.click();
|
||||
}
|
||||
}
|
||||
});
|
@ -52,7 +52,8 @@ $color-scheme: (
|
||||
),
|
||||
blue: (
|
||||
oxford: rgb(53, 75, 87),
|
||||
curious: rgb(26, 161, 217)
|
||||
curious: rgb(26, 161, 217),
|
||||
eastern: rgb(26, 132, 188)
|
||||
),
|
||||
grey: (
|
||||
light: rgb(237, 237, 237),
|
||||
|
@ -16,3 +16,4 @@
|
||||
@import "nacho/nacho-select";
|
||||
@import "nacho/nacho-pager";
|
||||
@import "nacho/nacho-breadcrumbs";
|
||||
@import "nacho/nacho-uploader";
|
||||
|
@ -1,5 +1,8 @@
|
||||
@import "../abstracts/functions";
|
||||
@import "../abstracts/mixins";
|
||||
|
||||
$item-spacing: 10px;
|
||||
|
||||
/**
|
||||
* Override Bootstrap navigation class, .navbar, ruleset
|
||||
*/
|
||||
@ -51,6 +54,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Styles the non primary action bar for an entity
|
||||
*/
|
||||
.secondary-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
|
||||
&__action + &__action {
|
||||
margin-left: $item-spacing;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Styles the action bar on a tab component
|
||||
*/
|
||||
@ -64,6 +80,15 @@
|
||||
background-color: #f8f8f8;
|
||||
z-index: z(dropdown);
|
||||
max-height: 50px;
|
||||
|
||||
&__content {
|
||||
white-space: nowrap;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
&__item + &__item {
|
||||
margin-left: $item-spacing;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -39,3 +39,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.policy-last-saved {
|
||||
margin-right: auto;
|
||||
|
||||
&__saved {
|
||||
font-weight: fw(normal, 6);
|
||||
}
|
||||
}
|
||||
|
@ -6,17 +6,20 @@
|
||||
);
|
||||
}
|
||||
|
||||
$main-button-color: set-color(blue, eastern);
|
||||
$button-text-color: set-color(white, base);
|
||||
|
||||
@include restyle-define(button, (
|
||||
// Button Component Variables
|
||||
restyle-var(height): 32px,
|
||||
restyle-var(horizontal-padding): 16px,
|
||||
restyle-var(main-color): $secondary-color,
|
||||
restyle-var(background-color): $secondary-color,
|
||||
restyle-var(main-color): $main-button-color,
|
||||
restyle-var(background-color): $main-button-color,
|
||||
restyle-var(font-size): 1.7rem,
|
||||
restyle-var(font-weight): 600,
|
||||
restyle-var(text-color): $text-color,
|
||||
restyle-var(text-color): $button-text-color,
|
||||
restyle-var(border-radius): 2px,
|
||||
restyle-var(disabled-text): tint($text-color, 70%),
|
||||
restyle-var(disabled-text): tint($button-text-color, 70%),
|
||||
|
||||
background-color: restyle-var(background-color),
|
||||
border: 0,
|
||||
@ -50,7 +53,7 @@
|
||||
|
||||
restyle-modifiers: (
|
||||
'with a border': (
|
||||
border: 1px solid restyle-var(text-color)
|
||||
border: 1px solid restyle-var(main-color)
|
||||
),
|
||||
small: (
|
||||
restyle-var(font-size): 1.5rem,
|
||||
@ -76,7 +79,7 @@
|
||||
),
|
||||
secondary: (
|
||||
restyle-var(background-color): transparent,
|
||||
restyle-var(text-color): $text-invert-color,
|
||||
restyle-var(text-color): $main-button-color,
|
||||
restyle-var(box-shadow-size): inset 0 0 0 1px,
|
||||
restyle-var(box-shadow-size-hover): inset 0 0 0 2px,
|
||||
restyle-var(box-shadow-size-active): inset 0 0 0 3px,
|
||||
|
@ -18,4 +18,10 @@ $height: 40px;
|
||||
overflow: hidden;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.search-button {
|
||||
border: 1px solid $secondary-color;
|
||||
background-color: $secondary-color;
|
||||
color: $text-color;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,10 @@
|
||||
.nacho-uploader {
|
||||
@include flex-center;
|
||||
display: inline-flex;
|
||||
|
||||
&__label {
|
||||
width: 250px;
|
||||
margin-bottom: 0;
|
||||
padding-right: 5px;
|
||||
}
|
||||
}
|
@ -3,4 +3,5 @@
|
||||
display: inline-block;
|
||||
border-radius: 2px;
|
||||
padding: 5px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div class="tab-body">
|
||||
<section class="action-bar">
|
||||
<div class="container">
|
||||
<button class="nacho-button nacho-button--large-inverse"
|
||||
<div class="container action-bar__content">
|
||||
<button class="nacho-button nacho-button--large-inverse action-bar__item"
|
||||
title={{if ownershipIsInvalid
|
||||
"Need at least two confirmed owners to make changes
|
||||
and no Invalid users"
|
||||
@ -11,7 +11,7 @@
|
||||
Save
|
||||
</button>
|
||||
|
||||
<button class="nacho-button nacho-button--large"
|
||||
<button class="nacho-button nacho-button--large action-bar__item"
|
||||
{{action "addOwner" owners}}>
|
||||
<i class="fa fa-plus" title="Add an Owner">
|
||||
</i>
|
||||
|
@ -13,8 +13,8 @@
|
||||
</div>
|
||||
{{else}}
|
||||
<section class="action-bar">
|
||||
<div class="container">
|
||||
<button class="nacho-button nacho-button--large-inverse"
|
||||
<div class="container action-bar__content">
|
||||
<button class="nacho-button nacho-button--large-inverse action-bar__item"
|
||||
title={{unless isDatasetFullyClassified
|
||||
"Ensure you have provided a yes/no value for all dataset tags"
|
||||
"Save"}}
|
||||
@ -23,7 +23,7 @@
|
||||
Save
|
||||
</button>
|
||||
|
||||
<button class="nacho-button nacho-button--large"
|
||||
<button class="nacho-button nacho-button--large action-bar__item"
|
||||
{{action "resetCompliance"}}>
|
||||
<i class="fa fa-times" title="Start Over">
|
||||
</i>
|
||||
@ -39,6 +39,25 @@
|
||||
</section>
|
||||
{{/if}}
|
||||
|
||||
<div class="secondary-actions">
|
||||
<div class="policy-last-saved">
|
||||
Last saved:
|
||||
<span class="policy-last-saved__saved">
|
||||
{{if isNewPrivacyPolicy 'Never'
|
||||
(moment-calendar policyModificationTimeInEpoch sameElse="MMM Do YYYY, h:mm a")}}
|
||||
</span>
|
||||
</div>
|
||||
{{json-upload receiveJsonFile=(action "onComplianceJsonUpload") class="secondary-actions__action"}}
|
||||
|
||||
{{#unless isNewPrivacyPolicy}}
|
||||
<button
|
||||
{{action "onComplianceDownloadJson"}}
|
||||
class="nacho-button nacho-button--large-inverse secondary-actions__action">
|
||||
Download as a JSON file
|
||||
</button>
|
||||
{{/unless}}
|
||||
</div>
|
||||
|
||||
{{#if isNewPrivacyPolicy}}
|
||||
<div class="alert alert-info" role="alert">
|
||||
<p>
|
||||
|
16
wherehows-web/app/templates/components/json-upload.hbs
Normal file
16
wherehows-web/app/templates/components/json-upload.hbs
Normal file
@ -0,0 +1,16 @@
|
||||
<input
|
||||
type="file"
|
||||
accept=".json"
|
||||
style="display:none"
|
||||
id="{{uploadInputId}}">
|
||||
|
||||
<label for="{{concat elementId '-button'}}" class="nacho-uploader__label">
|
||||
Have existing metadata from a similar dataset that you want to apply here?
|
||||
</label>
|
||||
|
||||
<button
|
||||
{{action "didSelectUpload"}}
|
||||
id="{{concat elementId '-button'}}"
|
||||
class="nacho-button nacho-button--large-inverse">
|
||||
Choose File
|
||||
</button>
|
@ -3,7 +3,7 @@
|
||||
<div class="input-group-btn">
|
||||
<div class="dropdown">
|
||||
<button type="button"
|
||||
class="btn btn-default dropdown-toggle nacho-button--large"
|
||||
class="btn btn-default dropdown-toggle nacho-button--large search-button"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false">
|
||||
@ -38,7 +38,7 @@
|
||||
class="form-control nacho-global-search__text-input"}}
|
||||
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default nacho-button--large"
|
||||
<button class="btn btn-default nacho-button--large search-button"
|
||||
id="global-search-button"
|
||||
type="submit" {{action "search"}}>
|
||||
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { datasetClassifiers } from 'wherehows-web/constants/dataset-classification';
|
||||
|
||||
/**
|
||||
* Builds a default shape for securitySpecification & privacyCompliancePolicy with default / unset values
|
||||
* for non null properties as per Avro schema
|
||||
@ -15,4 +17,78 @@ const createInitialComplianceInfo = datasetId => ({
|
||||
retentionPolicy: { retentionType: '' }
|
||||
});
|
||||
|
||||
export { createInitialComplianceInfo };
|
||||
/**
|
||||
*
|
||||
* @type {{complianceEntities: {type: string, of: {type: string, keys: [*]}}, datasetClassification: {type: string, keys: (*)}, fieldClassification: {type: string}}}
|
||||
*/
|
||||
const policyShape = {
|
||||
complianceEntities: {
|
||||
type: 'array',
|
||||
of: {
|
||||
type: 'object',
|
||||
keys: [
|
||||
'identifierField:string',
|
||||
'identifierType:string',
|
||||
'isSubject:boolean|object',
|
||||
'logicalType:string|object|undefined'
|
||||
]
|
||||
}
|
||||
},
|
||||
datasetClassification: { type: 'object', keys: Object.keys(datasetClassifiers).map(key => `${key}:boolean`) },
|
||||
fieldClassification: { type: 'object' }
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks that a policy is valid
|
||||
* @param candidatePolicy
|
||||
* @return {Boolean}
|
||||
*/
|
||||
const isPolicyExpectedShape = (candidatePolicy = {}) => {
|
||||
const candidateMatchesShape = policyKey => {
|
||||
const policyProps = policyShape[policyKey];
|
||||
const expectedType = policyProps.type;
|
||||
const policyKeyValue = candidatePolicy[policyKey];
|
||||
const isValueExpectedType = expectedType === 'array'
|
||||
? Array.isArray(policyKeyValue)
|
||||
: typeof policyKeyValue === expectedType;
|
||||
const typeDeclarations = {
|
||||
get array() {
|
||||
return policyProps.of.keys;
|
||||
},
|
||||
get object() {
|
||||
return policyProps.keys;
|
||||
}
|
||||
}[expectedType] || [];
|
||||
|
||||
if (!policyKeyValue || !isValueExpectedType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (expectedType === 'array') {
|
||||
return policyKeyValue.every(value => {
|
||||
if (!value && typeof value !== policyProps.of.type) {
|
||||
return false;
|
||||
}
|
||||
return typeDeclarations.every(typeString => {
|
||||
const [key, type] = typeString.split(':');
|
||||
return type.includes(typeof value[key]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (expectedType === typeof {}) {
|
||||
return typeDeclarations.every(typeString => {
|
||||
const [key, type] = typeString.split(':');
|
||||
return type.includes(typeof policyKeyValue[key]);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof candidatePolicy === 'object' && candidatePolicy) {
|
||||
return Object.keys(policyShape).every(candidateMatchesShape);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export { createInitialComplianceInfo, isPolicyExpectedShape };
|
||||
|
@ -0,0 +1,25 @@
|
||||
import { moduleForComponent, test } from 'ember-qunit';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
moduleForComponent('json-upload', 'Integration | Component | json upload', {
|
||||
integration: true
|
||||
});
|
||||
|
||||
test('it renders', function(assert) {
|
||||
|
||||
// Set any properties with this.set('myProperty', 'value');
|
||||
// Handle any actions with this.on('myAction', function(val) { ... });
|
||||
|
||||
this.render(hbs`{{json-upload}}`);
|
||||
|
||||
assert.equal(this.$().text().trim(), '');
|
||||
|
||||
// Template block usage:
|
||||
this.render(hbs`
|
||||
{{#json-upload}}
|
||||
template block text
|
||||
{{/json-upload}}
|
||||
`);
|
||||
|
||||
assert.equal(this.$().text().trim(), 'template block text');
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user