mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-11-04 04:28: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
 | 
			
		||||
 | 
			
		||||
  // 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,3 +1,50 @@
 | 
			
		||||
{{#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>
 | 
			
		||||
  </main>
 | 
			
		||||
  <footer>
 | 
			
		||||
    <div class="field is-grouped is-fullwidth has-top-margin-l">
 | 
			
		||||
      <div class="control">
 | 
			
		||||
        <button type="button" class="button is-primary" {{on "click" @onComplete}} data-test-done>
 | 
			
		||||
          Done
 | 
			
		||||
        </button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </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">
 | 
			
		||||
@@ -13,7 +60,7 @@
 | 
			
		||||
    <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
 | 
			
		||||
          Generate
 | 
			
		||||
        </button>
 | 
			
		||||
        <button {{on "click" this.cancel}} type="button" class="button has-left-margin-s" data-test-cancel>
 | 
			
		||||
          Cancel
 | 
			
		||||
@@ -26,3 +73,4 @@
 | 
			
		||||
      {{/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