UI/ PKI UI Redesign (#12541)

* installs node-forge

* correctly displays and formats cert metadata

* removes labels

* uses helper in hbs file

* adds named arg to helper

* pki-ca-cert displays common name, issue & expiry date

* alphabetizes some attrs

* adds test for date helper
This commit is contained in:
claire bontempo 2021-10-04 14:31:36 -07:00 committed by GitHub
parent b84100d4a0
commit 42ae96ed1c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 35465 additions and 48 deletions

3
changelog/12541.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
ui: parse and display pki cert metadata
```

View File

@ -1,3 +1,4 @@
import { parsePkiCert } from '../helpers/parse-pki-cert';
import ApplicationAdapter from './application';
export default ApplicationAdapter.extend({
@ -41,7 +42,14 @@ export default ApplicationAdapter.extend({
}
response.id = snapshot.id;
response.modelName = type.modelName;
store.pushPayload(type.modelName, response);
// only parse if certificate is attached to response
if (response.data && response.data.certificate) {
const caCertMetadata = parsePkiCert([response.data]);
const transformedResponse = { ...response, ...caCertMetadata };
store.pushPayload(type.modelName, transformedResponse);
} else {
store.pushPayload(type.modelName, response);
}
});
},

View File

@ -0,0 +1,20 @@
import { helper } from '@ember/component/helper';
import { pki } from 'node-forge';
export function parsePkiCert([model]) {
// model has to be the responseJSON from PKI serializer
if (!model.certificate) {
return;
}
const cert = pki.certificateFromPem(model.certificate);
const commonName = cert.subject.getField('CN') ? cert.subject.getField('CN').value : null;
const issueDate = cert.validity.notBefore;
const expiryDate = cert.validity.notAfter;
return {
common_name: commonName,
issue_date: issueDate,
expiry_date: expiryDate,
};
}
export default helper(parsePkiCert);

View File

@ -4,12 +4,15 @@ import { computed } from '@ember/object';
import Certificate from './pki-certificate';
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
// TODO: alphabetize attrs
export default Certificate.extend({
DISPLAY_FIELDS: computed(function() {
return [
'csr',
'certificate',
'expiration',
'commonName',
'issueDate',
'expiryDate',
'issuingCa',
'caChain',
'privateKey',
@ -17,30 +20,33 @@ export default Certificate.extend({
'serialNumber',
];
}),
addBasicConstraints: attr('boolean', {
label: 'Add a Basic Constraints extension with CA: true',
helpText:
'Only needed as a workaround in some compatibility scenarios with Active Directory Certificate Services',
}),
backend: attr('string', {
readOnly: true,
}),
caType: attr('string', {
possibleValues: ['root', 'intermediate'],
defaultValue: 'root',
label: 'CA Type',
readOnly: true,
}),
uploadPemBundle: attr('boolean', {
label: 'Upload PEM bundle',
readOnly: true,
commonName: attr('string'),
expiryDate: attr('string', {
label: 'Expiration date',
}),
issueDate: attr('string'),
pemBundle: attr('string', {
label: 'PEM bundle',
editType: 'file',
}),
addBasicConstraints: attr('boolean', {
label: 'Add a Basic Constraints extension with CA: true',
helpText:
'Only needed as a workaround in some compatibility scenarios with Active Directory Certificate Services',
uploadPemBundle: attr('boolean', {
label: 'Upload PEM bundle',
readOnly: true,
}),
fieldDefinition: computed('caType', 'uploadPemBundle', function() {
const type = this.caType;
const isUpload = this.uploadPemBundle;
@ -92,7 +98,6 @@ export default Certificate.extend({
return groups;
}),
type: attr('string', {
possibleValues: ['internal', 'exported'],
defaultValue: 'internal',
@ -145,7 +150,6 @@ export default Certificate.extend({
label: 'CSR',
masked: true,
}),
expiration: attr(),
deletePath: lazyCapabilities(apiPath`${'backend'}/root`, 'backend'),
canDeleteRoot: and('deletePath.canDelete', 'deletePath.canSudo'),

View File

@ -17,27 +17,30 @@ export default Model.extend({
DISPLAY_FIELDS: computed(function() {
return [
'certificate',
'commonName',
'issuingCa',
'caChain',
'privateKey',
'privateKeyType',
'serialNumber',
'revocationTime',
'issueDate',
'expiryDate',
'serialNumber',
];
}),
commonName: attr('string'),
expiryDate: attr('string', {
label: 'Expiration date',
}),
issueDate: attr('string'),
role: attr('object', {
readOnly: true,
}),
revocationTime: attr('number'),
commonName: attr('string', {
label: 'Common Name',
}),
altNames: attr('string', {
label: 'DNS/Email Subject Alternative Names (SANs)',
}),
ipSans: attr('string', {
label: 'IP Subject Alternative Names (SANs)',
}),
@ -47,22 +50,18 @@ export default Model.extend({
helpText:
'The format is the same as OpenSSL: <oid>;<type>:<value> where the only current valid type is UTF8',
}),
ttl: attr({
label: 'TTL',
editType: 'ttl',
}),
format: attr('string', {
defaultValue: 'pem',
possibleValues: ['pem', 'der', 'pem_bundle'],
}),
excludeCnFromSans: attr('boolean', {
label: 'Exclude Common Name from Subject Alternative Names (SANs)',
defaultValue: false,
}),
certificate: attr('string', {
masked: true,
}),

View File

@ -2,6 +2,7 @@ import RESTSerializer from '@ember-data/serializer/rest';
import { isNone, isBlank } from '@ember/utils';
import { assign } from '@ember/polyfills';
import { decamelize } from '@ember/string';
import { parsePkiCert } from '../helpers/parse-pki-cert';
export default RESTSerializer.extend({
keyForAttribute: function(attr) {
@ -41,7 +42,14 @@ export default RESTSerializer.extend({
normalizeResponse(store, primaryModelClass, payload, id, requestType) {
const responseJSON = this.normalizeItems(payload);
const { modelName } = primaryModelClass;
let transformedPayload = { [modelName]: responseJSON };
let transformedPayload, certMetadata;
// hits cert/list endpoint first which returns an array, only want to parse if response is not an array
if (!Array.isArray(responseJSON)) {
certMetadata = parsePkiCert([responseJSON]);
transformedPayload = { [modelName]: { ...certMetadata, ...responseJSON } };
} else {
transformedPayload = { [modelName]: responseJSON };
}
return this._super(store, primaryModelClass, transformedPayload, id, requestType);
},

View File

@ -23,6 +23,8 @@
.column {
align-self: center;
padding-left: 0px;
padding: $spacing-m;
&.info-table-row-edit {
padding-bottom: 0.3rem;
padding-top: 0.3rem;

View File

@ -10,25 +10,23 @@
{{#if (or model.certificate model.csr)}}
{{#each model.attrs as |attr|}}
{{#if attr.options.masked}}
<InfoTableRow @label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}} @value={{get model attr.name}}>
<InfoTableRow data-test-table-row
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
@value={{get model attr.name}}>
<MaskedInput
@value={{get model attr.name}}
@displayOnly={{true}}
@allowCopy={{true}}
/>
</InfoTableRow>
{{else if (eq attr.name "expiration")}}
{{info-table-row
data-test-table-row
label=(capitalize (or attr.options.label (humanize (dasherize attr.name))))
value=(date-format (get model attr.name) 'MMM dd, yyyy hh:mm:ss a')
}}
{{else if (and (get model attr.name) (or (eq attr.name "issueDate") (eq attr.name "expiryDate")))}}
<InfoTableRow data-test-table-row={{value}}
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
@value={{date-format (get model attr.name) 'MMM dd, yyyy hh:mm:ss a' isFormatted=true}}/>
{{else}}
{{info-table-row
data-test-table-row
label=(capitalize (or attr.options.label (humanize (dasherize attr.name))))
value=(get model attr.name)
}}
<InfoTableRow data-test-table-row={{value}}
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
@value={{get model attr.name}}/>
{{/if}}
{{/each}}
<div class="field is-grouped box is-fullwidth is-bottomless">
@ -94,15 +92,24 @@
/>
{{#each model.attrs as |attr|}}
{{#if attr.options.masked}}
<InfoTableRow @label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}} @value={{get model attr.name}}>
<InfoTableRow
data-test-table-row={{value}}
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
@value={{get model attr.name}}>
<MaskedInput
@value={{get model attr.name}}
@displayOnly={{true}}
@allowCopy={{true}}
/>
</InfoTableRow>
{{else if (and (get model attr.name) (or (eq attr.name "issueDate") (eq attr.name "expiryDate")))}}
<InfoTableRow data-test-table-row={{value}}
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
@value={{date-format (get model attr.name) 'MMM dd, yyyy hh:mm:ss a' isFormatted=true}}/>
{{else}}
{{info-table-row data-test-table-row label=(capitalize (or attr.options.label (humanize (dasherize attr.name)))) value=(get model attr.name)}}
<InfoTableRow data-test-table-row={{value}}
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
@value={{get model attr.name}}/>
{{/if}}
{{/each}}
<div class="field is-grouped box is-fullwidth is-bottomless">

View File

@ -13,22 +13,33 @@
<MessageError @model={{model}} />
{{#each model.attrs as |attr|}}
{{#if (eq attr.type "object")}}
<InfoTableRow @label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}} @value={{stringify (get model attr.name)}} />
<InfoTableRow data-test-table-row
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
@value={{stringify (get model attr.name)}} />
{{else}}
{{#if attr.options.masked}}
<InfoTableRow @label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}} @value={{get model attr.name}}>
<InfoTableRow data-test-table-row
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
@value={{get model attr.name}}>
<MaskedInput
@value={{get model attr.name}}
@displayOnly={{true}}
@allowCopy={{true}}
/>
</InfoTableRow>
{{else if (and (get model attr.name) (or (eq attr.name "issueDate") (eq attr.name "expiryDate")))}}
<InfoTableRow data-test-table-row
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
@value={{date-format (get model attr.name) 'MMM dd, yyyy hh:mm:ss a' isFormatted=true}} />
{{else}}
<InfoTableRow @label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}} @value={{get model attr.name}} />
<InfoTableRow data-test-table-row
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
@value={{get model attr.name}} />
{{/if}}
{{/if}}
{{/each}}
</div>
<div class="field is-grouped is-grouped-split box is-fullwidth is-bottomless">
<div class="field is-grouped">
<div class="control">

View File

@ -1,8 +1,11 @@
import { helper } from '@ember/component/helper';
import { format, parseISO } from 'date-fns';
export function dateFormat([date, style]) {
export function dateFormat([date, style], { isFormatted = false }) {
// see format breaking in upgrade to date-fns 2.x https://github.com/date-fns/date-fns/blob/master/CHANGELOG.md#changed-5
if (isFormatted) {
return format(new Date(date), style);
}
let number = typeof date === 'string' ? parseISO(date) : date;
if (!number) {
return;

35326
ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -134,6 +134,7 @@
"ivy-codemirror": "IvyApp/ivy-codemirror#fb09333c5144da47e14a9e6260f80577d5408374",
"jsonlint": "^1.6.3",
"loader.js": "^4.7.0",
"node-forge": "^0.10.0",
"node-sass": "^4.10.0",
"normalize.css": "4.1.1",
"pretender": "^3.4.3",

View File

@ -105,7 +105,9 @@ module('Acceptance | Enterprise | Transform secrets', function(hooks) {
await clickTrigger('#allowed_roles');
await settled();
await typeInSearch(roleName);
await settled();
await selectChoose('#allowed_roles', '.ember-power-select-option', 0);
await settled();
await transformationsPage.submit();
await settled();
assert.equal(

View File

@ -76,14 +76,18 @@ BXUV2Uwtxf+QCphnlht9muX2fsLIzDJea0JipWj1uf2H8OZsjE8=
await page.form.generateCA();
await settled();
assert.ok(page.form.rows.length > 0, 'shows all of the rows');
// TODO come back and figure out why not working after upgrade. I see it, it's a timing issue.
// assert.ok(page.form.certificateIsPresent, 'the certificate is included');
assert.ok(page.form.commonNameIsPresent, 'the common name displays');
assert.ok(page.form.issueDateIsPresent, 'the issue date displays');
assert.ok(page.form.expiryDateIsPresent, 'the expiration date displays');
assert
.dom('[data-test-value-div="Certificate"] [data-test-masked-input]')
.exists('certificate is present');
await page.form.back();
await settled();
await page.form.generateCA();
await settled();
assert.ok(
page.flash.latestMessage.includes('You tried to generate a new root CA'),
'shows warning message'

View File

@ -1,6 +1,7 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { format } from 'date-fns';
import hbs from 'htmlbars-inline-precompile';
module('Integration | Helper | date-format', function(hooks) {
@ -42,4 +43,14 @@ module('Integration | Helper | date-format', function(hooks) {
await render(hbs`<p data-test-date-format>Date: {{date-format tenDigitDate "MM/dd/yyyy"}}</p>`);
assert.dom('[data-test-date-format]').includesText('05/23/2021');
});
test('it supports already formatted dates', async function(assert) {
let formattedDate = new Date();
this.set('formattedDate', formattedDate);
await render(
hbs`<p data-test-date-format>Date: {{date-format formattedDate 'MMMM dd, yyyy hh:mm:ss a' isFormatted=true}}</p>`
);
assert.dom('[data-test-date-format]').includesText(format(formattedDate, 'MMMM dd, yyyy hh:mm:ss a'));
});
});

View File

@ -27,12 +27,15 @@ export default {
csr: text('[data-test-row-value="CSR"]', { normalize: false }),
csrField: fillable('[data-test-input="csr"]'),
certificate: text('[data-test-row-value="Certificate"]', { normalize: false }),
certificateIsPresent: isPresent('[data-test-row-value="Certificate"]'),
commonNameIsPresent: isPresent('[data-test-row-value="Common name"]'),
uploadCert: clickable('[data-test-input="uploadPemBundle"]'),
enterCertAsText: clickable('[data-test-text-toggle]'),
pemBundle: fillable('[data-test-text-file-textarea="true"]'),
commonName: fillable('[data-test-input="commonName"]'),
issueDateIsPresent: text('[data-test-row-value="Issue date"]'),
expiryDateIsPresent: text('[data-test-row-value="Expiration date"]'),
async generateCA(commonName = 'PKI CA', type = 'root') {
if (type === 'intermediate') {
return await this.replaceCA()

View File

@ -13701,6 +13701,11 @@ node-fetch@^2.6.1:
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
node-forge@^0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3"
integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==
node-gyp@^3.8.0:
version "3.8.0"
resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c"