diff --git a/ui/app/forms/secrets/pki/key.ts b/ui/app/forms/secrets/pki/key.ts new file mode 100644 index 0000000000..6d58d68648 --- /dev/null +++ b/ui/app/forms/secrets/pki/key.ts @@ -0,0 +1,72 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import Form from 'vault/forms/form'; +import FormField from 'vault/utils/forms/field'; +import FormFieldGroup from 'vault/utils/forms/field-group'; + +import type { Validations } from 'vault/vault/app-types'; + +export type PkiKeyFormData = { + key_name?: string; + key_id?: string; + type: 'internal' | 'exported'; + key_type: 'rsa' | 'ec' | 'ed25519'; + key_bits: number; +}; + +export default class PkiKeyForm extends Form { + validations: Validations = { + type: [{ type: 'presence', message: 'Type is required.' }], + key_type: [{ type: 'presence', message: 'Please select a key type.' }], + key_name: [ + { + type: 'isNot', + options: { value: 'default' }, + message: `Key name cannot be the reserved value 'default'`, + }, + ], + }; + + keyNameField = new FormField('key_name', 'string', { + subText: `Optional, human-readable name for this key. The name must be unique across all keys and cannot be 'default'.`, + }); + keyTypeField = new FormField('key_type', 'string', { + noDefault: true, + possibleValues: ['rsa', 'ec', 'ed25519'], + subText: 'The type of key that will be generated. Must be rsa, ed25519, or ec. ', + }); + + formFields = [this.keyNameField, this.keyTypeField]; + + formFieldGroups = [ + new FormFieldGroup('default', [ + this.keyNameField, + new FormField('type', 'string', { + noDefault: true, + possibleValues: ['internal', 'exported'], + subText: + 'The type of operation. If exported, the private key will be returned in the response; if internal the private key will not be returned and cannot be retrieved later.', + }), + ]), + new FormFieldGroup('Key parameters', [ + this.keyTypeField, + new FormField('key_bits', 'number', { + label: 'Key bits', + noDefault: true, + subText: 'Bit length of the key to generate.', + }), + ]), + ]; + + toJSON() { + const formState = super.toJSON(); + if (!this.isNew) { + // the only editable property is key_name which is optional so the form will always be valid + return { ...formState, isValid: true, state: {}, invalidFormMessage: '' }; + } + return formState; + } +} diff --git a/ui/lib/pki/addon/components/page/pki-key-details.hbs b/ui/lib/pki/addon/components/page/pki-key-details.hbs index 1b88df0d82..f0ad4a7623 100644 --- a/ui/lib/pki/addon/components/page/pki-key-details.hbs +++ b/ui/lib/pki/addon/components/page/pki-key-details.hbs @@ -16,18 +16,18 @@ />
{{/if}} - {{#if @key.privateKey}} + {{#if @key.private_key}} {{/if}} {{#if @canEdit}} - + Edit key {{/if}} @@ -35,7 +35,7 @@
- {{#if @key.privateKey}} + {{#if @key.private_key}}
Next steps @@ -43,16 +43,16 @@
{{/if}} - {{#each @key.formFields as |attr|}} + {{#each this.displayFields as |field|}} {{/each}} - {{#if @key.privateKey}} + {{#if @key.private_key}} - + {{/if}}
\ No newline at end of file diff --git a/ui/lib/pki/addon/components/page/pki-key-details.ts b/ui/lib/pki/addon/components/page/pki-key-details.ts index fe2bf24c74..1084393479 100644 --- a/ui/lib/pki/addon/components/page/pki-key-details.ts +++ b/ui/lib/pki/addon/components/page/pki-key-details.ts @@ -6,26 +6,38 @@ import Component from '@glimmer/component'; import { action } from '@ember/object'; import { service } from '@ember/service'; -import errorMessage from 'vault/utils/error-message'; + import type RouterService from '@ember/routing/router-service'; import type FlashMessageService from 'vault/services/flash-messages'; -import type PkiKeyModel from 'vault/models/pki/key'; +import type { PkiReadKeyResponse } from '@hashicorp/vault-client-typescript'; +import type ApiService from 'vault/services/api'; +import type SecretMountPath from 'vault/services/secret-mount-path'; + interface Args { - key: PkiKeyModel; + key: PkiReadKeyResponse; } export default class PkiKeyDetails extends Component { @service('app-router') declare readonly router: RouterService; @service declare readonly flashMessages: FlashMessageService; + @service declare readonly api: ApiService; + @service declare readonly secretMountPath: SecretMountPath; + + displayFields = ['key_id', 'key_name', 'key_type', 'key_bits']; + + get backend() { + return this.secretMountPath.currentPath; + } @action async deleteKey() { try { - await this.args.key.destroyRecord(); + await this.api.secrets.pkiDeleteKey(this.args.key.key_id as string, this.backend); this.flashMessages.success('Key deleted successfully.'); this.router.transitionTo('vault.cluster.secrets.backend.pki.keys.index'); } catch (error) { - this.flashMessages.danger(errorMessage(error)); + const { message } = await this.api.parseError(error); + this.flashMessages.danger(message); } } } diff --git a/ui/lib/pki/addon/components/page/pki-key-list.hbs b/ui/lib/pki/addon/components/page/pki-key-list.hbs index 0651c6bf45..fd54015f01 100644 --- a/ui/lib/pki/addon/components/page/pki-key-list.hbs +++ b/ui/lib/pki/addon/components/page/pki-key-list.hbs @@ -3,7 +3,7 @@ SPDX-License-Identifier: BUSL-1.1 }} - + <:actions> {{#if @canImportKey}} @@ -25,19 +25,19 @@ {{#each keys as |pkiKey|}}
- - {{or pkiKey.keyName pkiKey.id}} + + {{or pkiKey.key_name pkiKey.key_id}}
- {{#if pkiKey.keyName}} - + {{#if pkiKey.key_name}} + {{/if}}
@@ -55,16 +55,20 @@ {{#if @canRead}} Details + > + Details + {{/if}} {{#if @canEdit}} Edit + > + Edit + {{/if}} {{/if}} diff --git a/ui/lib/pki/addon/components/page/pki-key-list.ts b/ui/lib/pki/addon/components/page/pki-key-list.ts index 579ccfca56..3c3bb936e8 100644 --- a/ui/lib/pki/addon/components/page/pki-key-list.ts +++ b/ui/lib/pki/addon/components/page/pki-key-list.ts @@ -5,10 +5,9 @@ import Component from '@glimmer/component'; import { PKI_DEFAULT_EMPTY_STATE_MSG } from 'pki/routes/overview'; -import type PkiKeyModel from 'vault/models/pki/key'; interface Args { - keyModels: PkiKeyModel[]; + keys: { key_id: string; is_default: boolean; key_name: string }[]; mountPoint: string; backend: string; canImportKey: boolean; diff --git a/ui/lib/pki/addon/components/pki-key-form.hbs b/ui/lib/pki/addon/components/pki-key-form.hbs index 05843b5615..93cddb0d12 100644 --- a/ui/lib/pki/addon/components/pki-key-form.hbs +++ b/ui/lib/pki/addon/components/pki-key-form.hbs @@ -4,28 +4,29 @@ }} {{! private_key is only available after initial save }} -{{#if this.generatedKey.privateKey}} +{{#if this.generatedKey.private_key}} {{else}}
- - {{#if @model.isNew}} - {{#each @model.formFieldGroups as |fieldGroup|}} + + + {{#if @form.isNew}} + {{#each @form.formFieldGroups as |fieldGroup|}} {{#each-in fieldGroup as |group fields|}} {{#if (eq group "Key parameters")}} - + {{else}} - {{#each fields as |attr|}} + {{#each fields as |field|}} @@ -34,18 +35,25 @@ {{/each-in}} {{/each}} {{else}} - {{! only key name is edit-able }} - {{#let (find-by "name" "keyName" @model.formFields) as |keyName|}} - - {{/let}} - {{#let (find-by "name" "keyType" @model.formFields) as |keyType|}} - - {{/let}} + {{#each @form.formFields as |field|}} + {{! only key name is edit-able }} + {{#if (eq field.name "key_name")}} + + {{else}} + + {{/if}} + {{/each}} {{/if}}
diff --git a/ui/lib/pki/addon/components/pki-key-form.ts b/ui/lib/pki/addon/components/pki-key-form.ts index 2e10676c9b..c840384da4 100644 --- a/ui/lib/pki/addon/components/pki-key-form.ts +++ b/ui/lib/pki/addon/components/pki-key-form.ts @@ -8,63 +8,117 @@ import { service } from '@ember/service'; import { task } from 'ember-concurrency'; import { tracked } from '@glimmer/tracking'; import { waitFor } from '@ember/test-waiters'; -import errorMessage from 'vault/utils/error-message'; +import { action } from '@ember/object'; + import type FlashMessageService from 'vault/services/flash-messages'; -import type PkiKeyModel from 'vault/models/pki/key'; -import type { ValidationMap } from 'vault/app-types'; +import type PkiKeyForm from 'vault/forms/secrets/pki/key'; +import type { Capabilities, ValidationMap } from 'vault/app-types'; +import type ApiService from 'vault/services/api'; +import type SecretMountPathService from 'vault/services/secret-mount-path'; +import type RouterService from '@ember/routing/router-service'; +import type CapabilitiesService from 'vault/services/capabilities'; +import type { + PkiGenerateInternalKeyRequest, + PkiGenerateExportedKeyRequest, + PkiGenerateInternalKeyResponse, + PkiGenerateExportedKeyResponse, + PkiWriteKeyRequest, +} from '@hashicorp/vault-client-typescript'; + +type PkiGenerateKeyResponse = PkiGenerateInternalKeyResponse | PkiGenerateExportedKeyResponse; /** * @module PkiKeyForm * PkiKeyForm components are used to create and update PKI keys. * - * @example - * ```js - * - * ``` - * - * @param {Object} model - pki/key model. - * @callback onCancel - Callback triggered when cancel button is clicked. - * @callback onSave - Callback triggered on save success. + * @param {Form} form - pki key form. */ interface Args { - model: PkiKeyModel; - onSave: CallableFunction; + form: PkiKeyForm; + canUpdate?: boolean; + canDelete?: boolean; } -export default class PkiKeyForm extends Component { +export default class PkiKeyFormComponent extends Component { @service declare readonly flashMessages: FlashMessageService; + @service declare readonly api: ApiService; + @service declare readonly secretMountPath: SecretMountPathService; + @service('app-router') declare readonly router: RouterService; + @service declare readonly capabilities: CapabilitiesService; @tracked errorBanner = ''; @tracked invalidFormAlert = ''; @tracked modelValidations: ValidationMap | null = null; + @tracked declare generatedKey: PkiGenerateKeyResponse; + @tracked declare generatedKeyCapabilities: Capabilities; - @tracked generatedKey: PkiKeyModel | null = null; + save = task( + waitFor(async (event: Event) => { + event.preventDefault(); + try { + const { currentPath } = this.secretMountPath; + const { form } = this.args; + const { isValid, state, invalidFormMessage, data } = form.toJSON(); - @task - @waitFor - *save(event: Event) { - event.preventDefault(); - try { - const { isNew, keyName } = this.args.model; - const { isValid, state, invalidFormMessage } = this.args.model.validate(); - if (isNew) { this.modelValidations = isValid ? null : state; this.invalidFormAlert = invalidFormMessage; - } - if (!isValid && isNew) return; - this.generatedKey = yield this.args.model.save({ adapterOptions: { import: false } }); - this.flashMessages.success( - `Successfully ${isNew ? 'generated' : 'updated'} key${keyName ? ` ${keyName}.` : '.'}` - ); - // only transition to details if there is no private_key data to display - if (!this.generatedKey?.privateKey) { - this.args.onSave(); + if (isValid) { + const { type, key_id, ...payload } = data; + if (!form.isNew) { + this.generatedKey = await this.api.secrets.pkiWriteKey( + key_id as string, + currentPath, + payload as PkiWriteKeyRequest + ); + } else if (data.type === 'internal') { + this.generatedKey = await this.api.secrets.pkiGenerateInternalKey( + currentPath, + payload as PkiGenerateInternalKeyRequest + ); + } else { + this.generatedKey = await this.api.secrets.pkiGenerateExportedKey( + currentPath, + payload as PkiGenerateExportedKeyRequest + ); + } + + this.flashMessages.success( + `Successfully ${form.isNew ? 'generated' : 'updated'} key${ + data.key_name ? ` ${data.key_name}.` : '.' + }` + ); + + const { private_key, key_id: keyId } = this.generatedKey; + // only transition to details if there is no private_key data to display + if (!private_key) { + this.router.transitionTo('vault.cluster.secrets.backend.pki.keys.key.details', keyId); + } else { + // check capabilities on newly generated key + this.generatedKeyCapabilities = await this.capabilities.for('pkiKey', { + backend: currentPath, + keyId, + }); + } + } + } catch (error) { + const { message } = await this.api.parseError(error); + this.errorBanner = message; + this.invalidFormAlert = 'There was an error submitting this form.'; } - } catch (error) { - this.errorBanner = errorMessage(error); - this.invalidFormAlert = 'There was an error submitting this form.'; + }) + ); + + @action + onCancel() { + if (this.args.form.isNew) { + this.router.transitionTo('vault.cluster.secrets.backend.pki.keys.index'); + } else { + this.router.transitionTo( + 'vault.cluster.secrets.backend.pki.keys.key.details', + this.args.form.data.key_id + ); } } } diff --git a/ui/lib/pki/addon/components/pki-key-import.hbs b/ui/lib/pki/addon/components/pki-key-import.hbs index 8cbbde7455..f485648b06 100644 --- a/ui/lib/pki/addon/components/pki-key-import.hbs +++ b/ui/lib/pki/addon/components/pki-key-import.hbs @@ -12,9 +12,7 @@ Importing a PKI key

- {{#let (find-by "name" "keyName" @model.formFields) as |attr|}} - - {{/let}} +
@@ -29,7 +27,7 @@ @text="Cancel" @color="secondary" disabled={{this.submitForm.isRunning}} - {{on "click" this.cancel}} + {{on "click" @onCancel}} data-test-cancel /> diff --git a/ui/lib/pki/addon/components/pki-key-import.ts b/ui/lib/pki/addon/components/pki-key-import.ts index 058ae4e9ca..5b638a19aa 100644 --- a/ui/lib/pki/addon/components/pki-key-import.ts +++ b/ui/lib/pki/addon/components/pki-key-import.ts @@ -10,62 +10,63 @@ import { tracked } from '@glimmer/tracking'; import { waitFor } from '@ember/test-waiters'; import { service } from '@ember/service'; import trimRight from 'vault/utils/trim-right'; -import errorMessage from 'vault/utils/error-message'; -import type PkiKeyModel from 'vault/models/pki/key'; +import FormField from 'vault/utils/forms/field'; + import type FlashMessageService from 'vault/services/flash-messages'; +import type ApiService from 'vault/services/api'; +import type SecretMountPath from 'vault/services/secret-mount-path'; /** * @module PkiKeyImport * PkiKeyImport components are used to import PKI keys. * - * @example - * ```js - * - * ``` * - * @param {Object} model - pki/key model. * @callback onCancel - Callback triggered when cancel button is clicked. * @callback onSubmit - Callback triggered on submit success. */ interface Args { - model: PkiKeyModel; onSave: CallableFunction; onCancel: CallableFunction; } export default class PkiKeyImport extends Component { @service declare readonly flashMessages: FlashMessageService; + @service declare readonly api: ApiService; + @service declare readonly secretMountPath: SecretMountPath; @tracked errorBanner = ''; @tracked invalidFormAlert = ''; + @tracked declare keyName: string; + @tracked declare pemBundle: string; + + keyNameField = new FormField('keyName', 'string', { + subText: `Optional, human-readable name for this key. The name must be unique across all keys and cannot be 'default'.`, + }); @task @waitFor *submitForm(event: Event) { event.preventDefault(); try { - const { keyName } = this.args.model; - yield this.args.model.save({ adapterOptions: { import: true } }); - this.flashMessages.success(`Successfully imported key${keyName ? ` ${keyName}.` : '.'}`); + yield this.api.secrets.pkiImportKey(this.secretMountPath.currentPath, { + key_name: this.keyName, + pem_bundle: this.pemBundle, + }); + this.flashMessages.success(`Successfully imported key${this.keyName ? ` ${this.keyName}.` : '.'}`); this.args.onSave(); } catch (error) { - this.errorBanner = errorMessage(error); + const { message } = yield this.api.parseError(error); + this.errorBanner = message; this.invalidFormAlert = 'There was a problem importing key.'; } } @action onFileUploaded({ value, filename }: { value: string; filename: string }) { - this.args.model.pemBundle = value; - if (!this.args.model.keyName) { + this.pemBundle = value; + if (!this.keyName) { const trimmedFileName = trimRight(filename, ['.json', '.pem']); - this.args.model.keyName = trimmedFileName; + this.keyName = trimmedFileName; } } - - @action - cancel() { - this.args.model.unloadRecord(); - this.args.onCancel(); - } } diff --git a/ui/lib/pki/addon/components/pki-key-parameters.ts b/ui/lib/pki/addon/components/pki-key-parameters.ts index 3e46eac4e8..fab2c792cc 100644 --- a/ui/lib/pki/addon/components/pki-key-parameters.ts +++ b/ui/lib/pki/addon/components/pki-key-parameters.ts @@ -6,11 +6,11 @@ import Component from '@glimmer/component'; import { action } from '@ember/object'; import { underscore } from '@ember/string'; -import PkiConfigGenerateForm from 'vault/forms/secrets/pki/config/generate'; +import Form from 'vault/forms/form'; import type PkiRoleModel from 'vault/models/pki/role'; -import type PkiKeyModel from 'vault/models/pki/key'; -import type PkiActionModel from 'vault/models/pki/action'; +import type PkiConfigGenerateForm from 'vault/forms/secrets/pki/config/generate'; +import type PkiKeyForm from 'vault/forms/secrets/pki/key'; import type { HTMLElementEvent } from 'forms'; /** * @module PkiKeyParameters @@ -24,7 +24,7 @@ import type { HTMLElementEvent } from 'forms'; * @param {string} fields - Array of attributes from a formFieldGroup generated by the @withFormFields decorator ex: [{ name: 'attrName', type: 'string', options: {...} }] */ interface Args { - model: PkiRoleModel | PkiKeyModel | PkiActionModel | PkiConfigGenerateForm; + model: PkiRoleModel | PkiKeyForm | PkiConfigGenerateForm; } interface TypeOptions { rsa: string; @@ -48,7 +48,7 @@ export default class PkiKeyParameters extends Component { // shim to support both model and form types until all models can be migrated getValue = (key: string) => { const { model } = this.args; - if (model instanceof PkiConfigGenerateForm) { + if (model instanceof Form) { return model.data[underscore(key) as keyof typeof model.data]; } return model[key as keyof typeof model]; @@ -56,7 +56,7 @@ export default class PkiKeyParameters extends Component { setValue = (key: string, value: unknown) => { const { model } = this.args; - const modelKey = model instanceof PkiConfigGenerateForm ? underscore(key) : key; + const modelKey = model instanceof Form ? underscore(key) : key; model.set(modelKey, value); }; @@ -68,7 +68,7 @@ export default class PkiKeyParameters extends Component { @action handleSelection(name: string, selection: string) { this.setValue(name, selection); - if (name === 'keyType' && Object.keys(KEY_BITS_OPTIONS)?.includes(selection)) { + if (['keyType', 'key_type'].includes(name) && Object.keys(KEY_BITS_OPTIONS)?.includes(selection)) { const bitOptions = KEY_BITS_OPTIONS[selection as keyof TypeOptions]; if (bitOptions) { this.setValue('keyBits', bitOptions[0]); diff --git a/ui/lib/pki/addon/routes/keys/create.js b/ui/lib/pki/addon/routes/keys/create.js index c8cded5c16..4749dfc6bd 100644 --- a/ui/lib/pki/addon/routes/keys/create.js +++ b/ui/lib/pki/addon/routes/keys/create.js @@ -5,15 +5,14 @@ import Route from '@ember/routing/route'; import { service } from '@ember/service'; -import { withConfirmLeave } from 'core/decorators/confirm-leave'; +import PkiKeyForm from 'vault/forms/secrets/pki/key'; -@withConfirmLeave() export default class PkiKeysCreateRoute extends Route { @service secretMountPath; @service store; model() { - return this.store.createRecord('pki/key'); + return new PkiKeyForm({}, { isNew: true }); } setupController(controller, resolvedModel) { diff --git a/ui/lib/pki/addon/routes/keys/import.js b/ui/lib/pki/addon/routes/keys/import.js index 3641d3fa64..df6fe92f6b 100644 --- a/ui/lib/pki/addon/routes/keys/import.js +++ b/ui/lib/pki/addon/routes/keys/import.js @@ -5,17 +5,11 @@ import Route from '@ember/routing/route'; import { service } from '@ember/service'; -import { withConfirmLeave } from 'core/decorators/confirm-leave'; -@withConfirmLeave() export default class PkiKeysImportRoute extends Route { @service secretMountPath; @service store; - model() { - return this.store.createRecord('pki/key'); - } - setupController(controller, resolvedModel) { super.setupController(controller, resolvedModel); controller.breadcrumbs = [ diff --git a/ui/lib/pki/addon/routes/keys/index.js b/ui/lib/pki/addon/routes/keys/index.js index b05473878a..66294bc5ac 100644 --- a/ui/lib/pki/addon/routes/keys/index.js +++ b/ui/lib/pki/addon/routes/keys/index.js @@ -6,14 +6,15 @@ import Route from '@ember/routing/route'; import { service } from '@ember/service'; import { withConfig } from 'pki/decorators/check-issuers'; -import { hash } from 'rsvp'; import { PKI_DEFAULT_EMPTY_STATE_MSG } from 'pki/routes/overview'; +import { PkiListKeysListEnum } from '@hashicorp/vault-client-typescript'; +import { paginate } from 'core/utils/paginate-list'; @withConfig() export default class PkiKeysIndexRoute extends Route { - @service pagination; @service secretMountPath; - @service store; // used by @withConfig decorator + @service api; + @service capabilities; queryParams = { page: { @@ -21,26 +22,48 @@ export default class PkiKeysIndexRoute extends Route { }, }; - model(params) { + async fetchCapabilities(keyId) { + const { pathFor } = this.capabilities; + const backend = this.secretMountPath.currentPath; + const pathMap = { + import: pathFor('pkiKeysImport', { backend }), + generate: pathFor('pkiKeysImport', { backend }), + key: pathFor('pkiKey', { backend, keyId }), + }; + const perms = await this.capabilities.fetch(Object.values(pathMap)); + + return { + canImportKey: perms[pathMap.import].canUpdate, + canGenerateKey: perms[pathMap.generate].canUpdate, + canRead: perms[pathMap.key].canRead, + canEdit: perms[pathMap.key].canUpdate, + }; + } + + async model(params) { const page = Number(params.page) || 1; - return hash({ + const model = { hasConfig: this.pkiMountHasConfig, parentModel: this.modelFor('keys'), - keyModels: this.pagination - .lazyPaginatedQuery('pki/key', { - backend: this.secretMountPath.currentPath, - responsePath: 'data.keys', - page, - skipCache: page === 1, - }) - .catch((err) => { - if (err.httpStatus === 404) { - return []; - } else { - throw err; - } - }), - }); + }; + + try { + const response = await this.api.secrets.pkiListKeys( + this.secretMountPath.currentPath, + PkiListKeysListEnum.TRUE + ); + const keys = this.api.keyInfoToArray(response, 'key_id'); + const capabilities = await this.fetchCapabilities(keys[0].key_id); + Object.assign(model, { ...capabilities, keys: paginate(keys, { page }) }); + } catch (e) { + if (e.response.status === 404) { + model.keys = []; + } else { + throw e; + } + } + + return model; } setupController(controller, resolvedModel) { diff --git a/ui/lib/pki/addon/routes/keys/key.js b/ui/lib/pki/addon/routes/keys/key.js index a7e1885fe4..7e45d4518f 100644 --- a/ui/lib/pki/addon/routes/keys/key.js +++ b/ui/lib/pki/addon/routes/keys/key.js @@ -8,13 +8,19 @@ import { service } from '@ember/service'; export default class PkiKeyRoute extends Route { @service secretMountPath; - @service store; + @service api; + @service capabilities; - model() { - const { key_id } = this.paramsFor('keys/key'); - return this.store.queryRecord('pki/key', { - backend: this.secretMountPath.currentPath, - id: key_id, - }); + async model() { + const { key_id: keyId } = this.paramsFor('keys/key'); + const backend = this.secretMountPath.currentPath; + const { canUpdate, canDelete } = await this.capabilities.for('pkiKey', { backend, keyId }); + const key = await this.api.secrets.pkiReadKey(keyId, this.secretMountPath.currentPath); + return { + backend, + key, + canUpdate, + canDelete, + }; } } diff --git a/ui/lib/pki/addon/routes/keys/key/details.js b/ui/lib/pki/addon/routes/keys/key/details.js index edbe32dc67..b4f7da95a7 100644 --- a/ui/lib/pki/addon/routes/keys/key/details.js +++ b/ui/lib/pki/addon/routes/keys/key/details.js @@ -9,16 +9,13 @@ import { service } from '@ember/service'; export default class PkiKeyDetailsRoute extends Route { @service secretMountPath; - model() { - return this.modelFor('keys.key'); - } setupController(controller, resolvedModel) { super.setupController(controller, resolvedModel); controller.breadcrumbs = [ { label: 'Secrets', route: 'secrets', linkExternal: true }, { label: this.secretMountPath.currentPath, route: 'overview', model: resolvedModel.backend }, { label: 'Keys', route: 'keys.index', model: resolvedModel.backend }, - { label: resolvedModel.id }, + { label: resolvedModel.key.key_id }, ]; } } diff --git a/ui/lib/pki/addon/routes/keys/key/edit.js b/ui/lib/pki/addon/routes/keys/key/edit.js index ccdcaf3991..6c5deb1034 100644 --- a/ui/lib/pki/addon/routes/keys/key/edit.js +++ b/ui/lib/pki/addon/routes/keys/key/edit.js @@ -3,16 +3,16 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { withConfirmLeave } from 'core/decorators/confirm-leave'; import Route from '@ember/routing/route'; import { service } from '@ember/service'; +import PkiKeyForm from 'vault/forms/secrets/pki/key'; -@withConfirmLeave() export default class PkiKeyEditRoute extends Route { @service secretMountPath; model() { - return this.modelFor('keys.key'); + const { key } = this.modelFor('keys.key'); + return new PkiKeyForm(key); } setupController(controller, resolvedModel) { @@ -21,7 +21,7 @@ export default class PkiKeyEditRoute extends Route { { label: 'Secrets', route: 'secrets', linkExternal: true }, { label: this.secretMountPath.currentPath, route: 'overview', model: this.secretMountPath.currentPath }, { label: 'Keys', route: 'keys.index', model: this.secretMountPath.currentPath }, - { label: resolvedModel.id }, + { label: resolvedModel.data.key_id }, ]; } } diff --git a/ui/lib/pki/addon/templates/keys/create.hbs b/ui/lib/pki/addon/templates/keys/create.hbs index b21e5792fb..2915890411 100644 --- a/ui/lib/pki/addon/templates/keys/create.hbs +++ b/ui/lib/pki/addon/templates/keys/create.hbs @@ -14,8 +14,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/ui/lib/pki/addon/templates/keys/import.hbs b/ui/lib/pki/addon/templates/keys/import.hbs index 6bf7f9694f..9b31ea5fd7 100644 --- a/ui/lib/pki/addon/templates/keys/import.hbs +++ b/ui/lib/pki/addon/templates/keys/import.hbs @@ -15,7 +15,6 @@ \ No newline at end of file diff --git a/ui/lib/pki/addon/templates/keys/index.hbs b/ui/lib/pki/addon/templates/keys/index.hbs index 5a229c7131..79cb3dafe4 100644 --- a/ui/lib/pki/addon/templates/keys/index.hbs +++ b/ui/lib/pki/addon/templates/keys/index.hbs @@ -6,12 +6,12 @@ \ No newline at end of file diff --git a/ui/lib/pki/addon/templates/keys/key/details.hbs b/ui/lib/pki/addon/templates/keys/key/details.hbs index 3e3e8fbf82..2f37dbbe8c 100644 --- a/ui/lib/pki/addon/templates/keys/key/details.hbs +++ b/ui/lib/pki/addon/templates/keys/key/details.hbs @@ -15,4 +15,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/ui/lib/pki/addon/templates/keys/key/edit.hbs b/ui/lib/pki/addon/templates/keys/key/edit.hbs index efcfb2a78a..a8cec019df 100644 --- a/ui/lib/pki/addon/templates/keys/key/edit.hbs +++ b/ui/lib/pki/addon/templates/keys/key/edit.hbs @@ -15,8 +15,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/ui/tests/acceptance/pki/pki-engine-route-cleanup-test.js b/ui/tests/acceptance/pki/pki-engine-route-cleanup-test.js index 4c1c3503f9..2b46983b80 100644 --- a/ui/tests/acceptance/pki/pki-engine-route-cleanup-test.js +++ b/ui/tests/acceptance/pki/pki-engine-route-cleanup-test.js @@ -15,7 +15,6 @@ import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { PKI_CONFIGURE_CREATE, PKI_ISSUER_LIST, - PKI_KEYS, PKI_ROLE_DETAILS, } from 'vault/tests/helpers/pki/pki-selectors'; @@ -174,98 +173,4 @@ module('Acceptance | pki engine route cleanup test', function (hooks) { assert.strictEqual(issuers.length, 0, 'Issuer is removed from store'); }); }); - - module('key routes', function (hooks) { - hooks.beforeEach(async function () { - await login(); - // Configure PKI -- key creation not allowed unless configured - await visit(`/vault/secrets-engines/${this.mountPath}/pki/overview`); - await click(`${GENERAL.emptyStateActions} a`); - await click(PKI_CONFIGURE_CREATE.optionByKey('generate-root')); - await fillIn(GENERAL.inputByAttr('type'), 'internal'); - await fillIn(GENERAL.inputByAttr('common_name'), 'my-root-cert'); - await click(GENERAL.submitButton); - }); - test('create key exit', async function (assert) { - let keys, key; - await login(); - await visit(`/vault/secrets-engines/${this.mountPath}/pki/overview`); - await click(GENERAL.secretTab('Keys')); - keys = this.store.peekAll('pki/key'); - const configKeyId = keys.at(0).id; - assert.strictEqual(keys.length, 1, 'One key exists from config'); - // Create key - await click(PKI_KEYS.generateKey); - keys = this.store.peekAll('pki/key'); - key = keys.at(1); - assert.strictEqual(keys.length, 2, 'New key exists'); - assert.true(key.isNew, 'Role is new model'); - // Exit - await click(GENERAL.cancelButton); - keys = this.store.peekAll('pki/key'); - assert.strictEqual(keys.length, 1, 'Second key is removed from store'); - assert.strictEqual(keys.at(0).id, configKeyId); - assert.strictEqual(currentURL(), `/vault/secrets-engines/${this.mountPath}/pki/keys`, 'url is correct'); - - // Create again - await click(PKI_KEYS.generateKey); - assert.strictEqual(keys.length, 2, 'New key exists'); - keys = this.store.peekAll('pki/key'); - key = keys.at(1); - assert.true(key.isNew, 'Key is new model'); - // Exit - await click(OVERVIEW_BREADCRUMB); - assert.strictEqual( - currentURL(), - `/vault/secrets-engines/${this.mountPath}/pki/overview`, - 'url is correct' - ); - keys = this.store.peekAll('pki/key'); - assert.strictEqual(keys.length, 1, 'Key is removed from store'); - }); - test('edit key exit', async function (assert) { - let keys, key; - await login(); - await visit(`/vault/secrets-engines/${this.mountPath}/pki/overview`); - await click(GENERAL.secretTab('Keys')); - keys = this.store.peekAll('pki/key'); - assert.strictEqual(keys.length, 1, 'One key from config exists'); - assert.dom('.list-item-row').exists({ count: 1 }, 'single row for key'); - await click('.list-item-row'); - // Edit - await click(PKI_KEYS.keyEditLink); - await fillIn(GENERAL.inputByAttr('keyName'), 'foobar'); - keys = this.store.peekAll('pki/key'); - key = keys.at(0); - assert.true(key.hasDirtyAttributes, 'Key model is dirty'); - // Exit - await click(GENERAL.cancelButton); - assert.strictEqual( - currentURL(), - `/vault/secrets-engines/${this.mountPath}/pki/keys/${key.id}/details`, - 'url is correct' - ); - keys = this.store.peekAll('pki/key'); - assert.strictEqual(keys.length, 1, 'Key list has 1'); - assert.false(key.hasDirtyAttributes, 'Key dirty attrs have been rolled back'); - - // Edit again - await click(PKI_KEYS.keyEditLink); - await fillIn(GENERAL.inputByAttr('keyName'), 'foobar'); - keys = this.store.peekAll('pki/key'); - key = keys.at(0); - assert.true(key.hasDirtyAttributes, 'Key model is dirty'); - - // Exit via breadcrumb - await click(OVERVIEW_BREADCRUMB); - assert.strictEqual( - currentURL(), - `/vault/secrets-engines/${this.mountPath}/pki/overview`, - 'url is correct' - ); - keys = this.store.peekAll('pki/key'); - assert.strictEqual(keys.length, 1, 'Key list has 1'); - assert.false(key.hasDirtyAttributes, 'Key dirty attrs have been rolled back'); - }); - }); }); diff --git a/ui/tests/acceptance/pki/pki-engine-workflow-test.js b/ui/tests/acceptance/pki/pki-engine-workflow-test.js index b8f97e29d5..a3cfa5f902 100644 --- a/ui/tests/acceptance/pki/pki-engine-workflow-test.js +++ b/ui/tests/acceptance/pki/pki-engine-workflow-test.js @@ -303,7 +303,7 @@ module('Acceptance | pki workflow', function (hooks) { 'navigates back to details on cancel' ); await visit(`/vault/secrets-engines/${this.mountPath}/pki/keys/${keyId}/edit`); - await fillIn(GENERAL.inputByAttr('keyName'), 'test-key'); + await fillIn(GENERAL.inputByAttr('key_name'), 'test-key'); await click(GENERAL.submitButton); assert.strictEqual( currentURL(), @@ -317,7 +317,7 @@ module('Acceptance | pki workflow', function (hooks) { await click(PKI_KEYS.generateKey); assert.strictEqual(currentURL(), `/vault/secrets-engines/${this.mountPath}/pki/keys/create`); await fillIn(GENERAL.inputByAttr('type'), 'exported'); // exported keys generated private_key data - await fillIn(GENERAL.inputByAttr('keyType'), 'rsa'); + await fillIn(GENERAL.inputByAttr('key_type'), 'rsa'); await click(GENERAL.submitButton); keyId = find(GENERAL.infoRowValue('Key ID')).textContent?.trim(); assert.strictEqual( diff --git a/ui/tests/integration/components/pki/page/pki-key-details-test.js b/ui/tests/integration/components/pki/page/pki-key-details-test.js index 82f087f006..cc57ff72fa 100644 --- a/ui/tests/integration/components/pki/page/pki-key-details-test.js +++ b/ui/tests/integration/components/pki/page/pki-key-details-test.js @@ -8,46 +8,43 @@ import { setupRenderingTest } from 'ember-qunit'; import { click, 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'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { PKI_KEYS } from 'vault/tests/helpers/pki/pki-selectors'; +import sinon from 'sinon'; module('Integration | Component | pki key details page', function (hooks) { setupRenderingTest(hooks); setupEngine(hooks, 'pki'); - setupMirage(hooks); hooks.beforeEach(function () { this.owner.lookup('service:flash-messages').registerTypes(['success', 'danger']); - this.store = this.owner.lookup('service:store'); - this.secretMountPath = this.owner.lookup('service:secret-mount-path'); + this.backend = 'pki-test'; - this.secretMountPath.currentPath = this.backend; - this.store.pushPayload('pki/key', { - modelName: 'pki/key', - key_id: '724862ff-6438-bad0-b598-77a6c7f4e934', - key_type: 'ec', - key_name: 'test-key', - }); - this.model = this.store.peekRecord('pki/key', '724862ff-6438-bad0-b598-77a6c7f4e934'); + this.owner.lookup('service:secret-mount-path').update(this.backend); + + this.deleteStub = sinon.stub(this.owner.lookup('service:api').secrets, 'pkiDeleteKey').resolves(); + + this.key = { key_id: '724862ff-6438-bad0-b598-77a6c7f4e934', key_type: 'ec', key_name: 'test-key' }; + this.canDelete = true; + this.canEdit = true; + + this.renderComponent = () => + render( + hbs` + + `, + { owner: this.engine } + ); }); test('it renders the page component and deletes a key', async function (assert) { assert.expect(7); - this.server.delete(`${this.backend}/key/${this.model.keyId}`, () => { - assert.ok(true, 'confirming delete fires off destroyRecord()'); - }); - await render( - hbs` - - `, - { owner: this.engine } - ); + await this.renderComponent(); assert .dom(GENERAL.infoRowValue('Key ID')) @@ -59,39 +56,28 @@ module('Integration | Component | pki key details page', function (hooks) { assert.dom(PKI_KEYS.keyDeleteButton).exists('renders delete button'); await click(PKI_KEYS.keyDeleteButton); await click(GENERAL.confirmButton); + assert.true( + this.deleteStub.calledWith(this.key.key_id, this.backend), + 'pkiDeleteKey called with correct args' + ); }); test('it does not render actions when capabilities are false', async function (assert) { assert.expect(2); - await render( - hbs` - - `, - { owner: this.engine } - ); + this.canDelete = false; + this.canEdit = false; + + await this.renderComponent(); assert.dom(PKI_KEYS.keyDeleteButton).doesNotExist('does not render delete button if no permission'); assert.dom(PKI_KEYS.keyEditLink).doesNotExist('does not render edit button if no permission'); }); test('it renders the private key as a component when there is a private key', async function (assert) { - this.model.privateKey = 'private-key-value'; - await render( - hbs` - - `, - { owner: this.engine } - ); + this.key.private_key = 'private-key-value'; + await this.renderComponent(); assert.dom('[data-test-certificate-card]').exists('Certificate card renders for the private key'); }); }); diff --git a/ui/tests/integration/components/pki/page/pki-key-list-test.js b/ui/tests/integration/components/pki/page/pki-key-list-test.js index af1afdfeeb..fd9f3c79ef 100644 --- a/ui/tests/integration/components/pki/page/pki-key-list-test.js +++ b/ui/tests/integration/components/pki/page/pki-key-list-test.js @@ -8,7 +8,6 @@ import { setupRenderingTest } from 'ember-qunit'; import { click, 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'; import { STANDARD_META } from 'vault/tests/helpers/pagination'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { PKI_KEYS } from 'vault/tests/helpers/pki/pki-selectors'; @@ -16,49 +15,55 @@ import { PKI_KEYS } from 'vault/tests/helpers/pki/pki-selectors'; module('Integration | Component | pki key list page', function (hooks) { setupRenderingTest(hooks); setupEngine(hooks, 'pki'); - setupMirage(hooks); hooks.beforeEach(function () { - this.store = this.owner.lookup('service:store'); - this.secretMountPath = this.owner.lookup('service:secret-mount-path'); - this.secretMountPath.currentPath = 'pki-test'; - this.store.pushPayload('pki/key', { - modelName: 'pki/key', - key_id: '724862ff-6438-bad0-b598-77a6c7f4e934', - key_type: 'ec', - key_name: 'test-key', - }); - this.store.pushPayload('pki/key', { - modelName: 'pki/key', - key_id: '9fdddf12-9ce3-0268-6b34-dc1553b00175', - key_type: 'rsa', - key_name: 'another-key', - }); - const keyModels = this.store.peekAll('pki/key'); - keyModels.meta = STANDARD_META; - this.keyModels = keyModels; + this.backend = 'pki-test'; + this.owner.lookup('service:secret-mount-path').update(this.backend); + this.keys = [ + { + key_id: '724862ff-6438-bad0-b598-77a6c7f4e934', + key_type: 'ec', + key_name: 'test-key', + }, + { + key_id: '9fdddf12-9ce3-0268-6b34-dc1553b00175', + key_type: 'rsa', + key_name: 'another-key', + }, + ]; + this.keys.meta = STANDARD_META; + this.canImportKey = true; + this.canGenerateKey = true; + this.canRead = true; + this.canEdit = true; + + this.renderComponent = () => + render( + hbs` + , + `, + { owner: this.engine } + ); }); test('it renders empty state when no keys exist', async function (assert) { assert.expect(3); - this.keyModels = { - meta: { - total: 0, - currentPage: 1, - pageSize: 100, - }, + + this.keys.meta = { + currentPage: 1, + total: 0, + pageSize: 100, }; - await render( - hbs` - , - `, - { owner: this.engine } - ); + + await this.renderComponent(); + assert .dom('[data-test-empty-state-title]') .hasText('No keys yet', 'renders empty state that no keys exist'); @@ -68,19 +73,9 @@ module('Integration | Component | pki key list page', function (hooks) { test('it renders list of keys and actions when permission allowed', async function (assert) { assert.expect(6); - await render( - hbs` - , - `, - { owner: this.engine } - ); + + await this.renderComponent(); + assert.dom(PKI_KEYS.keyName).hasText('test-key', 'linked block renders key id'); assert .dom(PKI_KEYS.keyId) @@ -94,19 +89,14 @@ module('Integration | Component | pki key list page', function (hooks) { test('it hides actions when permission denied', async function (assert) { assert.expect(3); - await render( - hbs` - , - `, - { owner: this.engine } - ); + + this.canImportKey = false; + this.canGenerateKey = false; + this.canRead = false; + this.canEdit = false; + + await this.renderComponent(); + assert.dom(PKI_KEYS.importKey).doesNotExist('renders import action'); assert.dom(PKI_KEYS.generateKey).doesNotExist('renders generate action'); assert.dom(GENERAL.menuTrigger).doesNotExist('does not render popup menu when no permission'); diff --git a/ui/tests/integration/components/pki/pki-key-form-test.js b/ui/tests/integration/components/pki/pki-key-form-test.js index e07769ba65..fc6c6d9e4a 100644 --- a/ui/tests/integration/components/pki/pki-key-form-test.js +++ b/ui/tests/integration/components/pki/pki-key-form-test.js @@ -10,46 +10,52 @@ import { render, click, fillIn } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupEngine } from 'ember-engines/test-support'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import { PKI_KEY_FORM } from 'vault/tests/helpers/pki/pki-selectors'; +import { PKI_KEY_FORM, PKI_KEYS } from 'vault/tests/helpers/pki/pki-selectors'; +import PkiKeyForm from 'vault/forms/secrets/pki/key'; module('Integration | Component | pki key form', function (hooks) { setupRenderingTest(hooks); - setupMirage(hooks); setupEngine(hooks, 'pki'); // https://github.com/ember-engines/ember-engines/pull/653 hooks.beforeEach(function () { - this.store = this.owner.lookup('service:store'); - this.model = this.store.createRecord('pki/key'); this.backend = 'pki-test'; - this.secretMountPath = this.owner.lookup('service:secret-mount-path'); - this.secretMountPath.currentPath = this.backend; - this.onCancel = sinon.spy(); + this.owner.lookup('service:secret-mount-path').update(this.backend); + + const { secrets } = this.owner.lookup('service:api'); + this.response = { key_id: 'test-key-id' }; + this.writeStub = sinon.stub(secrets, 'pkiWriteKey').resolves(this.response); + this.genInternalStub = sinon.stub(secrets, 'pkiGenerateInternalKey').resolves(this.response); + this.genExportedStub = sinon + .stub(secrets, 'pkiGenerateExportedKey') + .resolves({ ...this.response, private_key: 'private-key' }); + + this.capabilitiesStub = sinon + .stub(this.owner.lookup('service:capabilities'), 'for') + .resolves({ canUpdate: true, canDelete: true }); + + this.transitionStub = sinon.stub(this.owner.lookup('service:router'), 'transitionTo'); + + this.form = new PkiKeyForm({}, { isNew: true }); + + this.renderComponent = () => render(hbs``, { owner: this.engine }); }); test('it should render fields and show validation messages', async function (assert) { assert.expect(7); - await render( - hbs` - - `, - { owner: this.engine } - ); - assert.dom(GENERAL.inputByAttr('keyName')).exists('renders name input'); + + await this.renderComponent(); + + assert.dom(GENERAL.inputByAttr('key_name')).exists('renders name input'); assert.dom(GENERAL.inputByAttr('type')).exists('renders type input'); - assert.dom(GENERAL.inputByAttr('keyType')).exists('renders key type input'); - assert.dom(GENERAL.inputByAttr('keyBits')).exists('renders key bits input'); + assert.dom(GENERAL.inputByAttr('key_type')).exists('renders key type input'); + assert.dom(GENERAL.inputByAttr('key_bits')).exists('renders key bits input'); await click(GENERAL.submitButton); assert .dom(GENERAL.validationErrorByAttr('type')) .hasTextContaining('Type is required.', 'renders presence validation for type of key'); assert - .dom(GENERAL.validationErrorByAttr('keyType')) + .dom(GENERAL.validationErrorByAttr('key_type')) .hasTextContaining('Please select a key type.', 'renders selection prompt for key type'); assert .dom(PKI_KEY_FORM.validationError) @@ -57,75 +63,92 @@ module('Integration | Component | pki key form', function (hooks) { }); test('it generates a key type=exported', async function (assert) { - assert.expect(4); - this.server.post(`/${this.backend}/keys/generate/exported`, (schema, req) => { - assert.ok(true, 'Request made to the correct endpoint to generate exported key'); - const request = JSON.parse(req.requestBody); - assert.propEqual( - request, - { - key_name: 'test-key', - key_type: 'rsa', - key_bits: '2048', - }, - 'sends params in correct type' - ); - return { key_id: 'test' }; - }); + assert.expect(9); - this.onSave = () => assert.ok(true, 'onSave callback fires on save success'); + await this.renderComponent(); - await render( - hbs` - - `, - { owner: this.engine } - ); - - await fillIn(GENERAL.inputByAttr('keyName'), 'test-key'); + await fillIn(GENERAL.inputByAttr('key_name'), 'test-key'); await fillIn(GENERAL.inputByAttr('type'), 'exported'); - assert.dom(GENERAL.inputByAttr('keyBits')).isDisabled('key bits disabled when no key type selected'); - await fillIn(GENERAL.inputByAttr('keyType'), 'rsa'); + assert.dom(GENERAL.inputByAttr('key_bits')).isDisabled('key bits disabled when no key type selected'); + await fillIn(GENERAL.inputByAttr('key_type'), 'rsa'); await click(GENERAL.submitButton); + + assert.true( + this.genExportedStub.calledWith(this.backend, { + key_name: 'test-key', + key_type: 'rsa', + key_bits: '2048', + }), + 'generates exported key with correct params' + ); + assert.true( + this.transitionStub.notCalled, + 'does not transition to key details when private_key is returned' + ); + assert.true( + this.capabilitiesStub.calledWith('pkiKey', { backend: this.backend, keyId: this.response.key_id }), + 'checks capabilities for new key' + ); + assert.dom(PKI_KEYS.keyDeleteButton).exists('renders delete button for new key after generation'); + assert.dom(GENERAL.button('Download')).exists('renders download button for private key after generation'); + assert.dom(PKI_KEYS.keyEditLink).exists('renders edit link for new key after generation'); + assert.dom(GENERAL.infoRowValue('Key ID')).hasText(this.response.key_id, 'key id renders'); + assert.dom('[data-test-certificate-card]').exists('Certificate card renders for the private key'); }); test('it generates a key type=internal', async function (assert) { - assert.expect(4); - this.server.post(`/${this.backend}/keys/generate/internal`, (schema, req) => { - assert.ok(true, 'Request made to the correct endpoint to generate internal key'); - const request = JSON.parse(req.requestBody); - assert.propEqual( - request, - { - key_name: 'test-key', - key_type: 'rsa', - key_bits: '2048', - }, - 'sends params in correct type' - ); - return { key_id: 'test' }; - }); - this.onSave = () => assert.ok(true, 'onSave callback fires on save success'); + assert.expect(3); - await render( - hbs` - - `, - { owner: this.engine } - ); + await this.renderComponent(); - await fillIn(GENERAL.inputByAttr('keyName'), 'test-key'); + await fillIn(GENERAL.inputByAttr('key_name'), 'test-key'); await fillIn(GENERAL.inputByAttr('type'), 'internal'); - assert.dom(GENERAL.inputByAttr('keyBits')).isDisabled('key bits disabled when no key type selected'); - await fillIn(GENERAL.inputByAttr('keyType'), 'rsa'); + assert.dom(GENERAL.inputByAttr('key_bits')).isDisabled('key bits disabled when no key type selected'); + await fillIn(GENERAL.inputByAttr('key_type'), 'rsa'); await click(GENERAL.submitButton); + + assert.true( + this.genInternalStub.calledWith(this.backend, { + key_name: 'test-key', + key_type: 'rsa', + key_bits: '2048', + }), + 'generates internal key with correct params' + ); + assert.true( + this.transitionStub.calledWith( + 'vault.cluster.secrets.backend.pki.keys.key.details', + this.response.key_id + ), + 'transitions to key details page on save' + ); + }); + + test('it should edit key', async function (assert) { + this.form = new PkiKeyForm({ key_id: 'test-edit-id', key_name: 'FooBar', key_type: 'rsa' }); + + await this.renderComponent(); + + assert.dom(GENERAL.inputByAttr('key_name')).hasValue('FooBar', 'name field has correct initial value'); + assert.dom(GENERAL.inputByAttr('key_type')).hasValue('rsa', 'key type field has correct initial value'); + assert.dom(GENERAL.inputByAttr('key_type')).isDisabled('key type is not editable'); + + await fillIn(GENERAL.inputByAttr('key_name'), 'BarBaz'); + await click(GENERAL.submitButton); + + assert.true( + this.writeStub.calledWith('test-edit-id', this.backend, { + key_name: 'BarBaz', + key_type: 'rsa', + }), + 'updates key with correct params' + ); + assert.true( + this.transitionStub.calledWith( + 'vault.cluster.secrets.backend.pki.keys.key.details', + this.response.key_id + ), + 'transitions to key details page on save' + ); }); });