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