Glimmerize Transit Key Actions (#25365)

* wip, all actions wired up, not finished or tested.

* move the rotate action to it's own component to clarify scope

* clean up and refresh component on queryParam change without forcing a refresh from component as before

* solve issue of carrying over props we want to keep

* clean up and add clearProps action

* transit key actions passing

* update assert and doc

* remove unecessary changes

* Address pr comments

* replace perform in submit action and instead pass it in as perform

* address claire's pr comments

* welp

* trying to rearrange closer to original

* addressing pr comments

* move things around

* pr comments

* remove ciphertext when last action was rewrap

* add args and istruncated and isfullwidth
This commit is contained in:
Angel Garbarino
2024-02-20 15:36:56 -07:00
committed by GitHub
parent 157799e5bc
commit 98bd45a999
17 changed files with 410 additions and 323 deletions

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { service } from '@ember/service';
import { action } from '@ember/object';
import errorMessage from 'vault/utils/error-message';
export default class TransitFormShow extends Component {
@service store;
@service router;
@service flashMessages;
@action async rotateKey() {
const { backend, id } = this.args.key;
try {
await this.store.adapterFor('transit-key').keyAction('rotate', { backend, id });
this.flashMessages.success('Key rotated.');
// must refresh to see the updated versions, a model refresh does not trigger the change.
await this.router.refresh();
} catch (e) {
this.flashMessages.danger(errorMessage(e));
}
}
}

View File

@@ -7,8 +7,5 @@ import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
export default class ExportComponent extends Component {
@tracked
wrapTTL = null;
@tracked
exportVersion = false;
@tracked exportVersion = false;
}

View File

@@ -0,0 +1,134 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
<div {{did-update this.updateProps @selectedAction}}>
<MessageError @errorMessage={{this.errors}} />
{{#if (eq @selectedAction "encrypt")}}
<TransitKeyAction::Encrypt
@key={{@key}}
@param={{this.props.param}}
@context={{this.props.context}}
@nonce={{this.props.nonce}}
@bits={{this.props.bits}}
@key_version={{this.props.key_version}}
@encodedBase64={{this.props.encodedBase64}}
@toggleEncodeBase64={{this.toggleEncodeBase64}}
@plaintext={{this.props.plaintext}}
@ciphertext={{this.props.ciphertext}}
@doSubmit={{perform this.doSubmit}}
@isModalActive={{this.isModalActive}}
data-test-transit-action={{@selectedAction}}
/>
{{else if (eq @selectedAction "decrypt")}}
<TransitKeyAction::Decrypt
@key={{@key}}
@ciphertext={{this.props.ciphertext}}
@context={{this.props.context}}
@nonce={{this.props.nonce}}
@isModalActive={{this.isModalActive}}
@plaintext={{this.props.plaintext}}
@doSubmit={{perform this.doSubmit}}
data-test-transit-action={{@selectedAction}}
/>
{{else if (eq @selectedAction "datakey")}}
<TransitKeyAction::Datakey
@key={{@key}}
@param={{this.props.param}}
@context={{this.props.context}}
@nonce={{this.props.nonce}}
@bits={{this.props.bits}}
@plaintext={{this.props.plaintext}}
@ciphertext={{this.props.ciphertext}}
@doSubmit={{perform this.doSubmit}}
@isModalActive={{this.isModalActive}}
data-test-transit-action={{@selectedAction}}
/>
{{else if (eq @selectedAction "rewrap")}}
<TransitKeyAction::Rewrap
@key={{@key}}
@param={{this.props.param}}
@context={{this.props.context}}
@nonce={{this.props.nonce}}
@key_version={{this.props.key_version}}
@ciphertext={{this.props.ciphertext}}
@isModalActive={{this.isModalActive}}
@doSubmit={{perform this.doSubmit}}
data-test-transit-action={{@selectedAction}}
/>
{{else if (eq @selectedAction "hmac")}}
<TransitKeyAction::Hmac
@key={{@key}}
@trackedInput={{this.props.input}}
@algorithm={{this.props.algorithm}}
@key_version={{this.props.key_version}}
@encodedBase64={{this.props.encodedBase64}}
@toggleEncodeBase64={{this.toggleEncodeBase64}}
@hmac={{this.props.hmac}}
@isModalActive={{this.isModalActive}}
@doSubmit={{perform this.doSubmit}}
data-test-transit-action={{@selectedAction}}
/>
{{else if (eq @selectedAction "verify")}}
<TransitKeyAction::Verify
@key={{@key}}
@trackedInput={{this.props.input}}
@signature={{this.props.signature}}
@signature_algorithm={{this.props.signature_algorithm}}
@hmac={{this.props.hmac}}
@hash_algorithm={{this.props.hash_algorithm}}
@context={{this.props.context}}
@prehashed={{this.props.prehashed}}
@encodedBase64={{this.props.encodedBase64}}
@verification={{this.props.verification}}
@valid={{this.props.valid}}
@toggleEncodeBase64={{this.toggleEncodeBase64}}
@keyIsRSA={{this.keyIsRSA}}
@isModalActive={{this.isModalActive}}
@doSubmit={{perform this.doSubmit}}
@submitIsRunning={{this.doSubmit.isRunning}}
@clearSpecificProps={{this.clearSpecificProps}}
data-test-transit-action={{@selectedAction}}
/>
{{else if (eq @selectedAction "sign")}}
<TransitKeyAction::Sign
@key={{@key}}
@trackedInput={{this.props.input}}
@hash_algorithm={{this.props.hash_algorithm}}
@signature={{this.props.signature}}
@signature_algorithm={{this.props.signature_algorithm}}
@key_version={{this.props.key_version}}
@context={{this.props.context}}
@prehashed={{this.props.prehashed}}
@encodedBase64={{this.props.encodedBase64}}
@toggleEncodeBase64={{this.toggleEncodeBase64}}
@isModalActive={{this.isModalActive}}
@doSubmit={{perform this.doSubmit}}
@submitIsRunning={{this.doSubmit.isRunning}}
data-test-transit-action={{@selectedAction}}
/>
{{else if (or (eq @selectedAction "export") (eq @key.supportedActions.firstObject "export"))}}
<TransitKeyAction::Export
@key={{@key}}
@keys={{this.props.keys}}
@trackedInput={{this.props.input}}
@hash_algorithm={{this.props.hash_algorithm}}
@signature={{this.props.signature}}
@signature_algorithm={{this.props.signature_algorithm}}
@key_version={{this.props.key_version}}
@context={{this.props.context}}
@prehashed={{this.props.prehashed}}
@encodedBase64={{this.props.encodedBase64}}
@exportKeyType={{this.props.exportKeyType}}
@exportKeyVersion={{this.props.exportKeyVersion}}
@wrappedToken={{this.props.wrappedToken}}
@wrappedTTL={{this.props.wrappedTTL}}
@toggleEncodeBase64={{this.toggleEncodeBase64}}
@isModalActive={{this.isModalActive}}
@doSubmit={{perform this.doSubmit}}
data-test-transit-action="export"
/>
{{/if}}
</div>

View File

@@ -3,15 +3,31 @@
* SPDX-License-Identifier: BUSL-1.1
*/
import { assign } from '@ember/polyfills';
import { copy } from 'ember-copy';
import { assert } from '@ember/debug';
import Component from '@glimmer/component';
import { service } from '@ember/service';
import Component from '@ember/component';
import { set, get, computed } from '@ember/object';
import { action } from '@ember/object';
import { assert } from '@ember/debug';
import { tracked } from '@glimmer/tracking';
import { task } from 'ember-concurrency';
import { waitFor } from '@ember/test-waiters';
import { encodeString } from 'vault/utils/b64';
import errorMessage from 'vault/utils/error-message';
const TRANSIT_PARAMS = {
/**
* @module TransitKeyActions
* TransitKeyActions component handles the actions a user can take on a transit key model. The model and props are updated on every tab change
*
* @example
* <TransitKeyActions
* @key={{this.model}}
* @selectedAction="hmac"
* />
*
* @param {string} selectedAction - This is the query param "action" value. Ex: hmac, verify, decrypt, etc. The only time this param can be empty is if a user is exporting a key
* @param {object} key - This is the transit key model.
*/
const STARTING_TRANSIT_PROPS = {
hash_algorithm: 'sha2-256',
algorithm: 'sha2-256',
signature_algorithm: 'pss',
@@ -35,161 +51,101 @@ const TRANSIT_PARAMS = {
exportKeyType: null,
exportKeyVersion: null,
wrappedToken: null,
wrappedTTL: '30m',
valid: null,
plaintextOriginal: null,
didDecode: false,
verification: 'Signature',
};
const PARAMS_FOR_ACTION = {
const PROPS_TO_KEEP = {
encrypt: ['plaintext', 'context', 'nonce', 'key_version'],
decrypt: ['ciphertext', 'context', 'nonce'],
sign: ['input', 'hash_algorithm', 'key_version', 'prehashed', 'signature_algorithm'],
verify: ['input', 'hmac', 'signature', 'hash_algorithm', 'prehashed'],
hmac: ['input', 'algorithm', 'key_version'],
encrypt: ['plaintext', 'context', 'nonce', 'key_version'],
decrypt: ['ciphertext', 'context', 'nonce'],
rewrap: ['ciphertext', 'context', 'nonce', 'key_version'],
datakey: [],
};
const SUCCESS_MESSAGE_FOR_ACTION = {
sign: 'Signed your data',
sign: 'Signed your data.',
// the verify action doesn't trigger a success message
hmac: 'Created your hash output',
encrypt: 'Created a wrapped token for your data',
decrypt: 'Decrypted the data from your token',
rewrap: 'Created a new token for your data',
datakey: 'Generated your key',
export: 'Exported your key',
hmac: 'Created your hash output.',
encrypt: 'Created a wrapped token for your data.',
decrypt: 'Decrypted the data from your token.',
rewrap: 'Created a new token for your data.',
datakey: 'Generated your key.',
export: 'Exported your key.',
};
export default Component.extend(TRANSIT_PARAMS, {
store: service(),
flashMessages: service(),
// public attrs
selectedAction: null,
key: null,
isModalActive: false,
export default class TransitKeyActions extends Component {
@service store;
@service flashMessages;
@service router;
onRefresh() {},
init() {
this._super(...arguments);
// TODO figure out why get is needed here Ember Upgrade
// eslint-disable-next-line ember/no-get
if (this.selectedAction) {
return;
@tracked isModalActive = false;
@tracked errors = null;
@tracked props = { ...STARTING_TRANSIT_PROPS }; // Shallow copy of the object. We don't want to mutate the original.
constructor() {
super(...arguments);
assert('@key is required for TransitKeyActions components', this.args.key);
if (this.firstSupportedAction === 'export' || this.args.selectedAction === 'export') {
this.props.exportKeyType = this.args.key.exportKeyTypes[0];
this.props.exportKeyVersion = this.args.key.validKeyVersions.lastObject;
}
// eslint-disable-next-line ember/no-get
set(this, 'selectedAction', get(this, 'key.supportedActions.firstObject'));
assert('`key` is required for `' + this.toString() + '`.', this.getModelInfo());
},
}
didReceiveAttrs() {
this._super(...arguments);
this.checkAction();
if (this.selectedAction === 'export') {
this.setExportKeyDefaults();
}
},
setExportKeyDefaults() {
const exportKeyType = this.key.exportKeyTypes.firstObject;
const exportKeyVersion = this.key.validKeyVersions.lastObject;
this.setProperties({
exportKeyType,
exportKeyVersion,
});
},
keyIsRSA: computed('key.type', function () {
const type = this.key.type;
get keyIsRSA() {
const { type } = this.args.key;
return type === 'rsa-2048' || type === 'rsa-3072' || type === 'rsa-4096';
}),
}
getModelInfo() {
const model = this.key || this.backend;
if (!model) {
return null;
}
const backend = model.backend || model.id;
const id = model.id;
return {
backend,
id,
};
},
checkAction() {
const currentAction = this.selectedAction;
const oldAction = this.oldSelectedAction;
this.resetParams(oldAction, currentAction);
set(this, 'oldSelectedAction', currentAction);
},
resetParams(oldAction, action) {
const params = copy(TRANSIT_PARAMS);
let paramsToKeep;
const clearWithoutCheck =
!oldAction ||
// don't save values from datakey
oldAction === 'datakey' ||
// can rewrap signatures — using that as a ciphertext later would be problematic
(oldAction === 'rewrap' && !this.key.supportsEncryption);
if (!clearWithoutCheck && action) {
paramsToKeep = PARAMS_FOR_ACTION[action];
}
if (paramsToKeep) {
paramsToKeep.forEach((param) => delete params[param]);
}
//resets params still left in the object to defaults
this.clearErrors();
this.setProperties(params);
if (action === 'export') {
this.setExportKeyDefaults();
}
},
handleError(e) {
this.set('errors', e.errors);
},
clearErrors() {
this.set('errors', null);
},
triggerSuccessMessage(action) {
const message = SUCCESS_MESSAGE_FOR_ACTION[action];
if (!message) return;
this.flashMessages.success(message);
},
get firstSupportedAction() {
return this.args.key.supportedActions[0];
}
handleSuccess(resp, options, action) {
let props = {};
if (resp && resp.data) {
if (action === 'export' && resp.data.keys) {
const { keys, type, name } = resp.data;
resp.data.keys = { keys, type, name };
}
props = assign({}, props, resp.data);
this.props = { ...this.props, ...resp.data };
// While we do not pass ciphertext as a value to the JsonEditor, so that navigating from rewrap to decrypt will not show ciphertext in the editor, we still want to clear it from the props after rewrapping.
if (action === 'rewrap' && !this.args.key.supportsEncryption) {
this.props.ciphertext = null;
}
}
if (options.wrapTTL) {
props = assign({}, props, { wrappedToken: resp.wrap_info.token });
this.props = { ...this.props, ...{ wrappedToken: resp.wrap_info.token } };
}
if (!this.isDestroyed && !this.isDestroying) {
this.toggleProperty('isModalActive');
this.setProperties(props);
this.isModalActive = true;
// verify doesn't trigger a success message
if (this.args.selectedAction !== 'verify') {
this.flashMessages.success(SUCCESS_MESSAGE_FOR_ACTION[action]);
}
if (action === 'rotate') {
this.onRefresh();
}
this.triggerSuccessMessage(action);
},
}
@action updateProps() {
this.errors = null;
// There are specific props we want to carry over from the previous tab.
// Ex: carrying over this.props.context from the encrypt tab to the decrypt tab, but not carrying over this.props.plaintext.
// To do this, we make a new object that contains the old this.props key/values from the previous tab that we want to keep. We then merge that new object into the STARTING_TRANSIT_PROPS object to come up with our new this.props tracked property.
// This action is passed to did-update in the component.
const transferredProps = PROPS_TO_KEEP[this.args.selectedAction]?.reduce(
(obj, key) => ({ ...obj, [key]: this.props[key] }),
{}
);
this.props = { ...STARTING_TRANSIT_PROPS, ...transferredProps };
}
compactData(data) {
const type = this.key.type;
const isRSA = type === 'rsa-2048' || type === 'rsa-3072' || type === 'rsa-4096';
return Object.keys(data).reduce((result, key) => {
if (key === 'signature_algorithm' && !isRSA) {
if (key === 'signature_algorithm' && !this.keyIsRSA) {
return result;
}
if (data[key]) {
@@ -197,51 +153,45 @@ export default Component.extend(TRANSIT_PARAMS, {
}
return result;
}, {});
},
}
actions: {
onActionChange(action) {
set(this, 'selectedAction', action);
this.checkAction();
},
@action toggleEncodeBase64() {
this.props.encodedBase64 = !this.props.encodedBase64;
}
onClear() {
this.resetParams(null, this.selectedAction);
},
@action clearSpecificProps(arrayToClear) {
arrayToClear.forEach((prop) => (this.props[prop] = null));
}
clearParams(params) {
const arr = Array.isArray(params) ? params : [params];
arr.forEach((param) => this.set(param, null));
},
doSubmit(data, options = {}, maybeEvent) {
const event = options.type === 'submit' ? options : maybeEvent;
if (event) {
event.preventDefault();
@task
@waitFor
*doSubmit(data, options = {}, maybeEvent) {
this.errors = null;
const event = options.type === 'submit' ? options : maybeEvent;
if (event) {
event.preventDefault();
}
const { backend, id } = this.args.key;
const action = this.args.selectedAction || this.firstSupportedAction;
const { ...formData } = data || {};
if (!this.props.encodedBase64) {
if (action === 'encrypt' && !!formData.plaintext) {
formData.plaintext = encodeString(formData.plaintext);
}
const { backend, id } = this.getModelInfo();
const action = this.selectedAction;
const { encodedBase64, ...formData } = data || {};
if (!encodedBase64) {
if (action === 'encrypt' && !!formData.plaintext) {
formData.plaintext = encodeString(formData.plaintext);
}
if ((action === 'hmac' || action === 'verify' || action === 'sign') && !!formData.input) {
formData.input = encodeString(formData.input);
}
if ((action === 'hmac' || action === 'verify' || action === 'sign') && !!formData.input) {
formData.input = encodeString(formData.input);
}
const payload = formData ? this.compactData(formData) : null;
this.setProperties({
errors: null,
result: null,
});
this.store
}
const payload = formData ? this.compactData(formData) : null;
try {
const resp = yield this.store
.adapterFor('transit-key')
.keyAction(action, { backend, id, payload }, options)
.then(
(resp) => this.handleSuccess(resp, options, action),
(...errArgs) => this.handleError(...errArgs)
);
},
},
});
.keyAction(action, { backend, id, payload }, options);
this.handleSuccess(resp, options, action);
} catch (e) {
this.errors = errorMessage(e);
}
}
}

View File

@@ -47,8 +47,17 @@
{{#if (not-eq @tab "actions")}}
<Toolbar>
<ToolbarActions>
{{#if (eq @tab "versions")}}
<TransitKeyActions @key={{@key}} @selectedAction="rotate" @capabilities={{@capabilities}} @onRefresh={{@refresh}} />
{{#if (and (eq @tab "versions") @key.canRotate)}}
<ConfirmAction
@buttonText="Rotate encryption key"
class="toolbar-button"
@buttonColor="secondary"
@confirmTitle="Rotate this key?"
@confirmMessage="After rotation, all key actions will default to using the newest version of the key."
@modalColor="warning"
@onConfirmAction={{this.rotateKey}}
data-test-transit-key-rotate
/>
{{/if}}
{{#if (eq @mode "show")}}
{{#if (or @capabilities.canUpdate @capabilities.canDelete)}}

View File

@@ -3,7 +3,7 @@
SPDX-License-Identifier: BUSL-1.1
~}}
<form onsubmit={{action @doSubmit (hash param=@param context=@context nonce=@nonce bits=@bits)}}>
<form {{on "submit" (fn @doSubmit (hash param=@param context=@context nonce=@nonce bits=@bits))}} ...attributes>
<div class="box is-sideless is-fullwidth is-marginless">
<NamespaceReminder @mode="perform" @noun="datakey creation" />
<div class="content has-bottom-margin-l">
@@ -78,7 +78,7 @@
Copy your generated key
</M.Header>
<M.Body>
{{#if (eq @param "plaintext")}}
{{#if @plaintext}}
<h2 class="has-text-weight-semibold is-6">Plaintext</h2>
<p class="sub-text">Plaintext is base64 encoded</p>
<Hds::Copy::Snippet
@@ -86,6 +86,8 @@
@textToCopy={{@plaintext}}
@color="secondary"
@container="#transit-datakey-modal"
@isFullWidth={{true}}
@isTruncated={{true}}
@onError={{(fn
(set-flash-message "Clipboard copy failed. Please make sure the browser Clipboard API is allowed." "danger")
)}}
@@ -96,6 +98,8 @@
@textToCopy={{@ciphertext}}
@color="secondary"
@container="#transit-datakey-modal"
@isFullWidth={{true}}
@isTruncated={{true}}
@onError={{(fn
(set-flash-message "Clipboard copy failed. Please make sure the browser Clipboard API is allowed." "danger")
)}}

View File

@@ -3,7 +3,7 @@
SPDX-License-Identifier: BUSL-1.1
~}}
<form onsubmit={{action @doSubmit (hash ciphertext=@ciphertext context=@context nonce=@nonce)}}>
<form {{on "submit" (fn @doSubmit (hash ciphertext=@ciphertext context=@context nonce=@nonce))}} ...attributes>
<div class="box is-sideless is-fullwidth is-marginless">
<div class="content has-bottom-margin-l">
<p>You can decrypt ciphertext using <code>{{@key.name}}</code> as the encryption key.</p>
@@ -12,7 +12,7 @@
<div id="ciphertext-control" class="control">
<JsonEditor
@title="Ciphertext"
@valueUpdated={{action (mut @ciphertext)}}
@valueUpdated={{fn (mut @ciphertext)}}
@mode="ruby"
@data-test-transit-input="ciphertext"
/>
@@ -60,11 +60,13 @@
</M.Header>
<M.Body>
<h2 class="has-text-weight-semibold is-6">Plaintext</h2>
<p class="sub-text">Plaintext is base64 encoded</p>
<p class="sub-text">Plaintext is base64 encoded.</p>
<Hds::Copy::Snippet
@textToCopy={{@plaintext}}
@color="secondary"
@container="#transit-decrypt-modal"
@isFullWidth={{true}}
@isTruncated={{true}}
@onError={{(fn
(set-flash-message "Clipboard copy failed. Please make sure the browser Clipboard API is allowed." "danger")
)}}

View File

@@ -4,30 +4,34 @@
~}}
<form
onsubmit={{action
@doSubmit
(hash plaintext=@plaintext context=@context nonce=@nonce key_version=@key_version encodedBase64=@encodedBase64)
}}
{{on "submit" (fn @doSubmit (hash plaintext=@plaintext context=@context nonce=@nonce key_version=@key_version))}}
...attributes
>
<div class="box is-sideless is-fullwidth is-marginless">
<NamespaceReminder @mode="perform" @noun="encryption" />
<div class="content has-bottom-margin-l">
<p>You can encrypt plaintext data using <code>{{@key.name}}</code> as the encryption key.</p>
</div>
<KeyVersionSelect @key={{@key}} @onVersionChange={{action (mut @key_version)}} @key_version={{@key_version}} />
<KeyVersionSelect @key={{@key}} @onVersionChange={{fn (mut @key_version)}} @key_version={{@key_version}} />
<div class="field">
<div id="plaintext-control" class="control is-relative">
<JsonEditor
@title="Plaintext"
@value={{@plaintext}}
@valueUpdated={{action (mut @plaintext)}}
@valueUpdated={{fn (mut @plaintext)}}
@mode="ruby"
@data-test-transit-input="plaintext"
/>
</div>
</div>
<div class="field">
<Input @type="checkbox" id="encodedBase64" @checked={{@encodedBase64}} data-test-transit-input="encodedBase64" />
<Input
@type="checkbox"
id="encodedBase64"
@checked={{@encodedBase64}}
{{on "change" @toggleEncodeBase64}}
data-test-transit-input="encodedBase64"
/>
<label for="encodedBase64">This data is already encoded in base64</label>
</div>
{{#if @key.derived}}
@@ -78,6 +82,8 @@
@textToCopy={{@ciphertext}}
@color="secondary"
@container="#transit-encrypt-modal"
@isFullWidth={{true}}
@isTruncated={{true}}
@onError={{(fn
(set-flash-message "Clipboard copy failed. Please make sure the browser Clipboard API is allowed." "danger")
)}}

View File

@@ -4,11 +4,15 @@
~}}
<form
onsubmit={{action
@doSubmit
(hash param=(compact (array @exportKeyType (if this.exportVersion @exportKeyVersion))))
(hash wrapTTL=this.wrapTTL)
{{on
"submit"
(fn
@doSubmit
(hash param=(compact (array @exportKeyType (if this.exportVersion @exportKeyVersion))))
(hash wrapTTL=@wrappedTTL)
)
}}
...attributes
>
<div class="box is-sideless is-fullwidth is-marginless">
<div class="content">
@@ -55,7 +59,7 @@
</div>
{{/if}}
</div>
<WrapTtl @onChange={{action (mut this.wrapTTL)}} />
<WrapTtl @onChange={{fn (mut @wrappedTTL)}} />
</div>
<div class="field is-grouped box is-fullwidth is-bottomless">
<div class="control">
@@ -70,12 +74,14 @@
</M.Header>
<M.Body>
<h2 class="title is-6">Wrapped key</h2>
{{#if this.wrapTTL}}
{{#if @wrappedTTL}}
<Hds::Copy::Snippet
class="has-bottom-margin-m"
@textToCopy={{@wrappedToken}}
@color="secondary"
@container="#transit-export-modal"
@isFullWidth={{true}}
@isTruncated={{true}}
@onError={{(fn
(set-flash-message "Clipboard copy failed. Please make sure the browser Clipboard API is allowed." "danger")
)}}

View File

@@ -3,12 +3,7 @@
SPDX-License-Identifier: BUSL-1.1
~}}
<form
onsubmit={{action
@doSubmit
(hash input=@input algorithm=@algorithm key_version=@key_version encodedBase64=@encodedBase64)
}}
>
<form {{on "submit" (fn @doSubmit (hash input=@trackedInput algorithm=@algorithm key_version=@key_version))}} ...attributes>
<div class="box is-sideless is-fullwidth is-marginless">
<NamespaceReminder @mode="perform" @noun="HMAC creation" />
<div class="content has-bottom-margin-l">
@@ -18,14 +13,20 @@
as the named key.
</p>
</div>
<KeyVersionSelect @key={{@key}} @onVersionChange={{action (mut @key_version)}} @key_version={{@key_version}} />
<KeyVersionSelect @key={{@key}} @onVersionChange={{fn (mut @key_version)}} @key_version={{@key_version}} />
<div class="field">
<div id="input-control" class="control is-relative">
<JsonEditor @title="Input" @valueUpdated={{action (mut @input)}} @mode="ruby" @data-test-transit-input="input" />
<JsonEditor @title="Input" @valueUpdated={{fn (mut @trackedInput)}} @mode="ruby" @data-test-transit-input="input" />
</div>
</div>
<div class="field">
<Input @type="checkbox" id="encodedBase64" @checked={{@encodedBase64}} data-test-transit-input="encodedBase64" />
<Input
@type="checkbox"
id="encodedBase64"
@checked={{@encodedBase64}}
{{on "change" @toggleEncodeBase64}}
data-test-transit-input="encodedBase64"
/>
<label for="encodedBase64">This data is already encoded in base64</label>
</div>
<div class="field">
@@ -60,6 +61,8 @@
@textToCopy={{@hmac}}
@color="secondary"
@container="#transit-hmac-modal"
@isFullWidth={{true}}
@isTruncated={{true}}
@onError={{(fn
(set-flash-message "Clipboard copy failed. Please make sure the browser Clipboard API is allowed." "danger")
)}}

View File

@@ -3,7 +3,10 @@
SPDX-License-Identifier: BUSL-1.1
~}}
<form onsubmit={{action @doSubmit (hash ciphertext=@ciphertext context=@context nonce=@nonce key_version=@key_version)}}>
<form
{{on "submit" (fn @doSubmit (hash ciphertext=@ciphertext context=@context nonce=@nonce key_version=@key_version))}}
...attributes
>
<div class="box is-sideless is-fullwidth is-marginless">
<NamespaceReminder @mode="perform" @noun="rewrap" />
<div class="content has-bottom-margin-l">
@@ -13,10 +16,10 @@
as the encryption key.
</p>
</div>
<KeyVersionSelect @key={{@key}} @onVersionChange={{action (mut @key_version)}} @key_version={{@key_version}} />
<KeyVersionSelect @key={{@key}} @onVersionChange={{fn (mut @key_version)}} @key_version={{@key_version}} />
<div class="field">
<div class="control is-expanded">
<JsonEditor @title="Ciphertext" @valueUpdated={{action (mut @ciphertext)}} @mode="ruby" />
<JsonEditor @title="Ciphertext" @valueUpdated={{fn (mut @ciphertext)}} @mode="ruby" />
</div>
</div>
{{#if @key.derived}}
@@ -72,6 +75,8 @@
@textToCopy={{@ciphertext}}
@color="secondary"
@container="#transit-rewrap-modal"
@isFullWidth={{true}}
@isTruncated={{true}}
@onError={{(fn
(set-flash-message "Clipboard copy failed. Please make sure the browser Clipboard API is allowed." "danger")
)}}

View File

@@ -4,18 +4,22 @@
~}}
<form
onsubmit={{action
@doSubmit
(hash
input=@input
hash_algorithm=@hash_algorithm
signature_algorithm=@signature_algorithm
key_version=@key_version
context=@context
prehashed=@prehashed
encodedBase64=@encodedBase64
{{on
"submit"
(fn
@doSubmit
(hash
input=@trackedInput
hash_algorithm=@hash_algorithm
signature_algorithm=@signature_algorithm
key_version=@key_version
context=@context
prehashed=@prehashed
encodedBase64=@encodedBase64
)
)
}}
...attributes
>
<div class="box is-sideless is-fullwidth is-marginless">
<NamespaceReminder @mode="perform" @noun="signing" />
@@ -26,20 +30,26 @@
as the encryption key and the specified hash algorithm.
</p>
</div>
<KeyVersionSelect @key={{@key}} @onVersionChange={{action (mut @key_version)}} @key_version={{@key_version}} />
<KeyVersionSelect @key={{@key}} @onVersionChange={{fn (mut @key_version)}} @key_version={{@key_version}} />
<div class="field">
<div class="control is-relative">
<JsonEditor
@title="Input"
@value={{@input}}
@valueUpdated={{action (mut @input)}}
@value={{@trackedInput}}
@valueUpdated={{fn (mut @trackedInput)}}
@mode="ruby"
@data-test-transit-input="input"
/>
</div>
</div>
<div class="field">
<Input @type="checkbox" id="encodedBase64" @checked={{@encodedBase64}} data-test-transit-input="encodedBase64" />
<Input
@type="checkbox"
id="encodedBase64"
@checked={{@encodedBase64}}
{{on "change" @toggleEncodeBase64}}
data-test-transit-input="encodedBase64"
/>
<label for="encodedBase64">This data is already encoded in base64</label>
</div>
{{#if @key.derived}}
@@ -114,7 +124,7 @@
</div>
<div class="field is-grouped box is-fullwidth is-bottomless">
<div class="control">
<Hds::Button @text="Sign" @icon={{if @loading "loading"}} type="submit" disabled={{@loading}} />
<Hds::Button @text="Sign" @icon={{if @submitIsRunning "loading"}} type="submit" disabled={{@submitIsRunning}} />
</div>
</div>
</form>
@@ -129,6 +139,8 @@
@textToCopy={{@signature}}
@color="secondary"
@container="#transit-sign-modal"
@isFullWidth={{true}}
@isTruncated={{true}}
@onError={{(fn
(set-flash-message "Clipboard copy failed. Please make sure the browser Clipboard API is allowed." "danger")
)}}

View File

@@ -4,19 +4,23 @@
~}}
<form
onsubmit={{action
@doSubmit
(hash
input=@input
signature=@signature
signature_algorithm=@signature_algorithm
hmac=@hmac
hash_algorithm=@hash_algorithm
context=@context
prehashed=@prehashed
encodedBase64=@encodedBase64
{{on
"submit"
(fn
@doSubmit
(hash
input=@trackedInput
signature=@signature
signature_algorithm=@signature_algorithm
hmac=@hmac
hash_algorithm=@hash_algorithm
context=@context
prehashed=@prehashed
encodedBase64=@encodedBase64
)
)
}}
...attributes
>
<div class="box is-sideless is-fullwidth is-marginless">
<div class="content has-bottom-margin-l">
@@ -26,15 +30,21 @@
<div class="control is-relative">
<JsonEditor
@title="Input"
@value={{@input}}
@valueUpdated={{action (mut @input)}}
@value={{@trackedInput}}
@valueUpdated={{fn (mut @trackedInput)}}
@mode="ruby"
@data-test-transit-input="input"
/>
</div>
</div>
<div class="field">
<Input @type="checkbox" id="encodedBase64" @checked={{@encodedBase64}} data-test-transit-input="encodedBase64" />
<Input
@type="checkbox"
id="encodedBase64"
@checked={{@encodedBase64}}
{{on "change" @toggleEncodeBase64}}
data-test-transit-input="encodedBase64"
/>
<label for="encodedBase64">This data is already encoded in base64</label>
</div>
{{#if (and @key.supportsSigning @key.derived (not @hmac))}}
@@ -64,7 +74,7 @@
id="verification"
onchange={{queue
(action (mut @verification) value="target.value")
(action @clearParams (array "hmac" "signature"))
(fn @clearSpecificProps (array "hmac" "signature"))
}}
>
{{#each (array "Signature" "HMAC") as |type|}}
@@ -145,18 +155,13 @@
{{#if (or (and @verification (eq @verification "HMAC")) @hmac)}}
<div class="field is-flex-column is-flex-grow-1">
<div class="control is-flex-column is-flex-grow-1">
<JsonEditor @title="HMAC" @value={{@hmac}} @valueUpdated={{action (mut @hmac)}} @mode="ruby" />
<JsonEditor @title="HMAC" @value={{@hmac}} @valueUpdated={{fn (mut @hmac)}} @mode="ruby" />
</div>
</div>
{{else}}
<div class="field is-flex-column is-flex-grow-1">
<div class="control is-flex-column is-flex-grow-1">
<JsonEditor
@title="Signature"
@value={{@signature}}
@valueUpdated={{action (mut @signature)}}
@mode="ruby"
/>
<JsonEditor @title="Signature" @value={{@signature}} @valueUpdated={{fn (mut @signature)}} @mode="ruby" />
</div>
</div>
{{/if}}
@@ -165,7 +170,7 @@
{{else}}
<div class="field">
<div class="control">
<JsonEditor @title="HMAC" @value={{@hmac}} @valueUpdated={{action (mut @hmac)}} @mode="ruby" />
<JsonEditor @title="HMAC" @value={{@hmac}} @valueUpdated={{fn (mut @hmac)}} @mode="ruby" />
</div>
</div>
<div class="field">
@@ -186,7 +191,7 @@
</div>
<div class="field is-grouped box is-fullwidth is-bottomless">
<div class="control">
<Hds::Button @text="Verify" @icon={{if @loading "loading"}} type="submit" disabled={{@loading}} />
<Hds::Button @text="Verify" @icon={{if @submitIsRunning "loading"}} type="submit" disabled={{@submitIsRunning}} />
</div>
</div>
</form>

View File

@@ -1,55 +0,0 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
{{#if (eq this.selectedAction "rotate")}}
{{#if this.key.canRotate}}
<ConfirmAction
@buttonText="Rotate encryption key"
class="toolbar-button"
@buttonColor="secondary"
@confirmTitle="Rotate this key?"
@confirmMessage="After rotation, all key actions will default to using the newest version of the key."
@modalColor="warning"
@onConfirmAction={{action "doSubmit"}}
data-test-transit-key-rotate
/>
{{/if}}
{{else}}
<MessageError @errors={{this.errors}} />
{{#if this.selectedAction}}
<div data-test-transit-action={{this.selectedAction}}>
{{! template-lint-disable no-passed-in-event-handlers }}
{{component
(concat "transit-key-action/" this.selectedAction)
key=this.key
keys=this.keys
keyIsRSA=this.keyIsRSA
verification=this.verification
hmac=this.hmac
param=this.param
key_version=this.key_version
isModalActive=this.isModalActive
ciphertext=this.ciphertext
plaintext=this.plaintext
context=this.context
nonce=this.nonce
input=this.input
bits=this.bits
algorithm=this.algorithm
signature=this.signature
signature_algorithm=this.signature_algorithm
hash_algorithm=this.hash_algorithm
prehashed=this.prehashed
wrappedToken=this.wrappedToken
exportKeyType=this.exportKeyType
exportKeyVersion=this.exportKeyVersion
encodedBase64=this.encodedBase64
valid=this.valid
doSubmit=(action "doSubmit")
clearParams=(action "clearParams")
}}
</div>
{{/if}}
{{/if}}

View File

@@ -57,12 +57,6 @@
</nav>
</div>
<TransitKeyActions
@selectedAction={{this.selectedAction}}
@backend={{this.backend}}
@key={{this.model}}
@capabilities={{this.capabilities}}
@onRefresh={{action "refresh"}}
/>
<TransitKeyActions @selectedAction={{this.selectedAction}} @key={{this.model}} />
</div>
</div>

View File

@@ -23,7 +23,7 @@ const SELECTORS = {
form: (item) => `[data-test-transit-key="${item}"]`,
versionRow: (version) => `[data-test-transit-version="${version}"]`,
rotate: {
trigger: '[data-test-confirm-action-trigger]',
trigger: '[data-test-transit-key-rotate]',
confirm: '[data-test-confirm-button]',
},
};
@@ -344,6 +344,7 @@ module('Acceptance | transit (flaky)', function (hooks) {
await click(SELECTORS.versionsTab);
assert.dom(SELECTORS.versionRow(1)).hasTextContaining('Version 1', `${name}: only one key version`);
await waitUntil(() => find(SELECTORS.rotate.trigger));
await click(SELECTORS.rotate.trigger);
await click(SELECTORS.rotate.confirm);

View File

@@ -60,7 +60,7 @@ module('Integration | Component | transit key actions', function (hooks) {
render(hbs`
<TransitKeyActions />`);
const err = await promise;
assert.ok(err.message.includes('`key` is required for'), 'asserts without key');
assert.ok(err.message.includes('@key is required for'), 'asserts without key');
});
test('it renders', async function (assert) {
@@ -102,19 +102,6 @@ module('Integration | Component | transit key actions', function (hooks) {
.exists({ count: 1 }, 'renders signature_algorithm field on verify with rsa key');
});
test('it renders: rotate', async function (assert) {
this.set('key', { backend: 'transit', id: 'akey', supportedActions: ['rotate'] });
await render(hbs`
<TransitKeyActions @selectedAction="rotate" @key={{this.key}} />`);
assert.dom('*').hasText('', 'renders an empty div');
this.set('key.canRotate', true);
assert
.dom('button')
.hasText('Rotate encryption key', 'renders confirm-button when key.canRotate is true');
});
async function doEncrypt(assert, actions = [], keyattrs = {}) {
const keyDefaults = { backend: 'transit', id: 'akey', supportedActions: ['encrypt'].concat(actions) };
@@ -312,7 +299,7 @@ module('Integration | Component | transit key actions', function (hooks) {
validKeyVersions: [1],
});
await render(hbs`
<TransitKeyActions @key={{this.key}} />`);
<TransitKeyActions @key={{this.key}} @selectedAction="hmac" />`);
await fillIn('#algorithm', 'sha2-384');
await blur('#algorithm');
await fillIn('[data-test-component="code-mirror-modifier"] textarea', 'plaintext');