mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-01 11:08:10 +00:00
UI: display CSR after generation (#19114)
* add show page for generated CSR * fix typo, make key-id copyable * add tests * move pki tests to designated folder * list keys when in between state after CSR generation * update tests
This commit is contained in:
@@ -169,8 +169,11 @@ export default class PkiActionModel extends Model {
|
|||||||
@attr('string', { readOnly: true }) issuerId; // returned from generate-root action
|
@attr('string', { readOnly: true }) issuerId; // returned from generate-root action
|
||||||
|
|
||||||
// For generating and signing a CSR
|
// For generating and signing a CSR
|
||||||
@attr('string') csr;
|
@attr('string', { label: 'CSR', masked: true }) csr;
|
||||||
@attr caChain;
|
@attr caChain;
|
||||||
|
@attr('string', { label: 'Key ID' }) keyId;
|
||||||
|
@attr('string', { masked: true }) privateKey;
|
||||||
|
@attr('string') privateKeyType;
|
||||||
|
|
||||||
get backend() {
|
get backend() {
|
||||||
return this.secretMountPath.currentPath;
|
return this.secretMountPath.currentPath;
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
<InfoTableRow
|
<InfoTableRow
|
||||||
@label={{capitalize (or attr.options.detailsLabel attr.options.label (humanize (dasherize attr.name)))}}
|
@label={{capitalize (or attr.options.detailsLabel attr.options.label (humanize (dasherize attr.name)))}}
|
||||||
@value={{get @key attr.name}}
|
@value={{get @key attr.name}}
|
||||||
|
@addCopyButton={{eq attr.name "keyId"}}
|
||||||
/>
|
/>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{#if @key.privateKey}}
|
{{#if @key.privateKey}}
|
||||||
|
|||||||
@@ -44,8 +44,8 @@
|
|||||||
{{else if (eq @config.actionType "generate-csr")}}
|
{{else if (eq @config.actionType "generate-csr")}}
|
||||||
<PkiGenerateCsr
|
<PkiGenerateCsr
|
||||||
@model={{@config}}
|
@model={{@config}}
|
||||||
@onSave={{transition-to "vault.cluster.secrets.backend.pki.issuers"}}
|
|
||||||
@onCancel={{@onCancel}}
|
@onCancel={{@onCancel}}
|
||||||
|
@onComplete={{transition-to "vault.cluster.secrets.backend.pki.issuers"}}
|
||||||
/>
|
/>
|
||||||
{{else}}
|
{{else}}
|
||||||
<EmptyState
|
<EmptyState
|
||||||
|
|||||||
@@ -1,28 +1,76 @@
|
|||||||
<form {{on "submit" (perform this.save)}}>
|
{{#if @model.id}}
|
||||||
<MessageError @errorMessage={{this.error}} class="has-top-margin-s" />
|
{{! Model only has ID once form has been submitted and saved }}
|
||||||
<h2 class="title is-size-5 has-border-bottom-light page-header">
|
<Toolbar />
|
||||||
CSR parameters
|
<main data-test-generate-csr-result>
|
||||||
</h2>
|
<div class="box is-sideless is-fullwidth is-shadowless">
|
||||||
|
<AlertBanner @title="Next steps" @type="warning">
|
||||||
{{#each this.formFields as |field|}}
|
Copy the CSR below for a parent issuer to sign and then import the signed certificate back into this mount.
|
||||||
<FormField @attr={{field}} @model={{@model}} @modelValidations={{this.modelValidations}} />
|
{{#if @model.privateKey}}
|
||||||
{{/each}}
|
The
|
||||||
|
<code>private_key</code>
|
||||||
<PkiGenerateToggleGroups @model={{@model}} />
|
is only available once. Make sure you copy and save it now.
|
||||||
|
{{/if}}
|
||||||
<div class="field is-grouped box is-fullwidth is-bottomless has-top-margin-l">
|
</AlertBanner>
|
||||||
<div class="control">
|
{{#each this.showFields as |fieldName|}}
|
||||||
<button type="submit" class="button is-primary" data-test-save>
|
{{#let (find-by "name" fieldName @model.allFields) as |attr|}}
|
||||||
Done
|
{{#let (get @model attr.name) as |value|}}
|
||||||
</button>
|
<InfoTableRow
|
||||||
<button {{on "click" this.cancel}} type="button" class="button has-left-margin-s" data-test-cancel>
|
@label={{or attr.options.label (humanize (dasherize attr.name))}}
|
||||||
Cancel
|
@value={{value}}
|
||||||
</button>
|
@addCopyButton={{eq attr.name "keyId"}}
|
||||||
|
>
|
||||||
|
{{#if (and attr.options.masked value)}}
|
||||||
|
<MaskedInput @value={{value}} @displayOnly={{true}} @allowCopy={{true}} />
|
||||||
|
{{else if (eq attr.name "keyId")}}
|
||||||
|
<LinkTo @route="keys.key.details" @model={{@model.keyId}}>
|
||||||
|
{{@model.keyId}}
|
||||||
|
</LinkTo>
|
||||||
|
{{else}}
|
||||||
|
{{! this block only ever renders privateKey and privateKeyType }}
|
||||||
|
<span class="{{unless value 'tag'}}">{{or value "internal"}}</span>
|
||||||
|
{{/if}}
|
||||||
|
</InfoTableRow>
|
||||||
|
{{/let}}
|
||||||
|
{{/let}}
|
||||||
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
{{#if this.alert}}
|
</main>
|
||||||
|
<footer>
|
||||||
|
<div class="field is-grouped is-fullwidth has-top-margin-l">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<AlertInline @type="danger" @paddingTop={{true}} @message={{this.alert}} @mimicRefresh={{true}} data-test-alert />
|
<button type="button" class="button is-primary" {{on "click" @onComplete}} data-test-done>
|
||||||
|
Done
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
</div>
|
||||||
</div>
|
</footer>
|
||||||
</form>
|
{{else}}
|
||||||
|
<form {{on "submit" (perform this.save)}}>
|
||||||
|
<MessageError @errorMessage={{this.error}} class="has-top-margin-s" />
|
||||||
|
<h2 class="title is-size-5 has-border-bottom-light page-header">
|
||||||
|
CSR parameters
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{{#each this.formFields as |field|}}
|
||||||
|
<FormField @attr={{field}} @model={{@model}} @modelValidations={{this.modelValidations}} />
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
<PkiGenerateToggleGroups @model={{@model}} />
|
||||||
|
|
||||||
|
<div class="field is-grouped box is-fullwidth is-bottomless has-top-margin-l">
|
||||||
|
<div class="control">
|
||||||
|
<button type="submit" class="button is-primary" data-test-save>
|
||||||
|
Generate
|
||||||
|
</button>
|
||||||
|
<button {{on "click" this.cancel}} type="button" class="button has-left-margin-s" data-test-cancel>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{{#if this.alert}}
|
||||||
|
<div class="control">
|
||||||
|
<AlertInline @type="danger" @paddingTop={{true}} @message={{this.alert}} @mimicRefresh={{true}} data-test-alert />
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{{/if}}
|
||||||
@@ -12,11 +12,11 @@ import errorMessage from 'vault/utils/error-message';
|
|||||||
interface Args {
|
interface Args {
|
||||||
model: PkiActionModel;
|
model: PkiActionModel;
|
||||||
useIssuer: boolean;
|
useIssuer: boolean;
|
||||||
onSave: CallableFunction;
|
onComplete: CallableFunction;
|
||||||
onCancel: CallableFunction;
|
onCancel: CallableFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class PkiGenerateIntermediateComponent extends Component<Args> {
|
export default class PkiGenerateCsrComponent extends Component<Args> {
|
||||||
@service declare readonly flashMessages: FlashMessageService;
|
@service declare readonly flashMessages: FlashMessageService;
|
||||||
|
|
||||||
@tracked modelValidations = null;
|
@tracked modelValidations = null;
|
||||||
@@ -24,6 +24,8 @@ export default class PkiGenerateIntermediateComponent extends Component<Args> {
|
|||||||
@tracked alert: string | null = null;
|
@tracked alert: string | null = null;
|
||||||
|
|
||||||
formFields;
|
formFields;
|
||||||
|
// fields rendered after CSR generation
|
||||||
|
showFields = ['csr', 'keyId', 'privateKey', 'privateKeyType'];
|
||||||
|
|
||||||
constructor(owner: unknown, args: Args) {
|
constructor(owner: unknown, args: Args) {
|
||||||
super(owner, args);
|
super(owner, args);
|
||||||
@@ -57,13 +59,12 @@ export default class PkiGenerateIntermediateComponent extends Component<Args> {
|
|||||||
*save(event: Event): Generator<Promise<boolean | PkiActionModel>> {
|
*save(event: Event): Generator<Promise<boolean | PkiActionModel>> {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
try {
|
try {
|
||||||
const { model, onSave } = this.args;
|
const { model } = this.args;
|
||||||
const { isValid, state, invalidFormMessage } = model.validate();
|
const { isValid, state, invalidFormMessage } = model.validate();
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
const useIssuer = yield this.getCapability();
|
const useIssuer = yield this.getCapability();
|
||||||
yield model.save({ adapterOptions: { actionType: 'generate-csr', useIssuer } });
|
yield model.save({ adapterOptions: { actionType: 'generate-csr', useIssuer } });
|
||||||
this.flashMessages.success('Successfully generated CSR.');
|
this.flashMessages.success('Successfully generated CSR.');
|
||||||
onSave();
|
|
||||||
} else {
|
} else {
|
||||||
this.modelValidations = state;
|
this.modelValidations = state;
|
||||||
this.alert = invalidFormMessage;
|
this.alert = invalidFormMessage;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
{{#each this.showFields as |fieldName|}}
|
{{#each this.showFields as |fieldName|}}
|
||||||
{{#let (find-by "name" fieldName @model.allFields) as |attr|}}
|
{{#let (find-by "name" fieldName @model.allFields) as |attr|}}
|
||||||
<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 @model attr.name}}>
|
||||||
{{#if (and attr.options.masked (get @model attr.name))}}
|
{{#if (and attr.options.masked (get @model attr.name))}}
|
||||||
<MaskedInput @value={{get @model attr.name}} @displayOnly={{true}} @allowCopy={{true}} />
|
<MaskedInput @value={{get @model attr.name}} @displayOnly={{true}} @allowCopy={{true}} />
|
||||||
{{else if (eq attr.name "serialNumber")}}
|
{{else if (eq attr.name "serialNumber")}}
|
||||||
|
|||||||
@@ -12,5 +12,5 @@
|
|||||||
<PkiGenerateCsr
|
<PkiGenerateCsr
|
||||||
@model={{this.model}}
|
@model={{this.model}}
|
||||||
@onCancel={{transition-to "vault.cluster.secrets.backend.pki.issuers.index"}}
|
@onCancel={{transition-to "vault.cluster.secrets.backend.pki.issuers.index"}}
|
||||||
@onSave={{transition-to "vault.cluster.secrets.backend.pki.issuers.index"}}
|
@onComplete={{transition-to "vault.cluster.secrets.backend.pki.issuers.index"}}
|
||||||
/>
|
/>
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
}}
|
}}
|
||||||
@isEngine={{true}}
|
@isEngine={{true}}
|
||||||
/>
|
/>
|
||||||
{{#if this.model.hasConfig}}
|
{{#if (or this.model.hasConfig this.model.keyModels)}}
|
||||||
<Page::PkiKeyList
|
<Page::PkiKeyList
|
||||||
@keyModels={{this.model.keyModels}}
|
@keyModels={{this.model.keyModels}}
|
||||||
@mountPoint={{this.mountPoint}}
|
@mountPoint={{this.mountPoint}}
|
||||||
|
|||||||
@@ -136,10 +136,12 @@ module('Acceptance | pki workflow', function (hooks) {
|
|||||||
await fillIn(SELECTORS.configuration.typeField, 'exported');
|
await fillIn(SELECTORS.configuration.typeField, 'exported');
|
||||||
await fillIn(SELECTORS.configuration.inputByName('commonName'), 'my-common-name');
|
await fillIn(SELECTORS.configuration.inputByName('commonName'), 'my-common-name');
|
||||||
await click('[data-test-save]');
|
await click('[data-test-save]');
|
||||||
|
await assert.dom(SELECTORS.configuration.csrDetails).exists('renders CSR details after save');
|
||||||
|
await click('[data-test-done]');
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
currentURL(),
|
currentURL(),
|
||||||
`/vault/secrets/${this.mountPath}/pki/issuers`,
|
`/vault/secrets/${this.mountPath}/pki/issuers`,
|
||||||
'Transitions to issuers on save success'
|
'Transitions to issuers after viewing csr details'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,4 +10,6 @@ export const SELECTORS = {
|
|||||||
...GENERATE_ROOT,
|
...GENERATE_ROOT,
|
||||||
// pki-ca-cert-import
|
// pki-ca-cert-import
|
||||||
importForm: '[data-test-pki-ca-cert-import-form]',
|
importForm: '[data-test-pki-ca-cert-import-form]',
|
||||||
|
// generate-intermediate
|
||||||
|
csrDetails: '[data-test-generate-csr-result]',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ import { hbs } from 'ember-cli-htmlbars';
|
|||||||
import { setupEngine } from 'ember-engines/test-support';
|
import { setupEngine } from 'ember-engines/test-support';
|
||||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||||
|
|
||||||
module('Integration | Component | PkiGenerateCsr', function (hooks) {
|
module('Integration | Component | pki generate csr', function (hooks) {
|
||||||
setupRenderingTest(hooks);
|
setupRenderingTest(hooks);
|
||||||
setupEngine(hooks, 'pki');
|
setupEngine(hooks, 'pki');
|
||||||
setupMirage(hooks);
|
setupMirage(hooks);
|
||||||
|
|
||||||
hooks.beforeEach(async function () {
|
hooks.beforeEach(async function () {
|
||||||
this.owner.lookup('service:secretMountPath').update('pki-test');
|
this.owner.lookup('service:secretMountPath').update('pki-test');
|
||||||
|
this.store = this.owner.lookup('service:store');
|
||||||
this.model = this.owner
|
this.model = this.owner
|
||||||
.lookup('service:store')
|
.lookup('service:store')
|
||||||
.createRecord('pki/action', { actionType: 'generate-csr' });
|
.createRecord('pki/action', { actionType: 'generate-csr' });
|
||||||
@@ -32,9 +33,7 @@ module('Integration | Component | PkiGenerateCsr', function (hooks) {
|
|||||||
assert.strictEqual(payload.common_name, 'foo', 'Request made to correct endpoint on save');
|
assert.strictEqual(payload.common_name, 'foo', 'Request made to correct endpoint on save');
|
||||||
});
|
});
|
||||||
|
|
||||||
this.onSave = () => assert.ok(true, 'onSave action fires');
|
await render(hbs`<PkiGenerateCsr @model={{this.model}} @onComplete={{this.onComplete}} />`, {
|
||||||
|
|
||||||
await render(hbs`<PkiGenerateCsr @model={{this.model}} @onSave={{this.onSave}} />`, {
|
|
||||||
owner: this.engine,
|
owner: this.engine,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -55,6 +54,9 @@ module('Integration | Component | PkiGenerateCsr', function (hooks) {
|
|||||||
await fillIn('[data-test-input="type"]', 'exported');
|
await fillIn('[data-test-input="type"]', 'exported');
|
||||||
await fillIn('[data-test-input="commonName"]', 'foo');
|
await fillIn('[data-test-input="commonName"]', 'foo');
|
||||||
await click('[data-test-save]');
|
await click('[data-test-save]');
|
||||||
|
|
||||||
|
const savedRecord = this.store.peekAll('pki/action').firstObject;
|
||||||
|
assert.false(savedRecord.isNew, 'record is saved');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it should display validation errors', async function (assert) {
|
test('it should display validation errors', async function (assert) {
|
||||||
@@ -78,4 +80,65 @@ module('Integration | Component | PkiGenerateCsr', function (hooks) {
|
|||||||
|
|
||||||
await click('[data-test-cancel]');
|
await click('[data-test-cancel]');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('it should show generated CSR for type=exported', async function (assert) {
|
||||||
|
assert.expect(6);
|
||||||
|
this.model.id = '1235-someId';
|
||||||
|
this.model.csr = '-----BEGIN CERTIFICATE REQUEST-----...-----END CERTIFICATE REQUEST-----';
|
||||||
|
this.model.keyId = '9179de78-1275-a1cf-ebb0-a4eb2e376636';
|
||||||
|
this.model.privateKey = '-----BEGIN RSA PRIVATE KEY-----...-----END RSA PRIVATE KEY-----';
|
||||||
|
this.model.privateKeyType = 'rsa';
|
||||||
|
this.onComplete = () => assert.ok(true, 'onComplete action fires');
|
||||||
|
|
||||||
|
await render(hbs`<PkiGenerateCsr @model={{this.model}} @onComplete={{this.onComplete}} />`, {
|
||||||
|
owner: this.engine,
|
||||||
|
});
|
||||||
|
assert
|
||||||
|
.dom('[data-test-alert-banner="alert"]')
|
||||||
|
.hasText(
|
||||||
|
'Next steps Copy the CSR below for a parent issuer to sign and then import the signed certificate back into this mount. The private_key is only available once. Make sure you copy and save it now.',
|
||||||
|
'renders Next steps alert banner'
|
||||||
|
);
|
||||||
|
assert
|
||||||
|
.dom('[data-test-value-div="CSR"] [data-test-masked-input] button')
|
||||||
|
.hasAttribute('data-clipboard-text', this.model.csr, 'it renders copyable csr');
|
||||||
|
assert
|
||||||
|
.dom('[data-test-value-div="Key ID"] button')
|
||||||
|
.hasAttribute('data-clipboard-text', this.model.keyId, 'it renders copyable key_id');
|
||||||
|
assert
|
||||||
|
.dom('[data-test-value-div="Private key"] [data-test-masked-input] button')
|
||||||
|
.hasAttribute('data-clipboard-text', this.model.privateKey, 'it renders copyable private_key');
|
||||||
|
assert
|
||||||
|
.dom('[data-test-value-div="Private key type"]')
|
||||||
|
.hasText(this.model.privateKeyType, 'renders private_key_type');
|
||||||
|
await click('[data-test-done]');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it should show generated CSR for type=internal', async function (assert) {
|
||||||
|
assert.expect(5);
|
||||||
|
this.model.id = '1235-someId';
|
||||||
|
this.model.csr = '-----BEGIN CERTIFICATE REQUEST-----...-----END CERTIFICATE REQUEST-----';
|
||||||
|
this.model.keyId = '9179de78-1275-a1cf-ebb0-a4eb2e376636';
|
||||||
|
this.onComplete = () => {};
|
||||||
|
|
||||||
|
await render(hbs`<PkiGenerateCsr @model={{this.model}} @onComplete={{this.onComplete}} />`, {
|
||||||
|
owner: this.engine,
|
||||||
|
});
|
||||||
|
assert
|
||||||
|
.dom('[data-test-alert-banner="alert"]')
|
||||||
|
.hasText(
|
||||||
|
'Next steps Copy the CSR below for a parent issuer to sign and then import the signed certificate back into this mount.',
|
||||||
|
'renders Next steps alert banner'
|
||||||
|
);
|
||||||
|
assert
|
||||||
|
.dom('[data-test-value-div="CSR"] [data-test-masked-input] button')
|
||||||
|
.hasAttribute('data-clipboard-text', this.model.csr, 'it renders copyable csr');
|
||||||
|
assert
|
||||||
|
.dom('[data-test-value-div="Key ID"] button')
|
||||||
|
.hasAttribute('data-clipboard-text', this.model.keyId, 'it renders copyable key_id');
|
||||||
|
assert.dom('[data-test-value-div="Private key"]').hasText('internal', 'does not render private key');
|
||||||
|
assert
|
||||||
|
.dom('[data-test-value-div="Private key type"]')
|
||||||
|
.hasText('internal', 'does not render private key type');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user