mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-28 04:10:44 -04:00
* VAULT-44904 - edm for keymgmt key views * resolved pr review comments * moved distribution fields to component and added util tests * fixed review comments * updated key-edit component to use form and fixed failing tests * VAULT-44905 - edm keymgmt provider views --------- Co-authored-by: mohit-hashicorp <mohit.ojha@hashicorp.com> Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
parent
86e46a1691
commit
3679be155f
14 changed files with 726 additions and 159 deletions
|
|
@ -15,16 +15,15 @@
|
|||
<div class="field" data-test-keymgmt-dist-key>
|
||||
<SearchSelect
|
||||
@id="key"
|
||||
@models={{array "keymgmt/key"}}
|
||||
@options={{this.keyOptions}}
|
||||
@onChange={{this.handleKeySelect}}
|
||||
@passObject={{true}}
|
||||
@inputValue={{this.formData.key}}
|
||||
@subText="Type to use the name of an existing key that you’d like to add to this provider, or to create one."
|
||||
@wildcardLabel="key"
|
||||
@label="Key name"
|
||||
@fallbackComponent="string-list"
|
||||
@fallbackComponent={{unless this.canListKeys "string-list"}}
|
||||
@selectLimit="1"
|
||||
@backend={{@backend}}
|
||||
@disallowNewItems={{false}}
|
||||
>
|
||||
{{#if (and this.validMatchError.key (not this.isNewKey))}}
|
||||
|
|
@ -94,15 +93,14 @@
|
|||
<div class="field">
|
||||
<SearchSelect
|
||||
@id="provider"
|
||||
@models={{array "keymgmt/provider"}}
|
||||
@options={{this.providerOptions}}
|
||||
@onChange={{this.handleProvider}}
|
||||
@passObject={{false}}
|
||||
@inputValue={{this.formData.provider}}
|
||||
@subText="Select a provider in Vault. If it doesn’t exist yet, you’ll need to add it first."
|
||||
@label="Provider"
|
||||
@fallbackComponent="input-search"
|
||||
@fallbackComponent={{unless this.canListProviders "input-search"}}
|
||||
@selectLimit="1"
|
||||
@backend={{@backend}}
|
||||
@disallowNewItems={{true}}
|
||||
data-test-keymgmt-dist-provider
|
||||
>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,11 @@ import { service } from '@ember/service';
|
|||
import { tracked } from '@glimmer/tracking';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { waitFor } from '@ember/test-waiters';
|
||||
import { KeyManagementUpdateKeyRequestTypeEnum } from '@hashicorp/vault-client-typescript';
|
||||
import {
|
||||
KeyManagementUpdateKeyRequestTypeEnum,
|
||||
SecretsApiKeyManagementListKeysListEnum,
|
||||
SecretsApiKeyManagementListKmsProvidersListEnum,
|
||||
} from '@hashicorp/vault-client-typescript';
|
||||
|
||||
const KEY_TYPES = Object.values(KeyManagementUpdateKeyRequestTypeEnum);
|
||||
|
||||
|
|
@ -21,7 +25,7 @@ const KEY_TYPES = Object.values(KeyManagementUpdateKeyRequestTypeEnum);
|
|||
* ```js
|
||||
* <KeymgmtDistribute @backend="keymgmt" @key="my-key" @provider="my-kms" />
|
||||
* ```
|
||||
* @param {string} backend - name of backend, which will be the basis of other store queries
|
||||
* @param {string} backend - name of backend, which is used in API requests
|
||||
* @param {string} [key] - key is the name of the existing key which is being distributed. Will hide the key field in UI
|
||||
* @param {string} [provider] - provider is the name of the existing provider which is being distributed to. Will hide the provider field in UI
|
||||
*/
|
||||
|
|
@ -39,14 +43,16 @@ const VALID_TYPES_BY_PROVIDER = {
|
|||
azurekeyvault: ['rsa-2048', 'rsa-3072', 'rsa-4096'],
|
||||
};
|
||||
export default class KeymgmtDistribute extends Component {
|
||||
@service store;
|
||||
@service api;
|
||||
@service flashMessages;
|
||||
@service router;
|
||||
|
||||
@tracked keyModel;
|
||||
@tracked isNewKey = false;
|
||||
@tracked keyOptions = [];
|
||||
@tracked canListKeys = true;
|
||||
@tracked providerType;
|
||||
@tracked providerOptions = [];
|
||||
@tracked canListProviders = true;
|
||||
@tracked formData;
|
||||
@tracked formErrors;
|
||||
|
||||
|
|
@ -59,9 +65,13 @@ export default class KeymgmtDistribute extends Component {
|
|||
// Side effects to get types of key or provider passed in
|
||||
if (this.args.provider) {
|
||||
this.getProviderType(this.args.provider);
|
||||
} else {
|
||||
this.fetchProviderOptions();
|
||||
}
|
||||
if (this.args.key) {
|
||||
this.getKeyInfo(this.args.key);
|
||||
} else {
|
||||
this.fetchKeyOptions();
|
||||
}
|
||||
this.formData.operations = [];
|
||||
}
|
||||
|
|
@ -145,19 +155,52 @@ export default class KeymgmtDistribute extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
async fetchKeyOptions() {
|
||||
try {
|
||||
const { keys } = await this.api.secrets.keyManagementListKeys(
|
||||
this.args.backend,
|
||||
SecretsApiKeyManagementListKeysListEnum.TRUE
|
||||
);
|
||||
this.keyOptions = (keys || []).map((name) => ({ id: name, name }));
|
||||
this.canListKeys = true;
|
||||
} catch (error) {
|
||||
const { status } = await this.api.parseError(error);
|
||||
if (status === 403) {
|
||||
this.canListKeys = false;
|
||||
}
|
||||
this.keyOptions = [];
|
||||
}
|
||||
}
|
||||
|
||||
async getProviderType(id) {
|
||||
if (!id) {
|
||||
this.providerType = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const provider = await this.store
|
||||
.queryRecord('keymgmt/provider', {
|
||||
backend: this.args.backend,
|
||||
id,
|
||||
})
|
||||
.catch(() => {});
|
||||
this.providerType = provider?.provider;
|
||||
try {
|
||||
const { data } = await this.api.secrets.keyManagementReadKmsProvider(id, this.args.backend);
|
||||
this.providerType = data.provider;
|
||||
} catch {
|
||||
this.providerType = '';
|
||||
}
|
||||
}
|
||||
|
||||
async fetchProviderOptions() {
|
||||
try {
|
||||
const { keys } = await this.api.secrets.keyManagementListKmsProviders(
|
||||
this.args.backend,
|
||||
SecretsApiKeyManagementListKmsProvidersListEnum.TRUE
|
||||
);
|
||||
this.providerOptions = (keys || []).map((name) => ({ id: name, name }));
|
||||
this.canListProviders = true;
|
||||
} catch (error) {
|
||||
const { status } = await this.api.parseError(error);
|
||||
if (status === 403) {
|
||||
this.canListProviders = false;
|
||||
}
|
||||
this.providerOptions = [];
|
||||
}
|
||||
}
|
||||
|
||||
destroyKey() {
|
||||
|
|
@ -224,13 +267,33 @@ export default class KeymgmtDistribute extends Component {
|
|||
|
||||
@action
|
||||
async handleKeySelect(selected) {
|
||||
const selectedKey = selected[0] || null;
|
||||
if (!selectedKey) {
|
||||
let keyName;
|
||||
let isNew = false;
|
||||
|
||||
if (typeof selected === 'string') {
|
||||
keyName = selected;
|
||||
} else if (Array.isArray(selected)) {
|
||||
const selectedKey = selected[0] || null;
|
||||
if (!selectedKey) {
|
||||
this.formData.key = null;
|
||||
return this.destroyKey();
|
||||
}
|
||||
|
||||
if (typeof selectedKey === 'string') {
|
||||
keyName = selectedKey;
|
||||
} else {
|
||||
keyName = selectedKey.id;
|
||||
isNew = !!selectedKey.isNew;
|
||||
}
|
||||
}
|
||||
|
||||
if (!keyName) {
|
||||
this.formData.key = null;
|
||||
return this.destroyKey();
|
||||
}
|
||||
this.formData.key = selectedKey.id;
|
||||
return this.getKeyInfo(selectedKey.id, selectedKey.isNew);
|
||||
|
||||
this.formData.key = keyName;
|
||||
return this.getKeyInfo(keyName, isNew);
|
||||
}
|
||||
|
||||
@task
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { action } from '@ember/object';
|
|||
import { tracked } from '@glimmer/tracking';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { waitFor } from '@ember/test-waiters';
|
||||
import { isValidProvider } from 'vault/utils/keymgmt-provider-validator';
|
||||
import { isValidProvider } from 'vault/utils/keymgmt-provider-utils';
|
||||
|
||||
/**
|
||||
* @module KeymgmtKeyEdit
|
||||
|
|
|
|||
|
|
@ -10,20 +10,24 @@
|
|||
</Page::Header>
|
||||
|
||||
{{#if this.isDistributing}}
|
||||
<Keymgmt::Distribute @backend={{@model.backend}} @provider={{@model.id}} @onClose={{fn (mut this.isDistributing) false}} />
|
||||
<Keymgmt::Distribute
|
||||
@backend={{@form.data.backend}}
|
||||
@provider={{@form.data.name}}
|
||||
@onClose={{fn (mut this.isDistributing) false}}
|
||||
/>
|
||||
{{else}}
|
||||
{{#if this.isShowing}}
|
||||
<div class="tabs-container box is-sideless is-fullwidth is-paddingless is-marginless">
|
||||
<nav class="tabs" aria-label="navigation for managing providers">
|
||||
<ul>
|
||||
<li class={{unless this.viewingKeys "active"}} data-test-kms-provider-tab="details">
|
||||
<LinkTo @route="vault.cluster.secrets.backend.show" @model={{@model.id}} @query={{hash tab=""}}>
|
||||
<LinkTo @route="vault.cluster.secrets.backend.show" @model={{@form.data.name}} @query={{hash tab=""}}>
|
||||
Details
|
||||
</LinkTo>
|
||||
</li>
|
||||
{{#if @model.canListKeys}}
|
||||
{{#if @capabilities.canListKeys}}
|
||||
<li class={{if this.viewingKeys "active"}} data-test-kms-provider-tab="keys">
|
||||
<LinkTo @route="vault.cluster.secrets.backend.show" @model={{@model.id}} @query={{hash tab="keys"}}>
|
||||
<LinkTo @route="vault.cluster.secrets.backend.show" @model={{@form.data.name}} @query={{hash tab="keys"}}>
|
||||
Keys
|
||||
</LinkTo>
|
||||
</li>
|
||||
|
|
@ -34,10 +38,10 @@
|
|||
{{#unless this.viewingKeys}}
|
||||
<Toolbar
|
||||
data-test-kms-provider-details-actions
|
||||
aria-label="options for managing for key/management provider {{@model.id}}"
|
||||
aria-label="options for managing for key/management provider {{@form.data.name}}"
|
||||
>
|
||||
<ToolbarActions>
|
||||
{{#if @model.canDelete}}
|
||||
{{#if @capabilities.canDelete}}
|
||||
<ConfirmAction
|
||||
@buttonText="Delete provider"
|
||||
class="toolbar-button"
|
||||
|
|
@ -45,20 +49,20 @@
|
|||
@confirmTitle="Delete this provider?"
|
||||
@onConfirmAction={{this.onDelete}}
|
||||
@disabledMessage={{if
|
||||
@model.keys.length
|
||||
this.keyCount
|
||||
(concat
|
||||
"This provider cannot be deleted until all "
|
||||
@model.keys.length
|
||||
this.keyCount
|
||||
" key(s) distributed to it are revoked. This can be done from the Keys tab."
|
||||
)
|
||||
}}
|
||||
data-test-kms-provider-delete
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if (and @model.canDelete (or @model.canListKeys @model.canEdit))}}
|
||||
{{#if (and @capabilities.canDelete (or @capabilities.canListKeys @capabilities.canEdit))}}
|
||||
<div class="toolbar-separator"></div>
|
||||
{{/if}}
|
||||
{{#if (or @model.canListKeys @model.canCreateKeys)}}
|
||||
{{#if (or @capabilities.canListKeys @capabilities.canCreateKeys)}}
|
||||
<Hds::Button
|
||||
@text="Distribute key"
|
||||
@icon="chevron-right"
|
||||
|
|
@ -69,13 +73,13 @@
|
|||
data-test-distribute-key
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if @model.canEdit}}
|
||||
{{#if @capabilities.canEdit}}
|
||||
<ToolbarSecretLink
|
||||
@secret={{@model.id}}
|
||||
@secret={{@form.data.name}}
|
||||
@mode="edit"
|
||||
@replace={{true}}
|
||||
@queryParams={{hash itemType="provider"}}
|
||||
disabled={{(not @model.canEdit)}}
|
||||
disabled={{(not @capabilities.canEdit)}}
|
||||
>
|
||||
Update credentials
|
||||
</ToolbarSecretLink>
|
||||
|
|
@ -87,16 +91,21 @@
|
|||
<form aria-label="update credentials" {{on "submit" this.onSave}}>
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
{{#if this.isCreating}}
|
||||
{{#each @model.createFields as |attr index|}}
|
||||
{{#each @form.createFields as |attr index|}}
|
||||
{{#if (eq index 2)}}
|
||||
<div class="has-border-top-light">
|
||||
<h2 class="title is-5 has-top-margin-l has-bottom-margin-m" data-test-kms-provider-config-title>
|
||||
Provider configuration
|
||||
</h2>
|
||||
</div>
|
||||
{{#if @model.provider}}
|
||||
{{#if @form.data.provider}}
|
||||
{{! Only show last field if provider selected }}
|
||||
<FormField @attr={{attr}} @model={{@model}} @modelValidations={{this.modelValidations}} />
|
||||
<FormField
|
||||
@attr={{attr}}
|
||||
@model={{@form}}
|
||||
@modelValidations={{this.modelValidations}}
|
||||
@onChange={{this.onFieldChange}}
|
||||
/>
|
||||
{{else}}
|
||||
<Hds::ApplicationState class="top-padding-32 bottom-padding-32 is-marginless" as |A|>
|
||||
<A.Header @title="No provider selected" @titleTag="h2" data-test-empty-state-title />
|
||||
|
|
@ -104,7 +113,12 @@
|
|||
</Hds::ApplicationState>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<FormField @attr={{attr}} @model={{@model}} @modelValidations={{this.modelValidations}} />
|
||||
<FormField
|
||||
@attr={{attr}}
|
||||
@model={{@form}}
|
||||
@modelValidations={{this.modelValidations}}
|
||||
@onChange={{this.onFieldChange}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
|
@ -116,8 +130,13 @@
|
|||
Old credentials cannot be read and will be lost as soon as new ones are added. Do this carefully.
|
||||
</p>
|
||||
{{/unless}}
|
||||
{{#each @model.credentialFields as |cred|}}
|
||||
<FormField @attr={{cred}} @model={{@model}} @modelValidations={{this.modelValidations}} />
|
||||
{{#each @form.credentialFields as |cred|}}
|
||||
<FormField
|
||||
@attr={{cred}}
|
||||
@model={{@form}}
|
||||
@modelValidations={{this.modelValidations}}
|
||||
@onChange={{this.onFieldChange}}
|
||||
/>
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class="field is-grouped box is-fullwidth is-bottomless">
|
||||
|
|
@ -133,7 +152,7 @@
|
|||
@text="Cancel"
|
||||
@color="secondary"
|
||||
@route={{if this.isCreating @root.path "vault.cluster.secrets.backend.show"}}
|
||||
@model={{if this.isCreating @root.model @model.id}}
|
||||
@model={{if this.isCreating @root.model @form.data.name}}
|
||||
@query={{if this.isCreating (hash tab="provider") (hash itemType="provider")}}
|
||||
disabled={{this.saveTask.isRunning}}
|
||||
data-test-kms-provider-cancel
|
||||
|
|
@ -147,8 +166,8 @@
|
|||
<div class="has-bottom-margin-s">
|
||||
{{#if this.viewingKeys}}
|
||||
{{#let (options-for-backend "keymgmt" "key") as |options|}}
|
||||
{{#if @model.keys.meta.total}}
|
||||
{{#each @model.keys as |key|}}
|
||||
{{#if @form.data.keys.meta.total}}
|
||||
{{#each @form.data.keys as |key|}}
|
||||
<SecretList::Item
|
||||
@item={{key}}
|
||||
@backendModel={{@root}}
|
||||
|
|
@ -161,11 +180,11 @@
|
|||
/>
|
||||
{{/each}}
|
||||
<Hds::Pagination::Numbered
|
||||
@currentPage={{@model.keys.meta.currentPage}}
|
||||
@currentPageSize={{@model.keys.meta.pageSize}}
|
||||
@currentPage={{@form.data.keys.meta.currentPage}}
|
||||
@currentPageSize={{@form.data.keys.meta.pageSize}}
|
||||
@route="vault.cluster.secrets.backend.show"
|
||||
@showSizeSelector={{false}}
|
||||
@totalItems={{@model.keys.meta.total}}
|
||||
@totalItems={{@form.data.keys.meta.total}}
|
||||
@onPageChange={{perform this.fetchKeys}}
|
||||
/>
|
||||
|
||||
|
|
@ -185,28 +204,28 @@
|
|||
{{/if}}
|
||||
{{/let}}
|
||||
{{else}}
|
||||
{{#each @model.showFields as |attr|}}
|
||||
{{#if attr.hasBlock}}
|
||||
<InfoTableRow @label={{attr.label}} @value={{attr.value}} data-test-kms-provider-field={{attr.name}}>
|
||||
{{#if attr.icon}}
|
||||
<Icon @name={{attr.icon}} class="icon" />
|
||||
{{/if}}
|
||||
{{#if attr.isLink}}
|
||||
<LinkTo @route="vault.cluster.secrets.backend.show" @model={{@model.id}} @query={{hash tab="keys"}}>
|
||||
{{attr.value}}
|
||||
{{#each this.displayFields as |field|}}
|
||||
{{#if (eq field "provider")}}
|
||||
<InfoTableRow
|
||||
@label={{this.label field}}
|
||||
@value={{this.providerTypeName @form.data.provider}}
|
||||
data-test-kms-provider-field={{field}}
|
||||
>
|
||||
<Icon @name={{this.providerIcon @form.data.provider}} class="icon" />
|
||||
{{this.providerTypeName @form.data.provider}}
|
||||
</InfoTableRow>
|
||||
{{else if (eq field "keys")}}
|
||||
<InfoTableRow @label={{this.label field}} @value={{this.keysValue}} data-test-kms-provider-field={{field}}>
|
||||
{{#if this.keyCount}}
|
||||
<LinkTo @route="vault.cluster.secrets.backend.show" @model={{@form.data.name}} @query={{hash tab="keys"}}>
|
||||
{{this.keysValue}}
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
{{attr.value}}
|
||||
{{this.keysValue}}
|
||||
{{/if}}
|
||||
</InfoTableRow>
|
||||
{{else}}
|
||||
<InfoTableRow
|
||||
@alwaysRender={{true}}
|
||||
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
|
||||
@value={{get @model attr.name}}
|
||||
@defaultShown={{attr.options.defaultShown}}
|
||||
@formatDate={{if (eq attr.type "date") "MMM d yyyy, h:mm:ss aaa"}}
|
||||
/>
|
||||
<InfoTableRow @alwaysRender={{true}} @label={{this.label field}} @value={{get @form.data field}} />
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ import { action } from '@ember/object';
|
|||
import { tracked } from '@glimmer/tracking';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { waitFor } from '@ember/test-waiters';
|
||||
import { removeFromArray } from 'vault/helpers/remove-from-array';
|
||||
import { paginate } from 'core/utils/paginate-list';
|
||||
import { getKeymgmtProviderIcon } from 'vault/utils/keymgmt-provider-utils';
|
||||
import { SecretsApiKeyManagementListKeysInKmsProviderListEnum } from '@hashicorp/vault-client-typescript';
|
||||
|
||||
/**
|
||||
* @module KeymgmtProviderEdit
|
||||
|
|
@ -17,17 +19,23 @@ import { removeFromArray } from 'vault/helpers/remove-from-array';
|
|||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <KeymgmtProviderEdit @model={model} @mode="show" />
|
||||
* <KeymgmtProviderEdit @form={form} @mode="show" />
|
||||
* ```
|
||||
* @param {object} model - model is the data from the store
|
||||
* @param {object} form - form is the data backing create/edit/show
|
||||
* @param {string} mode - mode controls which view is shown on the component - show | create |
|
||||
* @param {string} [tab] - Options are "details" or "keys" for the show mode only
|
||||
*/
|
||||
|
||||
export default class KeymgmtProviderEdit extends Component {
|
||||
@service api;
|
||||
@service capabilities;
|
||||
@service router;
|
||||
@service flashMessages;
|
||||
|
||||
@tracked modelValidations;
|
||||
@tracked invalidFormAlert;
|
||||
@tracked isDistributing = false;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
// key count displayed in details tab and keys are listed in keys tab
|
||||
|
|
@ -36,25 +44,62 @@ export default class KeymgmtProviderEdit extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
@tracked modelValidations;
|
||||
displayFields = ['name', 'provider', 'key_collection', 'keys'];
|
||||
|
||||
breadcrumbs = [
|
||||
{
|
||||
label: 'Vault',
|
||||
icon: 'vault',
|
||||
route: 'vault.cluster.dashboard',
|
||||
},
|
||||
{
|
||||
label: 'Secrets engines',
|
||||
route: 'vault.cluster.secrets.backends',
|
||||
},
|
||||
{
|
||||
label: this.args.model.backend,
|
||||
route: 'vault.cluster.secrets.backend.list-root',
|
||||
model: this.args.model.backend,
|
||||
},
|
||||
{ label: this.title },
|
||||
];
|
||||
label(field) {
|
||||
const labels = {
|
||||
name: 'Provider name',
|
||||
provider: 'Type',
|
||||
key_collection: 'Key Vault instance name',
|
||||
keys: 'Keys',
|
||||
};
|
||||
return labels[field] || field;
|
||||
}
|
||||
|
||||
providerTypeName(provider) {
|
||||
return (
|
||||
{
|
||||
azurekeyvault: 'Azure Key Vault',
|
||||
awskms: 'AWS Key Management Service',
|
||||
gcpckms: 'Google Cloud Key Management Service',
|
||||
}[provider] || provider
|
||||
);
|
||||
}
|
||||
|
||||
providerIcon(provider) {
|
||||
return getKeymgmtProviderIcon(provider);
|
||||
}
|
||||
|
||||
get keyCount() {
|
||||
return this.args.form.data.keys?.length || 0;
|
||||
}
|
||||
|
||||
get keysValue() {
|
||||
if (this.keyCount) {
|
||||
return `${this.keyCount} ${this.keyCount > 1 ? 'keys' : 'key'}`;
|
||||
}
|
||||
return this.args.capabilities?.canListKeys ? 'None' : 'You do not have permission to list keys';
|
||||
}
|
||||
|
||||
get breadcrumbs() {
|
||||
return [
|
||||
{
|
||||
label: 'Vault',
|
||||
icon: 'vault',
|
||||
route: 'vault.cluster.dashboard',
|
||||
},
|
||||
{
|
||||
label: 'Secrets engines',
|
||||
route: 'vault.cluster.secrets.backends',
|
||||
},
|
||||
{
|
||||
label: this.args.form.data.backend,
|
||||
route: 'vault.cluster.secrets.backend.list-root',
|
||||
model: this.args.form.data.backend,
|
||||
},
|
||||
{ label: this.title },
|
||||
];
|
||||
}
|
||||
|
||||
get title() {
|
||||
if (this.isDistributing) {
|
||||
|
|
@ -69,7 +114,7 @@ export default class KeymgmtProviderEdit extends Component {
|
|||
}
|
||||
|
||||
get subtitle() {
|
||||
return this.isShowing ? this.args.model.id : '';
|
||||
return this.isShowing ? this.args.form.data.name : '';
|
||||
}
|
||||
|
||||
get isShowing() {
|
||||
|
|
@ -85,30 +130,81 @@ export default class KeymgmtProviderEdit extends Component {
|
|||
@task
|
||||
@waitFor
|
||||
*saveTask() {
|
||||
const { model } = this.args;
|
||||
const { form } = this.args;
|
||||
const { backend, name, provider, key_collection, credentials } = form.data;
|
||||
try {
|
||||
yield model.save();
|
||||
this.router.transitionTo('vault.cluster.secrets.backend.show', model.id, {
|
||||
yield this.api.secrets.keyManagementWriteKmsProvider(name, backend, {
|
||||
provider,
|
||||
key_collection,
|
||||
credentials,
|
||||
});
|
||||
|
||||
this.router.transitionTo('vault.cluster.secrets.backend.show', name, {
|
||||
queryParams: { itemType: 'provider' },
|
||||
});
|
||||
} catch (error) {
|
||||
this.flashMessages.danger(error.errors.join('. '));
|
||||
const { message } = yield this.api.parseError(error);
|
||||
this.flashMessages.danger(message);
|
||||
}
|
||||
}
|
||||
|
||||
@task
|
||||
@waitFor
|
||||
*fetchKeys(page) {
|
||||
const { form, capabilities } = this.args;
|
||||
const backend = form.data.backend;
|
||||
const providerName = form.data.name;
|
||||
|
||||
if (!capabilities?.canListKeys) {
|
||||
form.data.keys = [];
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
yield this.args.model.fetchKeys(page);
|
||||
const { keys } = yield this.api.secrets.keyManagementListKeysInKmsProvider(
|
||||
providerName,
|
||||
backend,
|
||||
SecretsApiKeyManagementListKeysInKmsProviderListEnum.TRUE
|
||||
);
|
||||
|
||||
const keyNames = keys || [];
|
||||
const pathsToFetch = keyNames.map((keyName) =>
|
||||
this.capabilities.pathFor('keymgmtKey', { backend, name: keyName })
|
||||
);
|
||||
const keyCapabilities = yield this.capabilities.fetch(pathsToFetch);
|
||||
|
||||
const keysList = keyNames.map((keyName) => {
|
||||
const keyPath = this.capabilities.pathFor('keymgmtKey', { backend, name: keyName });
|
||||
return {
|
||||
id: keyName,
|
||||
name: keyName,
|
||||
backend,
|
||||
icon: 'key',
|
||||
type: 'key',
|
||||
canRead: keyCapabilities[keyPath]?.canRead || false,
|
||||
canEdit: keyCapabilities[keyPath]?.canUpdate || false,
|
||||
canDelete: keyCapabilities[keyPath]?.canDelete || false,
|
||||
};
|
||||
});
|
||||
|
||||
form.data.keys = paginate(keysList, { page: Number(page) || 1 });
|
||||
} catch (error) {
|
||||
this.flashMessages.danger(error.errors.join('. '));
|
||||
const { message, status } = yield this.api.parseError(error);
|
||||
if (status === 404) {
|
||||
form.data.keys = [];
|
||||
} else {
|
||||
this.flashMessages.danger(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
async onSave(event) {
|
||||
event.preventDefault();
|
||||
const { isValid, state } = await this.args.model.validate();
|
||||
const { isValid, state, invalidFormMessage } = this.args.form.toJSON();
|
||||
this.modelValidations = isValid ? null : state;
|
||||
this.invalidFormAlert = invalidFormMessage;
|
||||
|
||||
if (isValid) {
|
||||
this.modelValidations = null;
|
||||
this.saveTask.perform();
|
||||
|
|
@ -116,24 +212,38 @@ export default class KeymgmtProviderEdit extends Component {
|
|||
this.modelValidations = state;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
onFieldChange(path) {
|
||||
if (path !== 'provider') return;
|
||||
|
||||
// Clear stale validation state on updating Type field so old provider errors do not persist.
|
||||
this.modelValidations = null;
|
||||
this.invalidFormAlert = null;
|
||||
}
|
||||
|
||||
@action
|
||||
async onDelete() {
|
||||
try {
|
||||
const { model, root } = this.args;
|
||||
await model.destroyRecord();
|
||||
const { form, root } = this.args;
|
||||
await this.api.secrets.keyManagementDeleteKmsProvider(form.data.name, form.data.backend);
|
||||
this.router.transitionTo(root.path, root.model, { queryParams: { tab: 'provider' } });
|
||||
} catch (error) {
|
||||
this.flashMessages.danger(error.errors.join('. '));
|
||||
const { message } = await this.api.parseError(error);
|
||||
this.flashMessages.danger(message);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
async onDeleteKey(model) {
|
||||
try {
|
||||
const providerKeys = removeFromArray(this.args.model.keys, model);
|
||||
await model.destroyRecord();
|
||||
this.args.model.keys = providerKeys;
|
||||
const backend = this.args.form.data.backend;
|
||||
const providerName = this.args.form.data.name;
|
||||
await this.api.secrets.keyManagementDeleteKeyInKmsProvider(model.id, providerName, backend);
|
||||
this.fetchKeys.perform(this.args.form.data.keys?.meta.currentPage || 1);
|
||||
} catch (error) {
|
||||
this.flashMessages.danger(error.errors.join('. '));
|
||||
const { message } = await this.api.parseError(error);
|
||||
this.flashMessages.danger(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,20 +53,27 @@ export default Controller.extend(ListController, BackendCrumbMixin, {
|
|||
});
|
||||
},
|
||||
|
||||
delete(item) {
|
||||
async delete(item) {
|
||||
const name = item.id;
|
||||
// Handle keymgmt keys (plain objects from API service)
|
||||
// Handle keymgmt list items (plain objects from API service)
|
||||
if (this.backendType === 'keymgmt' && item.type === 'key') {
|
||||
this.api.secrets
|
||||
.keyManagementDeleteKey(name, item.backend)
|
||||
.then(() => {
|
||||
this.flashMessages.success(`${name} was successfully deleted.`);
|
||||
this.send('reload');
|
||||
})
|
||||
.catch(async (e) => {
|
||||
const { message } = await this.api.parseError(e);
|
||||
this.flashMessages.danger(message);
|
||||
});
|
||||
try {
|
||||
await this.api.secrets.keyManagementDeleteKey(name, item.backend);
|
||||
this.flashMessages.success(`${name} was successfully deleted.`);
|
||||
this.send('reload');
|
||||
} catch (e) {
|
||||
const { message } = await this.api.parseError(e);
|
||||
this.flashMessages.danger(message);
|
||||
}
|
||||
} else if (this.backendType === 'keymgmt' && item.type === 'provider') {
|
||||
try {
|
||||
await this.api.secrets.keyManagementDeleteKmsProvider(name, item.backend);
|
||||
this.flashMessages.success(`${name} was successfully deleted.`);
|
||||
this.send('reload');
|
||||
} catch (e) {
|
||||
const { message } = await this.api.parseError(e);
|
||||
this.flashMessages.danger(message);
|
||||
}
|
||||
} else {
|
||||
// Handle Ember Data models
|
||||
item
|
||||
|
|
|
|||
161
ui/app/forms/keymgmt/provider.ts
Normal file
161
ui/app/forms/keymgmt/provider.ts
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2026
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import type { Validations } from 'vault/app-types';
|
||||
import { get } from '@ember/object';
|
||||
import Form from 'vault/forms/form';
|
||||
import FormField from 'vault/utils/forms/field';
|
||||
import FormFieldGroup from 'vault/utils/forms/field-group';
|
||||
import {
|
||||
KeyManagementWriteKmsProviderRequest,
|
||||
KeyManagementWriteKmsProviderRequestProviderEnum,
|
||||
} from '@hashicorp/vault-client-typescript';
|
||||
|
||||
type ProviderFormData = KeyManagementWriteKmsProviderRequest & {
|
||||
name: string;
|
||||
keys?: Array<Record<string, unknown>>;
|
||||
};
|
||||
|
||||
type ProviderCredentialValidationModel = {
|
||||
provider?: keyof typeof CRED_PROPS;
|
||||
credentialProps?: string[];
|
||||
credentials?: Record<string, string | undefined | null>;
|
||||
};
|
||||
|
||||
interface Validator {
|
||||
message: string;
|
||||
validator(model: ProviderCredentialValidationModel): boolean | string;
|
||||
}
|
||||
|
||||
export const PROVIDER_TYPES = Object.values(KeyManagementWriteKmsProviderRequestProviderEnum);
|
||||
|
||||
export const CRED_PROPS = {
|
||||
azurekeyvault: ['client_id', 'client_secret', 'tenant_id'],
|
||||
awskms: ['access_key', 'secret_key', 'session_token', 'endpoint'],
|
||||
gcpckms: ['service_account_file'],
|
||||
};
|
||||
|
||||
const CRED_PROPS_LABEL = {
|
||||
client_id: 'Client ID',
|
||||
client_secret: 'Client secret',
|
||||
tenant_id: 'Tenant ID',
|
||||
access_key: 'Access key',
|
||||
secret_key: 'Secret key',
|
||||
session_token: 'Session token',
|
||||
endpoint: 'Endpoint',
|
||||
service_account_file: 'Service account file',
|
||||
};
|
||||
|
||||
export const OPTIONAL_CRED_PROPS = ['session_token', 'endpoint'];
|
||||
|
||||
export default class KeymgmtProviderForm extends Form<ProviderFormData> {
|
||||
icon = 'key';
|
||||
idPrefix = 'provider/';
|
||||
type = 'provider';
|
||||
|
||||
constructor(...args: ConstructorParameters<typeof Form<ProviderFormData>>) {
|
||||
super(...args);
|
||||
|
||||
// Provider read responses do not include credentials, but the form binds nested
|
||||
// values like "credentials.client_id". Ensure the parent object always exists.
|
||||
if (!this.data.credentials) {
|
||||
this.data.credentials = {};
|
||||
}
|
||||
}
|
||||
|
||||
get credentialProps() {
|
||||
const provider = this.data.provider;
|
||||
if (!provider) return [];
|
||||
return CRED_PROPS[provider as KeyManagementWriteKmsProviderRequestProviderEnum] || [];
|
||||
}
|
||||
|
||||
get createFields() {
|
||||
return [
|
||||
new FormField('provider', 'string', {
|
||||
label: 'Type',
|
||||
subText: 'Choose the provider type.',
|
||||
possibleValues: PROVIDER_TYPES,
|
||||
noDefault: true,
|
||||
}),
|
||||
new FormField('name', 'string', {
|
||||
label: 'Provider name',
|
||||
subText:
|
||||
'This is the name of the provider that will be displayed in Vault. This cannot be edited later.',
|
||||
}),
|
||||
new FormField('key_collection', 'string', {
|
||||
label: 'Key Vault instance name',
|
||||
subText: 'The name of a Key Vault instance must be supplied. This cannot be edited later.',
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
get credentialFields() {
|
||||
return this.credentialProps.map((prop) => {
|
||||
const options = {
|
||||
label: CRED_PROPS_LABEL[prop as keyof typeof CRED_PROPS_LABEL],
|
||||
...(prop === 'service_account_file'
|
||||
? { subText: 'The path to a Google service account key file, not the file itself.' }
|
||||
: {}),
|
||||
};
|
||||
return new FormField(`credentials.${prop}`, 'string', options);
|
||||
});
|
||||
}
|
||||
|
||||
get formFieldGroups() {
|
||||
const groups: FormFieldGroup[] = [];
|
||||
|
||||
if (this.isNew) {
|
||||
// Create mode: provider type, name, key_collection, credentials
|
||||
groups.push(new FormFieldGroup('default', [...this.createFields]));
|
||||
|
||||
// Add credential fields based on provider type
|
||||
if (this.credentialProps.length > 0) {
|
||||
groups.push(new FormFieldGroup('credentials', this.credentialFields));
|
||||
}
|
||||
} else {
|
||||
// Edit mode: only credentials if any
|
||||
if (this.credentialProps.length > 0) {
|
||||
groups.push(new FormFieldGroup('credentials', this.credentialFields));
|
||||
}
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
// since we have dynamic credential attributes based on provider we need a dynamic presence validator
|
||||
// add validators for all cred props and return true for value if not associated with selected provider
|
||||
get credValidators() {
|
||||
return (Object.keys(CRED_PROPS) as Array<keyof typeof CRED_PROPS>).reduce(
|
||||
(obj, providerKey) => {
|
||||
CRED_PROPS[providerKey].forEach((prop: string) => {
|
||||
if (!OPTIONAL_CRED_PROPS.includes(prop)) {
|
||||
obj[`credentials.${prop}`] = [
|
||||
{
|
||||
message: `${CRED_PROPS_LABEL[prop as keyof typeof CRED_PROPS_LABEL]} is required`,
|
||||
validator(model: ProviderCredentialValidationModel) {
|
||||
const selectedProvider = model.provider;
|
||||
if (!selectedProvider) return true;
|
||||
|
||||
if (!CRED_PROPS[selectedProvider]?.includes(prop)) return true;
|
||||
|
||||
const value = get(model, `credentials.${prop}`);
|
||||
return typeof value === 'string' ? value.trim().length > 0 : Boolean(value);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
});
|
||||
return obj;
|
||||
},
|
||||
{} as Record<string, Validator[]>
|
||||
);
|
||||
}
|
||||
|
||||
validations: Validations = {
|
||||
name: [{ type: 'presence', message: 'Provider name is required' }],
|
||||
key_collection: [{ type: 'presence', message: 'Key Vault instance name is required' }],
|
||||
...this.credValidators,
|
||||
};
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import { hash } from 'rsvp';
|
|||
import { service } from '@ember/service';
|
||||
import EditBase from './secret-edit';
|
||||
import KeymgmtKeyForm from 'vault/forms/keymgmt/key';
|
||||
import KeymgmtProviderForm from 'vault/forms/keymgmt/provider';
|
||||
import { KeyManagementUpdateKeyRequestTypeEnum } from '@hashicorp/vault-client-typescript';
|
||||
|
||||
const secretModel = (store, backend, key) => {
|
||||
|
|
@ -40,6 +41,14 @@ export default EditBase.extend({
|
|||
return new KeymgmtKeyForm(defaultValues, { isNew: true });
|
||||
}
|
||||
|
||||
if (modelType === 'keymgmt/provider') {
|
||||
const defaultValues = {
|
||||
backend,
|
||||
credentials: {},
|
||||
};
|
||||
return new KeymgmtProviderForm(defaultValues, { isNew: true });
|
||||
}
|
||||
|
||||
if (modelType === 'role-ssh') {
|
||||
return this.store.createRecord(modelType, { keyType: 'ca' });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,9 +14,13 @@ import engineDisplayData from 'vault/helpers/engines-display-data';
|
|||
import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends';
|
||||
import { getEnginePathParam } from 'vault/utils/backend-route-helpers';
|
||||
import { getEffectiveEngineType } from 'vault/utils/external-plugin-helpers';
|
||||
import { getKeymgmtProviderIcon } from 'vault/utils/keymgmt-provider-utils';
|
||||
import { getModelTypeForEngine } from 'vault/utils/model-helpers/secret-engine-helpers';
|
||||
import { normalizePath } from 'vault/utils/path-encoding-helpers';
|
||||
import { SecretsApiKeyManagementListKeysListEnum } from '@hashicorp/vault-client-typescript';
|
||||
import {
|
||||
SecretsApiKeyManagementListKeysListEnum,
|
||||
SecretsApiKeyManagementListKmsProvidersListEnum,
|
||||
} from '@hashicorp/vault-client-typescript';
|
||||
|
||||
const SUPPORTED_BACKENDS = supportedSecretBackends();
|
||||
|
||||
|
|
@ -146,6 +150,60 @@ export default Route.extend({
|
|||
}
|
||||
},
|
||||
|
||||
async fetchProvidersWithCapabilities(backend) {
|
||||
const { keys: providerNames } = await this.api.secrets.keyManagementListKmsProviders(
|
||||
backend,
|
||||
SecretsApiKeyManagementListKmsProvidersListEnum.TRUE
|
||||
);
|
||||
|
||||
const providersWithData = await Promise.all(
|
||||
(providerNames || []).map(async (providerName) => {
|
||||
const { data } = await this.api.secrets.keyManagementReadKmsProvider(providerName, backend);
|
||||
return {
|
||||
...data,
|
||||
id: providerName,
|
||||
name: providerName,
|
||||
backend,
|
||||
type: 'provider',
|
||||
icon: getKeymgmtProviderIcon(data.provider),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const pathsToFetch = providersWithData.map((provider) =>
|
||||
this.capabilitiesService.pathFor('keymgmtProvider', { backend, id: provider.id })
|
||||
);
|
||||
const capabilities = await this.capabilitiesService.fetch(pathsToFetch);
|
||||
|
||||
const providersList = providersWithData.map((provider) => {
|
||||
const providerPath = this.capabilitiesService.pathFor('keymgmtProvider', {
|
||||
backend,
|
||||
id: provider.id,
|
||||
});
|
||||
return {
|
||||
...provider,
|
||||
canRead: capabilities[providerPath]?.canRead || false,
|
||||
canEdit: capabilities[providerPath]?.canUpdate || false,
|
||||
canDelete: capabilities[providerPath]?.canDelete || false,
|
||||
};
|
||||
});
|
||||
|
||||
return { providersList, capabilities };
|
||||
},
|
||||
|
||||
async fetchKeymgmtProviders(backend, page, pageFilter) {
|
||||
try {
|
||||
const { providersList } = await this.fetchProvidersWithCapabilities(backend);
|
||||
return paginate(providersList, { page, filter: pageFilter });
|
||||
} catch (error) {
|
||||
const { status } = await this.api.parseError(error);
|
||||
if (status === 404) {
|
||||
return [];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async model(params) {
|
||||
const secret = this.secretParam() || '';
|
||||
const backend = getEnginePathParam(this);
|
||||
|
|
@ -154,11 +212,15 @@ export default Route.extend({
|
|||
const modelType = this.getModelType(effectiveType, params.tab);
|
||||
|
||||
// Handle keymgmt keys with API service
|
||||
const isKeymgmtKeys = effectiveType === 'keymgmt' && params.tab !== 'provider';
|
||||
|
||||
let secrets;
|
||||
if (isKeymgmtKeys) {
|
||||
secrets = await this.fetchKeymgmtKeys(backend, getValidPage(params.page), params.pageFilter);
|
||||
if (effectiveType === 'keymgmt') {
|
||||
const page = getValidPage(params.page);
|
||||
const filter = params.pageFilter;
|
||||
secrets =
|
||||
params.tab === 'provider'
|
||||
? await this.fetchKeymgmtProviders(backend, page, filter)
|
||||
: await this.fetchKeymgmtKeys(backend, page, filter);
|
||||
|
||||
this.set('has404', false);
|
||||
} else {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -13,8 +13,9 @@ import { keyIsFolder, parentKeyForKey } from 'core/utils/key-utils';
|
|||
import { getEffectiveEngineType } from 'vault/utils/external-plugin-helpers';
|
||||
import { getModelTypeForEngine } from 'vault/utils/model-helpers/secret-engine-helpers';
|
||||
import { getBackendEffectiveType, getEnginePathParam } from 'vault/utils/backend-route-helpers';
|
||||
import { isValidProvider } from 'vault/utils/keymgmt-provider-validator';
|
||||
import { isValidProvider } from 'vault/utils/keymgmt-provider-utils';
|
||||
import KeymgmtKeyForm from 'vault/forms/keymgmt/key';
|
||||
import KeymgmtProviderForm from 'vault/forms/keymgmt/provider';
|
||||
import { SecretsApiKeyManagementListKmsProvidersForKeyListEnum } from '@hashicorp/vault-client-typescript';
|
||||
|
||||
/**
|
||||
|
|
@ -250,6 +251,44 @@ export default Route.extend({
|
|||
};
|
||||
},
|
||||
|
||||
async fetchKeymgmtProvider(backend, name) {
|
||||
const { data } = await this.api.secrets.keyManagementReadKmsProvider(name, backend);
|
||||
|
||||
const form = new KeymgmtProviderForm(
|
||||
{
|
||||
...data,
|
||||
name,
|
||||
backend,
|
||||
keys: [],
|
||||
},
|
||||
{ isNew: false }
|
||||
);
|
||||
|
||||
return form;
|
||||
},
|
||||
|
||||
async fetchKeymgmtProviderCapabilities(backend, name) {
|
||||
const providerPath = this.capabilitiesService.pathFor('keymgmtProvider', { backend, id: name });
|
||||
const providersPath = this.capabilitiesService.pathFor('keymgmtProviders', { backend });
|
||||
const providerKeysPath = this.capabilitiesService.pathFor('keymgmtProviderKeys', { backend, id: name });
|
||||
|
||||
const capabilities = await this.capabilitiesService.fetch([
|
||||
providerPath,
|
||||
providersPath,
|
||||
providerKeysPath,
|
||||
]);
|
||||
|
||||
return {
|
||||
canDelete: capabilities[providerPath]?.canDelete,
|
||||
canUpdate: capabilities[providerPath]?.canUpdate,
|
||||
canEdit: capabilities[providerPath]?.canUpdate,
|
||||
canRead: capabilities[providerPath]?.canRead,
|
||||
canList: capabilities[providersPath]?.canList,
|
||||
canListKeys: capabilities[providerKeysPath]?.canList,
|
||||
canCreateKeys: capabilities[providerKeysPath]?.canCreate,
|
||||
};
|
||||
},
|
||||
|
||||
async handleSecretModelError(capabilitiesPromise, secretId, modelType, error) {
|
||||
// capabilities is a promise proxy, not a real object
|
||||
// to work around this we explicitly assign it to a const and await it
|
||||
|
|
@ -288,11 +327,15 @@ export default Route.extend({
|
|||
let secretModel;
|
||||
let capabilities;
|
||||
|
||||
// Handle keymgmt/key with API service
|
||||
// Handle keymgmt resources with API service
|
||||
if (modelType === 'keymgmt/key') {
|
||||
secretModel = await this.fetchKeymgmtKey(backend, secret);
|
||||
const caps = await this.fetchKeymgmtKeyCapabilities(backend, secret);
|
||||
capabilities = caps;
|
||||
} else if (modelType === 'keymgmt/provider') {
|
||||
secretModel = await this.fetchKeymgmtProvider(backend, secret);
|
||||
const caps = await this.fetchKeymgmtProviderCapabilities(backend, secret);
|
||||
capabilities = caps;
|
||||
} else {
|
||||
capabilities = this.capabilities(secret, modelType);
|
||||
try {
|
||||
|
|
@ -325,15 +368,15 @@ export default Route.extend({
|
|||
const backendType = this.backendType();
|
||||
const mode = this.routeName.split('.').pop().replace('-root', '');
|
||||
|
||||
// Handle keymgmt/key differently - Resource or Form doesn't have setProperties
|
||||
// Handle keymgmt forms differently - Resource or Form doesn't have setProperties
|
||||
const modelType = this.modelType(backend, secret);
|
||||
if (modelType !== 'keymgmt/key') {
|
||||
if (!['keymgmt/key', 'keymgmt/provider'].includes(modelType)) {
|
||||
model.secret.setProperties({ backend });
|
||||
}
|
||||
|
||||
controller.setProperties({
|
||||
model: model.secret,
|
||||
form: modelType === 'keymgmt/key' ? model.secret : null,
|
||||
form: ['keymgmt/key', 'keymgmt/provider'].includes(modelType) ? model.secret : null,
|
||||
capabilities: model.capabilities,
|
||||
baseKey: { id: secret },
|
||||
mode,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ export const PATH_MAP = {
|
|||
keymgmtKey: apiPath`${'backend'}/key/${'name'}`,
|
||||
keymgmtKeys: apiPath`${'backend'}/key`,
|
||||
keymgmtKeyProviders: apiPath`${'backend'}/key/${'name'}/kms`,
|
||||
keymgmtProvider: apiPath`${'backend'}/kms/${'id'}`,
|
||||
keymgmtProviders: apiPath`${'backend'}/kms`,
|
||||
keymgmtProviderKeys: apiPath`${'backend'}/kms/${'id'}/key`,
|
||||
kmipCredentialsRevoke: apiPath`${'backend'}/scope/${'scope'}/role/${'role'}/credentials/revoke`,
|
||||
kmipRole: apiPath`${'backend'}/scopes/${'scope'}/roles/${'name'}`,
|
||||
kmipScope: apiPath`${'backend'}/scopes/${'name'}`,
|
||||
|
|
|
|||
30
ui/app/utils/keymgmt-provider-utils.ts
Normal file
30
ui/app/utils/keymgmt-provider-utils.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2026
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
/**
|
||||
* Validates if a provider value is a valid string that can be used for API calls.
|
||||
* Provider must be a non-empty string to be valid.
|
||||
* This prevents passing objects like { permissionsError: true } to API calls.
|
||||
* @param {*} provider - The provider value to validate
|
||||
* @returns {boolean} true if provider is a valid non-empty string, false otherwise
|
||||
*/
|
||||
export function isValidProvider(provider: unknown): boolean {
|
||||
return typeof provider === 'string' && provider.trim().length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the icon name associated with a keymgmt provider type.
|
||||
* @param {string | undefined} providerType - The keymgmt provider type
|
||||
* @returns {string} Icon token used by the UI
|
||||
*/
|
||||
export function getKeymgmtProviderIcon(providerType?: string): string {
|
||||
return (
|
||||
{
|
||||
azurekeyvault: 'azure-color',
|
||||
awskms: 'aws-color',
|
||||
gcpckms: 'gcp-color',
|
||||
}[providerType || ''] || 'key'
|
||||
);
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import { setupMirage } from 'ember-cli-mirage/test-support';
|
|||
import { click, settled, fillIn } from '@ember/test-helpers';
|
||||
import { setRunOptions } from 'ember-a11y-testing/test-support';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import KeymgmtProviderForm from 'vault/forms/keymgmt/provider';
|
||||
|
||||
const ts = 'data-test-kms-provider';
|
||||
const root = {
|
||||
|
|
@ -25,20 +26,18 @@ module('Integration | Component | keymgmt/provider-edit', function (hooks) {
|
|||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.store.push({
|
||||
data: {
|
||||
id: 'foo-bar',
|
||||
type: 'keymgmt/provider',
|
||||
attributes: {
|
||||
name: 'foo-bar',
|
||||
provider: 'azurekeyvault',
|
||||
keyCollection: 'keyvault-1',
|
||||
backend: 'keymgmt',
|
||||
},
|
||||
},
|
||||
this.form = new KeymgmtProviderForm({
|
||||
name: 'foo-bar',
|
||||
provider: 'azurekeyvault',
|
||||
key_collection: 'keyvault-1',
|
||||
backend: 'keymgmt',
|
||||
});
|
||||
this.model = this.store.peekRecord('keymgmt/provider', 'foo-bar');
|
||||
this.capabilities = {
|
||||
canDelete: false,
|
||||
canListKeys: false,
|
||||
canEdit: false,
|
||||
canCreateKeys: false,
|
||||
};
|
||||
this.root = root;
|
||||
this.owner.lookup('service:router').reopen({
|
||||
currentURL: '/ui/vault/secrets-engines/keymgmt/show/foo-bar',
|
||||
|
|
@ -60,11 +59,7 @@ module('Integration | Component | keymgmt/provider-edit', function (hooks) {
|
|||
test('it should render show view', async function (assert) {
|
||||
assert.expect(11);
|
||||
|
||||
// override capability getters
|
||||
Object.defineProperties(this.model, {
|
||||
canDelete: { value: true },
|
||||
canListKeys: { value: true },
|
||||
});
|
||||
this.capabilities = { canDelete: true, canListKeys: true, canEdit: false, canCreateKeys: false };
|
||||
|
||||
this.server.post('/sys/capabilities-self', () => ({}));
|
||||
this.server.get('/keymgmt/kms/foo-bar/key', () => {
|
||||
|
|
@ -83,7 +78,8 @@ module('Integration | Component | keymgmt/provider-edit', function (hooks) {
|
|||
await render(hbs`
|
||||
<Keymgmt::ProviderEdit
|
||||
@root={{this.root}}
|
||||
@model={{this.model}}
|
||||
@form={{this.form}}
|
||||
@capabilities={{this.capabilities}}
|
||||
@mode="show"
|
||||
@tab={{this.tab}}
|
||||
/>`);
|
||||
|
|
@ -117,11 +113,7 @@ module('Integration | Component | keymgmt/provider-edit', function (hooks) {
|
|||
test('it should delete a provider', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
// override capability getters
|
||||
Object.defineProperties(this.model, {
|
||||
canDelete: { value: true },
|
||||
canListKeys: { value: true },
|
||||
});
|
||||
this.capabilities = { canDelete: true, canListKeys: true, canEdit: false, canCreateKeys: false };
|
||||
|
||||
this.server.post('/sys/capabilities-self', () => ({}));
|
||||
this.server.get('/keymgmt/kms/foo-bar/key', () => {
|
||||
|
|
@ -146,7 +138,8 @@ module('Integration | Component | keymgmt/provider-edit', function (hooks) {
|
|||
await render(hbs`
|
||||
<Keymgmt::ProviderEdit
|
||||
@root={{this.root}}
|
||||
@model={{this.model}}
|
||||
@form={{this.form}}
|
||||
@capabilities={{this.capabilities}}
|
||||
@mode="show"
|
||||
@tab={{this.tab}}
|
||||
/>`);
|
||||
|
|
@ -162,10 +155,8 @@ module('Integration | Component | keymgmt/provider-edit', function (hooks) {
|
|||
test('it should render create view', async function (assert) {
|
||||
assert.expect(14);
|
||||
|
||||
this.server.put('/keymgmt/kms/foo', (schema, req) => {
|
||||
this.server.post('/keymgmt/kms/foo', (schema, req) => {
|
||||
const params = {
|
||||
name: 'foo',
|
||||
backend: 'keymgmt',
|
||||
provider: 'gcpckms',
|
||||
key_collection: 'keyvault-1',
|
||||
credentials: {
|
||||
|
|
@ -186,12 +177,12 @@ module('Integration | Component | keymgmt/provider-edit', function (hooks) {
|
|||
assert.deepEqual(itemType, 'provider', 'Correct query params sent in transitionTo on save');
|
||||
},
|
||||
});
|
||||
this.model = this.store.createRecord('keymgmt/provider', { backend: 'keymgmt' });
|
||||
this.form = new KeymgmtProviderForm({ backend: 'keymgmt' }, { isNew: true });
|
||||
|
||||
await render(hbs`
|
||||
<Keymgmt::ProviderEdit
|
||||
@root={{this.root}}
|
||||
@model={{this.model}}
|
||||
@form={{this.form}}
|
||||
@mode="create"
|
||||
/>`);
|
||||
|
||||
|
|
@ -216,18 +207,16 @@ module('Integration | Component | keymgmt/provider-edit', function (hooks) {
|
|||
assert.dom(`[data-test-input="credentials.service_account_file"]`).exists(`GCP - cred field renders`);
|
||||
|
||||
await fillIn('[data-test-input="name"]', 'foo');
|
||||
await fillIn('[data-test-input="keyCollection"]', 'keyvault-1');
|
||||
await fillIn('[data-test-input="key_collection"]', 'keyvault-1');
|
||||
await fillIn('[data-test-input="credentials.service_account_file"]', 'test');
|
||||
await click(`[${ts}-submit]`);
|
||||
});
|
||||
|
||||
test('it should render edit view', async function (assert) {
|
||||
assert.expect(3);
|
||||
assert.expect(7);
|
||||
|
||||
this.server.put('/keymgmt/kms/foo', (schema, req) => {
|
||||
this.server.post('/keymgmt/kms/foo-bar', (schema, req) => {
|
||||
const params = {
|
||||
name: 'foo-bar',
|
||||
backend: 'keymgmt',
|
||||
provider: 'azurekeyvault',
|
||||
key_collection: 'keyvault-1',
|
||||
credentials: {
|
||||
|
|
@ -246,14 +235,14 @@ module('Integration | Component | keymgmt/provider-edit', function (hooks) {
|
|||
'vault.cluster.secrets.backend.show',
|
||||
'Show route sent in transitionTo on save'
|
||||
);
|
||||
assert.strictEqual(model, 'foo', 'Model id sent in transitionTo on save');
|
||||
assert.strictEqual(model, 'foo-bar', 'Provider name sent in transitionTo on save');
|
||||
assert.deepEqual(itemType, 'provider', 'Correct query params sent in transitionTo on save');
|
||||
},
|
||||
});
|
||||
await render(hbs`
|
||||
<Keymgmt::ProviderEdit
|
||||
@root={{this.root}}
|
||||
@model={{this.model}}
|
||||
@form={{this.form}}
|
||||
@mode="edit"
|
||||
/>`);
|
||||
|
||||
|
|
@ -266,4 +255,28 @@ module('Integration | Component | keymgmt/provider-edit', function (hooks) {
|
|||
}
|
||||
await click(`[${ts}-submit]`);
|
||||
});
|
||||
|
||||
test('it should clear all validations when provider type changes', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
this.form = new KeymgmtProviderForm({ backend: 'keymgmt' }, { isNew: true });
|
||||
|
||||
await render(hbs`
|
||||
<Keymgmt::ProviderEdit
|
||||
@root={{this.root}}
|
||||
@form={{this.form}}
|
||||
@mode="create"
|
||||
/>`);
|
||||
|
||||
await click(`[${ts}-submit]`);
|
||||
assert.dom('[data-test-validation-error]').exists('Validation errors shown after submit');
|
||||
|
||||
await fillIn('[data-test-input="provider"]', 'gcpckms');
|
||||
assert
|
||||
.dom('[data-test-validation-error]')
|
||||
.doesNotExist('Validation errors cleared when provider changes');
|
||||
|
||||
await click(`[${ts}-submit]`);
|
||||
assert.dom('[data-test-validation-error]').exists('Validation errors re-appear after next submit');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
49
ui/tests/unit/utils/keymgmt-provider-utils-test.ts
Normal file
49
ui/tests/unit/utils/keymgmt-provider-utils-test.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2026
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { getKeymgmtProviderIcon, isValidProvider } from 'vault/utils/keymgmt-provider-utils';
|
||||
import { module, test } from 'qunit';
|
||||
|
||||
module('Unit | Util | keymgmt-provider-utils', function () {
|
||||
test('it returns true for valid provider strings', function (assert) {
|
||||
assert.true(isValidProvider('azure-provider'), 'Valid provider name');
|
||||
assert.true(isValidProvider('test-provider'), 'Another valid provider');
|
||||
assert.true(isValidProvider('a'), 'Single character string');
|
||||
assert.true(isValidProvider(' valid-provider '), 'Provider with leading/trailing spaces');
|
||||
});
|
||||
|
||||
test('it returns false for objects', function (assert) {
|
||||
assert.false(isValidProvider({ permissionsError: true }), 'Object with permissionsError');
|
||||
assert.false(isValidProvider({}), 'Empty object');
|
||||
assert.false(isValidProvider({ name: 'provider' }), 'Object with properties');
|
||||
});
|
||||
|
||||
test('it returns false for non-string primitives', function (assert) {
|
||||
assert.false(isValidProvider(123), 'Number returns false');
|
||||
assert.false(isValidProvider(true), 'Boolean true returns false');
|
||||
assert.false(isValidProvider(false), 'Boolean false returns false');
|
||||
});
|
||||
|
||||
test('it returns false for arrays', function (assert) {
|
||||
assert.false(isValidProvider([]), 'Empty array');
|
||||
assert.false(isValidProvider(['provider']), 'Array with string');
|
||||
});
|
||||
|
||||
test('it returns provider-specific icons for known provider types', function (assert) {
|
||||
assert.strictEqual(getKeymgmtProviderIcon('azurekeyvault'), 'azure-color', 'Azure icon is returned');
|
||||
assert.strictEqual(getKeymgmtProviderIcon('awskms'), 'aws-color', 'AWS icon is returned');
|
||||
assert.strictEqual(getKeymgmtProviderIcon('gcpckms'), 'gcp-color', 'GCP icon is returned');
|
||||
});
|
||||
|
||||
test('it returns default icon for unknown or empty provider types', function (assert) {
|
||||
assert.strictEqual(
|
||||
getKeymgmtProviderIcon('unknown-provider'),
|
||||
'key',
|
||||
'Unknown provider uses default icon'
|
||||
);
|
||||
assert.strictEqual(getKeymgmtProviderIcon(''), 'key', 'Empty string uses default icon');
|
||||
assert.strictEqual(getKeymgmtProviderIcon(), 'key', 'Undefined provider uses default icon');
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue