mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-01 19:17:58 +00:00
Add Response Wrapping For Secrets (#5664)
This commit is contained in:
@@ -19,6 +19,11 @@ export default ApplicationAdapter.extend({
|
|||||||
return this._url(backend, path) + `?version=${version}`;
|
return this._url(backend, path) + `?version=${version}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
urlForQueryRecord(id) {
|
||||||
|
let [backend, path, version] = JSON.parse(id);
|
||||||
|
return this._url(backend, path) + `?version=${version}`;
|
||||||
|
},
|
||||||
|
|
||||||
findRecord() {
|
findRecord() {
|
||||||
return this._super(...arguments).catch(errorOrModel => {
|
return this._super(...arguments).catch(errorOrModel => {
|
||||||
// if it's a real 404, this will be an error, if not
|
// if it's a real 404, this will be an error, if not
|
||||||
@@ -30,6 +35,17 @@ export default ApplicationAdapter.extend({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
queryRecord(id, options) {
|
||||||
|
return this.ajax(this.urlForQueryRecord(id), 'GET', options).then(resp => {
|
||||||
|
if (options.wrapTTL) {
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
resp.id = id;
|
||||||
|
resp.backend = backend;
|
||||||
|
return resp;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
urlForCreateRecord(modelName, snapshot) {
|
urlForCreateRecord(modelName, snapshot) {
|
||||||
let backend = snapshot.belongsTo('secret').belongsTo('engine').id;
|
let backend = snapshot.belongsTo('secret').belongsTo('engine').id;
|
||||||
let path = snapshot.attr('path');
|
let path = snapshot.attr('path');
|
||||||
|
|||||||
@@ -34,22 +34,29 @@ export default ApplicationAdapter.extend({
|
|||||||
return url;
|
return url;
|
||||||
},
|
},
|
||||||
|
|
||||||
optionsForQuery(id, action) {
|
optionsForQuery(id, action, wrapTTL) {
|
||||||
let data = {};
|
let data = {};
|
||||||
if (action === 'query') {
|
if (action === 'query') {
|
||||||
data['list'] = true;
|
data.list = true;
|
||||||
|
}
|
||||||
|
if (wrapTTL) {
|
||||||
|
return { data, wrapTTL };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { data };
|
return { data };
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchByQuery(query, action) {
|
fetchByQuery(query, action) {
|
||||||
const { id, backend } = query;
|
const { id, backend, wrapTTL } = query;
|
||||||
return this.ajax(this.urlForSecret(backend, id), 'GET', this.optionsForQuery(id, action)).then(resp => {
|
return this.ajax(this.urlForSecret(backend, id), 'GET', this.optionsForQuery(id, action, wrapTTL)).then(
|
||||||
|
resp => {
|
||||||
|
if (wrapTTL) {
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
resp.id = id;
|
resp.id = id;
|
||||||
resp.backend = backend;
|
resp.backend = backend;
|
||||||
return resp;
|
return resp;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
query(store, type, query) {
|
query(store, type, query) {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export default Component.extend(FocusOnInsertMixin, {
|
|||||||
wizard: service(),
|
wizard: service(),
|
||||||
router: service(),
|
router: service(),
|
||||||
store: service(),
|
store: service(),
|
||||||
|
flashMessages: service(),
|
||||||
|
|
||||||
// a key model
|
// a key model
|
||||||
key: null,
|
key: null,
|
||||||
@@ -31,6 +32,10 @@ export default Component.extend(FocusOnInsertMixin, {
|
|||||||
|
|
||||||
secretData: null,
|
secretData: null,
|
||||||
|
|
||||||
|
wrappedData: null,
|
||||||
|
isWrapping: false,
|
||||||
|
showWrapButton: computed.not('wrappedData'),
|
||||||
|
|
||||||
// called with a bool indicating if there's been a change in the secretData
|
// called with a bool indicating if there's been a change in the secretData
|
||||||
onDataChange() {},
|
onDataChange() {},
|
||||||
onRefresh() {},
|
onRefresh() {},
|
||||||
@@ -235,6 +240,53 @@ export default Component.extend(FocusOnInsertMixin, {
|
|||||||
set(this.modelForData, 'secretData', this.secretData.toJSON());
|
set(this.modelForData, 'secretData', this.secretData.toJSON());
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleWrapClick() {
|
||||||
|
this.set('isWrapping', true);
|
||||||
|
if (this.isV2) {
|
||||||
|
this.store
|
||||||
|
.adapterFor('secret-v2-version')
|
||||||
|
.queryRecord(this.modelForData.id, { wrapTTL: 1800 })
|
||||||
|
.then(resp => {
|
||||||
|
this.set('wrappedData', resp.wrap_info.token);
|
||||||
|
this.flashMessages.success('Secret Successfully Wrapped!');
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.flashMessages.error('Could Not Wrap Secret');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.set('isWrapping', false);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.store
|
||||||
|
.adapterFor('secret')
|
||||||
|
.queryRecord(null, null, { backend: this.model.backend, id: this.modelForData.id, wrapTTL: 1800 })
|
||||||
|
.then(resp => {
|
||||||
|
this.set('wrappedData', resp.wrap_info.token);
|
||||||
|
this.flashMessages.success('Secret Successfully Wrapped!');
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.flashMessages.error('Could Not Wrap Secret');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.set('isWrapping', false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearWrappedData() {
|
||||||
|
this.set('wrappedData', null);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleCopySuccess() {
|
||||||
|
this.flashMessages.success('Copied Wrapped Data!');
|
||||||
|
this.send('clearWrappedData');
|
||||||
|
},
|
||||||
|
|
||||||
|
handleCopyError() {
|
||||||
|
this.flashMessages.error('Could Not Copy Wrapped Data');
|
||||||
|
this.send('clearWrappedData');
|
||||||
|
},
|
||||||
|
|
||||||
createOrUpdateKey(type, event) {
|
createOrUpdateKey(type, event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
let model = this.modelForData;
|
let model = this.modelForData;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const DEFAULTS = {
|
|||||||
creation_ttl: null,
|
creation_ttl: null,
|
||||||
data: '{\n}',
|
data: '{\n}',
|
||||||
unwrap_data: null,
|
unwrap_data: null,
|
||||||
|
details: null,
|
||||||
wrapTTL: null,
|
wrapTTL: null,
|
||||||
sum: null,
|
sum: null,
|
||||||
random_bytes: null,
|
random_bytes: null,
|
||||||
@@ -33,6 +34,7 @@ export default Component.extend(DEFAULTS, {
|
|||||||
algorithm: 'sha2-256',
|
algorithm: 'sha2-256',
|
||||||
|
|
||||||
tagName: '',
|
tagName: '',
|
||||||
|
unwrapActiveTab: 'data',
|
||||||
|
|
||||||
didReceiveAttrs() {
|
didReceiveAttrs() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
@@ -76,7 +78,13 @@ export default Component.extend(DEFAULTS, {
|
|||||||
let props = {};
|
let props = {};
|
||||||
let secret = (resp && resp.data) || resp.auth;
|
let secret = (resp && resp.data) || resp.auth;
|
||||||
if (secret && action === 'unwrap') {
|
if (secret && action === 'unwrap') {
|
||||||
props = assign({}, props, { unwrap_data: secret });
|
let details = {
|
||||||
|
'Request ID': resp.request_id,
|
||||||
|
'Lease ID': resp.lease_id || 'None',
|
||||||
|
Renewable: resp.renewable ? 'Yes' : 'No',
|
||||||
|
'Lease Duration': resp.lease_duration || 'None',
|
||||||
|
};
|
||||||
|
props = assign({}, props, { unwrap_data: secret }, { details: details });
|
||||||
}
|
}
|
||||||
props = assign({}, props, secret);
|
props = assign({}, props, secret);
|
||||||
if (resp && resp.wrap_info) {
|
if (resp && resp.wrap_info) {
|
||||||
|
|||||||
@@ -12,6 +12,10 @@
|
|||||||
padding-top: $spacing-s;
|
padding-top: $spacing-s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.has-padding {
|
||||||
|
padding: $size-10 $size-8;
|
||||||
|
}
|
||||||
|
|
||||||
// we want to style the boxes the same everywhere so they
|
// we want to style the boxes the same everywhere so they
|
||||||
// need to be the same font and small
|
// need to be the same font and small
|
||||||
.masked-input.masked .masked-value {
|
.masked-input.masked .masked-value {
|
||||||
|
|||||||
@@ -3,22 +3,23 @@
|
|||||||
|
|
||||||
ul {
|
ul {
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
|
min-height: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
li {
|
li {
|
||||||
&:focus {
|
&:focus {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
&.is-active a {
|
&.is-active a, &.is-active .tab {
|
||||||
border-color: $blue;
|
border-color: $blue;
|
||||||
color: $blue;
|
color: $blue;
|
||||||
}
|
}
|
||||||
&:first-child a {
|
&:first-child a, &:first-child .tab {
|
||||||
margin-left: $size-5;
|
margin-left: $size-5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a, .tab {
|
||||||
color: $grey-dark;
|
color: $grey-dark;
|
||||||
font-weight: $font-weight-semibold;
|
font-weight: $font-weight-semibold;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
<CopyButton
|
<CopyButton
|
||||||
@clipboardText={{value}}
|
@clipboardText={{value}}
|
||||||
@class="copy-button button is-compact"
|
@class="copy-button button is-compact"
|
||||||
|
@success={{success}}
|
||||||
data-test-copy-button
|
data-test-copy-button
|
||||||
>
|
>
|
||||||
<ICon @glyph="copy" aria-hidden="true" @size=16 />
|
<ICon @glyph="copy" aria-hidden="true" @size=16 />
|
||||||
|
|||||||
@@ -53,8 +53,65 @@
|
|||||||
<label for="json" class="has-text-grey">JSON</label>
|
<label for="json" class="has-text-grey">JSON</label>
|
||||||
</div>
|
</div>
|
||||||
{{#if (and (eq mode 'show') (or canEditV2Secret canEdit))}}
|
{{#if (and (eq mode 'show') (or canEditV2Secret canEdit))}}
|
||||||
<div class="control">
|
|
||||||
{{#let (concat 'vault.cluster.secrets.backend.' (if (eq mode 'show') 'edit' 'show')) as |targetRoute|}}
|
{{#let (concat 'vault.cluster.secrets.backend.' (if (eq mode 'show') 'edit' 'show')) as |targetRoute|}}
|
||||||
|
{{#unless (and isV2 (or modelForData.destroyed modelForData.deleted))}}
|
||||||
|
<div class="control">
|
||||||
|
<BasicDropdown
|
||||||
|
@class="popup-menu"
|
||||||
|
@horizontalPosition="auto-right"
|
||||||
|
@verticalPosition="below"
|
||||||
|
@onClose={{action "clearWrappedData"}}
|
||||||
|
as |D|
|
||||||
|
>
|
||||||
|
<D.trigger
|
||||||
|
data-test-popup-menu-trigger="true"
|
||||||
|
@class={{concat "link link-plain has-text-weight-semibold" (if D.isOpen " is-active")}}
|
||||||
|
@tagName="button"
|
||||||
|
>
|
||||||
|
Copy Secret
|
||||||
|
</D.trigger>
|
||||||
|
<D.content @class="popup-menu-content is-wide">
|
||||||
|
<nav class="box menu">
|
||||||
|
<ul class="menu-list">
|
||||||
|
<li class="action">
|
||||||
|
<CopyButton
|
||||||
|
@class="link link-plain has-text-weight-semibold is-ghost"
|
||||||
|
@clipboardText={{codemirrorString}}
|
||||||
|
@success={{action (set-flash-message "JSON Copied!")}}
|
||||||
|
data-test-copy-button
|
||||||
|
>
|
||||||
|
Copy JSON
|
||||||
|
</CopyButton>
|
||||||
|
</li>
|
||||||
|
<li class="action">
|
||||||
|
{{#if showWrapButton}}
|
||||||
|
<button
|
||||||
|
class="link link-plain has-text-weight-semibold is-ghost {{if isWrapping "is-loading"}}"
|
||||||
|
type="button"
|
||||||
|
{{action "handleWrapClick"}}
|
||||||
|
data-test-wrap-button
|
||||||
|
disabled={{isWrapping}}
|
||||||
|
>
|
||||||
|
Wrap Secret
|
||||||
|
</button>
|
||||||
|
{{else}}
|
||||||
|
<MaskedInput
|
||||||
|
@class="has-padding"
|
||||||
|
@displayOnly={{true}}
|
||||||
|
@allowCopy={{true}}
|
||||||
|
@value={{wrappedData}}
|
||||||
|
@success={{action "handleCopySuccess"}}
|
||||||
|
@error={{action "handleCopyError"}}
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</D.content>
|
||||||
|
</BasicDropdown>
|
||||||
|
</div>
|
||||||
|
{{/unless}}
|
||||||
|
<div class="control">
|
||||||
{{#if isV2}}
|
{{#if isV2}}
|
||||||
<LinkTo
|
<LinkTo
|
||||||
@params={{array targetRoute model.id (query-params version=this.modelForData.version)}}
|
@params={{array targetRoute model.id (query-params version=this.modelForData.version)}}
|
||||||
@@ -74,8 +131,8 @@
|
|||||||
Edit Secret
|
Edit Secret
|
||||||
</LinkTo>
|
</LinkTo>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/let}}
|
|
||||||
</div>
|
</div>
|
||||||
|
{{/let}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if (and (eq @mode "show") this.isV2)}}
|
{{#if (and (eq @mode "show") this.isV2)}}
|
||||||
<div class="control">
|
<div class="control">
|
||||||
|
|||||||
@@ -8,10 +8,18 @@
|
|||||||
|
|
||||||
{{#if unwrap_data}}
|
{{#if unwrap_data}}
|
||||||
<div class="box is-sideless is-fullwidth is-marginless">
|
<div class="box is-sideless is-fullwidth is-marginless">
|
||||||
|
<nav class="tabs">
|
||||||
|
<ul>
|
||||||
|
<li role="tab" aria-selected={{if (eq unwrapActiveTab "data") "true" "false"}} class="{{if (eq unwrapActiveTab "data") "is-active"}}">
|
||||||
|
<button class="link link-plain tab has-text-weight-semibold" {{action (mut unwrapActiveTab)}}>Data</button>
|
||||||
|
</li>
|
||||||
|
<li role="tab" aria-selected={{if (eq unwrapActiveTab "data") "true" "false"}} class="{{if (eq unwrapActiveTab "details") "is-active"}}">
|
||||||
|
<button class="link link-plain tab has-text-weight-semibold" {{action (mut unwrapActiveTab) "details"}}>Wrap Details</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{{#if (eq unwrapActiveTab "data")}}
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="is-label">
|
|
||||||
Unwrapped data
|
|
||||||
</label>
|
|
||||||
<div class="control">
|
<div class="control">
|
||||||
{{json-editor
|
{{json-editor
|
||||||
value=(stringify unwrap_data)
|
value=(stringify unwrap_data)
|
||||||
@@ -21,6 +29,22 @@
|
|||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="field box is-fullwidth is-shadowless is-paddingless is-marginless">
|
||||||
|
{{#each-in details as |key detail|}}
|
||||||
|
{{#info-table-row label=key value=key}}
|
||||||
|
{{#if (or (eq detail "No") (eq detail "None"))}}
|
||||||
|
<ICon @class=has-text-grey @size="16" @glyph="cancel-square-outline" /> {{detail}}
|
||||||
|
{{else}}
|
||||||
|
{{#if (eq detail "Yes") }}
|
||||||
|
<ICon @class="has-text-success" @size="16" @glyph="checkmark-circled-outline" />
|
||||||
|
{{/if}}
|
||||||
|
{{detail}}
|
||||||
|
{{/if}}
|
||||||
|
{{/info-table-row}}
|
||||||
|
{{/each-in}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
<div class="field is-grouped box is-fullwidth is-bottomless">
|
<div class="field is-grouped box is-fullwidth is-bottomless">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
<div class="column">
|
<div class="column">
|
||||||
{{#if list.item.deleted}}
|
{{#if list.item.deleted}}
|
||||||
<span class="has-text-grey is-size-8">
|
<span class="has-text-grey is-size-8">
|
||||||
<ICon @glyph="false" @size="16" />Deleted
|
<ICon @glyph="cancel-square-outline" @size="16" />Deleted
|
||||||
</span>
|
</span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if list.item.destroyed}}
|
{{#if list.item.destroyed}}
|
||||||
|
|||||||
Reference in New Issue
Block a user