Unverified Commit 5fcd87a0 authored by madalynrose's avatar madalynrose Committed by GitHub
Browse files

Add Response Wrapping For Secrets (#5664)

parent 98ea0a39
Showing with 230 additions and 60 deletions
+230 -60
......@@ -19,6 +19,11 @@ export default ApplicationAdapter.extend({
return this._url(backend, path) + `?version=${version}`;
},
urlForQueryRecord(id) {
let [backend, path, version] = JSON.parse(id);
return this._url(backend, path) + `?version=${version}`;
},
findRecord() {
return this._super(...arguments).catch(errorOrModel => {
// 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) {
let backend = snapshot.belongsTo('secret').belongsTo('engine').id;
let path = snapshot.attr('path');
......
......@@ -34,22 +34,29 @@ export default ApplicationAdapter.extend({
return url;
},
optionsForQuery(id, action) {
optionsForQuery(id, action, wrapTTL) {
let data = {};
if (action === 'query') {
data['list'] = true;
data.list = true;
}
if (wrapTTL) {
return { data, wrapTTL };
}
return { data };
},
fetchByQuery(query, action) {
const { id, backend } = query;
return this.ajax(this.urlForSecret(backend, id), 'GET', this.optionsForQuery(id, action)).then(resp => {
resp.id = id;
resp.backend = backend;
return resp;
});
const { id, backend, wrapTTL } = query;
return this.ajax(this.urlForSecret(backend, id), 'GET', this.optionsForQuery(id, action, wrapTTL)).then(
resp => {
if (wrapTTL) {
return resp;
}
resp.id = id;
resp.backend = backend;
return resp;
}
);
},
query(store, type, query) {
......
......@@ -17,6 +17,7 @@ export default Component.extend(FocusOnInsertMixin, {
wizard: service(),
router: service(),
store: service(),
flashMessages: service(),
// a key model
key: null,
......@@ -31,6 +32,10 @@ export default Component.extend(FocusOnInsertMixin, {
secretData: null,
wrappedData: null,
isWrapping: false,
showWrapButton: computed.not('wrappedData'),
// called with a bool indicating if there's been a change in the secretData
onDataChange() {},
onRefresh() {},
......@@ -235,6 +240,53 @@ export default Component.extend(FocusOnInsertMixin, {
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) {
event.preventDefault();
let model = this.modelForData;
......
......@@ -14,6 +14,7 @@ const DEFAULTS = {
creation_ttl: null,
data: '{\n}',
unwrap_data: null,
details: null,
wrapTTL: null,
sum: null,
random_bytes: null,
......@@ -33,6 +34,7 @@ export default Component.extend(DEFAULTS, {
algorithm: 'sha2-256',
tagName: '',
unwrapActiveTab: 'data',
didReceiveAttrs() {
this._super(...arguments);
......@@ -76,7 +78,13 @@ export default Component.extend(DEFAULTS, {
let props = {};
let secret = (resp && resp.data) || resp.auth;
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);
if (resp && resp.wrap_info) {
......
......@@ -12,6 +12,10 @@
padding-top: $spacing-s;
}
.has-padding {
padding: $size-10 $size-8;
}
// we want to style the boxes the same everywhere so they
// need to be the same font and small
.masked-input.masked .masked-value {
......
......@@ -3,22 +3,23 @@
ul {
border-color: transparent;
min-height: 3rem;
}
li {
&:focus {
box-shadow: none;
}
&.is-active a {
&.is-active a, &.is-active .tab {
border-color: $blue;
color: $blue;
}
&:first-child a {
&:first-child a, &:first-child .tab {
margin-left: $size-5;
}
}
a {
a, .tab {
color: $grey-dark;
font-weight: $font-weight-semibold;
text-decoration: none;
......
......@@ -19,6 +19,7 @@
<CopyButton
@clipboardText={{value}}
@class="copy-button button is-compact"
@success={{success}}
data-test-copy-button
>
<ICon @glyph="copy" aria-hidden="true" @size=16 />
......
......@@ -26,7 +26,7 @@
<ConfirmAction
@buttonClasses="button is-compact is-ghost has-icon-right"
@onConfirmAction={{action "deleteKey"}}
@confirmMessage={{if isV2
@confirmMessage={{if isV2
(concat "This will permanently delete " model.id " and all its versions. Are you sure you want to do this?")
(concat "Are you sure you want to delete " model.id "?")
}}
......@@ -53,45 +53,102 @@
<label for="json" class="has-text-grey">JSON</label>
</div>
{{#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|}}
{{#if isV2}}
<LinkTo
@params={{array targetRoute model.id (query-params version=this.modelForData.version)}}
@replace={{true}}
class="link link-plain has-text-weight-semibold"
data-test-secret-edit="true"
>
Create new version
</LinkTo>
{{else}}
<LinkTo
@params={{array targetRoute model.id}}
@replace={{true}}
class="link link-plain has-text-weight-semibold"
data-test-secret-edit="true"
>
Edit Secret
</LinkTo>
{{#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}}
<LinkTo
@params={{array targetRoute model.id (query-params version=this.modelForData.version)}}
@replace={{true}}
class="link link-plain has-text-weight-semibold"
data-test-secret-edit="true"
>
Create new version
</LinkTo>
{{else}}
<LinkTo
@params={{array targetRoute model.id}}
@replace={{true}}
class="link link-plain has-text-weight-semibold"
data-test-secret-edit="true"
>
Edit Secret
</LinkTo>
{{/if}}
{{/let}}
</div>
</div>
{{/let}}
{{/if}}
{{#if (and (eq @mode "show") this.isV2)}}
<div class="control">
<SecretVersionMenu @version={{this.modelForData}} />
</div>
<div class="control">
<BasicDropdown
@class="popup-menu"
@horizontalPosition="auto-right"
@verticalPosition="below"
<BasicDropdown
@class="popup-menu"
@horizontalPosition="auto-right"
@verticalPosition="below"
as |D|
>
<D.trigger
<D.trigger
data-test-popup-menu-trigger="true"
@class={{concat "popup-menu-trigger button is-ghost has-text-grey" (if D.isOpen " is-active")}}
@tagName="button"
@class={{concat "popup-menu-trigger button is-ghost has-text-grey" (if D.isOpen " is-active")}}
@tagName="button"
>
History <ICon @glyph="chevron-right" @size="11" />
</D.trigger>
......@@ -103,8 +160,8 @@
@mode="versions"
@secret={{this.model.id}}
@class="has-text-black has-text-weight-semibold has-bottom-shadow"
>
View version history
>
View version history
</SecretLink>
</li>
</ul>
......
......@@ -8,19 +8,43 @@
{{#if unwrap_data}}
<div class="box is-sideless is-fullwidth is-marginless">
<div class="field">
<label class="is-label">
Unwrapped data
</label>
<div class="control">
{{json-editor
value=(stringify unwrap_data)
options=(hash
readOnly=true
)
}}
<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="control">
{{json-editor
value=(stringify unwrap_data)
options=(hash
readOnly=true
)
}}
</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 class="field is-grouped box is-fullwidth is-bottomless">
<div class="control">
......
......@@ -34,7 +34,7 @@
<div class="column">
{{#if list.item.deleted}}
<span class="has-text-grey is-size-8">
<ICon @glyph="false" @size="16" />Deleted
<ICon @glyph="cancel-square-outline" @size="16" />Deleted
</span>
{{/if}}
{{#if list.item.destroyed}}
......@@ -53,7 +53,7 @@
@secret={{model.id}}
@class="has-text-black has-text-weight-semibold"
@queryParams={{query-params version=list.item.version}}
>
>
View version {{list.item.version}}
</SecretLink>
</li>
......@@ -63,11 +63,11 @@
@secret={{model.id}}
@class="has-text-black has-text-weight-semibold"
@queryParams={{query-params version=list.item.version}}
>
>
Create new version from {{list.item.version}}
</SecretLink>
</li>
</SecretVersionMenu>
</Item.menu>
</ListItem>
</ListView>
\ No newline at end of file
</ListView>
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment