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:
parent
b84100d4a0
commit
42ae96ed1c
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
ui: parse and display pki cert metadata
|
||||
```
|
|
@ -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;
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -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);
|
|
@ -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'),
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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",
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue