From b1a9e4fe5845c0b91b9c80929c635beb6d0aa9ee Mon Sep 17 00:00:00 2001
From: Chelsea Shaw <82459713+hashishaw@users.noreply.github.com>
Date: Mon, 12 Jul 2021 12:50:30 -0500
Subject: [PATCH] UI/control group db cred (#12024)
---
changelog/12024.txt | 3 +
ui/app/adapters/database/credential.js | 38 ++++++++--
ui/app/adapters/secret-v2-version.js | 1 -
.../generate-credentials-database.js | 71 ++-----------------
ui/app/components/secret-list-header-tab.js | 5 +-
ui/app/models/database/credential.js | 1 +
.../cluster/secrets/backend/credentials.js | 35 ++++++++-
ui/app/serializers/database/credential.js | 8 +--
.../components/database-role-edit.hbs | 2 +-
.../generate-credentials-database.hbs | 46 ++++++------
.../components/get-credentials-card.hbs | 14 ++--
.../secret-list/database-list-item.hbs | 2 +-
.../cluster/secrets/backend/credentials.hbs | 4 +-
13 files changed, 117 insertions(+), 113 deletions(-)
create mode 100644 changelog/12024.txt
diff --git a/changelog/12024.txt b/changelog/12024.txt
new file mode 100644
index 000000000..52e39a0fd
--- /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 e182ef8fc..f054dfca1 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)}/${creds}/${encodeURIComponent(secret)}`,
+ `${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)}`,
+ '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 5bdd02129..4a6ab374d 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 867b739f1..5cead9e9d 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 14a2ae15f..3627a2505 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 114d36966..080a738c4 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 7ec251bbe..f3f5b60be 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 da4b9003a..c0c157fb2 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-role-edit.hbs b/ui/app/templates/components/database-role-edit.hbs
index 1f4b460f1..8381befec 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 0dd408e42..4661500df 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 f45b22d4e..8e2d06d8c 100644
--- a/ui/app/templates/components/get-credentials-card.hbs
+++ b/ui/app/templates/components/get-credentials-card.hbs
@@ -1,4 +1,4 @@
-
+ />
+
+
diff --git a/ui/app/templates/components/secret-list/database-list-item.hbs b/ui/app/templates/components/secret-list/database-list-item.hbs
index d868f76bd..b74bea636 100644
--- a/ui/app/templates/components/secret-list/database-list-item.hbs
+++ b/ui/app/templates/components/secret-list/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/vault/cluster/secrets/backend/credentials.hbs b/ui/app/templates/vault/cluster/secrets/backend/credentials.hbs
index e2af4716e..855a53b8e 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 --}}