UI: Show parsed certificate data in PKI (#19990)

This commit is contained in:
Chelsea Shaw 2023-04-11 16:04:35 -05:00 committed by GitHub
parent 0b3f24a2d8
commit 282279121d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 322 additions and 113 deletions

View File

@ -26,6 +26,15 @@ const validations = {
message: `Issuer name must be unique across all issuers and not be the reserved value 'default'.`,
},
],
keyName: [
{
validator(model) {
if (model.keyName === 'default') return false;
return true;
},
message: `Key name cannot be the reserved value 'default'`,
},
],
};
/**
@ -44,6 +53,10 @@ export default class PkiActionModel extends Model {
/* actionType import */
@attr('string') pemBundle;
// parsed attrs from parse-pki-cert util if certificate on response
@attr parsedCertificate;
// readonly attrs returned after importing
@attr importedIssuers;
@attr importedKeys;
@ -55,7 +68,6 @@ export default class PkiActionModel extends Model {
// readonly attrs returned right after root generation
@attr serialNumber;
@attr('string', { label: 'Issuing CA', readOnly: true, masked: true }) issuingCa;
@attr keyName;
// end of readonly
@attr('string', {
@ -64,9 +76,9 @@ export default class PkiActionModel extends Model {
})
type;
@attr('string') issuerName; // REQUIRED for generate-root actionType, cannot be "default"
@attr('string') issuerName;
@attr('string') keyName; // cannot be "default"
@attr('string') keyName;
@attr('string', {
defaultValue: 'default',

View File

@ -18,14 +18,8 @@ import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
* render if included in certDisplayFields below).
*/
const certDisplayFields = [
'certificate',
'commonName',
'revocationTime',
'serialNumber',
'notValidBefore',
'notValidAfter',
];
// also displays parsedCertificate values in the template
const certDisplayFields = ['certificate', 'commonName', 'revocationTime', 'serialNumber'];
@withFormFields(certDisplayFields)
export default class PkiCertificateBaseModel extends Model {
@ -41,8 +35,10 @@ export default class PkiCertificateBaseModel extends Model {
assert('You must provide a helpUrl for OpenAPI', true);
}
@attr('string') commonName;
// The attributes parsed from parse-pki-cert util live here
@attr parsedCertificate;
@attr('string') commonName;
@attr({
label: 'Not valid after',
detailsLabel: 'Issued certificates expire after',
@ -98,11 +94,6 @@ export default class PkiCertificateBaseModel extends Model {
@attr('number', { formatDate: true }) revocationTime;
@attr('string') serialNumber;
// read only attrs parsed from certificate contents in serializer on GET requests (see parse-pki-cert.js)
@attr('number', { formatDate: true }) notValidAfter; // set by ttl or notAfter (customTtL above)
@attr('number', { formatDate: true }) notValidBefore; // date certificate was issued
@attr('string') signatureBits;
@lazyCapabilities(apiPath`${'backend'}/revoke`, 'backend') revokePath;
get canRevoke() {
return this.revokePath.get('isLoading') || this.revokePath.get('canCreate') !== false;

View File

@ -19,21 +19,8 @@ const inputFields = [
];
const displayFields = [
{
default: [
'certificate',
'caChain',
'commonName',
'issuerName',
'issuerId',
'subjectSerialNumber',
'keyId',
'altNames',
'uriSans',
'ipSans',
'otherSans',
'notValidBefore',
'notValidAfter',
],
default: ['certificate', 'caChain', 'commonName', 'issuerName', 'issuerId', 'keyId'],
// also displays parsedCertificate values in the template
},
{ 'Issuer URLs': issuerUrls },
];
@ -59,9 +46,9 @@ export default class PkiIssuerModel extends Model {
@attr({ masked: true }) certificate;
// parsed from certificate contents in serializer (see parse-pki-cert.js)
@attr commonName;
@attr('number', { formatDate: true }) notValidAfter;
@attr('number', { formatDate: true }) notValidBefore;
@attr parsedCertificate;
@attr('string') commonName;
@attr subjectSerialNumber; // this is not the UUID serial number field randomly generated by Vault for leaf certificates
@attr({ label: 'Subject Alternative Names (SANs)' }) altNames;
@attr({ label: 'IP SANs' }) ipSans;

View File

@ -12,6 +12,15 @@ import { withModelValidations } from 'vault/decorators/model-validations';
const validations = {
type: [{ type: 'presence', message: 'Type is required.' }],
keyType: [{ type: 'presence', message: 'Please select a key type.' }],
keyName: [
{
validator(model) {
if (model.keyName === 'default') return false;
return true;
},
message: `Key name cannot be the reserved value 'default'`,
},
],
};
const displayFields = ['keyId', 'keyName', 'keyType', 'keyBits'];
const formFieldGroups = [{ default: ['keyName', 'type'] }, { 'Key parameters': ['keyType', 'keyBits'] }];

View File

@ -6,14 +6,28 @@
import { underscore } from '@ember/string';
import { keyParamsByType } from 'pki/utils/action-params';
import ApplicationSerializer from '../application';
import { parseCertificate } from 'vault/utils/parse-pki-cert';
export default class PkiActionSerializer extends ApplicationSerializer {
attrs = {
customTtl: { serialize: false },
type: { serialize: false },
subjectSerialNumber: { serialize: false },
};
normalizeResponse(store, primaryModelClass, payload, id, requestType) {
if (payload.data.certificate) {
// Parse certificate back from the API and add to payload
const parsedCert = parseCertificate(payload.data.certificate);
const data = {
...payload.data,
common_name: parsedCert.common_name,
parsed_certificate: parsedCert,
};
return super.normalizeResponse(store, primaryModelClass, { ...payload, data }, id, requestType);
}
return super.normalizeResponse(...arguments);
}
serialize(snapshot, requestType) {
const data = super.serialize(snapshot);
// requestType is a custom value specified from the pki/action adapter

View File

@ -17,14 +17,13 @@ export default class PkiCertificateBaseSerializer extends ApplicationSerializer
if (payload.data.certificate) {
// Parse certificate back from the API and add to payload
const parsedCert = parseCertificate(payload.data.certificate);
const json = super.normalizeResponse(
return super.normalizeResponse(
store,
primaryModelClass,
{ ...payload, ...parsedCert },
{ ...payload, parsed_certificate: parsedCert, common_name: parsedCert.common_name },
id,
requestType
);
return json;
}
return super.normalizeResponse(...arguments);
}

View File

@ -4,29 +4,31 @@
*/
import { parseCertificate } from 'vault/utils/parse-pki-cert';
import { parsedParameters } from 'vault/utils/parse-pki-cert-oids';
import ApplicationSerializer from '../application';
export default class PkiIssuerSerializer extends ApplicationSerializer {
primaryKey = 'issuer_id';
constructor() {
super(...arguments);
// remove following attrs from serialization
const attrs = ['caChain', 'certificate', 'issuerId', 'keyId', ...parsedParameters];
this.attrs = attrs.reduce((attrObj, attr) => {
attrObj[attr] = { serialize: false };
return attrObj;
}, {});
}
attrs = {
caChain: { serialize: false },
certificate: { serialize: false },
issuerId: { serialize: false },
keyId: { serialize: false },
parsedCertificate: { serialize: false },
commonName: { serialize: false },
};
normalizeResponse(store, primaryModelClass, payload, id, requestType) {
if (payload.data.certificate) {
// Parse certificate back from the API and add to payload
const parsedCert = parseCertificate(payload.data.certificate);
const data = { issuer_ref: payload.issuer_id, ...payload.data, ...parsedCert };
const json = super.normalizeResponse(store, primaryModelClass, { ...payload, data }, id, requestType);
return json;
const data = {
issuer_ref: payload.issuer_id,
...payload.data,
parsed_certificate: parsedCert,
common_name: parsedCert.common_name,
};
return super.normalizeResponse(store, primaryModelClass, { ...payload, data }, id, requestType);
}
return super.normalizeResponse(...arguments);
}

View File

@ -2,10 +2,8 @@
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import camelizeKeys from 'vault/utils/camelize-object-keys';
//* lookup OIDs: http://oid-info.com/basic-search.htm
export const SUBJECT_OIDs = {
common_name: '2.5.4.3',
subject_serial_number: '2.5.4.5',
@ -77,13 +75,12 @@ export const SIGNATURE_ALGORITHM_OIDs = {
'1.3.101.112': '0', // Ed25519
};
// returns array of strings that correspond to model attributes
// can be passed to display views in details pages containing certificates
export const parsedParameters = [
...Object.keys(camelizeKeys(SUBJECT_OIDs)),
...Object.keys(camelizeKeys(EXTENSION_OIDs)),
...Object.keys(camelizeKeys(SAN_TYPES)),
'usePss',
'notValidBefore',
'notValidAfter',
// returns array of strings that correspond to possible returned values from parsing cert
export const parsedParameterKeys = [
...Object.keys(SUBJECT_OIDs),
...Object.keys(EXTENSION_OIDs),
...Object.keys(SAN_TYPES),
'use_pss',
'not_valid_before',
'not_valid_after',
];

View File

@ -46,6 +46,8 @@
<button
onclick={{action "toggleMask"}}
type="button"
aria-label={{if this.showValue "mask value" "show value"}}
title={{if this.showValue "mask value" "show value"}}
class="{{if (eq this.value '') 'has-text-grey'}} masked-input-toggle button"
data-test-button="toggle-masked"
>

View File

@ -38,6 +38,8 @@
{{/if}}
{{/each}}
<ParsedCertificateInfoRows @model={{@model.parsedCertificate}} />
{{#if @onBack}}
<div class="field is-grouped box is-fullwidth is-bottomless">
<div class="control">

View File

@ -121,6 +121,9 @@
/>
{{/if}}
{{/each}}
{{#if (eq group "default")}}
<ParsedCertificateInfoRows @model={{@issuer.parsedCertificate}} />
{{/if}}
</div>
{{/each-in}}
{{/each}}

View File

@ -111,6 +111,7 @@
{{#if (eq this.displayedForm "use-old-settings")}}
{{#if @newRootModel.id}}
<PkiInfoTableRows @model={{@newRootModel}} @displayFields={{this.displayFields}} />
<ParsedCertificateInfoRows @model={{@newRootModel.parsedCertificate}} />
<div class="field is-grouped is-fullwidth has-top-margin-l has-bottom-margin-s">
<div class="control">
<button type="button" class="button is-primary" {{on "click" @onComplete}} data-test-done>
@ -151,6 +152,7 @@
/>
{{#if this.showOldSettings}}
<PkiInfoTableRows @model={{@oldRoot}} @displayFields={{this.displayFields}} />
<ParsedCertificateInfoRows @model={{@oldRoot.parsedCertificate}} />
{{/if}}
</div>

View File

@ -12,8 +12,7 @@ import FlashMessageService from 'vault/services/flash-messages';
import SecretMountPath from 'vault/services/secret-mount-path';
import PkiIssuerModel from 'vault/models/pki/issuer';
import PkiActionModel from 'vault/vault/models/pki/action';
import { Breadcrumb } from 'vault/vault/app-types';
import { parsedParameters } from 'vault/utils/parse-pki-cert-oids';
import { Breadcrumb, ValidationMap } from 'vault/vault/app-types';
interface Args {
oldRoot: PkiIssuerModel;
@ -35,11 +34,11 @@ export default class PagePkiIssuerRotateRootComponent extends Component<Args> {
@tracked displayedForm = RADIO_BUTTON_KEY.oldSettings;
@tracked showOldSettings = false;
@tracked modelValidations: ValidationMap | null = null;
// form alerts below are only for "use old settings" option
// validations/errors for "customize new root" are handled by <PkiGenerateRoot> component
@tracked alertBanner = '';
@tracked invalidFormAlert = '';
@tracked modelValidations = null;
get generateOptions() {
return [
@ -71,7 +70,6 @@ export default class PagePkiIssuerRotateRootComponent extends Component<Args> {
'keyName',
'keyId',
'serialNumber',
...parsedParameters,
];
return this.args.newRootModel.id ? [...defaultFields, ...addKeyFields] : defaultFields;
}

View File

@ -0,0 +1,9 @@
{{#each this.possibleFields as |field|}}
{{#let (get @model field.key) as |value|}}
<InfoTableRow
@label={{or field.label (humanize (dasherize field.key))}}
@value={{value}}
@formatDate={{field.formatDate}}
/>
{{/let}}
{{/each}}

View File

@ -0,0 +1,41 @@
import Component from '@glimmer/component';
import { parsedParameterKeys } from 'vault/utils/parse-pki-cert-oids';
/**
* @module ParsedCertificateInfoRowsComponent
* Renders attributes parsed from a PKI certificate (provided from parse-pki-cert util). It will only render rows for
* defined values that match `parsedParameterKeys` imported from the helper. It never renders common_name, even though
* the value is returned from the parse cert util, because `common_name` is important to PKI and we render it at the top.
*
* @example ```js
* <ParsedCertificateInfoRows @model={{@model.parsedCertificate}} />
* ```
*
* @param {object} model - object of parsed attributes from parse-pki-cert util
*/
export default class ParsedCertificateInfoRowsComponent extends Component {
get possibleFields() {
// We show common name elsewhere on the details view, so no need to render it here
const fieldKeys = parsedParameterKeys.filter((k) => k !== 'common_name');
const attrsByKey = {
other_sans: { label: 'Other SANs' },
alt_names: { label: 'Subject Alternative Names (SANs)' },
uri_sans: { label: 'URI SANs' },
ip_sans: { label: 'IP SANs' },
permitted_dns_domains: { label: 'Permitted DNS domains' },
exclude_cn_from_sans: { label: 'Exclude CN from SANs' },
use_pss: { label: 'Use PSS' },
ttl: { label: 'TTL' },
ou: { label: 'Organizational units (OU)' },
not_valid_after: { formatDate: 'MMM d yyyy HH:mm:ss a zzzz' },
not_valid_before: { formatDate: 'MMM d yyyy HH:mm:ss a zzzz' },
};
return fieldKeys.map((key) => {
return {
key,
...attrsByKey[key],
};
});
}
}

View File

@ -54,7 +54,7 @@
<FormField @attr={{field}} @model={{@model}} @modelValidations={{this.modelValidations}} />
{{/each}}
<PkiGenerateToggleGroups @model={{@model}} />
<PkiGenerateToggleGroups @model={{@model}} @modelValidations={{this.modelValidations}} />
<div class="field is-grouped box is-fullwidth is-bottomless has-top-margin-l">
<div class="control">

View File

@ -13,6 +13,7 @@ import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
import FlashMessageService from 'vault/services/flash-messages';
import PkiActionModel from 'vault/models/pki/action';
import errorMessage from 'vault/utils/error-message';
import { ValidationMap } from 'vault/vault/app-types';
interface Args {
model: PkiActionModel;
@ -44,7 +45,7 @@ interface Args {
export default class PkiGenerateCsrComponent extends Component<Args> {
@service declare readonly flashMessages: FlashMessageService;
@tracked modelValidations = null;
@tracked modelValidations: ValidationMap | null = null;
@tracked error: string | null = null;
@tracked alert: string | null = null;

View File

@ -35,6 +35,7 @@
<InfoTableRow @label="Private key type" @value={{@model.privateKeyType}}>
<span class="{{unless @model.privateKeyType 'tag'}}">{{or @model.privateKeyType "internal"}}</span>
</InfoTableRow>
<ParsedCertificateInfoRows @model={{@model.parsedCertificate}} />
</main>
<footer>
<div class="field is-grouped is-fullwidth has-top-margin-l">
@ -62,7 +63,7 @@
{{/let}}
{{/each}}
<PkiGenerateToggleGroups @model={{@model}} />
<PkiGenerateToggleGroups @model={{@model}} @modelValidations={{this.modelValidations}} />
{{#if @urls}}
<fieldset class="box is-shadowless is-marginless is-borderless is-fullwidth" data-test-urls-section>

View File

@ -14,7 +14,7 @@ import PkiActionModel from 'vault/models/pki/action';
import PkiUrlsModel from 'vault/models/pki/urls';
import FlashMessageService from 'vault/services/flash-messages';
import errorMessage from 'vault/utils/error-message';
import { parsedParameters } from 'vault/utils/parse-pki-cert-oids';
import { ValidationMap } from 'vault/vault/app-types';
interface AdapterOptions {
actionType: string;
@ -53,7 +53,7 @@ export default class PkiGenerateRootComponent extends Component<Args> {
@service declare readonly flashMessages: FlashMessageService;
@service declare readonly router: RouterService;
@tracked modelValidations = null;
@tracked modelValidations: ValidationMap | null = null;
@tracked errorBanner = '';
@tracked invalidFormAlert = '';
@ -73,13 +73,13 @@ export default class PkiGenerateRootComponent extends Component<Args> {
get returnedFields() {
return [
'certificate',
'commonName',
'issuerId',
'issuerName',
'issuingCa',
'keyName',
'keyId',
'serialNumber',
...parsedParameters,
];
}

View File

@ -29,7 +29,7 @@
{{/if}}
</p>
{{#if this.keyParamFields}}
<PkiKeyParameters @model={{@model}} @fields={{this.keyParamFields}} />
<PkiKeyParameters @model={{@model}} @fields={{this.keyParamFields}} @modelValidations={{@modelValidations}} />
{{/if}}
{{else}}
<p class="has-bottom-margin-m" data-test-toggle-group-description>
@ -43,7 +43,13 @@
</p>
{{#each fields as |fieldName|}}
{{#let (find-by "name" fieldName @model.allFields) as |attr|}}
<FormField data-test-field @attr={{attr}} @model={{@model}} @showHelpText={{false}} />
<FormField
data-test-field
@attr={{attr}}
@model={{@model}}
@showHelpText={{false}}
@modelValidations={{@modelValidations}}
/>
{{/let}}
{{/each}}
{{/if}}

View File

@ -8,10 +8,12 @@ import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { keyParamsByType } from 'pki/utils/action-params';
import PkiActionModel from 'vault/models/pki/action';
import { ModelValidations } from 'vault/vault/app-types';
interface Args {
model: PkiActionModel;
groups: Map<[key: string], Array<string>> | null;
modelValidations?: ModelValidations;
}
export default class PkiGenerateToggleGroupsComponent extends Component<Args> {

View File

@ -13,7 +13,7 @@
</div>
<div class="box is-fullwidth is-sideless is-marginless is-paddingless" data-test-imported-bundle-mapping>
{{#each-in this.importedResponse as |issuer key|}}
<div class="box is-marginless no-top-shadow has-slim-padding">
<div class="box is-marginless no-top-shadow has-slim-padding" data-test-import-pair={{concat issuer "_" key}}>
<div class="is-flex-start">
<div class="is-flex-1 basis-0 has-bottom-margin-xs" data-test-imported-issuer>
{{#if issuer}}

View File

@ -25,7 +25,8 @@ import PkiActionModel from 'vault/models/pki/action';
*
* @param {Object} model - certificate model from route
* @callback onCancel - Callback triggered when cancel button is clicked.
* @callback onSubmit - Callback triggered on submit success.
* @callback onSave - Callback triggered on submit success.
* @callback onComplete - Callback triggered on "done" button click.
*/
interface AdapterOptions {

View File

@ -25,6 +25,7 @@
</InfoTableRow>
{{/let}}
{{/each}}
<ParsedCertificateInfoRows @model={{@model.parsedCertificate}} />
</div>
</main>
{{else}}

View File

@ -12,6 +12,7 @@ import { task } from 'ember-concurrency';
import PkiCertificateSignIntermediate from 'vault/models/pki/certificate/sign-intermediate';
import FlashMessageService from 'vault/services/flash-messages';
import errorMessage from 'vault/utils/error-message';
import { ValidationMap } from 'vault/vault/app-types';
interface Args {
onCancel: CallableFunction;
@ -22,7 +23,7 @@ export default class PkiSignIntermediateFormComponent extends Component<Args> {
@service declare readonly flashMessages: FlashMessageService;
@tracked errorBanner = '';
@tracked inlineFormAlert = '';
@tracked modelValidations = null;
@tracked modelValidations: ValidationMap | null = null;
@action cancel() {
this.args.model.unloadRecord();

View File

@ -344,6 +344,7 @@ module('Acceptance | pki workflow', function (hooks) {
await logout.visit();
});
test('details view renders correct number of info items', async function (assert) {
assert.expect(13);
await authPage.login(this.pkiAdminToken);
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
assert.dom(SELECTORS.issuersTab).exists('Issuers tab is present');
@ -356,9 +357,13 @@ module('Acceptance | pki workflow', function (hooks) {
`/vault/secrets/${this.mountPath}/pki/issuers/my-issuer/details`
);
assert.dom(SELECTORS.issuerDetails.title).hasText('View issuer certificate');
assert
.dom(`${SELECTORS.issuerDetails.defaultGroup} ${SELECTORS.issuerDetails.row}`)
.exists({ count: 13 }, 'Renders 13 info table items under default group');
['Certificate', 'CA Chain', 'Common name', 'Issuer name', 'Issuer ID', 'Default key ID'].forEach(
(label) => {
assert
.dom(`${SELECTORS.issuerDetails.defaultGroup} ${SELECTORS.issuerDetails.valueByName(label)}`)
.exists({ count: 1 }, `${label} value rendered`);
}
);
assert
.dom(`${SELECTORS.issuerDetails.urlsGroup} ${SELECTORS.issuerDetails.row}`)
.exists({ count: 3 }, 'Renders 3 info table items under URLs group');

View File

@ -34,8 +34,10 @@ module('Integration | Component | pki | Page::PkiCertificateDetails', function (
common_name: 'example.com Intermediate Authority',
issue_date: 1673540867000,
serial_number: id,
not_valid_after: 1831220897000,
not_valid_before: 1673540867000,
parsed_certificate: {
not_valid_after: 1831220897000,
not_valid_before: 1673540867000,
},
},
});
this.model = store.peekRecord('pki/certificate/base', id);
@ -67,7 +69,6 @@ module('Integration | Component | pki | Page::PkiCertificateDetails', function (
});
await render(hbs`<Page::PkiCertificateDetails @model={{this.model}} />`, { owner: this.engine });
assert
.dom('[data-test-component="info-table-row"]')
.exists({ count: 5 }, 'Correct number of fields render when certificate has not been revoked');

View File

@ -98,7 +98,7 @@ module('Integration | Component | page/pki-issuer-rotate-root', function (hooks)
assert.dom(SELECTORS.input('issuerName')).exists('renders issuer name input');
assert.strictEqual(findAll('[data-test-row-label]').length, 0, 'it hides the old root info table rows');
await click(SELECTORS.toggle);
assert.strictEqual(findAll('[data-test-row-label]').length, 11, 'it shows the old root info table rows');
assert.strictEqual(findAll('[data-test-row-label]').length, 19, 'it shows the old root info table rows');
assert
.dom(SELECTORS.infoRowValue('Issuer name'))
.hasText(this.oldRoot.issuerName, 'renders correct issuer data');

View File

@ -0,0 +1,40 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'vault/tests/helpers';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { setupEngine } from 'ember-engines/test-support';
module('Integration | Component | parsed-certificate-info-rows', function (hooks) {
setupRenderingTest(hooks);
setupEngine(hooks, 'pki');
test('it renders nothing if no valid attributes passed', async function (assert) {
this.set('parsedCertificate', {
foo: '',
common_name: 'not-shown',
});
await render(hbs`<ParsedCertificateInfoRows @model={{this.parsedCertificate}} />`, {
owner: this.engine,
});
assert.dom(this.element).hasText('');
});
test('it renders only valid attributes with values', async function (assert) {
this.set('parsedCertificate', {
common_name: 'not-shown',
use_pss: false,
alt_names: ['something', 'here'],
ttl: undefined,
});
await render(hbs`<ParsedCertificateInfoRows @model={{this.parsedCertificate}} />`, {
owner: this.engine,
});
assert.dom('[data-test-component="info-table-row"]').exists({ count: 2 }, 'renders 2 rows');
assert.dom('[data-test-value-div="Common name"]').doesNotExist('common name is never rendered');
assert.dom('[data-test-row-value="Subject Alternative Names (SANs)"]').hasText('something,here');
assert.dom('[data-test-value-div="Use PSS"]').hasText('No', 'Booleans are rendered');
assert.dom('[data-test-value-div="ttl"]').doesNotExist('ttl is not rendered because value undefined');
});
});

View File

@ -10,7 +10,7 @@ import { hbs } from 'ember-cli-htmlbars';
import { setupEngine } from 'ember-engines/test-support';
import { setupMirage } from 'ember-cli-mirage/test-support';
module('Integration | Component | ', function (hooks) {
module('Integration | Component | pki-generate-csr', function (hooks) {
setupRenderingTest(hooks);
setupEngine(hooks, 'pki');
setupMirage(hooks);
@ -18,6 +18,7 @@ module('Integration | Component | ', function (hooks) {
hooks.beforeEach(async function () {
this.owner.lookup('service:secretMountPath').update('pki-test');
this.store = this.owner.lookup('service:store');
this.onComplete = () => {};
this.model = this.owner
.lookup('service:store')
.createRecord('pki/action', { actionType: 'generate-csr' });
@ -36,6 +37,10 @@ module('Integration | Component | ', function (hooks) {
this.server.post('/pki-test/issuers/generate/intermediate/exported', (schema, req) => {
const payload = JSON.parse(req.requestBody);
assert.strictEqual(payload.common_name, 'foo', 'Request made to correct endpoint on save');
return {
request_id: '123',
data: {},
};
});
await render(hbs`<PkiGenerateCsr @model={{this.model}} @onComplete={{this.onComplete}} />`, {
@ -69,9 +74,12 @@ module('Integration | Component | ', function (hooks) {
this.onCancel = () => assert.ok(true, 'onCancel action fires');
await render(hbs`<PkiGenerateCsr @model={{this.model}} @onCancel={{this.onCancel}} />`, {
owner: this.engine,
});
await render(
hbs`<PkiGenerateCsr @model={{this.model}} @onCancel={{this.onCancel}} @onComplete={{this.onComplete}} />`,
{
owner: this.engine,
}
);
await click('[data-test-save]');

View File

@ -23,6 +23,7 @@ module('Integration | Component | PkiImportPemBundle', function (hooks) {
this.secretMountPath = this.owner.lookup('service:secret-mount-path');
this.secretMountPath.currentPath = this.backend;
this.pemBundle = issuerPemBundle;
this.onComplete = () => {};
});
test('it renders import and updates model', async function (assert) {
@ -33,6 +34,7 @@ module('Integration | Component | PkiImportPemBundle', function (hooks) {
@model={{this.model}}
@onCancel={{this.onCancel}}
@onSave={{this.onSave}}
@onComplete={{this.onComplete}}
/>
`,
{ owner: this.engine }
@ -57,7 +59,11 @@ module('Integration | Component | PkiImportPemBundle', function (hooks) {
},
'sends params in correct type'
);
return {};
return {
data: {
mapping: { 'issuer-id': 'key-id' },
},
};
});
this.onSave = () => assert.ok(true, 'onSave callback fires on save success');
@ -68,6 +74,7 @@ module('Integration | Component | PkiImportPemBundle', function (hooks) {
@model={{this.model}}
@onCancel={{this.onCancel}}
@onSave={{this.onSave}}
@onComplete={{this.onComplete}}
@adapterOptions={{hash actionType="import" useIssuer=true}}
/>
`,
@ -76,7 +83,7 @@ module('Integration | Component | PkiImportPemBundle', function (hooks) {
await click('[data-test-text-toggle]');
await fillIn('[data-test-text-file-textarea]', this.pemBundle);
assert.strictEqual(this.model.pemBundle, this.pemBundle);
assert.strictEqual(this.model.pemBundle, this.pemBundle, 'PEM bundle updated on model');
await click('[data-test-pki-import-pem-bundle]');
});
@ -92,7 +99,11 @@ module('Integration | Component | PkiImportPemBundle', function (hooks) {
},
'sends params in correct type'
);
return {};
return {
data: {
mapping: {},
},
};
});
this.onSave = () => assert.ok(true, 'onSave callback fires on save success');
@ -103,6 +114,7 @@ module('Integration | Component | PkiImportPemBundle', function (hooks) {
@model={{this.model}}
@onCancel={{this.onCancel}}
@onSave={{this.onSave}}
@onComplete={{this.onComplete}}
@adapterOptions={{hash actionType="import" useIssuer=false}}
/>
`,
@ -115,6 +127,54 @@ module('Integration | Component | PkiImportPemBundle', function (hooks) {
await click('[data-test-pki-import-pem-bundle]');
});
test('it shows the bundle mapping on success', async function (assert) {
assert.expect(7);
this.server.post(`/${this.backend}/issuers/import/bundle`, () => {
return {
data: {
imported_issuers: ['issuer-id', 'another-issuer'],
imported_keys: ['key-id', 'another-key'],
mapping: { 'issuer-id': 'key-id', 'another-issuer': null },
},
};
});
this.onSave = () => assert.ok(true, 'onSave callback fires on save success');
this.onComplete = () => assert.ok(true, 'onComplete callback fires on done button click');
await render(
hbs`
<PkiImportPemBundle
@model={{this.model}}
@onCancel={{this.onCancel}}
@onSave={{this.onSave}}
@onComplete={{this.onComplete}}
@adapterOptions={{hash actionType="import" useIssuer=true}}
/>
`,
{ owner: this.engine }
);
await click('[data-test-text-toggle]');
await fillIn('[data-test-text-file-textarea]', this.pemBundle);
await click('[data-test-pki-import-pem-bundle]');
assert
.dom('[data-test-import-pair]')
.exists({ count: 2 }, 'Shows correct number of rows for imported items');
// Check that each row has expected values
assert.dom('[data-test-import-pair="issuer-id_key-id"] [data-test-imported-issuer]').hasText('issuer-id');
assert.dom('[data-test-import-pair="issuer-id_key-id"] [data-test-imported-key]').hasText('key-id');
assert
.dom('[data-test-import-pair="another-issuer_"] [data-test-imported-issuer]')
.hasText('another-issuer');
assert.dom('[data-test-import-pair="another-issuer_"] [data-test-imported-key]').hasText('None');
// TODO VAULT-14791
// assert.dom('[data-test-import-pair="_another-key"] [data-test-imported-issuer]').hasText('None');
// assert.dom('[data-test-import-pair="_another-key"] [data-test-imported-key]').hasText('another-key');
await click('[data-test-done]');
});
test('it should unload record on cancel', async function (assert) {
assert.expect(2);
this.onCancel = () => assert.ok(true, 'onCancel callback fires');
@ -123,6 +183,7 @@ module('Integration | Component | PkiImportPemBundle', function (hooks) {
<PkiImportPemBundle
@model={{this.model}}
@onCancel={{this.onCancel}}
@onComplete={{this.onComplete}}
@onSave={{this.onSave}}
/>
`,

View File

@ -19,6 +19,11 @@ module('Unit | Adapter | pki/action', function (hooks) {
this.backend = 'pki-test';
this.secretMountPath.currentPath = this.backend;
this.server.post('/sys/capabilities-self', allowAllCapabilitiesStub());
this.emptyResponse = {
// Action adapter uses request_id as ember data id for response
request_id: '123',
data: {},
};
});
test('it exists', function (assert) {
@ -38,7 +43,7 @@ module('Unit | Adapter | pki/action', function (hooks) {
this.server.post(`${this.backend}/config/ca`, () => {
assert.ok(true, 'request made to correct endpoint on create');
return {};
return this.emptyResponse;
});
await this.store
@ -50,7 +55,7 @@ module('Unit | Adapter | pki/action', function (hooks) {
assert.expect(1);
this.server.post(`${this.backend}/issuers/import/bundle`, () => {
assert.ok(true, 'request made to correct endpoint on create');
return {};
return this.emptyResponse;
});
await this.store
@ -65,19 +70,19 @@ module('Unit | Adapter | pki/action', function (hooks) {
const adapterOptions = { adapterOptions: { actionType: 'generate-root', useIssuer: false } };
this.server.post(`${this.backend}/root/generate/internal`, () => {
assert.ok(true, 'request made correctly when type = internal');
return {};
return this.emptyResponse;
});
this.server.post(`${this.backend}/root/generate/exported`, () => {
assert.ok(true, 'request made correctly when type = exported');
return {};
return this.emptyResponse;
});
this.server.post(`${this.backend}/root/generate/existing`, () => {
assert.ok(true, 'request made correctly when type = exising');
return {};
return this.emptyResponse;
});
this.server.post(`${this.backend}/root/generate/kms`, () => {
assert.ok(true, 'request made correctly when type = kms');
return {};
return this.emptyResponse;
});
await this.store
@ -107,19 +112,19 @@ module('Unit | Adapter | pki/action', function (hooks) {
const adapterOptions = { adapterOptions: { actionType: 'generate-root', useIssuer: true } };
this.server.post(`${this.backend}/issuers/generate/root/internal`, () => {
assert.ok(true, 'request made correctly when type = internal');
return {};
return this.emptyResponse;
});
this.server.post(`${this.backend}/issuers/generate/root/exported`, () => {
assert.ok(true, 'request made correctly when type = exported');
return {};
return this.emptyResponse;
});
this.server.post(`${this.backend}/issuers/generate/root/existing`, () => {
assert.ok(true, 'request made correctly when type = exising');
return {};
return this.emptyResponse;
});
this.server.post(`${this.backend}/issuers/generate/root/kms`, () => {
assert.ok(true, 'request made correctly when type = kms');
return {};
return this.emptyResponse;
});
await this.store
@ -151,19 +156,19 @@ module('Unit | Adapter | pki/action', function (hooks) {
const adapterOptions = { adapterOptions: { actionType: 'generate-csr', useIssuer: false } };
this.server.post(`${this.backend}/intermediate/generate/internal`, () => {
assert.ok(true, 'request made correctly when type = internal');
return {};
return this.emptyResponse;
});
this.server.post(`${this.backend}/intermediate/generate/exported`, () => {
assert.ok(true, 'request made correctly when type = exported');
return {};
return this.emptyResponse;
});
this.server.post(`${this.backend}/intermediate/generate/existing`, () => {
assert.ok(true, 'request made correctly when type = exising');
return {};
return this.emptyResponse;
});
this.server.post(`${this.backend}/intermediate/generate/kms`, () => {
assert.ok(true, 'request made correctly when type = kms');
return {};
return this.emptyResponse;
});
await this.store
@ -193,19 +198,19 @@ module('Unit | Adapter | pki/action', function (hooks) {
const adapterOptions = { adapterOptions: { actionType: 'generate-csr', useIssuer: true } };
this.server.post(`${this.backend}/issuers/generate/intermediate/internal`, () => {
assert.ok(true, 'request made correctly when type = internal');
return {};
return this.emptyResponse;
});
this.server.post(`${this.backend}/issuers/generate/intermediate/exported`, () => {
assert.ok(true, 'request made correctly when type = exported');
return {};
return this.emptyResponse;
});
this.server.post(`${this.backend}/issuers/generate/intermediate/existing`, () => {
assert.ok(true, 'request made correctly when type = exising');
return {};
return this.emptyResponse;
});
this.server.post(`${this.backend}/issuers/generate/intermediate/kms`, () => {
assert.ok(true, 'request made correctly when type = kms');
return {};
return this.emptyResponse;
});
await this.store
@ -242,7 +247,7 @@ module('Unit | Adapter | pki/action', function (hooks) {
this.server.post(`${mount}/issuer/${issuerRef}/sign-intermediate`, () => {
assert.ok(true, 'request made to correct mount');
return {};
return this.emptyResponse;
});
await this.store

View File

@ -18,7 +18,13 @@ export interface FormFieldGroupOptions {
[key: string]: Array<string>;
}
export interface ModelValidation {
export interface ValidationMap {
[key: string]: {
isValid: boolean;
errors: Array<string>;
};
}
export interface ModelValidations {
isValid: boolean;
state: {
[key: string]: {

View File

@ -5,6 +5,7 @@
import Model from '@ember-data/model';
import { FormField, FormFieldGroups, ModelValidations } from 'vault/app-types';
import { ParsedCertificateData } from 'vault/vault/utils/parse-pki-cert';
export default class PkiIssuerModel extends Model {
secretMountPath: class;
get useOpenAPI(): boolean;
@ -21,6 +22,7 @@ export default class PkiIssuerModel extends Model {
issuingCertificates: string;
crlDistributionPoints: string;
ocspServers: string;
parsedCertificate: ParsedCertificateData;
/** these are all instances of the capabilities model which should be converted to native class and typed
rotateExported: any;
rotateInternal: any;

View File

@ -9,7 +9,7 @@ interface ParsedCertificateData {
// certificate values
common_name: string;
serial_number: string;
subject_serial_number: string;
ou: string;
organization: string;
country: string;