diff --git a/ui/app/adapters/kv/config.js b/ui/app/adapters/kv/config.js deleted file mode 100644 index ea3b8699aa..0000000000 --- a/ui/app/adapters/kv/config.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import ApplicationAdapter from '../application'; -export default class KvConfigAdapter extends ApplicationAdapter { - namespace = 'v1'; - - urlForFindRecord(id) { - return `${this.buildURL()}/${id}/config`; - } -} diff --git a/ui/app/adapters/kv/data.js b/ui/app/adapters/kv/data.js deleted file mode 100644 index 9ed2039a52..0000000000 --- a/ui/app/adapters/kv/data.js +++ /dev/null @@ -1,166 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import ApplicationAdapter from '../application'; -import { kvDataPath, kvDeletePath, kvDestroyPath, kvSubkeysPath, kvUndeletePath } from 'vault/utils/kv-path'; -import { assert } from '@ember/debug'; -import ControlGroupError from 'vault/lib/control-group-error'; - -export default class KvDataAdapter extends ApplicationAdapter { - namespace = 'v1'; - - _url(fullPath) { - return `${this.buildURL()}/${fullPath}`; - } - - createRecord(store, type, snapshot) { - const { backend, path } = snapshot.record; - const url = this._url(kvDataPath(backend, path)); - - return this.ajax(url, 'POST', { data: this.serialize(snapshot) }).then((res) => { - return { - data: { - id: kvDataPath(backend, path, res.data.version), - backend, - path, - ...res.data, - }, - }; - }); - } - - fetchSubkeys(backend, path, query) { - const url = this._url(kvSubkeysPath(backend, path, query)); - return ( - this.ajax(url, 'GET') - .then((resp) => resp.data) - // deleted/destroyed secret versions throw an error - // but still have metadata that we want to return - .catch((errorOrResponse) => { - return this.parseErrorOrResponse(errorOrResponse, { backend, path }, true); - }) - ); - } - - fetchWrapInfo(query) { - const { backend, path, version, wrapTTL } = query; - const id = kvDataPath(backend, path, version); - return this.ajax(this._url(id), 'GET', { wrapTTL }).then((resp) => resp.wrap_info); - } - - // patching a secret happens without retrieving the ember data model - // so we use a custom method instead of updateRecord - patchSecret(backend, path, patchData, version) { - const url = this._url(kvDataPath(backend, path)); - const data = { - options: { cas: version }, - data: patchData, - }; - return this.ajax(url, 'PATCH', { data }); - } - - queryRecord(store, type, query) { - const { backend, path, version } = query; - // ID is the full path for the data (including version) - let id = kvDataPath(backend, path, version); - return this.ajax(this._url(id), 'GET') - .then((resp) => { - // if no version is queried, add version from response to ID - // otherwise duplicate ember data models will exist in store - // (one with an ID that includes the version and one without) - if (!version) { - id = kvDataPath(backend, path, resp.data.metadata.version); - } - return { - ...resp, - data: { - id, - backend, - path, - ...resp.data, - }, - }; - }) - .catch((errorOrResponse) => { - return this.parseErrorOrResponse(errorOrResponse, { id, backend, path, version }); - }); - } - - /* Five types of delete operations */ - deleteRecord(store, type, snapshot) { - const { backend, path } = snapshot.record; - const { deleteType, deleteVersions } = snapshot.adapterOptions; - - if (!backend || !path) { - throw new Error('The request to delete or undelete is missing required attributes.'); - } - - switch (deleteType) { - case 'delete-latest-version': - return this.ajax(this._url(kvDataPath(backend, path)), 'DELETE'); - case 'delete-version': - return this.ajax(this._url(kvDeletePath(backend, path)), 'POST', { - data: { versions: deleteVersions }, - }); - case 'destroy': - return this.ajax(this._url(kvDestroyPath(backend, path)), 'PUT', { - data: { versions: deleteVersions }, - }); - case 'undelete': - return this.ajax(this._url(kvUndeletePath(backend, path)), 'POST', { - data: { versions: deleteVersions }, - }); - default: - assert('deleteType must be one of delete-latest-version, delete-version, destroy, or undelete.'); - } - } - - handleResponse(status, headers, payload, requestData) { - // after deleting a secret version, data is null and the API returns a 404 - // but there could be relevant metadata - if (status === 404 && payload.data?.metadata) { - return super.handleResponse(200, headers, payload, requestData); - } - return super.handleResponse(...arguments); - } - - parseErrorOrResponse(errorOrResponse, secretDataBaseResponse, isSubkeys = false) { - // if it's a legitimate error - throw it! - if (errorOrResponse instanceof ControlGroupError) { - throw errorOrResponse; - } - - const errorCode = errorOrResponse.httpStatus; - if (errorCode === 403) { - return { - data: { - ...secretDataBaseResponse, - fail_read_error_code: errorCode, - }, - }; - } - - // in the case of a deleted/destroyed secret the API returns a 404 because { data: null } - // however, there could be a metadata block with important information like deletion_time - // handleResponse below checks 404 status codes for metadata and updates the code to 200 if it exists. - // we still end up in the good ol' catch() block, but instead of a 404 adapter error we've "caught" - // the metadata that sneakily tried to hide from us - if (errorOrResponse.data) { - // subkeys response doesn't correspond to a model, no need to include base response - if (isSubkeys) return errorOrResponse.data; - - return { - ...errorOrResponse, - data: { - ...secretDataBaseResponse, - ...errorOrResponse.data, // includes the { metadata } key we want - }, - }; - } - - // If we get here, it's probably a 404 because it doesn't exist - throw errorOrResponse; - } -} diff --git a/ui/app/adapters/kv/metadata.js b/ui/app/adapters/kv/metadata.js deleted file mode 100644 index f87d341600..0000000000 --- a/ui/app/adapters/kv/metadata.js +++ /dev/null @@ -1,84 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import ApplicationAdapter from '../application'; -import { kvMetadataPath } from 'vault/utils/kv-path'; - -export default class KvMetadataAdapter extends ApplicationAdapter { - namespace = 'v1'; - - _url(fullPath) { - return `${this.buildURL()}/${fullPath}`; - } - - createRecord(store, type, snapshot) { - const { backend, path } = snapshot.record; - const id = kvMetadataPath(backend, path); - const url = this._url(id); - const data = this.serialize(snapshot); - return this.ajax(url, 'POST', { data }).then(() => { - return { - id, - data, - }; - }); - } - - updateRecord(store, type, snapshot) { - const { backend, path } = snapshot.record; - const id = kvMetadataPath(backend, path); - const url = this._url(id); - const data = this.serialize(snapshot); - return this.ajax(url, 'POST', { data }).then(() => { - return { - id, - data, - }; - }); - } - - query(store, type, query) { - const { backend, pathToSecret } = query; - // example of pathToSecret: beep/boop/ - return this.ajax(this._url(kvMetadataPath(backend, pathToSecret)), 'GET', { - data: { list: true }, - }).then((resp) => { - resp.backend = backend; - resp.path = pathToSecret; - return resp; - }); - } - - queryRecord(store, type, query) { - const { backend, path } = query; - // ID is the full path for the metadata - const id = kvMetadataPath(backend, path); - return this.ajax(this._url(id), 'GET').then((resp) => { - return { - id, - ...resp, - data: { - backend, - path, - ...resp.data, - }, - }; - }); - } - - // This method is called when deleting from the list or metadata details view. - // Otherwise, delete happens in kv/data adapter - deleteRecord(store, type, snapshot) { - const { backend, path, fullSecretPath } = snapshot.record; - // fullSecretPath is used when deleting from the LIST view and is defined via the serializer - // path is used when deleting from the metadata details view. - return this.ajax(this._url(kvMetadataPath(backend, fullSecretPath || path)), 'DELETE'); - } - - // custom method used if users do not have "read" permissions to fetch record - deleteMetadata(backend, path) { - return this.ajax(this._url(kvMetadataPath(backend, path)), 'DELETE'); - } -} diff --git a/ui/app/app.js b/ui/app/app.js index 6ae15248eb..09ddb5950d 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -105,7 +105,6 @@ export default class App extends Application { 'namespace', { 'app-router': 'router' }, 'secret-mount-path', - 'store', 'pagination', 'version', ], diff --git a/ui/app/components/dashboard/quick-actions-card.hbs b/ui/app/components/dashboard/quick-actions-card.hbs index 2e81fba965..a07458c44a 100644 --- a/ui/app/components/dashboard/quick-actions-card.hbs +++ b/ui/app/components/dashboard/quick-actions-card.hbs @@ -39,16 +39,16 @@ @ariaLabel="Action" /> - {{#if this.searchSelectParams.model}} + {{#if this.selectedAction}} {{! use special input to allow searching for KVv2 secrets inside a directory }} - {{#if (eq this.selectedEngine.type "kv")}} + {{#if this.searchSelectParams.isKV}} - {{else}} + {{else if this.searchSelectParams.model}}

{{this.searchSelectParams.title}}

- model.secretData !== undefined && typeof model.secretData !== 'object' ? false : true, - message: 'Vault expects data to be formatted as an JSON object.', - }, - ], -}; -@withModelValidations(validations) -@withFormFields() -export default class KvSecretDataModel extends Model { - @attr('string') backend; // dynamic path of secret -- set on response from value passed to queryRecord. - @attr('string', { - label: 'Path for this secret', - subText: 'Names with forward slashes define hierarchical path structures.', - }) - path; - @attr('object') secretData; // { key: value } data of the secret version - - // Params returned on the GET response. - @attr('string') createdTime; - @attr('object') customMetadata; - @attr('string') deletionTime; - @attr('boolean') destroyed; - @attr('number') version; - // Set in adapter if read failed - @attr('number') failReadErrorCode; - - // if creating a new version this value is set in the edit route's - // model hook from metadata or secret version, pending permissions - // if the value is not a number, don't send options.cas on payload - @attr('number') - casVersion; - - get state() { - if (this.destroyed) return 'destroyed'; - if (this.isSecretDeleted) return 'deleted'; - if (this.createdTime) return 'created'; - return ''; - } - - // cannot use isDeleted as model property name because of an ember property conflict - get isSecretDeleted() { - return isDeleted(this.deletionTime); - } - - // Permissions - @lazyCapabilities(apiPath`${'backend'}/data/${'path'}`, 'backend', 'path') dataPath; - @lazyCapabilities(apiPath`${'backend'}/metadata/${'path'}`, 'backend', 'path') metadataPath; - @lazyCapabilities(apiPath`${'backend'}/delete/${'path'}`, 'backend', 'path') deletePath; - @lazyCapabilities(apiPath`${'backend'}/destroy/${'path'}`, 'backend', 'path') destroyPath; - @lazyCapabilities(apiPath`${'backend'}/undelete/${'path'}`, 'backend', 'path') undeletePath; - @lazyCapabilities(apiPath`${'backend'}/subkeys/${'path'}`, 'backend', 'path') subkeysPath; - - get canDeleteLatestVersion() { - return this.dataPath.get('canDelete') !== false; - } - get canDeleteVersion() { - return this.deletePath.get('canUpdate') !== false; - } - get canUndelete() { - return this.undeletePath.get('canUpdate') !== false; - } - get canDestroyVersion() { - return this.destroyPath.get('canUpdate') !== false; - } - get canEditData() { - return this.dataPath.get('canUpdate') !== false; - } - get canReadData() { - return this.dataPath.get('canRead') !== false; - } - get canReadMetadata() { - return this.metadataPath.get('canRead') !== false; - } - get canUpdateMetadata() { - return this.metadataPath.get('canUpdate') !== false; - } - get canListMetadata() { - return this.metadataPath.get('canList') !== false; - } - get canDeleteMetadata() { - return this.metadataPath.get('canDelete') !== false; - } - get canReadSubkeys() { - return this.subkeysPath.get('canRead') !== false; - } -} diff --git a/ui/app/models/kv/metadata.js b/ui/app/models/kv/metadata.js deleted file mode 100644 index 44fe62dd35..0000000000 --- a/ui/app/models/kv/metadata.js +++ /dev/null @@ -1,118 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import Model, { attr } from '@ember-data/model'; -import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities'; -import { withModelValidations } from 'vault/decorators/model-validations'; -import { withFormFields } from 'vault/decorators/model-form-fields'; -import { keyIsFolder } from 'core/utils/key-utils'; -import { isDeleted } from 'kv/helpers/is-deleted'; - -const validations = { - maxVersions: [ - { type: 'number', message: 'Maximum versions must be a number.' }, - { type: 'length', options: { min: 1, max: 16 }, message: 'You cannot go over 16 characters.' }, - ], -}; -const formFieldProps = ['customMetadata', 'maxVersions', 'casRequired', 'deleteVersionAfter']; - -@withModelValidations(validations) -@withFormFields(formFieldProps) -export default class KvSecretMetadataModel extends Model { - @attr('string') backend; - @attr('string') path; - @attr('string') fullSecretPath; - - @attr('number', { - defaultValue: 0, - label: 'Maximum number of versions', - subText: - 'The number of versions to keep per key. Once the number of keys exceeds the maximum number set here, the oldest version will be permanently deleted.', - }) - maxVersions; - - @attr('boolean', { - defaultValue: false, - label: 'Require Check and Set', - subText: `Writes will only be allowed if the key's current version matches the version specified in the cas parameter.`, - }) - casRequired; - - @attr('string', { - defaultValue: '0s', - editType: 'ttl', - label: 'Automate secret deletion', - helperTextDisabled: `A secret's version must be manually deleted.`, - helperTextEnabled: 'Delete all new versions of this secret after:', - }) - deleteVersionAfter; - - // the API returns custom_metadata: null if empty but because the attr is an 'object' ember data transforms it to an empty object. - // this is important because we rely on the empty object as a truthy value in template conditionals - @attr('object', { - editType: 'kv', - isSectionHeader: true, - subText: 'An optional set of informational key-value pairs that will be stored with all secret versions.', - }) - customMetadata; - - // Additional Params only returned on the GET response. - @attr('string') createdTime; - @attr('number') currentVersion; - @attr('number') oldestVersion; - @attr('string') updatedTime; - @attr('object') versions; - - // used for KV list and list-directory view - get pathIsDirectory() { - // ex: beep/ - return keyIsFolder(this.path); - } - - // turns version object into an array for version dropdown menu - get sortedVersions() { - const array = []; - for (const key in this.versions) { - this.versions[key].isSecretDeleted = isDeleted(this.versions[key].deletion_time); - array.push({ version: key, ...this.versions[key] }); - } - // version keys are in order created with 1 being the oldest, we want newest first - return array.reverse(); - } - - // helps in long logic statements for state of a currentVersion - get currentSecret() { - if (!this.versions || !this.currentVersion) return false; - const data = this.versions[this.currentVersion]; - const state = data.destroyed ? 'destroyed' : isDeleted(data.deletion_time) ? 'deleted' : 'created'; - return { - state, - isDeactivated: state !== 'created', - deletionTime: data.deletion_time, - }; - } - - get permissionsPath() { - return this.fullSecretPath || this.path; - } - - // permissions needed for the list view where kv/data has not yet been called. Allows us to conditionally show action items in the LinkedBlock popups. - @lazyCapabilities(apiPath`${'backend'}/data/${'permissionsPath'}`, 'backend', 'permissionsPath') dataPath; - @lazyCapabilities(apiPath`${'backend'}/metadata/${'permissionsPath'}`, 'backend', 'permissionsPath') - metadataPath; - - get canDeleteMetadata() { - return this.metadataPath.get('canDelete') !== false; - } - get canReadMetadata() { - return this.metadataPath.get('canRead') !== false; - } - get canUpdateMetadata() { - return this.metadataPath.get('canUpdate') !== false; - } - get canCreateVersionData() { - return this.dataPath.get('canUpdate') !== false; - } -} diff --git a/ui/app/serializers/kv/data.js b/ui/app/serializers/kv/data.js deleted file mode 100644 index 1e75ac21aa..0000000000 --- a/ui/app/serializers/kv/data.js +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import ApplicationSerializer from '../application'; - -export default class KvDataSerializer extends ApplicationSerializer { - serialize(snapshot) { - const { secretData, casVersion } = snapshot.record; - if (typeof casVersion === 'number') { - /* if this is a number it is set by one of the following: - A) user is creating initial version of a secret - -> 0 : default value set in route - B) user is creating a new version of a secret: - -> metadata.current_version : has metadata read permissions (data permissions are irrelevant) - -> secret.version : has data read permissions. without metadata read access a user is unable to navigate, - to older secret versions so we assume creation is from the latest version */ - return { data: secretData, options: { cas: casVersion } }; - } - // a non-number value means no read permission for both data and metadata - return { data: secretData }; - } - - normalizeKvData(payload) { - const { data, metadata } = payload.data; - return { - ...payload, - data: { - ...payload.data, - // Rename to secret_data so it doesn't get removed by normalizer - secret_data: data, - ...metadata, - }, - }; - } - - normalizeResponse(store, primaryModelClass, payload, id, requestType) { - if (requestType === 'queryRecord') { - const transformed = this.normalizeKvData(payload); - return super.normalizeResponse(store, primaryModelClass, transformed, id, requestType); - } - return super.normalizeResponse(store, primaryModelClass, payload, id, requestType); - } -} diff --git a/ui/app/serializers/kv/metadata.js b/ui/app/serializers/kv/metadata.js deleted file mode 100644 index 7d5b4f16b5..0000000000 --- a/ui/app/serializers/kv/metadata.js +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { assert } from '@ember/debug'; -import ApplicationSerializer from '../application'; -import { kvMetadataPath } from 'vault/utils/kv-path'; - -export default class KvMetadataSerializer extends ApplicationSerializer { - attrs = { - backend: { serialize: false }, - path: { serialize: false }, - oldestVersion: { serialize: false }, - createdTime: { serialize: false }, - updatedTime: { serialize: false }, - currentVersion: { serialize: false }, - versions: { serialize: false }, - }; - - normalizeItems(payload) { - if (payload.data.keys) { - assert('payload.backend must be provided on kv/metadata list response', !!payload.backend); - return payload.data.keys.map((secret) => { - // If there is no payload.path then we're either on a "top level" secret or the first level directory of a nested secret. - // We set the path to the current secret or pathToSecret. e.g. my-secret or beep/boop/ - // We add a param called full_secret_path to the model which we use to navigate to the nested secret. e.g. beep/boop/bop. - const fullSecretPath = payload.path ? payload.path + secret : secret; - return { - id: kvMetadataPath(payload.backend, fullSecretPath), - path: secret, - backend: payload.backend, - full_secret_path: fullSecretPath, - }; - }); - } - return super.normalizeItems(payload); - } -} diff --git a/ui/lib/core/addon/components/kv-suggestion-input.hbs b/ui/lib/core/addon/components/kv-suggestion-input.hbs index 37e66737e2..d1899115db 100644 --- a/ui/lib/core/addon/components/kv-suggestion-input.hbs +++ b/ui/lib/core/addon/components/kv-suggestion-input.hbs @@ -29,6 +29,6 @@ data-test-kv-suggestion-select as |secret| > - {{secret.path}} + {{secret}} \ No newline at end of file diff --git a/ui/lib/core/addon/components/kv-suggestion-input.ts b/ui/lib/core/addon/components/kv-suggestion-input.ts index bd151e89c7..1522d74b35 100644 --- a/ui/lib/core/addon/components/kv-suggestion-input.ts +++ b/ui/lib/core/addon/components/kv-suggestion-input.ts @@ -10,9 +10,9 @@ import { action } from '@ember/object'; import { guidFor } from '@ember/object/internals'; import { run } from '@ember/runloop'; import { keyIsFolder, parentKeyForKey, keyWithoutParentKey } from 'core/utils/key-utils'; +import { KvV2ListListEnum } from '@hashicorp/vault-client-typescript'; -import type Store from '@ember-data/store'; -import type KvSecretMetadataModel from 'vault/models/kv/metadata'; +import type ApiService from 'vault/services/api'; /** * @module KvSuggestionInput @@ -49,12 +49,13 @@ interface PowerSelectAPI { } export default class KvSuggestionInputComponent extends Component { - @service declare readonly store: Store; + @service declare readonly api: ApiService; - @tracked secrets: KvSecretMetadataModel[] = []; + @tracked secrets: string[] = []; powerSelectAPI: PowerSelectAPI | undefined; - _cachedSecrets: KvSecretMetadataModel[] = []; // cache the response for filtering purposes + _cachedSecrets: string[] = []; // cache the response for filtering purposes inputId = `suggestion-input-${guidFor(this)}`; // add unique segment to id in case multiple instances of component are used on the same page + pathToSecret = ''; // keeps track of the full path to the secret as user builds it out constructor(owner: unknown, args: Args) { super(owner, args); @@ -63,18 +64,19 @@ export default class KvSuggestionInputComponent extends Component { } } - async fetchSecrets(isDirectory: boolean) { - const { mountPath } = this.args; + get isDirectory() { + return keyIsFolder(this.args.value); + } + + async fetchSecrets() { try { + const { mountPath } = this.args; const backend = keyIsFolder(mountPath) ? mountPath.slice(0, -1) : mountPath; const parentDirectory = parentKeyForKey(this.args.value); - const pathToSecret = isDirectory ? this.args.value : parentDirectory; - const kvModels = (await this.store.query('kv/metadata', { - backend, - pathToSecret, - })) as unknown; + this.pathToSecret = this.isDirectory ? this.args.value : parentDirectory; + const { keys } = await this.api.secrets.kvV2List(this.pathToSecret, backend, KvV2ListListEnum.TRUE); // this will be used to filter the existing result set when the search term changes within the same path - this._cachedSecrets = kvModels as KvSecretMetadataModel[]; + this._cachedSecrets = keys || []; return this._cachedSecrets; } catch (error) { console.log(error); // eslint-disable-line @@ -82,33 +84,32 @@ export default class KvSuggestionInputComponent extends Component { } } - filterSecrets(kvModels: KvSecretMetadataModel[] | undefined = [], isDirectory: boolean) { + filterSecrets(secrets: string[] | undefined = []) { const { value } = this.args; const secretName = keyWithoutParentKey(value) || ''; - return kvModels.filter((model) => { - if (!value || isDirectory) { + return secrets.filter((path) => { + if (!value || this.isDirectory) { return true; } - if (value === model.fullSecretPath) { + if (secretName === path) { // don't show suggestion if it's currently selected return false; } - return model.path.toLowerCase().includes(secretName.toLowerCase()); + return path.toLowerCase().includes(secretName.toLowerCase()); }); } @action async updateSuggestions() { const isFirstUpdate = !this._cachedSecrets.length; - const isDirectory = keyIsFolder(this.args.value); if (!this.args.mountPath) { this.secrets = []; - } else if (this.args.value && !isDirectory && this.secrets) { + } else if (this.args.value && !this.isDirectory && this.secrets) { // if we don't need to fetch from a new path, filter the previous result set with the updated search term - this.secrets = this.filterSecrets(this._cachedSecrets, isDirectory); + this.secrets = this.filterSecrets(this._cachedSecrets); } else { - const kvModels = await this.fetchSecrets(isDirectory); - this.secrets = this.filterSecrets(kvModels, isDirectory); + const secrets = await this.fetchSecrets(); + this.secrets = this.filterSecrets(secrets); } // don't do anything on first update -- allow dropdown to open on input click if (!isFirstUpdate) { @@ -131,11 +132,11 @@ export default class KvSuggestionInputComponent extends Component { } @action - onSuggestionSelect(secret: KvSecretMetadataModel) { + onSuggestionSelect(secret: string) { // user may partially type a value to filter result set and then select a suggestion // in this case the partially typed value must be replaced with suggestion value - // the fullSecretPath contains the previous selections or typed path segments - this.args.onChange(secret.fullSecretPath); + // pathToSecret contains the previous selections or typed path segments + this.args.onChange(`${this.pathToSecret}${secret}`); this.updateSuggestions(); // refocus the input after selection run(() => document.getElementById(this.inputId)?.focus()); diff --git a/ui/lib/kv/addon/components/page/list.js b/ui/lib/kv/addon/components/page/list.js index bb96dcd3e2..018055abec 100644 --- a/ui/lib/kv/addon/components/page/list.js +++ b/ui/lib/kv/addon/components/page/list.js @@ -13,9 +13,9 @@ import { pathIsDirectory } from 'kv/utils/kv-breadcrumbs'; /** * @module List - * ListPage component is a component to show a list of kv/metadata secrets. + * ListPage component is a component to show a list of secrets. * - * @param {array} secrets - An array of models generated form kv/metadata query. + * @param {array} secrets - An array of secrets * @param {string} backend - The name of the kv secret engine. * @param {string} pathToSecret - The directory name that the secret belongs to ex: beep/boop/ * @param {string} filterValue - The concatenation of the pathToSecret and pageFilter ex: beep/boop/my- diff --git a/ui/lib/kv/addon/components/page/secret/metadata/details.hbs b/ui/lib/kv/addon/components/page/secret/metadata/details.hbs index 09aaa70c50..f9c90cd043 100644 --- a/ui/lib/kv/addon/components/page/secret/metadata/details.hbs +++ b/ui/lib/kv/addon/components/page/secret/metadata/details.hbs @@ -56,51 +56,53 @@ @title="You do not have access to read custom metadata" @message="In order to read custom metadata you either need read access to the secret data and/or read access to metadata." /> - {{else if this.canRequestData}} + {{else}} {{! Offer opportunity to manually request /data/ for custom_metadata }} {{#if this.error.isControlGroup}} {{else if this.error}} {{/if}} - -
- - - Sensitive secret data will be retrieved. - - - -
-
- {{else}} - - {{#if @capabilities.canUpdateMetadata}} - - {{/if}} - + {{#if this.canRequestData}} + +
+ + + Sensitive secret data will be retrieved. + + + +
+
+ {{else}} + + {{#if @capabilities.canUpdateMetadata}} + + {{/if}} + + {{/if}} {{/if}} {{/each-in}} diff --git a/ui/lib/kv/addon/components/page/secret/metadata/details.js b/ui/lib/kv/addon/components/page/secret/metadata/details.js index 5c84ded817..d19f6a67ee 100644 --- a/ui/lib/kv/addon/components/page/secret/metadata/details.js +++ b/ui/lib/kv/addon/components/page/secret/metadata/details.js @@ -8,9 +8,10 @@ import { action } from '@ember/object'; import { tracked } from '@glimmer/tracking'; import { service } from '@ember/service'; import errorMessage from 'vault/utils/error-message'; +import { waitFor } from '@ember/test-waiters'; /** - * @module KvSecretMetadataDetails renders the details view for kv/metadata and button to delete (which deletes the whole secret) or edit metadata. + * @module KvSecretMetadataDetails renders the details view for kv metadata and button to delete (which deletes the whole secret) or edit metadata. * { - assert( - `'baseSetup()' requires mirage: import { setupMirage } from 'ember-cli-mirage/test-support'`, - context.server - ); - context.store = context.owner.lookup('service:store'); - context.server.post('/sys/capabilities-self', allowAllCapabilitiesStub()); - context.backend = 'kv-engine'; - context.path = 'my-secret'; - context.metadata = metadataModel(context, { withCustom: false }); -}; - -export const metadataModel = (context, { withCustom = false }) => { - const metadata = withCustom - ? context.server.create('kv-metadatum', 'withCustomMetadata') - : context.server.create('kv-metadatum'); - metadata.id = kvMetadataPath(context.backend, context.path); - context.store.pushPayload('kv/metadata', { - modelName: 'kv/metadata', - ...metadata, - }); - return context.store.peekRecord('kv/metadata', metadata.id); -}; diff --git a/ui/tests/integration/components/kv-suggestion-input-test.js b/ui/tests/integration/components/kv-suggestion-input-test.js index d75cc31de6..5af31e929f 100644 --- a/ui/tests/integration/components/kv-suggestion-input-test.js +++ b/ui/tests/integration/components/kv-suggestion-input-test.js @@ -5,10 +5,10 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import { render, click, fillIn, settled, typeIn } from '@ember/test-helpers'; +import { render, click, fillIn, typeIn } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; +import sinon from 'sinon'; const { searchSelect: { option, options }, @@ -17,16 +17,16 @@ const { module('Integration | Component | kv-suggestion-input', function (hooks) { setupRenderingTest(hooks); - setupMirage(hooks); hooks.beforeEach(function () { this.label = 'Input something'; this.mountPath = 'secret/'; - this.keys = ['foo/', 'my-secret']; - const response = () => ({ data: { keys: this.keys } }); - this.server.get('/:mount/metadata', response); - this.server.get('/:mount/metadata/*', response); - return render(hbs` + this.apiStub = sinon + .stub(this.owner.lookup('service:api').secrets, 'kvV2List') + .resolves({ keys: ['foo/', 'my-secret'] }); + + this.renderComponent = () => + render(hbs`