UI/control group db cred (#12024)
This commit is contained in:
parent
505e3f9a89
commit
b1a9e4fe58
|
@ -0,0 +1,3 @@
|
|||
```release-note:bug
|
||||
ui: fix control group access for database credential
|
||||
```
|
|
@ -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);
|
||||
},
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -8,4 +8,5 @@ export default Model.extend({
|
|||
lastVaultRotation: attr('string'),
|
||||
rotationPeriod: attr('number'),
|
||||
ttl: attr('number'),
|
||||
roleType: attr('string'),
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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 --}}
|
||||
|
|
Loading…
Reference in New Issue