mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-05 20:36:26 +02:00
* 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:
parent
9f946960bc
commit
35c746b2cf
72
ui/app/forms/secrets/pki/key.ts
Normal file
72
ui/app/forms/secrets/pki/key.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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]);
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 = [
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 },
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 },
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -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}} />
|
||||
@ -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}}
|
||||
/>
|
||||
@ -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}}
|
||||
/>
|
||||
@ -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}} />
|
||||
@ -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}} />
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user