From e873d27e83875653c15299ca05f4e1b366baa4bd Mon Sep 17 00:00:00 2001 From: Jordan Reimer Date: Tue, 24 Jan 2023 12:32:17 -0700 Subject: [PATCH] Pki Generate Intermediate CSR (#18807) * adds pki generate csr component * adds keyParamsByType helper to pki-generate-toggle-groups component * removes unused router service from pki-generate-csr component * updates common pki generate form fields * addresses feedback and adds tests --- ui/app/models/pki/action.js | 12 ++- ui/app/serializers/pki/action.js | 38 +++++--- ui/lib/core/addon/components/form-field.hbs | 2 +- .../addon/components/pki-configure-form.hbs | 22 +++-- .../addon/components/pki-configure-form.ts | 20 ---- .../pki/addon/components/pki-generate-csr.hbs | 28 ++++++ .../pki/addon/components/pki-generate-csr.ts | 76 +++++++++++++++ .../addon/components/pki-generate-root.hbs | 53 +--------- .../pki/addon/components/pki-generate-root.js | 37 ------- .../components/pki-generate-toggle-groups.hbs | 51 ++++++++++ .../components/pki-generate-toggle-groups.ts | 48 +++++++++ .../routes/issuers/generate-intermediate.js | 13 ++- .../issuers/generate-intermediate.hbs | 17 +++- .../pki/pki-engine-workflow-test.js | 15 +++ .../components/pki/pki-generate-csr-test.js | 81 ++++++++++++++++ .../pki/pki-generate-toggle-groups-test.js | 97 +++++++++++++++++++ ui/tsconfig.json | 2 + ui/types/vault/app-types.ts | 15 +++ ui/types/vault/models/capabilities.d.ts | 21 ++++ ui/types/vault/models/pki/action.d.ts | 11 ++- ui/types/vault/utils/field-to-attrs.d.ts | 6 ++ 21 files changed, 522 insertions(+), 143 deletions(-) create mode 100644 ui/lib/pki/addon/components/pki-generate-csr.hbs create mode 100644 ui/lib/pki/addon/components/pki-generate-csr.ts create mode 100644 ui/lib/pki/addon/components/pki-generate-toggle-groups.hbs create mode 100644 ui/lib/pki/addon/components/pki-generate-toggle-groups.ts create mode 100644 ui/tests/integration/components/pki/pki-generate-csr-test.js create mode 100644 ui/tests/integration/components/pki/pki-generate-toggle-groups-test.js create mode 100644 ui/types/vault/models/capabilities.d.ts create mode 100644 ui/types/vault/utils/field-to-attrs.d.ts diff --git a/ui/app/models/pki/action.js b/ui/app/models/pki/action.js index 07160545e..ddfefdb13 100644 --- a/ui/app/models/pki/action.js +++ b/ui/app/models/pki/action.js @@ -1,5 +1,6 @@ import Model, { attr } from '@ember-data/model'; import { inject as service } from '@ember/service'; +import { tracked } from '@glimmer/tracking'; import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities'; import { withFormFields } from 'vault/decorators/model-form-fields'; import { withModelValidations } from 'vault/decorators/model-validations'; @@ -10,7 +11,7 @@ const validations = { issuerName: [ { validator(model) { - if (model.issuerName === 'default') return false; + if (model.actionType === 'generate-root' && model.issuerName === 'default') return false; return true; }, message: 'Issuer name must be unique across all issuers and not be the reserved value default.', @@ -23,6 +24,8 @@ const validations = { export default class PkiActionModel extends Model { @service secretMountPath; + @tracked actionType; // used to toggle between different form fields when creating configuration + /* actionType import */ @attr('string') pemBundle; @@ -33,7 +36,7 @@ export default class PkiActionModel extends Model { }) type; - @attr('string') issuerName; // REQUIRED, cannot be "default" + @attr('string') issuerName; // REQUIRED for generate-root actionType, cannot be "default" @attr('string') keyName; // cannot be "default" @@ -123,6 +126,11 @@ export default class PkiActionModel extends Model { }) serialNumber; + @attr('boolean', { + subText: 'Whether to add a Basic Constraints extension with CA: true.', + }) + addBasicConstraints; + @attr({ label: 'Backdate validity', detailsLabel: 'Issued certificate backdating', diff --git a/ui/app/serializers/pki/action.js b/ui/app/serializers/pki/action.js index b4852fe9b..4978f43c2 100644 --- a/ui/app/serializers/pki/action.js +++ b/ui/app/serializers/pki/action.js @@ -25,35 +25,41 @@ export default class PkiActionSerializer extends ApplicationSerializer { _allowedParamsByType(actionType, type) { const keyFields = keyParamsByType(type).map((attrName) => underscore(attrName).toLowerCase()); + const commonProps = [ + 'alt_names', + 'common_name', + 'country', + 'exclude_cn_from_sans', + 'format', + 'ip_sans', + 'locality', + 'organization', + 'other_sans', + 'ou', + 'postal_code', + 'province', + 'serial_number', + 'street_address', + 'type', + 'uri_sans', + ...keyFields, + ]; switch (actionType) { case 'import': return ['pem_bundle']; case 'generate-root': return [ - 'alt_names', - 'common_name', - 'country', - 'exclude_cn_from_sans', - 'format', - 'ip_sans', + ...commonProps, 'issuer_name', - 'locality', 'max_path_length', 'not_after', 'not_before_duration', - 'organization', - 'other_sans', - 'ou', 'permitted_dns_domains', - 'postal_code', 'private_key_format', - 'province', - 'serial_number', - 'street_address', 'ttl', - 'type', - ...keyFields, ]; + case 'generate-csr': + return [...commonProps, 'add_basic_constraints']; default: // if type doesn't match, serialize all return null; diff --git a/ui/lib/core/addon/components/form-field.hbs b/ui/lib/core/addon/components/form-field.hbs index 3bdfe1494..50695e8f4 100644 --- a/ui/lib/core/addon/components/form-field.hbs +++ b/ui/lib/core/addon/components/form-field.hbs @@ -304,7 +304,7 @@ {{on "input" this.onChangeWithEvent}} {{on "change" this.onChangeWithEvent}} {{on "keyup" this.handleKeyUp}} - class="input {{if this.validationError 'has-error-border'}} has-bottom-margin-m" + class="input {{if this.validationError 'has-error-border' 'has-bottom-margin-m'}}" maxLength={{@attr.options.characterLimit}} /> {{#if @attr.options.validationAttr}} diff --git a/ui/lib/pki/addon/components/pki-configure-form.hbs b/ui/lib/pki/addon/components/pki-configure-form.hbs index 2ddfc8360..49bdaa49c 100644 --- a/ui/lib/pki/addon/components/pki-configure-form.hbs +++ b/ui/lib/pki/addon/components/pki-configure-form.hbs @@ -2,7 +2,7 @@
{{#each this.configTypes as |option|}}
-
- {{#if (eq this.actionType "import")}} + {{#if (eq @config.actionType "import")}} - {{else if (eq this.actionType "generate-root")}} + {{else if (eq @config.actionType "generate-root")}} + {{else if (eq @config.actionType "generate-csr")}} + - {{else if (eq this.actionType "generate-csr")}} - POST /intermediate/generate/:type ~or~ /issuers/generate/intermediate/:type {{else}} { @service declare readonly store: Store; @service declare readonly router: Router; @service declare readonly flashMessages: FlashMessageService; - @tracked actionType = ''; get configTypes() { return [ @@ -51,24 +49,6 @@ export default class PkiConfigureForm extends Component { ]; } - shouldUseIssuerEndpoint() { - const { config } = this.args; - // To determine which endpoint the config adapter should use, - // we want to check capabilities on the newer endpoints (those - // prefixed with "issuers") and use the old path as fallback - // if user does not have permissions. - switch (this.actionType) { - case 'import': - return config.canImportBundle; - case 'generate-root': - return config.canGenerateIssuerRoot; - case 'generate-csr': - return config.canGenerateIssuerIntermediate; - default: - return false; - } - } - @action cancel() { this.args.config.rollbackAttributes(); this.router.transitionTo('vault.cluster.secrets.backend.pki.overview'); diff --git a/ui/lib/pki/addon/components/pki-generate-csr.hbs b/ui/lib/pki/addon/components/pki-generate-csr.hbs new file mode 100644 index 000000000..b9af2e6fe --- /dev/null +++ b/ui/lib/pki/addon/components/pki-generate-csr.hbs @@ -0,0 +1,28 @@ +
+ + + + {{#each this.formFields as |field|}} + + {{/each}} + + + +
+
+ + +
+ {{#if this.alert}} +
+ +
+ {{/if}} +
+ \ No newline at end of file diff --git a/ui/lib/pki/addon/components/pki-generate-csr.ts b/ui/lib/pki/addon/components/pki-generate-csr.ts new file mode 100644 index 000000000..736265f0b --- /dev/null +++ b/ui/lib/pki/addon/components/pki-generate-csr.ts @@ -0,0 +1,76 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import { task } from 'ember-concurrency'; +import { waitFor } from '@ember/test-waiters'; +import { expandAttributeMeta } from 'vault/utils/field-to-attrs'; +import FlashMessageService from 'vault/services/flash-messages'; +import PkiActionModel from 'vault/models/pki/action'; +import errorMessage from 'vault/utils/error-message'; + +interface Args { + model: PkiActionModel; + useIssuer: boolean; + onSave: CallableFunction; + onCancel: CallableFunction; +} + +export default class PkiGenerateIntermediateComponent extends Component { + @service declare readonly flashMessages: FlashMessageService; + + @tracked modelValidations = null; + @tracked error: string | null = null; + @tracked alert: string | null = null; + + formFields; + + constructor(owner: unknown, args: Args) { + super(owner, args); + this.formFields = expandAttributeMeta(this.args.model, [ + 'type', + 'commonName', + 'excludeCnFromSans', + 'format', + 'serialNumber', + 'addBasicConstraints', + ]); + } + + @action + cancel() { + this.args.model.unloadRecord(); + this.args.onCancel(); + } + + async getCapability(): Promise { + try { + const issuerCapabilities = await this.args.model.generateIssuerCsrPath; + return issuerCapabilities.get('canCreate') === true; + } catch (error) { + return false; + } + } + + @task + @waitFor + *save(event: Event): Generator> { + event.preventDefault(); + try { + const { model, onSave } = 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; + } + } catch (e) { + this.error = errorMessage(e); + this.alert = 'There was a problem generating the CSR.'; + } + } +} diff --git a/ui/lib/pki/addon/components/pki-generate-root.hbs b/ui/lib/pki/addon/components/pki-generate-root.hbs index e69204809..0dc6f7395 100644 --- a/ui/lib/pki/addon/components/pki-generate-root.hbs +++ b/ui/lib/pki/addon/components/pki-generate-root.hbs @@ -20,58 +20,7 @@ {{/let}} {{/each}} - {{! togglable groups }} - {{#each-in this.groups as |group fields|}} - - {{#if (eq this.showGroup group)}} -
- {{#if (eq group "Key parameters")}} -

- {{#if (eq @model.type "internal")}} - This certificate type is internal. This means that the private key will not be returned and cannot be retrieved - later. Below, you will name the key and define its type and key bits. - {{else if (eq @model.type "kms")}} - This certificate type is kms, meaning managed keys will be used. Below, you will name the key and tell Vault - where to find it in your KMS or HSM. - Learn more about managed keys. - {{else if (eq @model.type "exported")}} - This certificate type is exported. This means the private key will be returned in the response. Below, you will - name the key and define its type and key bits. - {{else if (eq @model.type "existing")}} - You chose to use an existing key. This means that we’ll use the key reference to create the CSR or root. Please - provide the reference to the key. - {{else}} - Please choose a type to see key parameter options. - {{/if}} -

- {{#if this.keyParamFields}} - - {{/if}} - {{else}} -

- {{#if (eq group "Subject Alternative Name (SAN) Options")}} - SAN fields are an extension that allow you specify additional host names (sites, IP addresses, common names, - etc.) to be protected by a single certificate. - {{else if (eq group "Additional subject fields")}} - These fields provide more information about the client to which the certificate belongs. - {{/if}} -

- {{#each fields as |fieldName|}} - {{#let (find-by "name" fieldName @model.allFields) as |attr|}} - - {{/let}} - {{/each}} - {{/if}} -
- {{/if}} - {{/each-in}} + {{#if @urls}}
diff --git a/ui/lib/pki/addon/components/pki-generate-root.js b/ui/lib/pki/addon/components/pki-generate-root.js index 28d8d2dd1..fdb15e217 100644 --- a/ui/lib/pki/addon/components/pki-generate-root.js +++ b/ui/lib/pki/addon/components/pki-generate-root.js @@ -4,7 +4,6 @@ import { waitFor } from '@ember/test-waiters'; import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; import { task } from 'ember-concurrency'; -import { keyParamsByType } from 'pki/utils/action-params'; import errorMessage from 'vault/utils/error-message'; /** @@ -32,11 +31,6 @@ export default class PkiGenerateRootComponent extends Component { @tracked errorBanner = ''; @tracked invalidFormAlert = ''; - @action - toggleGroup(group, isOpen) { - this.showGroup = isOpen ? group : null; - } - get defaultFields() { return [ 'type', @@ -49,14 +43,6 @@ export default class PkiGenerateRootComponent extends Component { 'maxPathLength', ]; } - get keyParamFields() { - const { type } = this.args.model; - if (!type) return null; - const fields = keyParamsByType(type); - return fields.map((fieldName) => { - return this.args.model.allFields.find((attr) => attr.name === fieldName); - }); - } @action cancel() { // Generate root form will always have a new model @@ -64,29 +50,6 @@ export default class PkiGenerateRootComponent extends Component { this.args.onCancel(); } - get groups() { - return { - 'Key parameters': this.keyParamFields, - 'Subject Alternative Name (SAN) Options': [ - 'excludeCnFromSans', - 'serialNumber', - 'altNames', - 'ipSans', - 'uriSans', - 'otherSans', - ], - 'Additional subject fields': [ - 'ou', - 'organization', - 'country', - 'locality', - 'province', - 'streetAddress', - 'postalCode', - ], - }; - } - @action checkFormValidity() { if (this.args.model.validate) { diff --git a/ui/lib/pki/addon/components/pki-generate-toggle-groups.hbs b/ui/lib/pki/addon/components/pki-generate-toggle-groups.hbs new file mode 100644 index 000000000..72e8e0744 --- /dev/null +++ b/ui/lib/pki/addon/components/pki-generate-toggle-groups.hbs @@ -0,0 +1,51 @@ +{{#each-in this.groups as |group fields|}} + + {{#if (eq this.showGroup group)}} +
+ {{#if (eq group "Key parameters")}} +

+ {{#if (eq @model.type "internal")}} + This certificate type is internal. This means that the private key will not be returned and cannot be retrieved + later. Below, you will name the key and define its type and key bits. + {{else if (eq @model.type "kms")}} + This certificate type is kms, meaning managed keys will be used. Below, you will name the key and tell Vault + where to find it in your KMS or HSM. + Learn more about managed keys. + {{else if (eq @model.type "exported")}} + This certificate type is exported. This means the private key will be returned in the response. Below, you will + name the key and define its type and key bits. + {{else if (eq @model.type "existing")}} + You chose to use an existing key. This means that we’ll use the key reference to create the CSR or root. Please + provide the reference to the key. + {{else}} + Please choose a type to see key parameter options. + {{/if}} +

+ {{#if this.keyParamFields}} + + {{/if}} + {{else}} +

+ {{#if (eq group "Subject Alternative Name (SAN) Options")}} + SAN fields are an extension that allow you specify additional host names (sites, IP addresses, common names, + etc.) to be protected by a single certificate. + {{else if (eq group "Additional subject fields")}} + These fields provide more information about the client to which the certificate belongs. + {{/if}} +

+ {{#each fields as |fieldName|}} + {{#let (find-by "name" fieldName @model.allFields) as |attr|}} + + {{/let}} + {{/each}} + {{/if}} +
+ {{/if}} +{{/each-in}} \ No newline at end of file diff --git a/ui/lib/pki/addon/components/pki-generate-toggle-groups.ts b/ui/lib/pki/addon/components/pki-generate-toggle-groups.ts new file mode 100644 index 000000000..e9d3b5c4c --- /dev/null +++ b/ui/lib/pki/addon/components/pki-generate-toggle-groups.ts @@ -0,0 +1,48 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import { keyParamsByType } from 'pki/utils/action-params'; +import PkiActionModel from 'vault/models/pki/action'; + +interface Args { + model: PkiActionModel; +} + +export default class PkiGenerateToggleGroupsComponent extends Component { + @tracked showGroup: string | null = null; + + get keyParamFields() { + const { type } = this.args.model; + if (!type) return null; + const fields = keyParamsByType(type); + return fields.map((fieldName) => { + return this.args.model.allFields.find((attr) => attr.name === fieldName); + }); + } + + get groups() { + const groups = { + 'Key parameters': this.keyParamFields, + 'Subject Alternative Name (SAN) Options': ['altNames', 'ipSans', 'uriSans', 'otherSans'], + 'Additional subject fields': [ + 'ou', + 'organization', + 'country', + 'locality', + 'province', + 'streetAddress', + 'postalCode', + ], + }; + // excludeCnFromSans and serialNumber are present in default fields for generate-csr -- only include for other types + if (this.args.model.actionType !== 'generate-csr') { + groups['Subject Alternative Name (SAN) Options'].unshift('excludeCnFromSans', 'serialNumber'); + } + return groups; + } + + @action + toggleGroup(group: string, isOpen: boolean) { + this.showGroup = isOpen ? group : null; + } +} diff --git a/ui/lib/pki/addon/routes/issuers/generate-intermediate.js b/ui/lib/pki/addon/routes/issuers/generate-intermediate.js index 28b45943e..f139af8a7 100644 --- a/ui/lib/pki/addon/routes/issuers/generate-intermediate.js +++ b/ui/lib/pki/addon/routes/issuers/generate-intermediate.js @@ -1,3 +1,12 @@ -import Route from '@ember/routing/route'; +import PkiIssuersIndexRoute from '.'; -export default class PkiIssuersGenerateIntermediateRoute extends Route {} +export default class PkiIssuersGenerateIntermediateRoute extends PkiIssuersIndexRoute { + model() { + return this.store.createRecord('pki/action', { actionType: 'generate-csr' }); + } + + setupController(controller, resolvedModel) { + super.setupController(controller, resolvedModel); + controller.breadcrumbs.push({ label: 'generate CSR' }); + } +} diff --git a/ui/lib/pki/addon/templates/issuers/generate-intermediate.hbs b/ui/lib/pki/addon/templates/issuers/generate-intermediate.hbs index 9f8e9fd6c..3a497c4c3 100644 --- a/ui/lib/pki/addon/templates/issuers/generate-intermediate.hbs +++ b/ui/lib/pki/addon/templates/issuers/generate-intermediate.hbs @@ -1 +1,16 @@ -route: issuers.generate-intermediate \ No newline at end of file + + + + + +

+ Generate intermediate CSR +

+
+
+ + \ No newline at end of file diff --git a/ui/tests/acceptance/pki/pki-engine-workflow-test.js b/ui/tests/acceptance/pki/pki-engine-workflow-test.js index c0f1656b8..b90eae5ee 100644 --- a/ui/tests/acceptance/pki/pki-engine-workflow-test.js +++ b/ui/tests/acceptance/pki/pki-engine-workflow-test.js @@ -127,6 +127,21 @@ module('Acceptance | pki workflow', function (hooks) { assert.dom(SELECTORS.issuerDetails.valueByName('Common name')).hasText('my-common-name'); assert.dom(SELECTORS.issuerDetails.valueByName('Issuer name')).hasText('my-first-issuer'); }); + + test('it should generate intermediate csr', async function (assert) { + await authPage.login(this.pkiAdminToken); + await visit(`/vault/secrets/${this.mountPath}/pki/overview`); + await click(SELECTORS.emptyStateLink); + await click(SELECTORS.configuration.optionByKey('generate-csr')); + await fillIn(SELECTORS.configuration.typeField, 'exported'); + await fillIn(SELECTORS.configuration.inputByName('commonName'), 'my-common-name'); + await click('[data-test-save]'); + assert.strictEqual( + currentURL(), + `/vault/secrets/${this.mountPath}/pki/issuers`, + 'Transitions to issuers on save success' + ); + }); }); module('roles', function (hooks) { diff --git a/ui/tests/integration/components/pki/pki-generate-csr-test.js b/ui/tests/integration/components/pki/pki-generate-csr-test.js new file mode 100644 index 000000000..f552e55b5 --- /dev/null +++ b/ui/tests/integration/components/pki/pki-generate-csr-test.js @@ -0,0 +1,81 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; +import { click, fillIn, render } from '@ember/test-helpers'; +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) { + setupRenderingTest(hooks); + setupEngine(hooks, 'pki'); + setupMirage(hooks); + + hooks.beforeEach(async function () { + this.owner.lookup('service:secretMountPath').update('pki-test'); + this.model = this.owner + .lookup('service:store') + .createRecord('pki/action', { actionType: 'generate-csr' }); + + this.server.post('/sys/capabilities-self', () => ({ + data: { + capabilities: ['root'], + 'pki-test/issuers/generate/intermediate/exported': ['root'], + }, + })); + }); + + test('it should render fields and save', async function (assert) { + assert.expect(9); + + this.server.post('/pki-test/issuers/generate/intermediate/exported', (schema, req) => { + const payload = JSON.parse(req.requestBody); + assert.strictEqual(payload.common_name, 'foo', 'Request made to correct endpoint on save'); + }); + + this.onSave = () => assert.ok(true, 'onSave action fires'); + + await render(hbs``, { + owner: this.engine, + }); + + const fields = [ + 'type', + 'commonName', + 'excludeCnFromSans', + 'format', + 'serialNumber', + 'addBasicConstraints', + ]; + fields.forEach((key) => { + assert.dom(`[data-test-input="${key}"]`).exists(`${key} form field renders`); + }); + + assert.dom('[data-test-toggle-group]').exists({ count: 3 }, 'Toggle groups render'); + + await fillIn('[data-test-input="type"]', 'exported'); + await fillIn('[data-test-input="commonName"]', 'foo'); + await click('[data-test-save]'); + }); + + test('it should display validation errors', async function (assert) { + assert.expect(4); + + this.onCancel = () => assert.ok(true, 'onCancel action fires'); + + await render(hbs``, { + owner: this.engine, + }); + + await click('[data-test-save]'); + + assert + .dom('[data-test-field-validation="type"]') + .hasText('Type is required.', 'Type validation error renders'); + assert + .dom('[data-test-field="commonName"] [data-test-inline-alert]') + .hasText('Common name is required.', 'Common name validation error renders'); + assert.dom('[data-test-alert]').hasText('There are 2 errors with this form.', 'Alert renders'); + + await click('[data-test-cancel]'); + }); +}); diff --git a/ui/tests/integration/components/pki/pki-generate-toggle-groups-test.js b/ui/tests/integration/components/pki/pki-generate-toggle-groups-test.js new file mode 100644 index 000000000..7e603f6b7 --- /dev/null +++ b/ui/tests/integration/components/pki/pki-generate-toggle-groups-test.js @@ -0,0 +1,97 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; +import { click, render, settled } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { setupEngine } from 'ember-engines/test-support'; + +const selectors = { + keys: '[data-test-toggle-group="Key parameters"]', + sanOptions: '[data-test-toggle-group="Subject Alternative Name (SAN) Options"]', + subjectFields: '[data-test-toggle-group="Additional subject fields"]', +}; + +module('Integration | Component | PkiGenerateToggleGroups', function (hooks) { + setupRenderingTest(hooks); + setupEngine(hooks, 'pki'); + + hooks.beforeEach(async function () { + this.model = this.owner + .lookup('service:store') + .createRecord('pki/action', { actionType: 'generate-root' }); + }); + + test('it should render key parameters', async function (assert) { + await render(hbs``, { owner: this.engine }); + + assert.dom(selectors.keys).hasText('Key parameters', 'Key parameters group renders'); + + await click(selectors.keys); + + assert + .dom('[data-test-toggle-group-description]') + .hasText( + 'Please choose a type to see key parameter options.', + 'Placeholder renders for key params when type is not selected' + ); + const fields = { + exported: ['keyName', 'keyType', 'keyBits'], + internal: ['keyName', 'keyType', 'keyBits'], + existing: ['keyRef'], + kms: ['keyName', 'managedKeyName', 'managedKeyId'], + }; + for (const type in fields) { + this.model.type = type; + await settled(); + assert + .dom('[data-test-field]') + .exists({ count: fields[type].length }, `Correct number of fields render for ${type} type`); + fields[type].forEach((key) => { + assert.dom(`[data-test-input="${key}"]`).exists(`${key} input renders for ${type} type`); + }); + } + }); + + test('it should render SAN options', async function (assert) { + await render(hbs``, { owner: this.engine }); + + assert + .dom(selectors.sanOptions) + .hasText('Subject Alternative Name (SAN) Options', 'SAN options group renders'); + + await click(selectors.sanOptions); + + const fields = ['excludeCnFromSans', 'serialNumber', 'altNames', 'ipSans', 'uriSans', 'otherSans']; + assert.dom('[data-test-field]').exists({ count: 6 }, `Correct number of fields render`); + fields.forEach((key) => { + assert.dom(`[data-test-input="${key}"]`).exists(`${key} input renders for generate-root actionType`); + }); + + this.model.actionType = 'generate-csr'; + await settled(); + + assert + .dom('[data-test-field]') + .exists({ count: 4 }, 'Correct number of fields render for generate-csr actionType'); + + assert + .dom('[data-test-input="excludeCnFromSans"]') + .doesNotExist('excludeCnFromSans field hidden for generate-csr actionType'); + assert + .dom('[data-test-input="serialNumber"]') + .doesNotExist('serialNumber field hidden for generate-csr actionType'); + }); + + test('it should render additional subject fields', async function (assert) { + await render(hbs``, { owner: this.engine }); + + assert.dom(selectors.subjectFields).hasText('Additional subject fields', 'SAN options group renders'); + + await click(selectors.subjectFields); + + const fields = ['ou', 'organization', 'country', 'locality', 'province', 'streetAddress', 'postalCode']; + assert.dom('[data-test-field]').exists({ count: fields.length }, 'Correct number of fields render'); + fields.forEach((key) => { + assert.dom(`[data-test-input="${key}"]`).exists(`${key} input renders`); + }); + }); +}); diff --git a/ui/tsconfig.json b/ui/tsconfig.json index e85cfbda6..0aed4c324 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -20,6 +20,8 @@ "vault/tests/*": ["tests/*"], "vault/mirage/*": ["mirage/*"], "vault/*": [ + "types/*", + "types/vault/*", "app/*", "lib/core/app/*", "lib/css/app/*", diff --git a/ui/types/vault/app-types.ts b/ui/types/vault/app-types.ts index f221d14e1..5ac6cf147 100644 --- a/ui/types/vault/app-types.ts +++ b/ui/types/vault/app-types.ts @@ -8,3 +8,18 @@ export interface FormField { export interface FormFieldGroups { [key: string]: Array; } + +export interface FormFieldGroupOptions { + [key: string]: Array; +} + +export interface ModelValidation { + isValid: boolean; + state: { + [key: string]: { + isValid: boolean; + errors: Array; + }; + }; + invalidFormMessage: string; +} diff --git a/ui/types/vault/models/capabilities.d.ts b/ui/types/vault/models/capabilities.d.ts new file mode 100644 index 000000000..9bbf14a1a --- /dev/null +++ b/ui/types/vault/models/capabilities.d.ts @@ -0,0 +1,21 @@ +import ComputedProperty from '@ember/object/computed'; +import Model from '@ember-data/model'; + +interface CapabilitiesModel extends Model { + path: string; + capabilities: Array; + canSudo: ComputedProperty; + canRead: ComputedProperty; + canCreate: ComputedProperty; + canUpdate: ComputedProperty; + canDelete: ComputedProperty; + canList: ComputedProperty; + // these don't seem to be used anywhere + // inferring type from key name + allowedParameters: Array; + deniedParameters: Array; +} + +export default CapabilitiesModel; +export const SUDO_PATHS: string[]; +export const SUDO_PATH_PREFIXES: string[]; diff --git a/ui/types/vault/models/pki/action.d.ts b/ui/types/vault/models/pki/action.d.ts index a732518bf..e630e7fb1 100644 --- a/ui/types/vault/models/pki/action.d.ts +++ b/ui/types/vault/models/pki/action.d.ts @@ -1,15 +1,20 @@ import Model from '@ember-data/model'; +import { FormField, ModelValidations } from 'vault/app-types'; +import CapabilitiesModel from '../capabilities'; export default class PkiActionModel extends Model { secretMountPath: unknown; pemBundle: string; type: string; + actionType: string | null; get backend(): string; // apiPaths for capabilities - importBundlePath: string; - generateIssuerRootPath: string; - generateIssuerCsrPath: string; + importBundlePath: Promise; + generateIssuerRootPath: Promise; + generateIssuerCsrPath: Promise; crossSignPath: string; + allFields: Array; + validate(): ModelValidations; // Capabilities get canImportBundle(): boolean; get canGenerateIssuerRoot(): boolean; diff --git a/ui/types/vault/utils/field-to-attrs.d.ts b/ui/types/vault/utils/field-to-attrs.d.ts new file mode 100644 index 000000000..1021244cd --- /dev/null +++ b/ui/types/vault/utils/field-to-attrs.d.ts @@ -0,0 +1,6 @@ +import Model from '@ember-data/model'; +import { FormField, FormFieldGroups, FormFieldGroupOptions } from 'vault/app-types'; + +export default function _default(modelClass: Model, fieldGroups: FormFieldGroupOptions): FormFieldGroups; + +export function expandAttributeMeta(modelClass: Model, attributeNames: Array): Array;