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:
claire bontempo
2023-02-10 10:05:57 -08:00
committed by GitHub
parent d9c8a8f629
commit 96889735f1
15 changed files with 159 additions and 39 deletions

View File

@@ -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;

View File

@@ -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}}

View File

@@ -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

View File

@@ -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}}

View File

@@ -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;

View File

@@ -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")}}

View File

@@ -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"}}
/> />

View File

@@ -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}}

View File

@@ -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'
); );
}); });
}); });

View File

@@ -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]',
}; };

View File

@@ -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');
});
}); });