UI/control group db cred (#12024)

This commit is contained in:
Chelsea Shaw 2021-07-12 12:50:30 -05:00 committed by GitHub
parent 505e3f9a89
commit b1a9e4fe58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 117 additions and 113 deletions

3
changelog/12024.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:bug
ui: fix control group access for database credential
```

View File

@ -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);
},

View File

@ -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',

View File

@ -8,82 +8,19 @@
* <GenerateCredentialsDatabase @backendPath="database" @backendType="database" @roleName="my-role"/>
* ```
* @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();
}

View File

@ -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;
};

View File

@ -8,4 +8,5 @@ export default Model.extend({
lastVaultRotation: attr('string'),
rotationPeriod: attr('number'),
ttl: attr('number'),
roleType: attr('string'),
});

View File

@ -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,
});
},

View File

@ -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;
}
},

View File

@ -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"}}
</button>
{{/if}}
{{#if @model.canEditRole}}

View File

@ -18,15 +18,15 @@
</p.levelLeft>
</PageHeader>
<div class={{unless (eq this.roleType 'noRoleFound') "box is-fullwidth is-sideless is-marginless"}}>
{{!-- ROLE TYPE NOT FOUND, returned when query on the creds and static creds both returned error --}}
{{#if (eq this.roleType 'noRoleFound') }}
<div class={{if @roleType "box is-fullwidth is-sideless is-marginless"}}>
{{!-- If no role type, that means both static and dynamic requests returned an error --}}
{{#unless @roleType }}
<EmptyState
@title={{this.errorTitle}}
@subTitle="Error {{this.errorHttpStatus}}"
@title={{errorTitle}}
@subTitle="Error {{@model.errorHttpStatus}}"
@icon="alert-circle-outline"
@bottomBorder={{true}}
@message={{this.errorMessage}}
@message={{@model.errorMessage}}
>
<nav class="breadcrumb">
<ul class="is-grouped-split">
@ -41,8 +41,8 @@
</ul>
</nav>
</EmptyState>
{{/if}}
{{#unless (or model.errorMessage (eq this.roleType 'noRoleFound'))}}
{{/unless}}
{{#unless (or @model.errorMessage (not @roleType))}}
<AlertBanner
@type="warning"
@message="You will not be able to access these credentials later, so please copy them now."
@ -50,44 +50,44 @@
/>
{{/unless}}
{{!-- DYNAMIC ROLE --}}
{{#if (and (eq this.roleType 'dynamic') model.username)}}
<InfoTableRow @label="Username" @value={{model.username}}>
{{#if (and (eq @roleType 'dynamic') @model.username)}}
<InfoTableRow @label="Username" @value={{@model.username}}>
<MaskedInput
@value={{model.username}}
@value={{@model.username}}
@name="Username"
@displayOnly={{true}}
@allowCopy={{true}}
/>
</InfoTableRow>
<InfoTableRow @label="Password" @value={{model.password}}>
<InfoTableRow @label="Password" @value={{@model.password}}>
<MaskedInput
@value={{model.password}}
@value={{@model.password}}
@name="Password"
@displayOnly={{true}}
@allowCopy={{true}}
/>
</InfoTableRow>
<InfoTableRow @label="Lease ID" @value={{model.leaseId}} />
<InfoTableRow @label="Lease Duration" @value={{format-duration model.leaseDuration }} />
<InfoTableRow @label="Lease ID" @value={{@model.leaseId}} />
<InfoTableRow @label="Lease Duration" @value={{format-duration @model.leaseDuration }} />
{{/if}}
{{!-- STATIC ROLE --}}
{{#if (and (eq this.roleType 'static') model.username)}}
{{#if (and (eq @roleType 'static') @model.username)}}
<InfoTableRow
@label="Last Vault rotation"
@value={{date-format model.lastVaultRotation 'MMMM d yyyy, h:mm:ss a'}}
@tooltipText={{model.lastVaultRotation}}
@value={{date-format @model.lastVaultRotation 'MMMM d yyyy, h:mm:ss a'}}
@tooltipText={{@model.lastVaultRotation}}
/>
<InfoTableRow @label="Password" @value={{model.password}}>
<InfoTableRow @label="Password" @value={{@model.password}}>
<MaskedInput
@value={{model.password}}
@value={{@model.password}}
@name="Password"
@displayOnly={{true}}
@allowCopy={{true}}
/>
</InfoTableRow>
<InfoTableRow @label="Username" @value={{model.username}} />
<InfoTableRow @label="Rotation Period" @value={{format-duration model.rotationPeriod}} />
<InfoTableRow @label="Time Remaining" @value={{format-duration model.ttl}} />
<InfoTableRow @label="Username" @value={{@model.username}} />
<InfoTableRow @label="Rotation Period" @value={{format-duration @model.rotationPeriod}} />
<InfoTableRow @label="Time Remaining" @value={{format-duration @model.ttl}} />
{{/if}}
</div>
<div class="has-top-bottom-margin">

View File

@ -1,4 +1,4 @@
<div class="selectable-card is-rounded no-flex">
<form class="selectable-card is-rounded no-flex">
<div class="is-flex-between is-fullwidth card-details" >
<h3 class="title is-5">{{@title}}</h3>
</div>
@ -15,13 +15,13 @@
@inputValue={{get model valuePath}}
data-test-search-roles
/>
<button
type="button"
<input
type="submit"
value={{@title}}
class="button is-secondary"
disabled={{buttonDisabled}}
onclick={{action "transitionToCredential"}}
data-test-get-credentials
>
{{@title}}
</button>
</div>
/>
</form>

View File

@ -53,7 +53,7 @@
{{#if @item.canGenerateCredentials}}
<li class="action">
<LinkTo @route="vault.cluster.secrets.backend.credentials" @model={{@item.id}} @query={{hash roleType=this.keyTypeValue}}>
Generate credentials
{{if (eq @item.type "static") "Get credentials" "Generate credentials"}}
</LinkTo>
</li>
{{/if}}

View File

@ -1,9 +1,9 @@
{{#if (eq model.backendType 'database')}}
<GenerateCredentialsDatabase
@backendPath={{model.backendPath}}
@backendType={{model.backendType}}
@roleName={{model.roleName}}
@roleType={{model.roleType}}
@roleType={{model.dbCred.roleType}}
@model={{model.dbCred}}
/>
{{else}}
{{!-- TODO smells a little to have action off of query param requiring a conditional --}}