mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-31 18:48:08 +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
	 claire bontempo
					claire bontempo