UI: Show parsed certificate data in PKI (#19990)
This commit is contained in:
parent
0b3f24a2d8
commit
282279121d
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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'] }];
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
];
|
||||
|
|
|
@ -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"
|
||||
>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -121,6 +121,9 @@
|
|||
/>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
{{#if (eq group "default")}}
|
||||
<ParsedCertificateInfoRows @model={{@issuer.parsedCertificate}} />
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/each-in}}
|
||||
{{/each}}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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}}
|
|
@ -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],
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
</InfoTableRow>
|
||||
{{/let}}
|
||||
{{/each}}
|
||||
<ParsedCertificateInfoRows @model={{@model.parsedCertificate}} />
|
||||
</div>
|
||||
</main>
|
||||
{{else}}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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]');
|
||||
|
||||
|
|
|
@ -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}}
|
||||
/>
|
||||
`,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]: {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -9,7 +9,7 @@ interface ParsedCertificateData {
|
|||
|
||||
// certificate values
|
||||
common_name: string;
|
||||
serial_number: string;
|
||||
subject_serial_number: string;
|
||||
ou: string;
|
||||
organization: string;
|
||||
country: string;
|
||||
|
|
Loading…
Reference in New Issue