[UI] Ember Data Migration - PKI Keys (#10825) (#10903)

* updates issuers list route to use api service

* updates issuer details route to use api service

* updates issuer edit route to use api service

* updates issuer cross sign route to use api service

* updates issuer sign intermediate route to use api service

* updates rotate root route to use api service

* fixes a11y violation in pki-issuer-edit component

* updates pki keys list view to api service

* updates pki key details view to api service

* updates pki key import view to api service

* updates pki key create/edit views to api service

Co-authored-by: Jordan Reimer <zofskeez@gmail.com>
This commit is contained in:
Vault Automation 2025-11-18 13:56:48 -05:00 committed by GitHub
parent 9f946960bc
commit 35c746b2cf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 525 additions and 463 deletions

View File

@ -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<PkiKeyFormData> {
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;
}
}

View File

@ -16,18 +16,18 @@
/>
<div class="toolbar-separator"></div>
{{/if}}
{{#if @key.privateKey}}
{{#if @key.private_key}}
<DownloadButton
class="toolbar-button"
@color="secondary"
@filename="{{@key.backend}}-{{or @key.keyName 'private-key'}}"
@data={{@key.privateKey}}
@filename="{{this.backend}}-{{or @key.key_name 'private-key'}}"
@data={{@key.private_key}}
@extension="pem"
@text="Download private key"
/>
{{/if}}
{{#if @canEdit}}
<ToolbarLink @route="keys.key.edit" @models={{array @key.backend @key.keyId}} data-test-pki-key-edit>
<ToolbarLink @route="keys.key.edit" @models={{array this.backend @key.key_id}} data-test-pki-key-edit>
Edit key
</ToolbarLink>
{{/if}}
@ -35,7 +35,7 @@
</Toolbar>
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
{{#if @key.privateKey}}
{{#if @key.private_key}}
<div class="has-top-margin-m">
<Hds::Alert data-test-pki-key-next-steps @type="inline" @color="highlight" class="has-bottom-margin-s" as |A|>
<A.Title>Next steps</A.Title>
@ -43,16 +43,16 @@
</Hds::Alert>
</div>
{{/if}}
{{#each @key.formFields as |attr|}}
{{#each this.displayFields as |field|}}
<InfoTableRow
@label={{capitalize (or attr.options.detailsLabel attr.options.label (humanize (dasherize attr.name)))}}
@value={{get @key attr.name}}
@addCopyButton={{eq attr.name "keyId"}}
@label={{if (eq field "key_id") "Key ID" (capitalize (humanize field))}}
@value={{get @key field}}
@addCopyButton={{eq field "key_id"}}
/>
{{/each}}
{{#if @key.privateKey}}
{{#if @key.private_key}}
<InfoTableRow @label="Private key">
<CertificateCard @data={{@key.privateKey}} />
<CertificateCard @data={{@key.private_key}} />
</InfoTableRow>
{{/if}}
</div>

View File

@ -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<Args> {
@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);
}
}
}

View File

@ -3,7 +3,7 @@
SPDX-License-Identifier: BUSL-1.1
}}
<PkiPaginatedList @backend={{@backend}} @listRoute="keys.index" @list={{@keyModels}} @hasConfig={{@hasConfig}}>
<PkiPaginatedList @backend={{@backend}} @listRoute="keys.index" @list={{@keys}} @hasConfig={{@hasConfig}}>
<:actions>
{{#if @canImportKey}}
<ToolbarLink @route="keys.import" @model={{@backend}} @type="upload" data-test-pki-key-import>
@ -25,19 +25,19 @@
{{#each keys as |pkiKey|}}
<LinkedBlock
class="list-item-row"
@params={{array "keys.key.details" @backend pkiKey.keyId}}
@params={{array "keys.key.details" @backend pkiKey.key_id}}
@linkPrefix={{@mountPoint}}
>
<div class="level is-mobile">
<div class="level-left">
<div>
<Icon @name="certificate" class="has-text-grey-light" />
<span class="has-text-weight-semibold is-underline" data-test-key={{if pkiKey.keyName "name" "id"}}>
{{or pkiKey.keyName pkiKey.id}}
<span class="has-text-weight-semibold is-underline" data-test-key={{if pkiKey.key_name "name" "id"}}>
{{or pkiKey.key_name pkiKey.key_id}}
</span>
<div class="is-flex-row has-left-margin-l has-top-margin-xs">
{{#if pkiKey.keyName}}
<Hds::Badge @text={{pkiKey.id}} data-test-key="id" />
{{#if pkiKey.key_name}}
<Hds::Badge @text={{pkiKey.key_id}} data-test-key="id" />
{{/if}}
</div>
</div>
@ -55,16 +55,20 @@
{{#if @canRead}}
<dd.Interactive
@route="keys.key.details"
@models={{array @backend pkiKey.keyId}}
@models={{array @backend pkiKey.key_id}}
data-test-key-menu-link="details"
>Details</dd.Interactive>
>
Details
</dd.Interactive>
{{/if}}
{{#if @canEdit}}
<dd.Interactive
@route="keys.key.edit"
@models={{array @backend pkiKey.keyId}}
@models={{array @backend pkiKey.key_id}}
data-test-key-menu-link="edit"
>Edit</dd.Interactive>
>
Edit
</dd.Interactive>
{{/if}}
</Hds::Dropdown>
{{/if}}

View File

@ -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;

View File

@ -4,28 +4,29 @@
}}
{{! private_key is only available after initial save }}
{{#if this.generatedKey.privateKey}}
{{#if this.generatedKey.private_key}}
<Page::PkiKeyDetails
@key={{this.generatedKey}}
@canDelete={{this.generatedKey.canDelete}}
@canEdit={{this.generatedKey.canEdit}}
@canDelete={{this.generatedKeyCapabilities.canDelete}}
@canEdit={{this.generatedKeyCapabilities.canUpdate}}
/>
{{else}}
<form {{on "submit" (perform this.save)}}>
<div class="box is-sideless is-fullwidth is-marginless">
<MessageError @errorMessage={{this.errorBanner}} class="has-top-margin-s" />
<NamespaceReminder @mode={{if @model.isNew "generate" "update"}} @noun="PKI key" />
{{#if @model.isNew}}
{{#each @model.formFieldGroups as |fieldGroup|}}
<NamespaceReminder @mode={{if @form.isNew "generate" "update"}} @noun="PKI key" />
{{#if @form.isNew}}
{{#each @form.formFieldGroups as |fieldGroup|}}
{{#each-in fieldGroup as |group fields|}}
{{#if (eq group "Key parameters")}}
<PkiKeyParameters @model={{@model}} @fields={{fields}} @modelValidations={{this.modelValidations}} />
<PkiKeyParameters @model={{@form}} @fields={{fields}} @modelValidations={{this.modelValidations}} />
{{else}}
{{#each fields as |attr|}}
{{#each fields as |field|}}
<FormField
data-test-field={{attr}}
@attr={{attr}}
@model={{@model}}
data-test-field={{field}}
@attr={{field}}
@model={{@form}}
@modelValidations={{this.modelValidations}}
@showHelpText={{false}}
/>
@ -34,18 +35,25 @@
{{/each-in}}
{{/each}}
{{else}}
{{! only key name is edit-able }}
{{#let (find-by "name" "keyName" @model.formFields) as |keyName|}}
<FormField data-test-field={{keyName}} @attr={{keyName}} @model={{@model}} @showHelpText={{false}} />
{{/let}}
{{#let (find-by "name" "keyType" @model.formFields) as |keyType|}}
<ReadonlyFormField @attr={{keyType}} @value={{@model.keyType}} />
{{/let}}
{{#each @form.formFields as |field|}}
{{! only key name is edit-able }}
{{#if (eq field.name "key_name")}}
<FormField
data-test-field={{field}}
@attr={{field}}
@model={{@form}}
@modelValidations={{this.modelValidations}}
@showHelpText={{false}}
/>
{{else}}
<ReadonlyFormField @attr={{field}} @value={{get @form.data field.name}} />
{{/if}}
{{/each}}
{{/if}}
</div>
<Hds::ButtonSet class="has-top-padding-s">
<Hds::Button
@text={{if @model.isNew "Generate key" "Edit key"}}
@text={{if @form.isNew "Generate key" "Edit key"}}
@icon={{if this.save.isRunning "loading"}}
type="submit"
disabled={{this.save.isRunning}}
@ -55,7 +63,7 @@
@text="Cancel"
@color="secondary"
disabled={{this.save.isRunning}}
{{on "click" @onCancel}}
{{on "click" this.onCancel}}
data-test-cancel
/>
</Hds::ButtonSet>

View File

@ -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
* <PkiKeyForm @model={{this.model}} @onCancel={{transition-to "vault.cluster"}} @onSave={{transition-to "vault.cluster"}} />
* ```
*
* @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<Args> {
export default class PkiKeyFormComponent extends Component<Args> {
@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
);
}
}
}

View File

@ -12,9 +12,7 @@
Importing a PKI key
</DocLink>
</p>
{{#let (find-by "name" "keyName" @model.formFields) as |attr|}}
<FormField data-test-field={{attr}} @attr={{attr}} @model={{@model}} @showHelpText={{false}} />
{{/let}}
<FormField data-test-field="keyName" @attr={{this.keyNameField}} @model={{this}} @showHelpText={{false}} />
<TextFile @onChange={{this.onFileUploaded}} @label="PEM Bundle" data-test-pki-key-file />
</div>
<Hds::ButtonSet class="has-top-padding-s">
@ -29,7 +27,7 @@
@text="Cancel"
@color="secondary"
disabled={{this.submitForm.isRunning}}
{{on "click" this.cancel}}
{{on "click" @onCancel}}
data-test-cancel
/>
</Hds::ButtonSet>

View File

@ -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
* <PkiKeyImport @model={{this.model}} />
* ```
*
* @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<Args> {
@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();
}
}

View File

@ -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<Args> {
// 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<Args> {
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<Args> {
@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]);

View File

@ -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) {

View File

@ -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 = [

View File

@ -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) {

View File

@ -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,
};
}
}

View File

@ -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 },
];
}
}

View File

@ -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 },
];
}
}

View File

@ -14,8 +14,4 @@
</p.levelLeft>
</PageHeader>
<PkiKeyForm
@model={{this.model}}
@onCancel={{transition-to "vault.cluster.secrets.backend.pki.keys.index"}}
@onSave={{transition-to "vault.cluster.secrets.backend.pki.keys.key.details" this.model.id}}
/>
<PkiKeyForm @form={{this.model}} />

View File

@ -15,7 +15,6 @@
</PageHeader>
<PkiKeyImport
@model={{this.model}}
@onCancel={{transition-to "vault.cluster.secrets.backend.pki.keys.index"}}
@onSave={{transition-to "vault.cluster.secrets.backend.pki.keys.key.details" this.model.id}}
/>

View File

@ -6,12 +6,12 @@
<PkiPageHeader @backend={{this.model.parentModel}} />
<Page::PkiKeyList
@keyModels={{this.model.keyModels}}
@keys={{this.model.keys}}
@mountPoint={{this.mountPoint}}
@backend={{this.model.parentModel.id}}
@canImportKey={{(get (get this.model.keyModels 0) "canImportKey")}}
@canGenerateKey={{(get (get this.model.keyModels 0) "canGenerateKey")}}
@canRead={{(get (get this.model.keyModels 0) "canRead")}}
@canEdit={{(get (get this.model.keyModels 0) "canEdit")}}
@canImportKey={{this.model.canImportKey}}
@canGenerateKey={{this.model.canGenerateKey}}
@canRead={{this.model.canRead}}
@canEdit={{this.model.canEdit}}
@hasConfig={{this.model.hasConfig}}
/>

View File

@ -15,4 +15,4 @@
</p.levelLeft>
</PageHeader>
<Page::PkiKeyDetails @key={{this.model}} @canDelete={{this.model.canDelete}} @canEdit={{this.model.canEdit}} />
<Page::PkiKeyDetails @key={{this.model.key}} @canDelete={{this.model.canDelete}} @canEdit={{this.model.canUpdate}} />

View File

@ -15,8 +15,4 @@
</p.levelLeft>
</PageHeader>
<PkiKeyForm
@model={{this.model}}
@onCancel={{transition-to "vault.cluster.secrets.backend.pki.keys.key.details" this.model.id}}
@onSave={{transition-to "vault.cluster.secrets.backend.pki.keys.key.details" this.model.id}}
/>
<PkiKeyForm @form={{this.model}} />

View File

@ -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');
});
});
});

View File

@ -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(

View File

@ -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`
<Page::PkiKeyDetails
@key={{this.key}}
@canDelete={{this.canDelete}}
@canEdit={{this.canEdit}}
/>
`,
{ 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`
<Page::PkiKeyDetails
@key={{this.model}}
@canDelete={{true}}
@canEdit={{true}}
/>
`,
{ 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`
<Page::PkiKeyDetails
@key={{this.model}}
@canDelete={{false}}
@canEdit={{false}}
/>
`,
{ 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 <CertificateCard> component when there is a private key', async function (assert) {
this.model.privateKey = 'private-key-value';
await render(
hbs`
<Page::PkiKeyDetails
@key={{this.model}}
@canDelete={{false}}
@canEdit={{false}}
/>
`,
{ 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');
});
});

View File

@ -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`
<Page::PkiKeyList
@keys={{this.keys}}
@mountPoint="vault.cluster.secrets.backend.pki"
@canImportKey={{this.canImportKey}}
@canGenerateKey={{this.canGenerateKey}}
@canRead={{this.canRead}}
@canEdit={{this.canEdit}}
/>,
`,
{ 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`
<Page::PkiKeyList
@keyModels={{this.keyModels}}
@mountPoint="vault.cluster.secrets.backend.pki"
@canImportKey={{true}}
@canGenerateKey={{true}}
/>,
`,
{ 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`
<Page::PkiKeyList
@keyModels={{this.keyModels}}
@mountPoint="vault.cluster.secrets.backend.pki"
@canImportKey={{true}}
@canGenerateKey={{true}}
@canRead={{true}}
@canEdit={{true}}
/>,
`,
{ 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`
<Page::PkiKeyList
@keyModels={{this.keyModels}}
@mountPoint="vault.cluster.secrets.backend.pki"
@canImportKey={{false}}
@canGenerateKey={{false}}
@canRead={{false}}
@canEdit={{false}}
/>,
`,
{ 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');

View File

@ -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`<PkiKeyForm @form={{this.form}} />`, { owner: this.engine });
});
test('it should render fields and show validation messages', async function (assert) {
assert.expect(7);
await render(
hbs`
<PkiKeyForm
@model={{this.model}}
@onCancel={{this.onCancel}}
@onSave={{this.onSave}}
/>
`,
{ 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`
<PkiKeyForm
@model={{this.model}}
@onCancel={{this.onCancel}}
@onSave={{this.onSave}}
/>
`,
{ 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`
<PkiKeyForm
@model={{this.model}}
@onCancel={{this.onCancel}}
@onSave={{this.onSave}}
/>
`,
{ 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'
);
});
});