diff --git a/changelog/12626.txt b/changelog/12626.txt new file mode 100644 index 0000000000000..7c3cc7cb2a9b1 --- /dev/null +++ b/changelog/12626.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui: Add KV secret search box when no metadata list access. +``` \ No newline at end of file diff --git a/ui/app/components/get-credentials-card.js b/ui/app/components/get-credentials-card.js index 9bc650aac3114..a989205bc72d8 100644 --- a/ui/app/components/get-credentials-card.js +++ b/ui/app/components/get-credentials-card.js @@ -5,11 +5,16 @@ * * @example * ```js - * + * * ``` * @param title=null {String} - The title displays the card title * @param searchLabel=null {String} - The text above the searchSelect component * @param models=null {Array} - An array of model types to fetch from the API. Passed through to SearchSelect component + * @param type=null {String} - Determines where the transitionTo goes. If role to backend.credentials, if secret backend.show + * @param shouldUseFallback=[false] {Boolean} - If true the input is used instead of search select. + * @param subText=[null] {String} - Text below title + * @param placeHolder=[null] {String} - Only works if shouldUseFallback is true. Displays the helper text inside the input. + * @param backend=null {String} - Name of the backend to look up on search. */ import Component from '@glimmer/component'; @@ -20,25 +25,36 @@ export default class GetCredentialsCard extends Component { @service router; @service store; @tracked role = ''; + @tracked secret = ''; @action async transitionToCredential() { const role = this.role; + const secret = this.secret; if (role) { this.router.transitionTo('vault.cluster.secrets.backend.credentials', role); } + if (secret) { + this.router.transitionTo('vault.cluster.secrets.backend.show', secret); + } } get buttonDisabled() { - return !this.role; + return !this.role && !this.secret; } @action handleRoleInput(value) { - // if it comes in from the fallback component then the value is a string otherwise it's an array - let role = value; - if (Array.isArray(value)) { - role = value[0]; + if (this.args.type === 'role') { + // if it comes in from the fallback component then the value is a string otherwise it's an array + // which currently only happens if type is role. + if (Array.isArray(value)) { + this.role = value[0]; + } else { + this.role = value; + } + } + if (this.args.type === 'secret') { + this.secret = value; } - this.role = role; } } diff --git a/ui/app/components/secret-create-or-update.js b/ui/app/components/secret-create-or-update.js index f58e9605e7851..1ecf0a8685d60 100644 --- a/ui/app/components/secret-create-or-update.js +++ b/ui/app/components/secret-create-or-update.js @@ -11,7 +11,7 @@ * @modelForData={{@modelForData}} * @isV2=true * @secretData={{@secretData}} - * @canCreateSecretMetadata=true + * @canCreateSecretMetadata=false * /> * ``` * @param {string} mode - create, edit, show determines what view to display @@ -20,7 +20,7 @@ * @param {object} modelForData - a class that helps track secret data, defined in secret-edit * @param {boolean} isV2 - whether or not KV1 or KV2 * @param {object} secretData - class that is created in secret-edit - * @param {boolean} canCreateSecretMetadata - based on permissions to the /metadata/ endpoint. If user has secret create access. + * @param {boolean} canUpdateSecretMetadata - based on permissions to the /metadata/ endpoint. If user has secret update. create is not enough for metadata. */ import Component from '@glimmer/component'; diff --git a/ui/app/components/secret-edit.js b/ui/app/components/secret-edit.js index f49ac0e770c9a..cb375f1debada 100644 --- a/ui/app/components/secret-edit.js +++ b/ui/app/components/secret-edit.js @@ -109,7 +109,8 @@ export default Component.extend(FocusOnInsertMixin, WithNavToNearestAncestor, { 'mode' ), canDeleteSecretMetadata: alias('checkMetadataCapabilities.canDelete'), - canCreateSecretMetadata: alias('checkMetadataCapabilities.canCreate'), + canUpdateSecretMetadata: alias('checkMetadataCapabilities.canUpdate'), + canReadSecretMetadata: alias('checkMetadataCapabilities.canRead'), requestInFlight: or('model.isLoading', 'model.isReloading', 'model.isSaving'), diff --git a/ui/app/routes/vault/cluster/secrets/backend/configuration.js b/ui/app/routes/vault/cluster/secrets/backend/configuration.js index 46a8482ca5e4c..2d7bcf32bb669 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/configuration.js +++ b/ui/app/routes/vault/cluster/secrets/backend/configuration.js @@ -4,19 +4,31 @@ import Route from '@ember/routing/route'; export default Route.extend({ wizard: service(), store: service(), - model() { + async model() { let backend = this.modelFor('vault.cluster.secrets.backend'); if (this.wizard.featureState === 'list') { this.wizard.transitionFeatureMachine(this.wizard.featureState, 'CONTINUE', backend.get('type')); } if (backend.isV2KV) { - // design wants specific default to show that can't be set in the model - backend.set('casRequired', backend.casRequired ? backend.casRequired : 'False'); - backend.set( - 'deleteVersionAfter', - backend.deleteVersionAfter !== '0s' ? backend.deleteVersionAfter : 'Never delete' - ); - backend.set('maxVersions', backend.maxVersions ? backend.maxVersions : 'Not set'); + let canRead = await this.store + .findRecord('capabilities', `${backend.id}/config`) + .then(response => response.canRead); + // only set these config params if they can read the config endpoint. + if (canRead) { + // design wants specific default to show that can't be set in the model + backend.set('casRequired', backend.casRequired ? backend.casRequired : 'False'); + backend.set( + 'deleteVersionAfter', + backend.deleteVersionAfter !== '0s' ? backend.deleteVersionAfter : 'Never delete' + ); + backend.set('maxVersions', backend.maxVersions ? backend.maxVersions : 'Not set'); + } else { + // remove the default values from the model if they don't have read access otherwise it will display the defaults even if they've been set (because they error on returning config data) + // normally would catch the config error in the secret-v2 adapter, but I need the functions to proceed, not stop. So we remove the values here. + backend.set('casRequired', null); + backend.set('deleteVersionAfter', null); + backend.set('maxVersions', null); + } } return backend; }, diff --git a/ui/app/routes/vault/cluster/secrets/backend/list.js b/ui/app/routes/vault/cluster/secrets/backend/list.js index f242444966d2c..4c657142d7466 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/list.js +++ b/ui/app/routes/vault/cluster/secrets/backend/list.js @@ -10,6 +10,7 @@ const SUPPORTED_BACKENDS = supportedSecretBackends(); export default Route.extend({ templateName: 'vault/cluster/secrets/backend/list', pathHelp: service('path-help'), + noMetadataPermissions: false, queryParams: { page: { refreshModel: true, @@ -111,6 +112,9 @@ export default Route.extend({ // if we're at the root we don't want to throw if (backendModel && err.httpStatus === 404 && secret === '') { return []; + } else if (backendModel.engineType === 'kv' && backendModel.isV2KV) { + this.set('noMetadataPermissions', true); + return []; } else { // else we're throwing and dealing with this in the error action throw err; @@ -149,6 +153,7 @@ export default Route.extend({ let backend = this.enginePathParam(); let backendModel = this.store.peekRecord('secret-engine', backend); let has404 = this.has404; + let noMetadataPermissions = this.noMetadataPermissions; // only clear store cache if this is a new model if (secret !== controller.get('baseKey.id')) { this.store.clearAllDatasets(); @@ -157,6 +162,7 @@ export default Route.extend({ controller.setProperties({ model, has404, + noMetadataPermissions, backend, backendModel, baseKey: { id: secret }, diff --git a/ui/app/routes/vault/cluster/secrets/backend/metadata.js b/ui/app/routes/vault/cluster/secrets/backend/metadata.js index b47b0ddc9e21e..bcfd261d97707 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/metadata.js +++ b/ui/app/routes/vault/cluster/secrets/backend/metadata.js @@ -3,6 +3,7 @@ import { inject as service } from '@ember/service'; export default class MetadataShow extends Route { @service store; + noReadAccess = false; beforeModel() { const { backend } = this.paramsFor('vault.cluster.secrets.backend'); @@ -11,14 +12,23 @@ export default class MetadataShow extends Route { model(params) { let { secret } = params; - return this.store.queryRecord('secret-v2', { - backend: this.backend, - id: secret, - }); + return this.store + .queryRecord('secret-v2', { + backend: this.backend, + id: secret, + }) + .catch(error => { + // there was an error likely in read metadata. + // still load the page and handle what you show by filtering for this property + if (error.httpStatus === 403) { + this.noReadAccess = true; + } + }); } setupController(controller, model) { controller.set('backend', this.backend); // for backendCrumb controller.set('model', model); + controller.set('noReadAccess', this.noReadAccess); } } diff --git a/ui/app/templates/components/get-credentials-card.hbs b/ui/app/templates/components/get-credentials-card.hbs index 8e2d06d8c0530..5a539af5af3ab 100644 --- a/ui/app/templates/components/get-credentials-card.hbs +++ b/ui/app/templates/components/get-credentials-card.hbs @@ -1,18 +1,21 @@ -
+

{{@title}}

{{@searchLabel}}

+

{{@subText}}

diff --git a/ui/app/templates/components/secret-create-or-update.hbs b/ui/app/templates/components/secret-create-or-update.hbs index 7a5cae09c9734..0efbe0da30914 100644 --- a/ui/app/templates/components/secret-create-or-update.hbs +++ b/ui/app/templates/components/secret-create-or-update.hbs @@ -97,7 +97,8 @@ {{/each}} {{/if}} - {{#if (and @isV2 @canCreateSecretMetadata) }} + {{!-- must have UPDATE permissions to add secret metadata. Create only will not work --}} + {{#if (and @isV2 @canUpdateSecretMetadata)}} {{/if}} - {{/if}} + {{/if}}