From 4a9669a1bcb80270ae967e022f4cca31ca466da7 Mon Sep 17 00:00:00 2001 From: Chelsea Shaw <82459713+hashishaw@users.noreply.github.com> Date: Tue, 20 Jul 2021 11:28:44 -0500 Subject: [PATCH] UI/database cg read role (#12111) * Add type param to secret show, handle CG in database role show * If roleType is passed to credential, only make one creds API call * Clean up db role adapter and serializer * url param roleType passed to credentials call * Role list capabilities check for static and dynamic separately * Add changelog * Consistent adapter response for single or double call * Prioritize dynamic response if control group on role/creds --- changelog/12111.txt | 3 + ui/app/adapters/database/credential.js | 27 +++--- ui/app/adapters/database/role.js | 87 ++++++++++++------- ui/app/components/database-role-edit.js | 6 +- .../cluster/secrets/backend/credentials.js | 4 +- .../vault/cluster/secrets/backend/show.js | 4 +- ui/app/helpers/secret-query-params.js | 5 +- ui/app/models/database/role.js | 2 + .../cluster/secrets/backend/credentials.js | 6 +- .../cluster/secrets/backend/secret-edit.js | 3 +- ui/app/serializers/database/role.js | 7 +- .../components/database-role-edit.hbs | 4 +- .../secret-list/database-list-item.hbs | 19 +++- 13 files changed, 116 insertions(+), 61 deletions(-) create mode 100644 changelog/12111.txt diff --git a/changelog/12111.txt b/changelog/12111.txt new file mode 100644 index 000000000..192b4a16a --- /dev/null +++ b/changelog/12111.txt @@ -0,0 +1,3 @@ +```release-note:bug +ui: Fix database role CG access +``` \ No newline at end of file diff --git a/ui/app/adapters/database/credential.js b/ui/app/adapters/database/credential.js index f054dfca1..62838bc05 100644 --- a/ui/app/adapters/database/credential.js +++ b/ui/app/adapters/database/credential.js @@ -1,5 +1,6 @@ -import RSVP from 'rsvp'; +import { allSettled } from 'rsvp'; import ApplicationAdapter from '../application'; +import ControlGroupError from 'vault/lib/control-group-error'; export default ApplicationAdapter.extend({ namespace: 'v1', @@ -20,18 +21,20 @@ export default ApplicationAdapter.extend({ fetchByQuery(store, query) { const { backend, secret } = query; - return RSVP.allSettled([this._staticCreds(backend, secret), this._dynamicCreds(backend, secret)]).then( + if (query.roleType === 'static') { + return this._staticCreds(backend, secret); + } else if (query.roleType === 'dynamic') { + return this._dynamicCreds(backend, secret); + } + return 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; + if (staticResp.state === 'rejected' && dynamicResp.state === 'rejected') { + let reason = staticResp.reason; + if (dynamicResp.reason instanceof ControlGroupError) { + throw dynamicResp.reason; + } + if (reason?.httpStatus < dynamicResp.reason?.httpStatus) { + reason = dynamicResp.reason; } throw reason; } diff --git a/ui/app/adapters/database/role.js b/ui/app/adapters/database/role.js index 4575bfecc..550537469 100644 --- a/ui/app/adapters/database/role.js +++ b/ui/app/adapters/database/role.js @@ -1,5 +1,6 @@ import { assign } from '@ember/polyfills'; import { assert } from '@ember/debug'; +import ControlGroupError from 'vault/lib/control-group-error'; import ApplicationAdapter from '../application'; import { allSettled } from 'rsvp'; import { addToArray } from 'vault/helpers/add-to-array'; @@ -24,11 +25,31 @@ export default ApplicationAdapter.extend({ }, staticRoles(backend, id) { - return this.ajax(this.urlFor(backend, id, 'static'), 'GET', this.optionsForQuery(id)); + return this.ajax(this.urlFor(backend, id, 'static'), 'GET', this.optionsForQuery(id)).then(resp => { + if (id) { + return { + ...resp, + type: 'static', + backend, + id, + }; + } + return resp; + }); }, dynamicRoles(backend, id) { - return this.ajax(this.urlFor(backend, id), 'GET', this.optionsForQuery(id)); + return this.ajax(this.urlFor(backend, id), 'GET', this.optionsForQuery(id)).then(resp => { + if (id) { + return { + ...resp, + type: 'dynamic', + backend, + id, + }; + } + return resp; + }); }, optionsForQuery(id) { @@ -39,39 +60,43 @@ export default ApplicationAdapter.extend({ return { data }; }, - fetchByQuery(store, query) { - const { backend, id } = query; - return this.ajax(this.urlFor(backend, id), 'GET', this.optionsForQuery(id)).then(resp => { - resp.id = id; - resp.backend = backend; - return resp; - }); - }, - queryRecord(store, type, query) { const { backend, id } = query; - const staticReq = this.staticRoles(backend, id); - const dynamicReq = this.dynamicRoles(backend, id); - return allSettled([staticReq, dynamicReq]).then(([staticResp, dynamicResp]) => { - if (!staticResp.value && !dynamicResp.value) { - // Throw error, both reqs failed - throw dynamicResp.reason; + if (query.type === 'static') { + return this.staticRoles(backend, id); + } else if (query?.type === 'dynamic') { + return this.dynamicRoles(backend, id); + } + // if role type is not defined, try both + return allSettled([this.staticRoles(backend, id), this.dynamicRoles(backend, id)]).then( + ([staticResp, dynamicResp]) => { + if (staticResp.state === 'rejected' && dynamicResp.state === 'rejected') { + let reason = staticResp.reason; + if (dynamicResp.reason instanceof ControlGroupError) { + throw dynamicResp.reason; + } + if (reason?.httpStatus < dynamicResp.reason?.httpStatus) { + reason = dynamicResp.reason; + } + throw reason; + } + // Names are distinct across both types of role, + // so only one request should ever come back with value + let type = staticResp.value ? 'static' : 'dynamic'; + let successful = staticResp.value || dynamicResp.value; + let resp = { + data: {}, + backend, + id, + type, + }; + + resp.data = assign({}, successful.data); + + return resp; } - // Names are distinct across both types of role, - // so only one request should ever come back with value - let type = staticResp.value ? 'static' : 'dynamic'; - let successful = staticResp.value || dynamicResp.value; - let resp = { - data: {}, - backend, - secret: id, - }; - - resp.data = assign({}, resp.data, successful.data, { backend, type, secret: id }); - - return resp; - }); + ); }, query(store, type, query) { diff --git a/ui/app/components/database-role-edit.js b/ui/app/components/database-role-edit.js index a8fa82d0f..cda7b7a9c 100644 --- a/ui/app/components/database-role-edit.js +++ b/ui/app/components/database-role-edit.js @@ -54,8 +54,10 @@ export default class DatabaseRoleEdit extends Component { } @action - generateCreds(roleId) { - this.router.transitionTo('vault.cluster.secrets.backend.credentials', roleId); + generateCreds(roleId, roleType = '') { + this.router.transitionTo('vault.cluster.secrets.backend.credentials', roleId, { + queryParams: { roleType }, + }); } @action diff --git a/ui/app/controllers/vault/cluster/secrets/backend/credentials.js b/ui/app/controllers/vault/cluster/secrets/backend/credentials.js index a885d7989..9c6455a0d 100644 --- a/ui/app/controllers/vault/cluster/secrets/backend/credentials.js +++ b/ui/app/controllers/vault/cluster/secrets/backend/credentials.js @@ -1,9 +1,11 @@ import Controller from '@ember/controller'; export default Controller.extend({ - queryParams: ['action'], + queryParams: ['action', 'roleType'], action: '', + roleType: '', reset() { this.set('action', ''); + this.set('roleType', ''); }, }); diff --git a/ui/app/controllers/vault/cluster/secrets/backend/show.js b/ui/app/controllers/vault/cluster/secrets/backend/show.js index ca73190cc..6f7ab2c06 100644 --- a/ui/app/controllers/vault/cluster/secrets/backend/show.js +++ b/ui/app/controllers/vault/cluster/secrets/backend/show.js @@ -3,12 +3,14 @@ import BackendCrumbMixin from 'vault/mixins/backend-crumb'; export default Controller.extend(BackendCrumbMixin, { backendController: controller('vault.cluster.secrets.backend'), - queryParams: ['tab', 'version'], + queryParams: ['tab', 'version', 'type'], version: '', tab: '', + type: '', reset() { this.set('tab', ''); this.set('version', ''); + this.set('type', ''); }, actions: { refresh: function() { diff --git a/ui/app/helpers/secret-query-params.js b/ui/app/helpers/secret-query-params.js index 483700274..be7a873f6 100644 --- a/ui/app/helpers/secret-query-params.js +++ b/ui/app/helpers/secret-query-params.js @@ -1,9 +1,12 @@ import { helper } from '@ember/component/helper'; -export function secretQueryParams([backendType]) { +export function secretQueryParams([backendType, type = '']) { if (backendType === 'transit') { return { tab: 'actions' }; } + if (backendType === 'database') { + return { type: type }; + } return; } diff --git a/ui/app/models/database/role.js b/ui/app/models/database/role.js index 18f2b9572..cdc557c8f 100644 --- a/ui/app/models/database/role.js +++ b/ui/app/models/database/role.js @@ -127,7 +127,9 @@ export default Model.extend({ staticPath: lazyCapabilities(apiPath`${'backend'}/static-roles/+`, 'backend'), canCreateStatic: alias('staticPath.canCreate'), credentialPath: lazyCapabilities(apiPath`${'backend'}/creds/${'id'}`, 'backend', 'id'), + staticCredentialPath: lazyCapabilities(apiPath`${'backend'}/static-creds/${'id'}`, 'backend', 'id'), canGenerateCredentials: alias('credentialPath.canRead'), + canGetCredentials: alias('staticCredentialPath.canRead'), databasePath: lazyCapabilities(apiPath`${'backend'}/config/${'database[0]'}`, 'backend', 'database'), canUpdateDb: alias('databasePath.canUpdate'), }); diff --git a/ui/app/routes/vault/cluster/secrets/backend/credentials.js b/ui/app/routes/vault/cluster/secrets/backend/credentials.js index f3f5b60be..ae38e7199 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/credentials.js +++ b/ui/app/routes/vault/cluster/secrets/backend/credentials.js @@ -23,8 +23,8 @@ export default Route.extend({ return this.pathHelp.getNewModel(modelType, backend); }, - getDatabaseCredential(backend, secret) { - return this.store.queryRecord('database/credential', { backend, secret }).catch(error => { + getDatabaseCredential(backend, secret, roleType = '') { + return this.store.queryRecord('database/credential', { backend, secret, roleType }).catch(error => { if (error instanceof ControlGroupError) { throw error; } @@ -57,7 +57,7 @@ export default Route.extend({ let roleType = params.roleType; let dbCred; if (backendType === 'database') { - dbCred = await this.getDatabaseCredential(backendPath, role); + dbCred = await this.getDatabaseCredential(backendPath, role, roleType); } if (!SUPPORTED_DYNAMIC_BACKENDS.includes(backendModel.get('type'))) { return this.transitionTo('vault.cluster.secrets.backend.list-root', backendPath); diff --git a/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js b/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js index 2de8a9e95..dabebd1c1 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js +++ b/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js @@ -219,6 +219,7 @@ export default Route.extend(UnloadModelRoute, { let secret = this.secretParam(); let backend = this.enginePathParam(); let modelType = this.modelType(backend, secret); + let type = params.type || ''; if (!secret) { secret = '\u0020'; } @@ -235,7 +236,7 @@ export default Route.extend(UnloadModelRoute, { let capabilities = this.capabilities(secret, modelType); try { - secretModel = await this.store.queryRecord(modelType, { id: secret, backend }); + secretModel = await this.store.queryRecord(modelType, { id: secret, backend, type }); } catch (err) { // we've failed the read request, but if it's a kv-type backend, we want to // do additional checks of the capabilities diff --git a/ui/app/serializers/database/role.js b/ui/app/serializers/database/role.js index e69bfa6d8..d4f480a3b 100644 --- a/ui/app/serializers/database/role.js +++ b/ui/app/serializers/database/role.js @@ -17,7 +17,7 @@ export default RESTSerializer.extend({ return roles; } let path = 'roles'; - if (payload.data.type === 'static') { + if (payload.type === 'static') { path = 'static-roles'; } let database = []; @@ -34,9 +34,10 @@ export default RESTSerializer.extend({ revocation_statement = payload.data.revocation_statements[0]; } return { - id: payload.secret, - name: payload.secret, + id: payload.id, backend: payload.backend, + name: payload.id, + type: payload.type, database, path, creation_statement, diff --git a/ui/app/templates/components/database-role-edit.hbs b/ui/app/templates/components/database-role-edit.hbs index 8381befec..408b6a083 100644 --- a/ui/app/templates/components/database-role-edit.hbs +++ b/ui/app/templates/components/database-role-edit.hbs @@ -32,10 +32,10 @@
{{/if}} {{#if @model.canGenerateCredentials}} -