Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI/control group db cred #12024

Merged
merged 6 commits into from
Jul 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/12024.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
ui: fix control group access for database credential
```
38 changes: 34 additions & 4 deletions ui/app/adapters/database/credential.js
Original file line number Diff line number Diff line change
@@ -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);
},
Expand Down
1 change: 0 additions & 1 deletion ui/app/adapters/secret-v2-version.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
71 changes: 4 additions & 67 deletions ui/app/components/generate-credentials-database.js
Original file line number Diff line number Diff line change
Expand Up @@ -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*() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

much cleaner, thank you for moving this logic out of the component.

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();
}
Expand Down
5 changes: 4 additions & 1 deletion ui/app/components/secret-list-header-tab.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down
1 change: 1 addition & 0 deletions ui/app/models/database/credential.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export default Model.extend({
lastVaultRotation: attr('string'),
rotationPeriod: attr('number'),
ttl: attr('number'),
roleType: attr('string'),
});
35 changes: 33 additions & 2 deletions ui/app/routes/vault/cluster/secrets/backend/credentials.js
Original file line number Diff line number Diff line change
@@ -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'];

Expand All @@ -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);
}
Expand All @@ -37,6 +67,7 @@ export default Route.extend({
backendType,
roleName: role,
roleType,
dbCred,
});
},

Expand Down
8 changes: 4 additions & 4 deletions ui/app/serializers/database/credential.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
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,
leaseDuration: payload.lease_duration,
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;
}
},

Expand Down
2 changes: 1 addition & 1 deletion ui/app/templates/components/database-role-edit.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -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"}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you for tackling this while you're in here

</button>
{{/if}}
{{#if @model.canEditRole}}
Expand Down
46 changes: 23 additions & 23 deletions ui/app/templates/components/generate-credentials-database.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@
</p.levelLeft>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

glimmerizing! 💙

</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">
Expand All @@ -41,53 +41,53 @@
</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."
data-test-warning
/>
{{/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">
Expand Down
14 changes: 7 additions & 7 deletions ui/app/templates/components/get-credentials-card.hbs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class="selectable-card is-rounded no-flex">
hashishaw marked this conversation as resolved.
Show resolved Hide resolved
<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>
Expand All @@ -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>
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand Down
Loading