diff --git a/changelog/12024.txt b/changelog/12024.txt new file mode 100644 index 000000000000..52e39a0fd63b --- /dev/null +++ b/changelog/12024.txt @@ -0,0 +1,3 @@ +```release-note:bug +ui: fix control group access for database credential +``` diff --git a/ui/app/adapters/database/credential.js b/ui/app/adapters/database/credential.js index e182ef8fc6df..f054dfca1856 100644 --- a/ui/app/adapters/database/credential.js +++ b/ui/app/adapters/database/credential.js @@ -1,16 +1,46 @@ +import RSVP from 'rsvp'; import ApplicationAdapter from '../application'; export default ApplicationAdapter.extend({ namespace: 'v1', - fetchByQuery(store, query) { - const { backend, roleType, secret } = query; - let creds = roleType === 'static' ? 'static-creds' : 'creds'; + _staticCreds(backend, secret) { + return this.ajax( + `${this.buildURL()}/${encodeURIComponent(backend)}/static-creds/${encodeURIComponent(secret)}`, + 'GET' + ).then(resp => ({ ...resp, roleType: 'static' })); + }, + + _dynamicCreds(backend, secret) { return this.ajax( - `${this.buildURL()}/${encodeURIComponent(backend)}/${creds}/${encodeURIComponent(secret)}`, + `${this.buildURL()}/${encodeURIComponent(backend)}/creds/${encodeURIComponent(secret)}`, 'GET' + ).then(resp => ({ ...resp, roleType: 'dynamic' })); + }, + + fetchByQuery(store, query) { + const { backend, secret } = query; + return RSVP.allSettled([this._staticCreds(backend, secret), this._dynamicCreds(backend, secret)]).then( + ([staticResp, dynamicResp]) => { + // If one comes back with wrapped response from control group, throw it + const accessor = staticResp.accessor || dynamicResp.accessor; + if (accessor) { + throw accessor; + } + // if neither has payload, throw reason with highest httpStatus + if (!staticResp.value && !dynamicResp.value) { + let reason = dynamicResp.reason; + if (reason?.httpStatus < staticResp.reason?.httpStatus) { + reason = staticResp.reason; + } + throw reason; + } + // Otherwise, return whichever one has a value + return staticResp.value || dynamicResp.value; + } ); }, + queryRecord(store, type, query) { return this.fetchByQuery(store, query); }, diff --git a/ui/app/adapters/secret-v2-version.js b/ui/app/adapters/secret-v2-version.js index 5aac49d731d9..ef1a4a2725ad 100644 --- a/ui/app/adapters/secret-v2-version.js +++ b/ui/app/adapters/secret-v2-version.js @@ -4,7 +4,6 @@ import { isEmpty } from '@ember/utils'; import { get } from '@ember/object'; import ApplicationAdapter from './application'; import { encodePath } from 'vault/utils/path-encoding-helpers'; -import ControlGroupError from 'vault/lib/control-group-error'; export default ApplicationAdapter.extend({ namespace: 'v1', diff --git a/ui/app/components/generate-credentials-database.js b/ui/app/components/generate-credentials-database.js index 867b739f1a55..5cead9e9d31b 100644 --- a/ui/app/components/generate-credentials-database.js +++ b/ui/app/components/generate-credentials-database.js @@ -8,82 +8,19 @@ * * ``` * @param {string} backendPath - the secret backend name. This is used in the breadcrumb. - * @param {object} backendType - the secret type. Expected to be database. + * @param {string} roleType - either 'static', 'dynamic', or falsey. * @param {string} roleName - the id of the credential returning. + * @param {object} model - database/credential model passed in. If no data, should have errorTitle, errorMessage, and errorHttpStatus */ -import { inject as service } from '@ember/service'; import Component from '@glimmer/component'; -import { task } from 'ember-concurrency'; import { action } from '@ember/object'; -import { tracked } from '@glimmer/tracking'; export default class GenerateCredentialsDatabase extends Component { - @service store; - // set on the component - backendType = null; - backendPath = null; - roleName = null; - @tracked roleType = ''; - @tracked model = null; - @tracked errorMessage = ''; - @tracked errorHttpStatus = ''; - @tracked errorTitle = 'Something went wrong'; - - constructor() { - super(...arguments); - this.fetchCredentials.perform(); + get errorTitle() { + return this.args.model.errorTitle || 'Something went wrong'; } - @task(function*() { - let { roleName, backendPath } = this.args; - try { - let newModel = yield this.store.queryRecord('database/credential', { - backend: backendPath, - secret: roleName, - roleType: 'static', - }); - this.model = newModel; - this.roleType = 'static'; - return; - } catch (error) { - this.errorHttpStatus = error.httpStatus; // set default http - this.errorMessage = `We ran into a problem and could not continue: ${error.errors[0]}`; - if (error.httpStatus === 403) { - // 403 is forbidden - this.errorTitle = 'You are not authorized'; - this.errorMessage = - "Role wasn't found or you do not have permissions. Ask your administrator if you think you should have access."; - } - } - try { - let newModel = yield this.store.queryRecord('database/credential', { - backend: backendPath, - secret: roleName, - roleType: 'dynamic', - }); - this.model = newModel; - this.roleType = 'dynamic'; - return; - } catch (error) { - if (error.httpStatus === 403) { - // 403 is forbidden - this.errorHttpStatus = error.httpStatus; // override default httpStatus which could be 400 which always happens on either dynamic or static depending on which kind of role you're querying - this.errorTitle = 'You are not authorized'; - this.errorMessage = - "Role wasn't found or you do not have permissions. Ask your administrator if you think you should have access."; - } - if (error.httpStatus == 500) { - // internal server error happens when empty creation statement on dynamic role creation only - this.errorHttpStatus = error.httpStatus; - this.errorTitle = 'Internal Error'; - this.errorMessage = error.errors[0]; - } - } - this.roleType = 'noRoleFound'; - }) - fetchCredentials; - @action redirectPreviousPage() { window.history.back(); } diff --git a/ui/app/components/secret-list-header-tab.js b/ui/app/components/secret-list-header-tab.js index 14a2ae15f821..3627a250527b 100644 --- a/ui/app/components/secret-list-header-tab.js +++ b/ui/app/components/secret-list-header-tab.js @@ -40,7 +40,10 @@ export default class SecretListHeaderTab extends Component { let array = []; // we only want to look at the canList, canCreate and canUpdate on the capabilities record capabilitiesArray.forEach(item => { - array.push(object[item]); + // object is sometimes null + if (object) { + array.push(object[item]); + } }); return array; }; diff --git a/ui/app/models/database/credential.js b/ui/app/models/database/credential.js index 114d36966612..080a738c4d25 100644 --- a/ui/app/models/database/credential.js +++ b/ui/app/models/database/credential.js @@ -8,4 +8,5 @@ export default Model.extend({ lastVaultRotation: attr('string'), rotationPeriod: attr('number'), ttl: attr('number'), + roleType: attr('string'), }); diff --git a/ui/app/routes/vault/cluster/secrets/backend/credentials.js b/ui/app/routes/vault/cluster/secrets/backend/credentials.js index 7ec251bbe577..f3f5b60be7bc 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/credentials.js +++ b/ui/app/routes/vault/cluster/secrets/backend/credentials.js @@ -1,6 +1,7 @@ import { resolve } from 'rsvp'; import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; +import ControlGroupError from 'vault/lib/control-group-error'; const SUPPORTED_DYNAMIC_BACKENDS = ['database', 'ssh', 'aws', 'pki']; @@ -22,13 +23,42 @@ export default Route.extend({ return this.pathHelp.getNewModel(modelType, backend); }, - model(params) { + getDatabaseCredential(backend, secret) { + return this.store.queryRecord('database/credential', { backend, secret }).catch(error => { + if (error instanceof ControlGroupError) { + throw error; + } + // Unless it's a control group error, we want to pass back error info + // so we can render it on the GenerateCredentialsDatabase component + let status = error?.httpStatus; + let title; + let message = `We ran into a problem and could not continue: ${ + error?.errors ? error.errors[0] : 'See Vault logs for details.' + }`; + if (status === 403) { + // 403 is forbidden + title = 'You are not authorized'; + message = + "Role wasn't found or you do not have permissions. Ask your administrator if you think you should have access."; + } + return { + errorHttpStatus: status, + errorTitle: title, + errorMessage: message, + }; + }); + }, + + async model(params) { let role = params.secret; let backendModel = this.backendModel(); let backendPath = backendModel.get('id'); let backendType = backendModel.get('type'); let roleType = params.roleType; - + let dbCred; + if (backendType === 'database') { + dbCred = await this.getDatabaseCredential(backendPath, role); + } if (!SUPPORTED_DYNAMIC_BACKENDS.includes(backendModel.get('type'))) { return this.transitionTo('vault.cluster.secrets.backend.list-root', backendPath); } @@ -37,6 +67,7 @@ export default Route.extend({ backendType, roleName: role, roleType, + dbCred, }); }, diff --git a/ui/app/serializers/database/credential.js b/ui/app/serializers/database/credential.js index da4b9003a843..c0c157fb2a96 100644 --- a/ui/app/serializers/database/credential.js +++ b/ui/app/serializers/database/credential.js @@ -1,12 +1,11 @@ import RESTSerializer from '@ember-data/serializer/rest'; export default RESTSerializer.extend({ - primaryKey: 'request_id', + primaryKey: 'username', normalizePayload(payload) { if (payload.data) { - const credentials = { - request_id: payload.request_id, + return { username: payload.data.username, password: payload.data.password, leaseId: payload.lease_id, @@ -14,8 +13,9 @@ export default RESTSerializer.extend({ lastVaultRotation: payload.data.last_vault_rotation, rotationPeriod: payload.data.rotation_period, ttl: payload.data.ttl, + // roleType is added on adapter + roleType: payload.roleType, }; - return credentials; } }, diff --git a/ui/app/templates/components/database-list-item.hbs b/ui/app/templates/components/database-list-item.hbs index 2badff9aae89..2fd9ed4f4e9b 100644 --- a/ui/app/templates/components/database-list-item.hbs +++ b/ui/app/templates/components/database-list-item.hbs @@ -53,7 +53,7 @@ {{#if @item.canGenerateCredentials}}
  • - Generate credentials + {{if (eq @item.type "static") "Get credentials" "Generate credentials"}}
  • {{/if}} diff --git a/ui/app/templates/components/database-role-edit.hbs b/ui/app/templates/components/database-role-edit.hbs index 4dd25ed2e5fc..1a642d6e650c 100644 --- a/ui/app/templates/components/database-role-edit.hbs +++ b/ui/app/templates/components/database-role-edit.hbs @@ -38,7 +38,7 @@ {{on 'click' (fn this.generateCreds @model.id)}} data-test-database-role-generate-creds > - Generate credentials + {{if (eq @model.type "static") "Get credentials" "Generate credentials"}} {{/if}} {{#if @model.canEditRole}} diff --git a/ui/app/templates/components/generate-credentials-database.hbs b/ui/app/templates/components/generate-credentials-database.hbs index 0dd408e426fa..4661500df45c 100644 --- a/ui/app/templates/components/generate-credentials-database.hbs +++ b/ui/app/templates/components/generate-credentials-database.hbs @@ -18,15 +18,15 @@ -
    - {{!-- ROLE TYPE NOT FOUND, returned when query on the creds and static creds both returned error --}} - {{#if (eq this.roleType 'noRoleFound') }} +
    + {{!-- If no role type, that means both static and dynamic requests returned an error --}} + {{#unless @roleType }} - {{/if}} - {{#unless (or model.errorMessage (eq this.roleType 'noRoleFound'))}} + {{/unless}} + {{#unless (or @model.errorMessage (not @roleType))}} {{/unless}} {{!-- DYNAMIC ROLE --}} - {{#if (and (eq this.roleType 'dynamic') model.username)}} - + {{#if (and (eq @roleType 'dynamic') @model.username)}} + - + - - + + {{/if}} {{!-- STATIC ROLE --}} - {{#if (and (eq this.roleType 'static') model.username)}} + {{#if (and (eq @roleType 'static') @model.username)}} - + - - - + + + {{/if}}
    diff --git a/ui/app/templates/components/get-credentials-card.hbs b/ui/app/templates/components/get-credentials-card.hbs index f45b22d4e8d6..8e2d06d8c053 100644 --- a/ui/app/templates/components/get-credentials-card.hbs +++ b/ui/app/templates/components/get-credentials-card.hbs @@ -1,4 +1,4 @@ -
    +

    {{@title}}

    @@ -15,13 +15,13 @@ @inputValue={{get model valuePath}} data-test-search-roles /> - -
    + /> + + diff --git a/ui/app/templates/vault/cluster/secrets/backend/credentials.hbs b/ui/app/templates/vault/cluster/secrets/backend/credentials.hbs index e2af4716e029..855a53b8e2b0 100644 --- a/ui/app/templates/vault/cluster/secrets/backend/credentials.hbs +++ b/ui/app/templates/vault/cluster/secrets/backend/credentials.hbs @@ -1,9 +1,9 @@ {{#if (eq model.backendType 'database')}} {{else}} {{!-- TODO smells a little to have action off of query param requiring a conditional --}}