UI: PKI Sign Intermediate (#18842)
This commit is contained in:
parent
419a92a632
commit
8788317b8a
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:feature
|
||||||
|
**New PKI UI**: Add beta support for new and improved PKI UI
|
||||||
|
```
|
|
@ -24,7 +24,7 @@ export default class PkiActionAdapter extends ApplicationAdapter {
|
||||||
? `${baseUrl}/issuers/generate/intermediate/${type}`
|
? `${baseUrl}/issuers/generate/intermediate/${type}`
|
||||||
: `${baseUrl}/intermediate/generate/${type}`;
|
: `${baseUrl}/intermediate/generate/${type}`;
|
||||||
case 'sign-intermediate':
|
case 'sign-intermediate':
|
||||||
return `${baseUrl}/issuer/${issuerName}/sign-intermediate`;
|
return `${baseUrl}/issuer/${encodePath(issuerName)}/sign-intermediate`;
|
||||||
default:
|
default:
|
||||||
assert('actionType must be one of import, generate-root, generate-csr or sign-intermediate');
|
assert('actionType must be one of import, generate-root, generate-csr or sign-intermediate');
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
||||||
|
import ApplicationAdapter from '../application';
|
||||||
|
|
||||||
|
export default class PkiSignIntermediateAdapter extends ApplicationAdapter {
|
||||||
|
namespace = 'v1';
|
||||||
|
|
||||||
|
createRecord(store, type, snapshot) {
|
||||||
|
const serializer = store.serializerFor(type.modelName);
|
||||||
|
const { backend, issuerRef } = snapshot.record;
|
||||||
|
const url = `${this.buildURL()}/${encodePath(backend)}/issuer/${encodePath(issuerRef)}/sign-intermediate`;
|
||||||
|
const data = serializer.serialize(snapshot, type);
|
||||||
|
return this.ajax(url, 'POST', { data }).then((result) => ({
|
||||||
|
// sign-intermediate can happen multiple times per issuer,
|
||||||
|
// so the ID needs to be unique from the issuer ID
|
||||||
|
id: result.request_id,
|
||||||
|
...result,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
|
@ -37,11 +37,11 @@ export default class PkiCertificateBaseModel extends Model {
|
||||||
@attr('string') commonName;
|
@attr('string') commonName;
|
||||||
|
|
||||||
// Attrs that come back from API POST request
|
// Attrs that come back from API POST request
|
||||||
@attr() caChain;
|
@attr({ masked: true, label: 'CA Chain' }) caChain;
|
||||||
@attr('string', { masked: true }) certificate;
|
@attr('string', { masked: true }) certificate;
|
||||||
@attr('number') expiration;
|
@attr('number') expiration;
|
||||||
@attr('number', { formatDate: true }) revocationTime;
|
@attr('number', { formatDate: true }) revocationTime;
|
||||||
@attr('string') issuingCa;
|
@attr('string', { label: 'Issuing CA', masked: true }) issuingCa;
|
||||||
@attr('string') privateKey;
|
@attr('string') privateKey;
|
||||||
@attr('string') privateKeyType;
|
@attr('string') privateKeyType;
|
||||||
@attr('string') serialNumber;
|
@attr('string') serialNumber;
|
||||||
|
|
|
@ -32,8 +32,6 @@ export default class PkiIssuerModel extends PkiCertificateBaseModel {
|
||||||
|
|
||||||
@attr isDefault; // readonly
|
@attr isDefault; // readonly
|
||||||
@attr('string') issuerId;
|
@attr('string') issuerId;
|
||||||
@attr('string', { displayType: 'masked' }) certificate;
|
|
||||||
@attr('string', { displayType: 'masked', label: 'CA Chain' }) caChain;
|
|
||||||
|
|
||||||
@attr('string', {
|
@attr('string', {
|
||||||
label: 'Default key ID',
|
label: 'Default key ID',
|
||||||
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
import { attr } from '@ember-data/model';
|
||||||
|
import { withFormFields } from 'vault/decorators/model-form-fields';
|
||||||
|
import { withModelValidations } from 'vault/decorators/model-validations';
|
||||||
|
import PkiCertificateBaseModel from './certificate/base';
|
||||||
|
|
||||||
|
const validations = {
|
||||||
|
csr: [{ type: 'presence', message: 'CSR is required.' }],
|
||||||
|
};
|
||||||
|
@withModelValidations(validations)
|
||||||
|
@withFormFields([
|
||||||
|
'csr',
|
||||||
|
'useCsrValues',
|
||||||
|
'commonName',
|
||||||
|
'customTtl',
|
||||||
|
'notBeforeDuration',
|
||||||
|
'format',
|
||||||
|
'permittedDnsDomains',
|
||||||
|
'maxPathLength',
|
||||||
|
])
|
||||||
|
export default class PkiSignIntermediateModel extends PkiCertificateBaseModel {
|
||||||
|
getHelpUrl(backend) {
|
||||||
|
return `/v1/${backend}/issuer/example/sign-intermediate?help=1`;
|
||||||
|
}
|
||||||
|
|
||||||
|
@attr issuerRef;
|
||||||
|
|
||||||
|
@attr('string', {
|
||||||
|
label: 'CSR',
|
||||||
|
editType: 'textarea',
|
||||||
|
subText: 'The PEM-encoded CSR to be signed.',
|
||||||
|
})
|
||||||
|
csr;
|
||||||
|
|
||||||
|
@attr('boolean', {
|
||||||
|
label: 'Use CSR values',
|
||||||
|
subText:
|
||||||
|
'Subject information and key usages specified in the CSR will be used over parameters provided here, and extensions in the CSR will be copied into the issued certificate.',
|
||||||
|
docLink: '/vault/api-docs/secret/pki#use_csr_values',
|
||||||
|
})
|
||||||
|
useCsrValues;
|
||||||
|
|
||||||
|
@attr({
|
||||||
|
label: 'Not valid after',
|
||||||
|
detailsLabel: 'Issued certificates expire after',
|
||||||
|
subText:
|
||||||
|
'The time after which this certificate will no longer be valid. This can be a TTL (a range of time from now) or a specific date.',
|
||||||
|
editType: 'yield',
|
||||||
|
})
|
||||||
|
customTtl;
|
||||||
|
|
||||||
|
@attr({
|
||||||
|
label: 'Backdate validity',
|
||||||
|
detailsLabel: 'Issued certificate backdating',
|
||||||
|
helperTextDisabled: 'Vault will use the default value, 30s',
|
||||||
|
helperTextEnabled:
|
||||||
|
'Also called the not_before_duration property. Allows certificates to be valid for a certain time period before now. This is useful to correct clock misalignment on various systems when setting up your CA.',
|
||||||
|
editType: 'ttl',
|
||||||
|
defaultValue: '30s',
|
||||||
|
})
|
||||||
|
notBeforeDuration;
|
||||||
|
|
||||||
|
@attr('string')
|
||||||
|
commonName;
|
||||||
|
|
||||||
|
@attr({
|
||||||
|
label: 'Permitted DNS domains',
|
||||||
|
subText:
|
||||||
|
'DNS domains for which certificates are allowed to be issued or signed by this CA certificate. Enter each value as a new input.',
|
||||||
|
})
|
||||||
|
permittedDnsDomains;
|
||||||
|
|
||||||
|
@attr({
|
||||||
|
subText: 'Specifies the maximum path length to encode in the generated certificate. -1 means no limit',
|
||||||
|
defaultValue: '-1',
|
||||||
|
})
|
||||||
|
maxPathLength;
|
||||||
|
|
||||||
|
/* Signing Options overrides */
|
||||||
|
@attr({
|
||||||
|
label: 'Use PSS',
|
||||||
|
subText:
|
||||||
|
'If checked, PSS signatures will be used over PKCS#1v1.5 signatures when a RSA-type issuer is used. Ignored for ECDSA/Ed25519 issuers.',
|
||||||
|
})
|
||||||
|
usePss;
|
||||||
|
|
||||||
|
@attr({
|
||||||
|
label: 'Subject Key Identifier (SKID)',
|
||||||
|
subText:
|
||||||
|
'Value for the subject key identifier, specified as a string in hex format. If this is empty, Vault will automatically calculate the SKID. ',
|
||||||
|
})
|
||||||
|
skid;
|
||||||
|
|
||||||
|
@attr({
|
||||||
|
possibleValues: ['0', '256', '384', '512'],
|
||||||
|
})
|
||||||
|
signatureBits;
|
||||||
|
}
|
|
@ -1,3 +1,7 @@
|
||||||
import ApplicationSerializer from '../application';
|
import ApplicationSerializer from '../application';
|
||||||
|
|
||||||
export default class PkiRoleSerializer extends ApplicationSerializer {}
|
export default class PkiRoleSerializer extends ApplicationSerializer {
|
||||||
|
attrs = {
|
||||||
|
name: { serialize: false },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -237,8 +237,11 @@
|
||||||
id={{@attr.name}}
|
id={{@attr.name}}
|
||||||
value={{or (get @model this.valuePath) @attr.options.defaultValue}}
|
value={{or (get @model this.valuePath) @attr.options.defaultValue}}
|
||||||
oninput={{this.onChangeWithEvent}}
|
oninput={{this.onChangeWithEvent}}
|
||||||
class="textarea"
|
class="textarea {{if this.validationError 'has-error-border'}}"
|
||||||
></textarea>
|
></textarea>
|
||||||
|
{{#if this.validationError}}
|
||||||
|
<AlertInline @type="danger" @message={{this.validationError}} @paddingTop={{true}} />
|
||||||
|
{{/if}}
|
||||||
{{else if (eq @attr.options.editType "password")}}
|
{{else if (eq @attr.options.editType "password")}}
|
||||||
<Input
|
<Input
|
||||||
data-test-input={{@attr.name}}
|
data-test-input={{@attr.name}}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<ToolbarActions>
|
<ToolbarActions>
|
||||||
{{#if @canRotate}}
|
{{!-- {{#if @canRotate}}
|
||||||
<ToolbarLink @route="issuers.generate-root" @type="rotate-cw" @issuer={{@issuer.id}} data-test-pki-issuer-rotate-root>
|
<ToolbarLink @route="issuers.generate-root" @type="rotate-cw" @issuer={{@issuer.id}} data-test-pki-issuer-rotate-root>
|
||||||
Rotate this root
|
Rotate this root
|
||||||
</ToolbarLink>
|
</ToolbarLink>
|
||||||
{{/if}}
|
{{/if}} --}}
|
||||||
{{#if @canCrossSign}}
|
{{#if @canCrossSign}}
|
||||||
<ToolbarLink
|
<ToolbarLink
|
||||||
@route="issuers.issuer.cross-sign"
|
@route="issuers.issuer.cross-sign"
|
||||||
|
@ -20,16 +20,52 @@
|
||||||
Sign Intermediate
|
Sign Intermediate
|
||||||
</ToolbarLink>
|
</ToolbarLink>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<DownloadButton
|
<BasicDropdown @class="popup-menu" @horizontalPosition="auto-right" @verticalPosition="below" as |D|>
|
||||||
class="toolbar-link"
|
<D.Trigger
|
||||||
@filename={{@issuer.id}}
|
data-test-popup-menu-trigger="true"
|
||||||
@data={{@issuer.certificate}}
|
class={{concat "toolbar-link" (if D.isOpen " is-active")}}
|
||||||
@extension="pem"
|
@htmlTag="button"
|
||||||
data-test-issuer-download
|
data-test-issuer-download
|
||||||
>
|
>
|
||||||
Download
|
Download
|
||||||
<Chevron @direction="down" @isButton={{true}} />
|
<Chevron @direction="down" @isButton={{true}} />
|
||||||
</DownloadButton>
|
</D.Trigger>
|
||||||
|
<D.Content @defaultClass="popup-menu-content">
|
||||||
|
<nav class="box menu" aria-label="snapshots actions">
|
||||||
|
<ul class="menu-list">
|
||||||
|
{{#if @pem}}
|
||||||
|
{{! should never be null, but if it is we don't want to let users download an empty file }}
|
||||||
|
<li class="action">
|
||||||
|
<DownloadButton
|
||||||
|
class="link"
|
||||||
|
@filename={{@issuer.id}}
|
||||||
|
@data={{@pem}}
|
||||||
|
@extension="pem"
|
||||||
|
data-test-issuer-download-type="pem"
|
||||||
|
>
|
||||||
|
PEM format
|
||||||
|
</DownloadButton>
|
||||||
|
</li>
|
||||||
|
{{/if}}
|
||||||
|
{{#if @der}}
|
||||||
|
{{! should never be null, but if it is we don't want to let users download an empty file }}
|
||||||
|
<li class="action">
|
||||||
|
<DownloadButton
|
||||||
|
class="link"
|
||||||
|
@filename={{@issuer.id}}
|
||||||
|
@data={{@der}}
|
||||||
|
@extension="der"
|
||||||
|
data-test-issuer-download-type="der"
|
||||||
|
>
|
||||||
|
DER format
|
||||||
|
</DownloadButton>
|
||||||
|
</li>
|
||||||
|
{{/if}}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</D.Content>
|
||||||
|
</BasicDropdown>
|
||||||
|
|
||||||
{{#if @canConfigure}}
|
{{#if @canConfigure}}
|
||||||
<ToolbarLink @route="issuers.issuer.edit" @issuer={{@issuer.id}} data-test-pki-issuer-configure>
|
<ToolbarLink @route="issuers.issuer.edit" @issuer={{@issuer.id}} data-test-pki-issuer-configure>
|
||||||
Configure
|
Configure
|
||||||
|
@ -58,7 +94,7 @@
|
||||||
</h2>
|
</h2>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#each fields as |attr|}}
|
{{#each fields as |attr|}}
|
||||||
{{#if (eq attr.options.displayType "masked")}}
|
{{#if attr.options.masked}}
|
||||||
<InfoTableRow @label={{or attr.options.label (humanize (dasherize attr.name))}} @value={{get @issuer attr.name}}>
|
<InfoTableRow @label={{or attr.options.label (humanize (dasherize attr.name))}} @value={{get @issuer attr.name}}>
|
||||||
<MaskedInput
|
<MaskedInput
|
||||||
@name={{or attr.options.label (humanize (dasherize attr.name))}}
|
@name={{or attr.options.label (humanize (dasherize attr.name))}}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import PkiActionModel from 'vault/models/pki/action';
|
||||||
|
|
||||||
interface Args {
|
interface Args {
|
||||||
model: PkiActionModel;
|
model: PkiActionModel;
|
||||||
|
groups: Map<[key: string], Array<string>> | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class PkiGenerateToggleGroupsComponent extends Component<Args> {
|
export default class PkiGenerateToggleGroupsComponent extends Component<Args> {
|
||||||
|
@ -21,6 +22,7 @@ export default class PkiGenerateToggleGroupsComponent extends Component<Args> {
|
||||||
}
|
}
|
||||||
|
|
||||||
get groups() {
|
get groups() {
|
||||||
|
if (this.args.groups) return this.args.groups;
|
||||||
const groups = {
|
const groups = {
|
||||||
'Key parameters': this.keyParamFields,
|
'Key parameters': this.keyParamFields,
|
||||||
'Subject Alternative Name (SAN) Options': ['altNames', 'ipSans', 'uriSans', 'otherSans'],
|
'Subject Alternative Name (SAN) Options': ['altNames', 'ipSans', 'uriSans', 'otherSans'],
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
{{#if @model.id}}
|
||||||
|
{{! Model only has ID once form has been submitted and saved }}
|
||||||
|
<Toolbar />
|
||||||
|
<main data-test-sign-intermediate-result>
|
||||||
|
<div class="box is-sideless is-fullwidth is-shadowless">
|
||||||
|
<AlertBanner
|
||||||
|
@title="Next steps"
|
||||||
|
@type="warning"
|
||||||
|
@message="The CA Chain and Issuing CA values will only be available once. Make sure you copy and save it now."
|
||||||
|
/>
|
||||||
|
|
||||||
|
{{#each this.showFields as |fieldName|}}
|
||||||
|
{{#let (find-by "name" fieldName @model.allFields) as |attr|}}
|
||||||
|
<InfoTableRow @label={{or attr.options.label (humanize (dasherize attr.name))}} @value={{get @issuer attr.name}}>
|
||||||
|
{{#if (and attr.options.masked (get @model attr.name))}}
|
||||||
|
<MaskedInput @value={{get @model attr.name}} @displayOnly={{true}} @allowCopy={{true}} />
|
||||||
|
{{else if (eq attr.name "serialNumber")}}
|
||||||
|
<LinkTo
|
||||||
|
@route="certificates.certificate.details"
|
||||||
|
@model={{@model.serialNumber}}
|
||||||
|
>{{@model.serialNumber}}</LinkTo>
|
||||||
|
{{else}}
|
||||||
|
<Icon @name="minus" />
|
||||||
|
{{/if}}
|
||||||
|
</InfoTableRow>
|
||||||
|
{{/let}}
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{{else}}
|
||||||
|
<form {{on "submit" (perform this.save)}} data-test-sign-intermediate-form>
|
||||||
|
<div class="box is-sideless is-fullwidth is-marginless">
|
||||||
|
<MessageError @errorMessage={{this.errorBanner}} class="has-top-margin-s" />
|
||||||
|
<NamespaceReminder @mode={{"create"}} @noun="signed intermediate" />
|
||||||
|
{{#each @model.formFields as |attr|}}
|
||||||
|
<FormField
|
||||||
|
data-test-field={{attr}}
|
||||||
|
@attr={{attr}}
|
||||||
|
@model={{@model}}
|
||||||
|
@modelValidations={{this.modelValidations}}
|
||||||
|
@showHelpText={{false}}
|
||||||
|
>
|
||||||
|
{{! attr customTtl has editType yield and will show this component }}
|
||||||
|
<PkiNotValidAfterForm @attr={{attr}} @model={{@model}} />
|
||||||
|
</FormField>
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
<PkiGenerateToggleGroups @model={{@model}} @groups={{this.groups}} />
|
||||||
|
</div>
|
||||||
|
<div class="has-top-padding-s">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="button is-primary {{if this.save.isRunning 'is-loading'}}"
|
||||||
|
disabled={{this.save.isRunning}}
|
||||||
|
data-test-pki-sign-intermediate-save
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button has-left-margin-s"
|
||||||
|
disabled={{this.save.isRunning}}
|
||||||
|
{{on "click" this.cancel}}
|
||||||
|
data-test-pki-sign-intermediate-cancel
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
{{#if this.inlineFormAlert}}
|
||||||
|
<div class="control">
|
||||||
|
<AlertInline
|
||||||
|
@type="danger"
|
||||||
|
@paddingTop={{true}}
|
||||||
|
@message={{this.inlineFormAlert}}
|
||||||
|
@mimicRefresh={{true}}
|
||||||
|
data-test-form-error
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{{/if}}
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { action } from '@ember/object';
|
||||||
|
import { service } from '@ember/service';
|
||||||
|
import { waitFor } from '@ember/test-waiters';
|
||||||
|
import Component from '@glimmer/component';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
import { task } from 'ember-concurrency';
|
||||||
|
import PkiIssuerModel from 'vault/models/pki/issuer';
|
||||||
|
import FlashMessageService from 'vault/services/flash-messages';
|
||||||
|
import errorMessage from 'vault/utils/error-message';
|
||||||
|
|
||||||
|
interface Args {
|
||||||
|
onCancel: CallableFunction;
|
||||||
|
model: PkiIssuerModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class PkiSignIntermediateFormComponent extends Component<Args> {
|
||||||
|
@service declare readonly flashMessages: FlashMessageService;
|
||||||
|
@tracked errorBanner = '';
|
||||||
|
@tracked inlineFormAlert = '';
|
||||||
|
@tracked modelValidations = null;
|
||||||
|
|
||||||
|
@action cancel() {
|
||||||
|
this.args.model.unloadRecord();
|
||||||
|
this.args.onCancel();
|
||||||
|
}
|
||||||
|
@task
|
||||||
|
@waitFor
|
||||||
|
*save(event: Event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const { isValid, state, invalidFormMessage } = this.args.model.validate();
|
||||||
|
this.modelValidations = isValid ? null : state;
|
||||||
|
this.inlineFormAlert = invalidFormMessage;
|
||||||
|
if (!isValid) return;
|
||||||
|
try {
|
||||||
|
yield this.args.model.save();
|
||||||
|
this.flashMessages.success('Successfully signed CSR.');
|
||||||
|
} catch (e) {
|
||||||
|
this.errorBanner = errorMessage(e);
|
||||||
|
this.inlineFormAlert = 'There was a problem signing the CSR.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get groups() {
|
||||||
|
return {
|
||||||
|
'Signing options': ['usePss', 'skid', 'signatureBits'],
|
||||||
|
'Subject Alternative Name (SAN) Options': ['altNames', 'ipSans', 'uriSans', 'otherSans'],
|
||||||
|
'Additional subject fields': [
|
||||||
|
'ou',
|
||||||
|
'organization',
|
||||||
|
'country',
|
||||||
|
'locality',
|
||||||
|
'province',
|
||||||
|
'streetAddress',
|
||||||
|
'postalCode',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get showFields() {
|
||||||
|
return ['serialNumber', 'certificate', 'issuingCa', 'caChain'];
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,8 +2,30 @@ import PkiIssuerIndexRoute from './index';
|
||||||
|
|
||||||
export default class PkiIssuerDetailsRoute extends PkiIssuerIndexRoute {
|
export default class PkiIssuerDetailsRoute extends PkiIssuerIndexRoute {
|
||||||
// Details route gets issuer data from PkiIssuerIndexRoute
|
// Details route gets issuer data from PkiIssuerIndexRoute
|
||||||
setupController(controller, resolvedModel) {
|
async setupController(controller, resolvedModel) {
|
||||||
super.setupController(controller, resolvedModel);
|
super.setupController(controller, resolvedModel);
|
||||||
controller.breadcrumbs.push({ label: resolvedModel.id });
|
controller.breadcrumbs.push({ label: resolvedModel.id });
|
||||||
|
const pem = await this.fetchCertByFormat(resolvedModel.id, 'pem');
|
||||||
|
const der = await this.fetchCertByFormat(resolvedModel.id, 'der');
|
||||||
|
controller.pem = pem;
|
||||||
|
controller.der = der;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private fetches cert by format so it's available for download
|
||||||
|
*/
|
||||||
|
fetchCertByFormat(issuerId, format) {
|
||||||
|
const endpoint = `/v1/${this.secretMountPath.currentPath}/issuer/${issuerId}/${format}`;
|
||||||
|
const adapter = this.store.adapterFor('application');
|
||||||
|
try {
|
||||||
|
return adapter.rawRequest(endpoint, 'GET', { unauthenticated: true }).then(function (response) {
|
||||||
|
if (format === 'der') {
|
||||||
|
return response.blob();
|
||||||
|
}
|
||||||
|
return response.text();
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,29 @@
|
||||||
import Route from '@ember/routing/route';
|
import Route from '@ember/routing/route';
|
||||||
|
import { service } from '@ember/service';
|
||||||
|
|
||||||
export default class PkiIssuerSignRoute extends Route {}
|
export default class PkiIssuerSignRoute extends Route {
|
||||||
|
@service store;
|
||||||
|
@service secretMountPath;
|
||||||
|
@service pathHelp;
|
||||||
|
|
||||||
|
beforeModel() {
|
||||||
|
// Must call this promise before the model hook otherwise it doesn't add OpenApi to record.
|
||||||
|
return this.pathHelp.getNewModel('pki/sign-intermediate', this.secretMountPath.currentPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
model() {
|
||||||
|
const { issuer_ref } = this.paramsFor('issuers/issuer');
|
||||||
|
return this.store.createRecord('pki/sign-intermediate', { issuerRef: issuer_ref });
|
||||||
|
}
|
||||||
|
setupController(controller, resolvedModel) {
|
||||||
|
super.setupController(controller, resolvedModel);
|
||||||
|
const backend = this.secretMountPath.currentPath || 'pki';
|
||||||
|
controller.breadcrumbs = [
|
||||||
|
{ label: 'secrets', route: 'secrets', linkExternal: true },
|
||||||
|
{ label: backend, route: 'overview' },
|
||||||
|
{ label: 'issuers', route: 'issuers.index' },
|
||||||
|
{ label: resolvedModel.issuerRef, route: 'issuers.issuer.details' },
|
||||||
|
{ label: 'sign intermediate' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -9,8 +9,11 @@
|
||||||
</h1>
|
</h1>
|
||||||
</p.levelLeft>
|
</p.levelLeft>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
<Page::PkiIssuerDetails
|
<Page::PkiIssuerDetails
|
||||||
@issuer={{this.model}}
|
@issuer={{this.model}}
|
||||||
|
@pem={{this.pem}}
|
||||||
|
@der={{this.der}}
|
||||||
@canRotate={{this.model.canRotateIssuer}}
|
@canRotate={{this.model.canRotateIssuer}}
|
||||||
@canCrossSign={{this.model.canCrossSign}}
|
@canCrossSign={{this.model.canCrossSign}}
|
||||||
@canSignIntermediate={{this.model.canSignIntermediate}}
|
@canSignIntermediate={{this.model.canSignIntermediate}}
|
||||||
|
|
|
@ -1 +1,16 @@
|
||||||
route: issuers.issuer.sign
|
<PageHeader as |p|>
|
||||||
|
<p.top>
|
||||||
|
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
|
||||||
|
</p.top>
|
||||||
|
<p.levelLeft>
|
||||||
|
<h1 class="title is-3" data-test-pki-issuer-page-title>
|
||||||
|
<Icon @name="file-text" @size="24" class="has-text-grey-light" />
|
||||||
|
Sign intermediate
|
||||||
|
</h1>
|
||||||
|
</p.levelLeft>
|
||||||
|
</PageHeader>
|
||||||
|
|
||||||
|
<PkiSignIntermediateForm
|
||||||
|
@model={{this.model}}
|
||||||
|
@onCancel={{transition-to "vault.cluster.secrets.backend.pki.issuers.issuer.details" model=this.model.issuerRef}}
|
||||||
|
/>
|
|
@ -0,0 +1,97 @@
|
||||||
|
import { module, test } from 'qunit';
|
||||||
|
import { setupRenderingTest } from 'vault/tests/helpers';
|
||||||
|
import { click, fillIn, render } from '@ember/test-helpers';
|
||||||
|
import { hbs } from 'ember-cli-htmlbars';
|
||||||
|
import Sinon from 'sinon';
|
||||||
|
import { setupEngine } from 'ember-engines/test-support';
|
||||||
|
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||||
|
|
||||||
|
const selectors = {
|
||||||
|
form: '[data-test-sign-intermediate-form]',
|
||||||
|
csrInput: '[data-test-input="csr"]',
|
||||||
|
toggleSigningOptions: '[data-test-toggle-group="Signing options"]',
|
||||||
|
toggleSANOptions: '[data-test-toggle-group="Subject Alternative Name (SAN) Options"]',
|
||||||
|
toggleAdditionalFields: '[data-test-toggle-group="Additional subject fields"]',
|
||||||
|
fieldByName: (name) => `[data-test-field="${name}"]`,
|
||||||
|
saveButton: '[data-test-pki-sign-intermediate-save]',
|
||||||
|
cancelButton: '[data-test-pki-sign-intermediate-cancel]',
|
||||||
|
fieldError: '[data-test-inline-alert]',
|
||||||
|
formError: '[data-test-form-error]',
|
||||||
|
resultsContainer: '[data-test-sign-intermediate-result]',
|
||||||
|
rowByName: (name) => `[data-test-row-label="${name}"]`,
|
||||||
|
valueByName: (name) => `[data-test-value-div="${name}"]`,
|
||||||
|
};
|
||||||
|
module('Integration | Component | pki-sign-intermediate-form', function (hooks) {
|
||||||
|
setupRenderingTest(hooks);
|
||||||
|
setupEngine(hooks, 'pki');
|
||||||
|
setupMirage(hooks);
|
||||||
|
|
||||||
|
hooks.beforeEach(async function () {
|
||||||
|
this.store = this.owner.lookup('service:store');
|
||||||
|
this.secretMountPath = this.owner.lookup('service:secret-mount-path');
|
||||||
|
this.secretMountPath.currentPath = 'pki-test';
|
||||||
|
this.model = this.store.createRecord('pki/sign-intermediate', { issuerRef: 'some-issuer' });
|
||||||
|
this.onCancel = Sinon.spy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders correctly on load', async function (assert) {
|
||||||
|
assert.expect(9);
|
||||||
|
await render(hbs`<PkiSignIntermediateForm @onCancel={{this.onCancel}} @model={{this.model}} />`, {
|
||||||
|
owner: this.engine,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.dom(selectors.form).exists('Form is rendered');
|
||||||
|
assert.dom(selectors.resultsContainer).doesNotExist('Results display not rendered');
|
||||||
|
assert.dom('[data-test-field]').exists({ count: 8 }, '8 default fields shown');
|
||||||
|
assert.dom(selectors.toggleSigningOptions).exists();
|
||||||
|
assert.dom(selectors.toggleSANOptions).exists();
|
||||||
|
assert.dom(selectors.toggleAdditionalFields).exists();
|
||||||
|
|
||||||
|
await click(selectors.toggleSigningOptions);
|
||||||
|
['usePss', 'skid', 'signatureBits'].forEach((name) => {
|
||||||
|
assert.dom(selectors.fieldByName(name)).exists();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it shows the returned values on successful save', async function (assert) {
|
||||||
|
assert.expect(13);
|
||||||
|
await render(hbs`<PkiSignIntermediateForm @onCancel={{this.onCancel}} @model={{this.model}} />`, {
|
||||||
|
owner: this.engine,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.server.post(`/pki-test/issuer/some-issuer/sign-intermediate`, function (schema, req) {
|
||||||
|
const payload = JSON.parse(req.requestBody);
|
||||||
|
assert.strictEqual(payload.csr, 'example-data', 'Request made to correct endpoint on save');
|
||||||
|
return {
|
||||||
|
request_id: 'some-id',
|
||||||
|
data: {
|
||||||
|
serial_number: '31:52:b9:09:40',
|
||||||
|
ca_chain: ['-----root pem------'],
|
||||||
|
issuing_ca: '-----issuing ca------',
|
||||||
|
certificate: '-----certificate------',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
await click(selectors.saveButton);
|
||||||
|
assert.dom(selectors.formError).hasText('There is an error with this form.', 'Shows validation errors');
|
||||||
|
assert.dom(selectors.csrInput).hasClass('has-error-border');
|
||||||
|
assert.dom(selectors.fieldError).hasText('CSR is required.');
|
||||||
|
|
||||||
|
await fillIn(selectors.csrInput, 'example-data');
|
||||||
|
await click(selectors.saveButton);
|
||||||
|
[
|
||||||
|
{ label: 'Serial number' },
|
||||||
|
{ label: 'CA Chain', masked: true },
|
||||||
|
{ label: 'Certificate', masked: true },
|
||||||
|
{ label: 'Issuing CA', masked: true },
|
||||||
|
].forEach(({ label, masked }) => {
|
||||||
|
assert.dom(selectors.rowByName(label)).exists();
|
||||||
|
if (masked) {
|
||||||
|
assert.dom(selectors.valueByName(label)).hasText('***********', `${label} is masked`);
|
||||||
|
} else {
|
||||||
|
assert.dom(selectors.valueByName(label)).hasText('31:52:b9:09:40', `Renders ${label}`);
|
||||||
|
assert.dom(`${selectors.valueByName(label)} a`).exists(`${label} is a link`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -8,6 +8,7 @@ const selectors = {
|
||||||
keys: '[data-test-toggle-group="Key parameters"]',
|
keys: '[data-test-toggle-group="Key parameters"]',
|
||||||
sanOptions: '[data-test-toggle-group="Subject Alternative Name (SAN) Options"]',
|
sanOptions: '[data-test-toggle-group="Subject Alternative Name (SAN) Options"]',
|
||||||
subjectFields: '[data-test-toggle-group="Additional subject fields"]',
|
subjectFields: '[data-test-toggle-group="Additional subject fields"]',
|
||||||
|
toggleByName: (name) => `[data-test-toggle-group="${name}"]`,
|
||||||
};
|
};
|
||||||
|
|
||||||
module('Integration | Component | PkiGenerateToggleGroups', function (hooks) {
|
module('Integration | Component | PkiGenerateToggleGroups', function (hooks) {
|
||||||
|
@ -94,4 +95,32 @@ module('Integration | Component | PkiGenerateToggleGroups', function (hooks) {
|
||||||
assert.dom(`[data-test-input="${key}"]`).exists(`${key} input renders`);
|
assert.dom(`[data-test-input="${key}"]`).exists(`${key} input renders`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('it should render groups according to the passed @groups', async function (assert) {
|
||||||
|
assert.expect(11);
|
||||||
|
const fieldsA = ['ou', 'organization'];
|
||||||
|
const fieldsZ = ['country', 'locality', 'province', 'streetAddress', 'postalCode'];
|
||||||
|
this.set('groups', {
|
||||||
|
'Group A': fieldsA,
|
||||||
|
'Group Z': fieldsZ,
|
||||||
|
});
|
||||||
|
await render(hbs`<PkiGenerateToggleGroups @model={{this.model}} @groups={{this.groups}} />`, {
|
||||||
|
owner: this.engine,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.dom(selectors.toggleByName('Group A')).hasText('Group A', 'First group renders');
|
||||||
|
assert.dom(selectors.toggleByName('Group Z')).hasText('Group Z', 'Second group renders');
|
||||||
|
|
||||||
|
await click(selectors.toggleByName('Group A'));
|
||||||
|
assert.dom('[data-test-field]').exists({ count: fieldsA.length }, 'Correct number of fields render');
|
||||||
|
fieldsA.forEach((key) => {
|
||||||
|
assert.dom(`[data-test-input="${key}"]`).exists(`${key} input renders`);
|
||||||
|
});
|
||||||
|
|
||||||
|
await click(selectors.toggleByName('Group Z'));
|
||||||
|
assert.dom('[data-test-field]').exists({ count: fieldsZ.length }, 'Correct number of fields render');
|
||||||
|
fieldsZ.forEach((key) => {
|
||||||
|
assert.dom(`[data-test-input="${key}"]`).exists(`${key} input renders`);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -38,7 +38,8 @@ module('Integration | Component | page/pki-issuer-details', function (hooks) {
|
||||||
this.context
|
this.context
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.dom(SELECTORS.rotateRoot).hasText('Rotate this root');
|
// Add back when rotate root capability is added
|
||||||
|
// assert.dom(SELECTORS.rotateRoot).hasText('Rotate this root');
|
||||||
assert.dom(SELECTORS.crossSign).hasText('Cross-sign Issuer');
|
assert.dom(SELECTORS.crossSign).hasText('Cross-sign Issuer');
|
||||||
assert.dom(SELECTORS.signIntermediate).hasText('Sign Intermediate');
|
assert.dom(SELECTORS.signIntermediate).hasText('Sign Intermediate');
|
||||||
assert.dom(SELECTORS.download).hasText('Download');
|
assert.dom(SELECTORS.download).hasText('Download');
|
||||||
|
|
|
@ -46,17 +46,15 @@ module('Integration | Component | pki-role-form', function (hooks) {
|
||||||
|
|
||||||
test('it should save a new pki role with various options selected', async function (assert) {
|
test('it should save a new pki role with various options selected', async function (assert) {
|
||||||
// Key usage, Key params and Not valid after options are tested in their respective component tests
|
// Key usage, Key params and Not valid after options are tested in their respective component tests
|
||||||
assert.expect(10);
|
assert.expect(9);
|
||||||
this.server.post(`/${this.model.backend}/roles/test-role`, (schema, req) => {
|
this.server.post(`/${this.model.backend}/roles/test-role`, (schema, req) => {
|
||||||
assert.ok(true, 'Request made to save role');
|
assert.ok(true, 'Request made to save role');
|
||||||
const request = JSON.parse(req.requestBody);
|
const request = JSON.parse(req.requestBody);
|
||||||
const roleName = request.name;
|
|
||||||
const allowedDomainsTemplate = request.allowed_domains_template;
|
const allowedDomainsTemplate = request.allowed_domains_template;
|
||||||
const policyIdentifiers = request.policy_identifiers;
|
const policyIdentifiers = request.policy_identifiers;
|
||||||
const allowedUriSansTemplate = request.allow_uri_sans_template;
|
const allowedUriSansTemplate = request.allow_uri_sans_template;
|
||||||
const allowedSerialNumbers = request.allowed_serial_numbers;
|
const allowedSerialNumbers = request.allowed_serial_numbers;
|
||||||
|
|
||||||
assert.strictEqual(roleName, 'test-role', 'correctly sends the role name');
|
|
||||||
assert.true(allowedDomainsTemplate, 'correctly sends allowed_domains_template');
|
assert.true(allowedDomainsTemplate, 'correctly sends allowed_domains_template');
|
||||||
assert.strictEqual(policyIdentifiers[0], 'some-oid', 'correctly sends policy_identifiers');
|
assert.strictEqual(policyIdentifiers[0], 'some-oid', 'correctly sends policy_identifiers');
|
||||||
assert.true(allowedUriSansTemplate, 'correctly sends allowed_uri_sans_template');
|
assert.true(allowedUriSansTemplate, 'correctly sends allowed_uri_sans_template');
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { module, test } from 'qunit';
|
||||||
|
import { setupTest } from 'vault/tests/helpers';
|
||||||
|
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||||
|
|
||||||
|
module('Unit | Adapter | pki/sign-intermediate', function (hooks) {
|
||||||
|
setupTest(hooks);
|
||||||
|
setupMirage(hooks);
|
||||||
|
|
||||||
|
hooks.beforeEach(function () {
|
||||||
|
this.store = this.owner.lookup('service:store');
|
||||||
|
this.secretMountPath = this.owner.lookup('service:secret-mount-path');
|
||||||
|
this.backend = 'pki-test';
|
||||||
|
this.secretMountPath.currentPath = this.backend;
|
||||||
|
this.payload = {
|
||||||
|
issuerRef: 'my-issuer-id',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it exists', function (assert) {
|
||||||
|
const adapter = this.owner.lookup('adapter:pki/sign-intermediate');
|
||||||
|
assert.ok(adapter);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it calls the correct endpoint on save', async function (assert) {
|
||||||
|
assert.expect(2);
|
||||||
|
|
||||||
|
this.server.post(`${this.backend}/issuer/my-issuer-id/sign-intermediate`, () => {
|
||||||
|
assert.ok(true, 'correct endpoint called');
|
||||||
|
return {
|
||||||
|
request_id: 'unique-request-id',
|
||||||
|
data: {
|
||||||
|
foo: 'bar',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await this.store.createRecord('pki/sign-intermediate', this.payload).save();
|
||||||
|
assert.strictEqual(result.id, 'unique-request-id', 'Resulting model has ID matching request ID');
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,6 +1,5 @@
|
||||||
import PkiCertificateBaseModel from './certificate/base';
|
import PkiCertificateBaseModel from './certificate/base';
|
||||||
import { FormField, FormFieldGroups } from 'vault/app-types';
|
import { FormField, FormFieldGroups, ModelValidations } from 'vault/app-types';
|
||||||
|
|
||||||
export default class PkiIssuerModel extends PkiCertificateBaseModel {
|
export default class PkiIssuerModel extends PkiCertificateBaseModel {
|
||||||
useOpenAPI(): boolean;
|
useOpenAPI(): boolean;
|
||||||
issuerId: string;
|
issuerId: string;
|
||||||
|
@ -22,8 +21,10 @@ export default class PkiIssuerModel extends PkiCertificateBaseModel {
|
||||||
-------------------- **/
|
-------------------- **/
|
||||||
formFields: Array<FormField>;
|
formFields: Array<FormField>;
|
||||||
formFieldGroups: FormFieldGroups;
|
formFieldGroups: FormFieldGroups;
|
||||||
|
allFields: Array<FormField>;
|
||||||
get canRotateIssuer(): boolean;
|
get canRotateIssuer(): boolean;
|
||||||
get canCrossSign(): boolean;
|
get canCrossSign(): boolean;
|
||||||
get canSignIntermediate(): boolean;
|
get canSignIntermediate(): boolean;
|
||||||
get canConfigure(): boolean;
|
get canConfigure(): boolean;
|
||||||
|
validate(): ModelValidations;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue