mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-01 19:17:58 +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
|
||||
|
||||
// For generating and signing a CSR
|
||||
@attr('string') csr;
|
||||
@attr('string', { label: 'CSR', masked: true }) csr;
|
||||
@attr caChain;
|
||||
@attr('string', { label: 'Key ID' }) keyId;
|
||||
@attr('string', { masked: true }) privateKey;
|
||||
@attr('string') privateKeyType;
|
||||
|
||||
get backend() {
|
||||
return this.secretMountPath.currentPath;
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
<InfoTableRow
|
||||
@label={{capitalize (or attr.options.detailsLabel attr.options.label (humanize (dasherize attr.name)))}}
|
||||
@value={{get @key attr.name}}
|
||||
@addCopyButton={{eq attr.name "keyId"}}
|
||||
/>
|
||||
{{/each}}
|
||||
{{#if @key.privateKey}}
|
||||
|
||||
@@ -44,8 +44,8 @@
|
||||
{{else if (eq @config.actionType "generate-csr")}}
|
||||
<PkiGenerateCsr
|
||||
@model={{@config}}
|
||||
@onSave={{transition-to "vault.cluster.secrets.backend.pki.issuers"}}
|
||||
@onCancel={{@onCancel}}
|
||||
@onComplete={{transition-to "vault.cluster.secrets.backend.pki.issuers"}}
|
||||
/>
|
||||
{{else}}
|
||||
<EmptyState
|
||||
|
||||
@@ -1,28 +1,76 @@
|
||||
<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>
|
||||
Done
|
||||
</button>
|
||||
<button {{on "click" this.cancel}} type="button" class="button has-left-margin-s" data-test-cancel>
|
||||
Cancel
|
||||
</button>
|
||||
{{#if @model.id}}
|
||||
{{! Model only has ID once form has been submitted and saved }}
|
||||
<Toolbar />
|
||||
<main data-test-generate-csr-result>
|
||||
<div class="box is-sideless is-fullwidth is-shadowless">
|
||||
<AlertBanner @title="Next steps" @type="warning">
|
||||
Copy the CSR below for a parent issuer to sign and then import the signed certificate back into this mount.
|
||||
{{#if @model.privateKey}}
|
||||
The
|
||||
<code>private_key</code>
|
||||
is only available once. Make sure you copy and save it now.
|
||||
{{/if}}
|
||||
</AlertBanner>
|
||||
{{#each this.showFields as |fieldName|}}
|
||||
{{#let (find-by "name" fieldName @model.allFields) as |attr|}}
|
||||
{{#let (get @model attr.name) as |value|}}
|
||||
<InfoTableRow
|
||||
@label={{or attr.options.label (humanize (dasherize attr.name))}}
|
||||
@value={{value}}
|
||||
@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>
|
||||
{{#if this.alert}}
|
||||
</main>
|
||||
<footer>
|
||||
<div class="field is-grouped is-fullwidth has-top-margin-l">
|
||||
<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>
|
||||
{{/if}}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</footer>
|
||||
{{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 {
|
||||
model: PkiActionModel;
|
||||
useIssuer: boolean;
|
||||
onSave: CallableFunction;
|
||||
onComplete: CallableFunction;
|
||||
onCancel: CallableFunction;
|
||||
}
|
||||
|
||||
export default class PkiGenerateIntermediateComponent extends Component<Args> {
|
||||
export default class PkiGenerateCsrComponent extends Component<Args> {
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
|
||||
@tracked modelValidations = null;
|
||||
@@ -24,6 +24,8 @@ export default class PkiGenerateIntermediateComponent extends Component<Args> {
|
||||
@tracked alert: string | null = null;
|
||||
|
||||
formFields;
|
||||
// fields rendered after CSR generation
|
||||
showFields = ['csr', 'keyId', 'privateKey', 'privateKeyType'];
|
||||
|
||||
constructor(owner: unknown, args: Args) {
|
||||
super(owner, args);
|
||||
@@ -57,13 +59,12 @@ export default class PkiGenerateIntermediateComponent extends Component<Args> {
|
||||
*save(event: Event): Generator<Promise<boolean | PkiActionModel>> {
|
||||
event.preventDefault();
|
||||
try {
|
||||
const { model, onSave } = this.args;
|
||||
const { model } = this.args;
|
||||
const { isValid, state, invalidFormMessage } = model.validate();
|
||||
if (isValid) {
|
||||
const useIssuer = yield this.getCapability();
|
||||
yield model.save({ adapterOptions: { actionType: 'generate-csr', useIssuer } });
|
||||
this.flashMessages.success('Successfully generated CSR.');
|
||||
onSave();
|
||||
} else {
|
||||
this.modelValidations = state;
|
||||
this.alert = invalidFormMessage;
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
{{#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}}>
|
||||
<InfoTableRow @label={{or attr.options.label (humanize (dasherize attr.name))}} @value={{get @model 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")}}
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
<PkiGenerateCsr
|
||||
@model={{this.model}}
|
||||
@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}}
|
||||
/>
|
||||
{{#if this.model.hasConfig}}
|
||||
{{#if (or this.model.hasConfig this.model.keyModels)}}
|
||||
<Page::PkiKeyList
|
||||
@keyModels={{this.model.keyModels}}
|
||||
@mountPoint={{this.mountPoint}}
|
||||
|
||||
@@ -136,10 +136,12 @@ module('Acceptance | pki workflow', function (hooks) {
|
||||
await fillIn(SELECTORS.configuration.typeField, 'exported');
|
||||
await fillIn(SELECTORS.configuration.inputByName('commonName'), 'my-common-name');
|
||||
await click('[data-test-save]');
|
||||
await assert.dom(SELECTORS.configuration.csrDetails).exists('renders CSR details after save');
|
||||
await click('[data-test-done]');
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
`/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,
|
||||
// pki-ca-cert-import
|
||||
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 { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
|
||||
module('Integration | Component | PkiGenerateCsr', function (hooks) {
|
||||
module('Integration | Component | pki generate csr', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupEngine(hooks, 'pki');
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(async function () {
|
||||
this.owner.lookup('service:secretMountPath').update('pki-test');
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.model = this.owner
|
||||
.lookup('service:store')
|
||||
.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');
|
||||
});
|
||||
|
||||
this.onSave = () => assert.ok(true, 'onSave action fires');
|
||||
|
||||
await render(hbs`<PkiGenerateCsr @model={{this.model}} @onSave={{this.onSave}} />`, {
|
||||
await render(hbs`<PkiGenerateCsr @model={{this.model}} @onComplete={{this.onComplete}} />`, {
|
||||
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="commonName"]', 'foo');
|
||||
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) {
|
||||
@@ -78,4 +80,65 @@ module('Integration | Component | PkiGenerateCsr', function (hooks) {
|
||||
|
||||
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