mirror of
https://github.com/hashicorp/vault.git
synced 2026-06-08 16:24:51 -04:00
* UI: Ember data migration: Transit secrets engine - Show & List views (#15015) * Adding api calls * fixing timestamps and actions * fixing routing and moving functions around for model creation * UI: Ember Data migration: Transit - Create & Edit (#15085) * adding in new create form * updating form to handle editing * yielding ttl, updating conditional renders * a lot of moving around * test fix 1 * test fix 2 * UI: Ember Data migration: Transit Secrets Engine - Key actions (#15176) * updating store use to use api calls per actions * forgot export, fixing some tests * test fixes * converting to .ts and minor tweaks * test fixes Co-authored-by: Dan Rivera <dan.rivera@hashicorp.com>
This commit is contained in:
parent
06d1577cdb
commit
6f613e8d28
19 changed files with 873 additions and 471 deletions
|
|
@ -10,36 +10,16 @@
|
|||
</Page::Header>
|
||||
|
||||
{{#if (eq this.mode "create")}}
|
||||
<TransitFormCreate
|
||||
@createOrUpdateKey={{action "createOrUpdateKey" this.mode}}
|
||||
@setValueOnKey={{action "setValueOnKey" "exportable"}}
|
||||
@autoRotateInvalid={{this.autoRotateInvalid}}
|
||||
@handleAutoRotateChange={{action "handleAutoRotateChange"}}
|
||||
@derivedChange={{action "derivedChange" value="target.checked"}}
|
||||
@convergentEncryptionChange={{action "convergentEncryptionChange" value="target.checked"}}
|
||||
@key={{this.key}}
|
||||
@errorMessage={{this.errorMessage}}
|
||||
@requestInFlight={{this.requestInFlight}}
|
||||
/>
|
||||
<TransitFormCreate @form={{this.form}} @errorMessage={{this.errorMessage}} />
|
||||
{{else if (eq this.mode "edit")}}
|
||||
<TransitFormEdit
|
||||
@createOrUpdateKey={{action "createOrUpdateKey" this.mode}}
|
||||
@setValueOnKey={{action "setValueOnKey" "deletionAllowed"}}
|
||||
@autoRotateInvalid={{this.autoRotateInvalid}}
|
||||
@handleAutoRotateChange={{action "handleAutoRotateChange"}}
|
||||
@deleteKey={{action "deleteKey"}}
|
||||
@key={{this.key}}
|
||||
@requestInFlight={{this.requestInFlight}}
|
||||
@model={{this.model}}
|
||||
/>
|
||||
<TransitFormEdit @form={{this.form}} @capabilities={{this.capabilities}} />
|
||||
{{else if (eq this.mode "show")}}
|
||||
<TransitFormShow
|
||||
@refresh={{action "refresh"}}
|
||||
@tab={{this.tab}}
|
||||
@key={{this.key}}
|
||||
@form={{this.form}}
|
||||
@capabilities={{this.capabilities}}
|
||||
@mode={{this.mode}}
|
||||
@model={{this.model}}
|
||||
@backend={{this.backend}}
|
||||
/>
|
||||
{{else}}
|
||||
<Hds::ApplicationState class="top-padding-32 is-marginless" as |A|>
|
||||
|
|
|
|||
|
|
@ -45,25 +45,25 @@ export default Component.extend(FocusOnInsertMixin, {
|
|||
route: 'vault.cluster.secrets',
|
||||
},
|
||||
{
|
||||
label: this.key.backend,
|
||||
label: this.form.data.backend,
|
||||
route: 'vault.cluster.secrets.backend.list-root',
|
||||
model: this.key.backend,
|
||||
model: this.form.data.backend,
|
||||
},
|
||||
];
|
||||
if (this.mode === 'show') {
|
||||
return [
|
||||
...baseCrumbs,
|
||||
{
|
||||
label: this.key.id,
|
||||
label: this.form.data.id,
|
||||
},
|
||||
];
|
||||
} else if (this.mode === 'edit') {
|
||||
return [
|
||||
...baseCrumbs,
|
||||
{
|
||||
label: this.key.id,
|
||||
label: this.form.data.id,
|
||||
route: 'vault.cluster.secrets.backend.show',
|
||||
models: [this.key.backend, this.key.id],
|
||||
models: [this.form.data.backend, this.form.data.id],
|
||||
query: { tab: 'details' },
|
||||
},
|
||||
{ label: 'Edit' },
|
||||
|
|
@ -87,7 +87,7 @@ export default Component.extend(FocusOnInsertMixin, {
|
|||
get subtitle() {
|
||||
if (this.mode === 'create' || this.mode === 'edit') return '';
|
||||
|
||||
return this.key?.id;
|
||||
return this.form.data.id;
|
||||
},
|
||||
|
||||
waitForKeyUp: task(function* () {
|
||||
|
|
|
|||
|
|
@ -3,141 +3,38 @@
|
|||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<form data-test-transit-create-form onsubmit={{@createOrUpdateKey}}>
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<MessageError @model={{@key}} @errorMessage={{@errorMessage}} />
|
||||
<form data-test-transit-create-form onsubmit={{perform this.save}}>
|
||||
<div class="box is-fullwidth is-marginless">
|
||||
<MessageError @model={{@form}} @errorMessage={{this.errorBanner}} />
|
||||
<NamespaceReminder @mode="create" @noun="transit key" />
|
||||
<div class="field">
|
||||
<label for="key-name" class="is-label">Name</label>
|
||||
<Input id="key-name" @value={{@key.name}} class="input" data-test-transit-key="name" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<TtlPicker
|
||||
@initialValue="30d"
|
||||
@initialEnabled={{false}}
|
||||
@label="Auto-rotation period"
|
||||
@helperTextDisabled="Key will never be automatically rotated"
|
||||
@helperTextEnabled="Key will be automatically rotated every"
|
||||
@onChange={{@handleAutoRotateChange}}
|
||||
@errorMessage={{if @autoRotateInvalid "Duration must be longer than 1 hour"}}
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="key-type" class="is-label">Type</label>
|
||||
<div class="control is-expanded">
|
||||
<div class="select is-fullwidth">
|
||||
<select
|
||||
name="key-type"
|
||||
id="key-type"
|
||||
onchange={{action (mut @key.type) value="target.value"}}
|
||||
data-test-transit-key="type"
|
||||
>
|
||||
<option selected={{eq @key.type "aes128-gcm96"}} value="aes128-gcm96">
|
||||
aes128-gcm96
|
||||
</option>
|
||||
<option selected={{eq @key.type "aes256-gcm96"}} value="aes256-gcm96">
|
||||
aes256-gcm96
|
||||
</option>
|
||||
<option selected={{eq @key.type "chacha20-poly1305"}} value="chacha20-poly1305">
|
||||
chacha20-poly1305
|
||||
</option>
|
||||
<option selected={{eq @key.type "ecdsa-p256"}} value="ecdsa-p256">
|
||||
ecdsa-p256
|
||||
</option>
|
||||
<option selected={{eq @key.type "ecdsa-p384"}} value="ecdsa-p384">
|
||||
ecdsa-p384
|
||||
</option>
|
||||
<option selected={{eq @key.type "ecdsa-p521"}} value="ecdsa-p521">
|
||||
ecdsa-p521
|
||||
</option>
|
||||
<option selected={{eq @key.type "ed25519"}} value="ed25519">
|
||||
ed25519
|
||||
</option>
|
||||
<option selected={{eq @key.type "rsa-2048"}} value="rsa-2048">
|
||||
rsa-2048
|
||||
</option>
|
||||
<option selected={{eq @key.type "rsa-3072"}} value="rsa-3072">
|
||||
rsa-3072
|
||||
</option>
|
||||
<option selected={{eq @key.type "rsa-4096"}} value="rsa-4096">
|
||||
rsa-4096
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="b-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="key-exportable"
|
||||
class="styled"
|
||||
checked={{@key.exportable}}
|
||||
onchange={{@setValueOnKey}}
|
||||
data-test-transit-key="exportable"
|
||||
|
||||
{{#each @form.formFields as |attr|}}
|
||||
<FormField @attr={{attr}} @model={{@form}} @modelValidations={{this.modelValidations}}>
|
||||
{{#if (eq attr.name "auto_rotate_period")}}
|
||||
<div class="field">
|
||||
<TtlPicker
|
||||
@initialValue="30d"
|
||||
@initialEnabled={{false}}
|
||||
@label="Auto-rotation period"
|
||||
@helperTextDisabled="Key will never be automatically rotated"
|
||||
@helperTextEnabled="Key will be automatically rotated every"
|
||||
@onChange={{this.handleAutoRotateChange}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
</FormField>
|
||||
{{/each}}
|
||||
|
||||
<div class="field is-grouped box is-fullwidth is-bottomless">
|
||||
<Hds::ButtonSet>
|
||||
<Hds::Button @text="Create key" data-test-transit-key="create" type="submit" />
|
||||
<Hds::Button
|
||||
@text="Cancel"
|
||||
@color="secondary"
|
||||
@model={{@form.data.backend}}
|
||||
@route="vault.cluster.secrets.backend.list-root"
|
||||
/>
|
||||
<label for="key-exportable" class="is-label">
|
||||
Exportable
|
||||
</label>
|
||||
</div>
|
||||
</Hds::ButtonSet>
|
||||
</div>
|
||||
{{#if
|
||||
(or
|
||||
(eq @key.type "aes128-gcm96")
|
||||
(eq @key.type "aes256-gcm96")
|
||||
(eq @key.type "chacha20-poly1305")
|
||||
(eq @key.type "ed25519")
|
||||
)
|
||||
}}
|
||||
<div class="field">
|
||||
<div class="b-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="key-derived"
|
||||
class="styled"
|
||||
checked={{@key.derived}}
|
||||
onchange={{@derivedChange}}
|
||||
data-test-transit-key="derived"
|
||||
/>
|
||||
<label for="key-derived" class="is-label">
|
||||
Derived
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if (or (eq @key.type "aes128-gcm96") (eq @key.type "aes256-gcm96") (eq @key.type "chacha20-poly1305"))}}
|
||||
<div class="field">
|
||||
<div class="b-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
data-test-transit-key="convergent-encryption"
|
||||
id="key-convergent"
|
||||
class="styled"
|
||||
checked={{@key.convergentEncryption}}
|
||||
onchange={{@convergentEncryptionChange}}
|
||||
/>
|
||||
<label for="key-convergent" class="is-label">
|
||||
Enable convergent encryption
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="field is-grouped box is-fullwidth is-bottomless">
|
||||
<Hds::ButtonSet>
|
||||
<Hds::Button
|
||||
@text="Create key"
|
||||
@icon={{if @requestInFlight "loading"}}
|
||||
data-test-transit-key="create"
|
||||
type="submit"
|
||||
disabled={{@requestInFlight}}
|
||||
/>
|
||||
<Hds::Button
|
||||
@text="Cancel"
|
||||
@color="secondary"
|
||||
@model={{@key.backend}}
|
||||
@route="vault.cluster.secrets.backend.list-root"
|
||||
/>
|
||||
</Hds::ButtonSet>
|
||||
</div>
|
||||
</form>
|
||||
60
ui/app/components/transit-form-create.ts
Normal file
60
ui/app/components/transit-form-create.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { HTMLElementEvent } from 'vault/forms';
|
||||
import FlashMessageService from 'vault/services/flash-messages';
|
||||
import ApiService from 'vault/services/api';
|
||||
import RouterService from '@ember/routing/router-service';
|
||||
import TransitKeyForm from 'vault/forms/transit/key';
|
||||
import { ValidationMap } from 'vault/vault/app-types';
|
||||
import { task } from 'ember-concurrency';
|
||||
|
||||
interface Args {
|
||||
form: TransitKeyForm;
|
||||
}
|
||||
export default class TransitFormCreate extends Component<Args> {
|
||||
@service declare readonly router: RouterService;
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
@service declare readonly api: ApiService;
|
||||
|
||||
@tracked errorBanner = '';
|
||||
@tracked modelValidations: ValidationMap | null = null;
|
||||
|
||||
@action
|
||||
handleAutoRotateChange(ttlObj: { enabled: boolean; goSafeTimeString: string }) {
|
||||
const { data } = this.args.form;
|
||||
if (ttlObj.enabled) {
|
||||
data.auto_rotate_period = ttlObj.goSafeTimeString;
|
||||
} else {
|
||||
data.auto_rotate_period = '0s';
|
||||
}
|
||||
}
|
||||
|
||||
@task
|
||||
*save(event: HTMLElementEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
|
||||
try {
|
||||
const { data, isValid, state } = this.args.form.toJSON();
|
||||
this.modelValidations = isValid ? null : state;
|
||||
|
||||
if (isValid) {
|
||||
yield this.api.secrets.transitCreateKey(data['name'] as string, data['backend'] as string, data);
|
||||
|
||||
this.flashMessages.success('Key successfully created.');
|
||||
this.router.transitionTo('vault.cluster.secrets.backend.show', data['backend'], data['name'], {
|
||||
queryParams: { tab: 'details' },
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
const { message } = yield this.api.parseError(error);
|
||||
this.errorBanner = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,101 +2,85 @@
|
|||
Copyright IBM Corp. 2016, 2025
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<form data-test-transit-edit-form onsubmit={{@createOrUpdateKey}}>
|
||||
<form data-test-transit-edit-form onsubmit={{this.save}}>
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<MessageError @model={{@key}} />
|
||||
<MessageError @model={{@form}} />
|
||||
<NamespaceReminder @mode="edit" @noun="transit key" />
|
||||
<div class="field">
|
||||
<div class="b-checkbox">
|
||||
<input
|
||||
id="key-allows-delete"
|
||||
type="checkbox"
|
||||
checked={{@key.deletionAllowed}}
|
||||
class="styled"
|
||||
onchange={{@setValueOnKey}}
|
||||
/>
|
||||
<label for="key-allows-delete" class="is-label">
|
||||
Allow deletion
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<TtlPicker
|
||||
@initialValue={{or @key.autoRotatePeriod "30d"}}
|
||||
@initialEnabled={{not (eq @key.autoRotatePeriod 0)}}
|
||||
@label="Auto-rotation period"
|
||||
@helperTextDisabled="Key will never be automatically rotated"
|
||||
@helperTextEnabled="Key will be automatically rotated every"
|
||||
@onChange={{@handleAutoRotateChange}}
|
||||
@errorMessage={{if @autoRotateInvalid "Duration must be longer than 1 hour"}}
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="key-min-decrypt-version" class="is-label">Minimum decryption version</label>
|
||||
<div class="control is-expanded">
|
||||
<div class="select is-fullwidth">
|
||||
<select
|
||||
name="key-min-decrypt-version"
|
||||
id="key-min-decrypt-version"
|
||||
onchange={{action (mut @key.minDecryptionVersion) value="target.value"}}
|
||||
>
|
||||
{{#each @key.keyVersions as |version|}}
|
||||
<option selected={{loose-equal @key.minDecryptionVersion version}} value={{version}}>
|
||||
<code>{{version}}</code>
|
||||
</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help">
|
||||
The minimum decryption version required to reverse transformations performed with the cryptographic key. Results from
|
||||
lower key versions may be rewrapped with the new key version using the
|
||||
<code>rewrap</code>
|
||||
action.
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="key-min-encrypt-version" class="is-label">Minimum encryption version</label>
|
||||
<div class="control is-expanded">
|
||||
<div class="select is-fullwidth">
|
||||
<select
|
||||
name="key-min-encrypt-version"
|
||||
id="key-min-encrypt-version"
|
||||
onchange={{action (mut @key.minEncryptionVersion) value="target.value"}}
|
||||
>
|
||||
<option selected={{loose-equal @key.minEncryptionVersion 0}} value={{0}}>
|
||||
<code>Latest</code>
|
||||
(currently
|
||||
{{@key.latestVersion}})
|
||||
</option>
|
||||
{{#each @key.encryptionKeyVersions as |version|}}
|
||||
<option selected={{loose-equal @key.minEncryptionVersion version}} value={{version}}>
|
||||
<code>{{version}}</code>
|
||||
</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help">
|
||||
The minimum version of the key that can be used to encrypt plaintext, sign payloads, or generate HMACs. You will be
|
||||
able to specify which version of the key to use for each of these actions. The value specified here must be greater
|
||||
than or equal to that specified in the
|
||||
<b>Minimum Decryption Version</b>
|
||||
selection above.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{{#each @form.formFields as |attr|}}
|
||||
<FormField @attr={{attr}} @model={{@form}} @modelValidations={{this.modelValidations}}>
|
||||
{{#if (eq attr.name "auto_rotate_period")}}
|
||||
<div class="field">
|
||||
<TtlPicker
|
||||
@initialValue={{or @form.data.auto_rotate_period "30d"}}
|
||||
@initialEnabled={{not (eq @form.data.auto_rotate_period 0)}}
|
||||
@label="Auto-rotation period"
|
||||
@helperTextDisabled="Key will never be automatically rotated"
|
||||
@helperTextEnabled="Key will be automatically rotated every"
|
||||
@onChange={{this.handleAutoRotateChange}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if (eq attr.name "min_decryption_version")}}
|
||||
<div class="control is-expanded">
|
||||
<div class="select is-fullwidth">
|
||||
<select
|
||||
name="min_decryption_version"
|
||||
id="min_decryption_version"
|
||||
onchange={{action (mut @form.data.min_decryption_version) value="target.value"}}
|
||||
>
|
||||
{{#each @form.data.keyVersions as |version|}}
|
||||
<option selected={{loose-equal @form.data.min_decryption_version version}} value={{version}}>
|
||||
<code>{{version}}</code>
|
||||
</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help">
|
||||
The minimum decryption version required to reverse transformations performed with the cryptographic key. Results
|
||||
from lower key versions may be rewrapped with the new key version using the
|
||||
<code>rewrap</code>
|
||||
action.
|
||||
</p>
|
||||
{{/if}}
|
||||
{{#if (eq attr.name "min_encryption_version")}}
|
||||
<div class="control is-expanded">
|
||||
<div class="select is-fullwidth">
|
||||
<select
|
||||
name="min_encryption_version"
|
||||
id="min_encryption_version"
|
||||
onchange={{action (mut @form.data.min_encryption_version) value="target.value"}}
|
||||
>
|
||||
<option selected={{loose-equal @form.data.min_encryption_version 0}} value={{0}}>
|
||||
<code>Latest</code>
|
||||
(currently
|
||||
{{@form.data.latest_version}})
|
||||
</option>
|
||||
{{#each @form.data.encryptionKeyVersions as |version|}}
|
||||
<option selected={{loose-equal @form.data.min_encryption_version version}} value={{version}}>
|
||||
<code>{{version}}</code>
|
||||
</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help">
|
||||
The minimum version of the key that can be used to encrypt plaintext, sign payloads, or generate HMACs. You will
|
||||
be able to specify which version of the key to use for each of these actions. The value specified here must be
|
||||
greater than or equal to that specified in the
|
||||
<b>Minimum Decryption Version</b>
|
||||
selection above.
|
||||
</p>
|
||||
{{/if}}
|
||||
</FormField>
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class="field is-grouped is-grouped-split box is-fullwidth is-bottomless">
|
||||
<div class="field is-grouped">
|
||||
{{#if @model.canUpdate}}
|
||||
{{#if @capabilities.canUpdate}}
|
||||
<div class="control">
|
||||
<Hds::Button
|
||||
@text="Update transit key"
|
||||
@icon={{if @requestInFlight "loading"}}
|
||||
type="submit"
|
||||
disabled={{@requestInFlight}}
|
||||
/>
|
||||
<Hds::Button @text="Update transit key" type="submit" />
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="control">
|
||||
|
|
@ -104,13 +88,13 @@
|
|||
@text="Cancel"
|
||||
@color="secondary"
|
||||
@route="vault.cluster.secrets.backend.show"
|
||||
@models={{array @key.backend @key.id}}
|
||||
@models={{array @form.data.backend @form.data.id}}
|
||||
@query={{hash tab="details"}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{{#if @model.canDelete}}
|
||||
<ConfirmAction @buttonText="Delete transit key" @onConfirmAction={{@deleteKey}} />
|
||||
{{#if @capabilities.canDelete}}
|
||||
<ConfirmAction @buttonText="Delete transit key" @onConfirmAction={{this.deleteKey}} />
|
||||
{{/if}}
|
||||
</div>
|
||||
</form>
|
||||
93
ui/app/components/transit-form-edit.ts
Normal file
93
ui/app/components/transit-form-edit.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import ApiService from 'vault/services/api';
|
||||
import FlashMessageService from 'vault/services/flash-messages';
|
||||
import RouterService from '@ember/routing/router-service';
|
||||
import TransitKeyForm from 'vault/forms/transit/key';
|
||||
import { ValidationMap } from 'vault/vault/app-types';
|
||||
import { HTMLElementEvent } from 'vault/forms';
|
||||
|
||||
type TransitKeyConfig = {
|
||||
name?: string;
|
||||
backend?: string;
|
||||
auto_rotate_period?: string;
|
||||
min_decryption_version?: number;
|
||||
min_encryption_version?: number;
|
||||
deletion_allowed?: boolean;
|
||||
};
|
||||
|
||||
interface Args {
|
||||
form: TransitKeyForm;
|
||||
}
|
||||
export default class TransitFormEdit extends Component<Args> {
|
||||
@service declare readonly router: RouterService;
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
@service declare readonly api: ApiService;
|
||||
|
||||
@tracked errorBanner = '';
|
||||
@tracked modelValidations: ValidationMap | null = null;
|
||||
|
||||
@action
|
||||
handleAutoRotateChange(ttlObj: { enabled: boolean; goSafeTimeString: string }) {
|
||||
const { data } = this.args.form;
|
||||
if (ttlObj.enabled) {
|
||||
data.auto_rotate_period = ttlObj.goSafeTimeString;
|
||||
} else {
|
||||
data.auto_rotate_period = '0s';
|
||||
}
|
||||
}
|
||||
|
||||
@action async deleteKey() {
|
||||
const { backend, id } = this.args.form.data;
|
||||
try {
|
||||
await this.api.secrets.transitDeleteKey(id as string, backend as string);
|
||||
this.flashMessages.success(`'${id}' key deleted.`);
|
||||
this.router.transitionTo('vault.cluster.secrets.backend.list-root');
|
||||
} catch (error) {
|
||||
const { message } = await this.api.parseError(error);
|
||||
this.errorBanner = message;
|
||||
}
|
||||
}
|
||||
|
||||
@action async save(event: HTMLElementEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
|
||||
try {
|
||||
const { data, isValid, state } = this.args.form.toJSON();
|
||||
this.modelValidations = isValid ? null : state;
|
||||
|
||||
if (isValid) {
|
||||
const {
|
||||
name,
|
||||
backend,
|
||||
auto_rotate_period,
|
||||
min_decryption_version,
|
||||
min_encryption_version,
|
||||
deletion_allowed,
|
||||
}: TransitKeyConfig = data;
|
||||
|
||||
await this.api.secrets.transitConfigureKey(name as string, backend as string, {
|
||||
auto_rotate_period,
|
||||
min_decryption_version,
|
||||
min_encryption_version,
|
||||
deletion_allowed,
|
||||
});
|
||||
|
||||
this.flashMessages.success('Key successfully updated.');
|
||||
this.router.transitionTo('vault.cluster.secrets.backend.show', backend, name, {
|
||||
queryParams: { tab: 'details' },
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
const { message } = await this.api.parseError(error);
|
||||
this.errorBanner = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,9 +8,9 @@
|
|||
<ul>
|
||||
<li class={{if (eq @tab "actions") "is-active"}}>
|
||||
<SecretLink
|
||||
@secret={{@key.id}}
|
||||
@secret={{@form.data.id}}
|
||||
@mode="show"
|
||||
@backend={{@key.backend}}
|
||||
@backend={{@form.data.backend}}
|
||||
@replace={{true}}
|
||||
@queryParams={{hash tab="actions"}}
|
||||
data-test-transit-key-actions-link={{true}}
|
||||
|
|
@ -21,9 +21,9 @@
|
|||
|
||||
<li class={{if (eq @tab "details") "is-active"}}>
|
||||
<SecretLink
|
||||
@secret={{@key.id}}
|
||||
@secret={{@form.data.id}}
|
||||
@mode="show"
|
||||
@backend={{@key.backend}}
|
||||
@backend={{@form.data.backend}}
|
||||
@replace={{true}}
|
||||
@queryParams={{hash tab="details"}}
|
||||
data-test-transit-link="details"
|
||||
|
|
@ -34,9 +34,9 @@
|
|||
|
||||
<li class={{if (eq @tab "versions") "is-active"}}>
|
||||
<SecretLink
|
||||
@secret={{@key.id}}
|
||||
@secret={{@form.data.id}}
|
||||
@mode="show"
|
||||
@backend={{@key.backend}}
|
||||
@backend={{@form.data.backend}}
|
||||
@replace={{true}}
|
||||
@queryParams={{hash tab="versions"}}
|
||||
data-test-transit-link="versions"
|
||||
|
|
@ -50,7 +50,7 @@
|
|||
{{#if (not-eq @tab "actions")}}
|
||||
<Toolbar>
|
||||
<ToolbarActions>
|
||||
{{#if (and (eq @tab "versions") @key.canRotate)}}
|
||||
{{#if (and (eq @tab "versions") @capabilities.canRotate)}}
|
||||
<ConfirmAction
|
||||
@buttonText="Rotate key"
|
||||
class="toolbar-button"
|
||||
|
|
@ -63,8 +63,8 @@
|
|||
/>
|
||||
{{/if}}
|
||||
{{#if (eq @mode "show")}}
|
||||
{{#if (or @model.canUpdate @model.canDelete)}}
|
||||
<ToolbarSecretLink @secret={{@key.id}} @backend={{@key.backend}} @mode="edit" replace={{true}}>
|
||||
{{#if (or @capabilities.canUpdate @capabilities.canDelete)}}
|
||||
<ToolbarSecretLink @secret={{@form.data.id}} @backend={{@form.data.backend}} @mode="edit" replace={{true}}>
|
||||
Edit key
|
||||
</ToolbarSecretLink>
|
||||
{{/if}}
|
||||
|
|
@ -75,9 +75,9 @@
|
|||
|
||||
{{#if (eq @tab "actions")}}
|
||||
<div class="transit-card-container">
|
||||
{{#each @model.supportedActions as |supportedAction|}}
|
||||
{{#each @form.data.supportedActions as |supportedAction|}}
|
||||
<LinkedBlock
|
||||
@params={{array "vault.cluster.secrets.backend.actions" @model.backend @model.id}}
|
||||
@params={{array "vault.cluster.secrets.backend.actions" @form.data.backend @form.data.id}}
|
||||
@queryParams={{hash action=supportedAction.name}}
|
||||
class="transit-card"
|
||||
data-test-transit-card={{supportedAction.name}}
|
||||
|
|
@ -86,7 +86,7 @@
|
|||
<Icon
|
||||
@name={{supportedAction.glyph}}
|
||||
class="has-text-grey auto-width"
|
||||
aria-label={{concat @backend.path " options"}}
|
||||
aria-label={{concat @form.data.backend.path " options"}}
|
||||
/>
|
||||
</div>
|
||||
<div class="transit-description">
|
||||
|
|
@ -105,8 +105,10 @@
|
|||
{{/each}}
|
||||
</div>
|
||||
{{else if (eq @tab "versions")}}
|
||||
{{#if (or (eq @key.type "aes256-gcm96") (eq @key.type "chacha20-poly1305") (eq @key.type "aes128-gcm96"))}}
|
||||
{{#each-in @key.keys as |version creationTimestamp|}}
|
||||
{{#if
|
||||
(or (eq @form.data.type "aes256-gcm96") (eq @form.data.type "chacha20-poly1305") (eq @form.data.type "aes128-gcm96"))
|
||||
}}
|
||||
{{#each-in @form.data.keys as |version creationTimestamp|}}
|
||||
<div class="linked-block list-item-row no-destination" data-test-transit-version={{version}}>
|
||||
<div class="columns is-mobile">
|
||||
<div class="column is-3">
|
||||
|
|
@ -118,13 +120,13 @@
|
|||
<div class="column is-4">
|
||||
<div class="td is-borderless">
|
||||
<small class="help has-text-grey">
|
||||
{{date-from-now creationTimestamp addSuffix=true}}
|
||||
{{this.getTimestamp creationTimestamp}}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-5">
|
||||
<div class="td is-borderless">
|
||||
{{#if (coerce-eq @key.minDecryptionVersion version)}}
|
||||
{{#if (coerce-eq @form.data.min_decryption_version version)}}
|
||||
<p class="help level level-left">
|
||||
<Icon @name="check-circle-fill" class="has-text-success" />
|
||||
Current minimum decryption version
|
||||
|
|
@ -136,7 +138,7 @@
|
|||
</div>
|
||||
{{/each-in}}
|
||||
{{else}}
|
||||
{{#each-in @key.keys as |version meta|}}
|
||||
{{#each-in @form.data.keys as |version meta|}}
|
||||
<div class="linked-block list-item-row" data-test-transit-version={{version}}>
|
||||
<div class="columns is-mobile">
|
||||
<div class="column is-3">
|
||||
|
|
@ -154,7 +156,7 @@
|
|||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="td is-borderless">
|
||||
{{#if (coerce-eq @key.minDecryptionVersion version)}}
|
||||
{{#if (coerce-eq @form.data.min_decryption_version version)}}
|
||||
<p class="help level level-left">
|
||||
<Icon @name="check-circle-fill" class="has-text-success" />
|
||||
Current minimum decryption version
|
||||
|
|
@ -178,15 +180,15 @@
|
|||
{{/each-in}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<InfoTableRow @label="Type" @value={{@key.type}} />
|
||||
<InfoTableRow @label="Type" @value={{@form.data.type}} />
|
||||
<InfoTableRow
|
||||
@label="Auto-rotation period"
|
||||
@value={{or (format-duration @key.autoRotatePeriod) "Key will not be automatically rotated"}}
|
||||
@value={{or (format-duration @form.data.auto_rotate_period) "Key will not be automatically rotated"}}
|
||||
/>
|
||||
<InfoTableRow @label="Deletion allowed" @value={{stringify @key.deletionAllowed}} />
|
||||
<InfoTableRow @label="Deletion allowed" @value={{stringify @form.data.deletion_allowed}} />
|
||||
|
||||
{{#if @key.derived}}
|
||||
<InfoTableRow @label="Derived" @value={{@key.derived}} />
|
||||
<InfoTableRow @label="Convergent encryption" @value={{@key.convergentEncryption}} />
|
||||
{{#if @form.data.derived}}
|
||||
<InfoTableRow @label="Derived" @value={{@form.data.derived}} />
|
||||
<InfoTableRow @label="Convergent encryption" @value={{@form.data.convergent_encryption}} />
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
|
@ -6,28 +6,30 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { buildWaiter } from '@ember/test-waiters';
|
||||
import errorMessage from 'vault/utils/error-message';
|
||||
|
||||
const waiter = buildWaiter('transit-form-show');
|
||||
import { dateFromNow } from 'core/helpers/date-from-now';
|
||||
|
||||
export default class TransitFormShow extends Component {
|
||||
@service store;
|
||||
@service router;
|
||||
@service flashMessages;
|
||||
@service api;
|
||||
|
||||
@action async rotateKey() {
|
||||
const waiterToken = waiter.beginAsync();
|
||||
const { backend, id } = this.args.key;
|
||||
const { backend, id } = this.args.form.data;
|
||||
try {
|
||||
await this.store.adapterFor('transit-key').keyAction('rotate', { backend, id });
|
||||
await this.api.secrets.transitRotateKey(id, backend, {});
|
||||
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));
|
||||
} finally {
|
||||
waiter.endAsync(waiterToken);
|
||||
const { message } = this.api.parseError(e);
|
||||
this.flashMessages.danger(message);
|
||||
}
|
||||
}
|
||||
|
||||
// Investigate - possibly a bug?
|
||||
// api returns the same value for creation time as ember data (eg. 1633024800) - but the date isn't rendering the same (ie. ED model pipe returns correct time but api value returns as 56 years ago)
|
||||
// not sure why the ED model data is taken as milliseconds and the api value is taken as seconds, but this is a workaround to get the correct time.
|
||||
getTimestamp(time) {
|
||||
return dateFromNow([time * 1000], { addSuffix: true });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ 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';
|
||||
|
||||
/**
|
||||
* @module TransitKeyActions
|
||||
|
|
@ -90,9 +89,9 @@ const SUCCESS_MESSAGE_FOR_ACTION = {
|
|||
};
|
||||
|
||||
export default class TransitKeyActions extends Component {
|
||||
@service store;
|
||||
@service flashMessages;
|
||||
@service router;
|
||||
@service api;
|
||||
|
||||
@tracked isModalActive = false;
|
||||
@tracked errors = null;
|
||||
|
|
@ -126,7 +125,7 @@ export default class TransitKeyActions extends Component {
|
|||
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) {
|
||||
if (action === 'rewrap' && !this.args.key.supports_encryption) {
|
||||
this.props.ciphertext = null;
|
||||
}
|
||||
}
|
||||
|
|
@ -211,13 +210,43 @@ export default class TransitKeyActions extends Component {
|
|||
const payload = formData ? this.compactData(formData) : null;
|
||||
|
||||
try {
|
||||
const resp = yield this.store
|
||||
.adapterFor('transit-key')
|
||||
.keyAction(action, { backend, id, payload }, options);
|
||||
|
||||
let resp;
|
||||
if (action === 'encrypt') {
|
||||
resp = yield this.api.secrets.transitEncrypt(id, backend, payload);
|
||||
} else if (action === 'decrypt') {
|
||||
resp = yield this.api.secrets.transitDecrypt(id, backend, payload);
|
||||
} else if (action === 'datakey') {
|
||||
resp = yield this.api.secrets.transitGenerateDataKey(id, payload.param, backend, {
|
||||
...payload,
|
||||
});
|
||||
} else if (action === 'rewrap') {
|
||||
resp = yield this.api.secrets.transitRewrap(id, backend, payload);
|
||||
} else if (action === 'sign') {
|
||||
resp = yield this.api.secrets.transitSign(id, backend, payload);
|
||||
} else if (action === 'verify') {
|
||||
resp = yield this.api.secrets.transitVerify(id, backend, payload);
|
||||
} else if (action === 'hmac') {
|
||||
resp = yield this.api.secrets.transitGenerateHmac(id, backend, payload);
|
||||
} else if (action === 'export') {
|
||||
const [type, version] = payload.param;
|
||||
// if wrap ttl is present, the header needs to be set on the request for the api service to return the wrapped token in response - otherwise it would ignore and return data keys
|
||||
const overrideOptions = options.wrapTTL
|
||||
? { headers: { 'X-Vault-Wrap-TTL': options.wrapTTL }, wrapTTL: options.wrapTTL }
|
||||
: {};
|
||||
resp = version
|
||||
? yield this.api.secrets.transitExportKeyVersion(
|
||||
id,
|
||||
`${type}-key`,
|
||||
version,
|
||||
backend,
|
||||
overrideOptions
|
||||
)
|
||||
: yield this.api.secrets.transitExportKey(id, `${type}-key`, backend, overrideOptions);
|
||||
}
|
||||
this.handleSuccess(resp, options, action);
|
||||
} catch (e) {
|
||||
this.errors = errorMessage(e);
|
||||
const { message } = yield this.api.parseError(e);
|
||||
this.errors = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
133
ui/app/forms/transit/key.ts
Normal file
133
ui/app/forms/transit/key.ts
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2026
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Form from 'vault/forms/form';
|
||||
import FormField from 'vault/utils/forms/field';
|
||||
import type { TransitCreateKeyRequest } from '@hashicorp/vault-client-typescript';
|
||||
import { Validations } from 'vault/vault/app-types';
|
||||
import { durationToSeconds } from 'core/utils/duration-utils';
|
||||
|
||||
type TransitKeyData = TransitCreateKeyRequest & {
|
||||
backend?: string;
|
||||
id?: string;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export default class TransitKeyForm extends Form<TransitKeyData> {
|
||||
// these set funcs just exist on edit
|
||||
set convergentEncryptionValue(val: boolean | undefined) {
|
||||
if (val === true) {
|
||||
this.data.derived = val;
|
||||
}
|
||||
this.data.convergent_encryption = val;
|
||||
}
|
||||
|
||||
set derivedValue(val: boolean | undefined) {
|
||||
if (val === false) {
|
||||
this.data.convergent_encryption = val;
|
||||
}
|
||||
this.data.derived = val;
|
||||
}
|
||||
|
||||
get formFields() {
|
||||
let fields = [];
|
||||
if (this.isNew) {
|
||||
fields.push(
|
||||
...[
|
||||
new FormField('name', 'string', {
|
||||
editDisabled: !this.isNew,
|
||||
}),
|
||||
new FormField('auto_rotate_period', undefined, {
|
||||
editType: 'yield',
|
||||
label: ' ',
|
||||
}),
|
||||
new FormField('type', 'string', {
|
||||
possibleValues: [
|
||||
'aes128-gcm96',
|
||||
'aes256-gcm96',
|
||||
'chacha20-poly1305',
|
||||
'ecdsa-p256',
|
||||
'ecdsa-p384',
|
||||
'ecdsa-p521',
|
||||
'ed25519',
|
||||
'rsa-2048',
|
||||
'rsa-3072',
|
||||
'rsa-4096',
|
||||
],
|
||||
}),
|
||||
new FormField('exportable', 'boolean', {
|
||||
label: 'Exportable',
|
||||
editType: 'checkbox',
|
||||
}),
|
||||
new FormField('derived', 'boolean', {
|
||||
label: 'Derived',
|
||||
editType: 'checkbox',
|
||||
}),
|
||||
new FormField('convergent_encryption', 'boolean', {
|
||||
label: 'Enable convergent encryption',
|
||||
editType: 'checkbox',
|
||||
}),
|
||||
]
|
||||
);
|
||||
if (this.data.type?.startsWith('ecdsa') || this.data.type?.startsWith('rsa')) {
|
||||
fields = fields.filter((field) => !['convergent_encryption', 'derived'].includes(field.name));
|
||||
} else if (this.data.type === 'ed25519') {
|
||||
fields = fields.filter((field) => field.name !== 'convergent_encryption');
|
||||
}
|
||||
} else {
|
||||
fields.push(
|
||||
...[
|
||||
new FormField('deletion_allowed', 'boolean', {
|
||||
label: 'Allow deletion',
|
||||
editType: 'checkbox',
|
||||
}),
|
||||
new FormField('auto_rotate_period', undefined, {
|
||||
label: ' ',
|
||||
editType: 'yield',
|
||||
}),
|
||||
new FormField('min_decryption_version', undefined, {
|
||||
label: 'Minimum decryption version',
|
||||
editType: 'yield',
|
||||
}),
|
||||
new FormField('min_encryption_version', undefined, {
|
||||
label: 'Minimum encryption version',
|
||||
editType: 'yield',
|
||||
}),
|
||||
]
|
||||
);
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
validations: Validations = {
|
||||
name: [{ type: 'presence', message: 'Name is required' }],
|
||||
auto_rotate_period: [
|
||||
{
|
||||
validator(data: TransitKeyData) {
|
||||
const { auto_rotate_period } = data;
|
||||
if (auto_rotate_period === undefined) {
|
||||
return true;
|
||||
} else {
|
||||
const duration = durationToSeconds(auto_rotate_period);
|
||||
// regardless of generateSigningKey, if one key is set they both need to be set.
|
||||
return duration === 0 || duration >= 3600;
|
||||
}
|
||||
},
|
||||
message: 'Duration must be longer than 1 hour or set to 0 to disable auto-rotation.',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
toJSON() {
|
||||
const { isValid, state, invalidFormMessage } = super.toJSON();
|
||||
const data = { ...this.data } as Record<string, unknown>;
|
||||
// set auto_rotate_period to 0 if it's false to avoid api validation error since the form field is a string and the api expects a number
|
||||
if (data['auto_rotate_period'] === false) {
|
||||
data['auto_rotate_period'] = 0;
|
||||
}
|
||||
|
||||
return { isValid, state, invalidFormMessage, data };
|
||||
}
|
||||
}
|
||||
|
|
@ -30,21 +30,21 @@ export default EditBase.extend({
|
|||
setupController(controller, model) {
|
||||
this._super(...arguments);
|
||||
const { selectedAction } = this.paramsFor(this.routeName);
|
||||
controller.set('selectedAction', selectedAction || model.secret.supportedActions[0]);
|
||||
controller.set('selectedAction', selectedAction || model.secret.data.supportedActions[0]);
|
||||
controller.set('breadcrumbs', [
|
||||
{
|
||||
label: 'Secrets',
|
||||
route: 'vault.cluster.secrets',
|
||||
},
|
||||
{
|
||||
label: model.secret.backend,
|
||||
label: model.secret.data.backend,
|
||||
route: 'vault.cluster.secrets.backend.list-root',
|
||||
model: model.secret.backend,
|
||||
model: model.secret.data.backend,
|
||||
},
|
||||
{
|
||||
label: model.secret.id,
|
||||
label: model.secret.data.id,
|
||||
route: 'vault.cluster.secrets.backend.show',
|
||||
models: [model.secret.backend, model.secret.id],
|
||||
models: [model.secret.data.backend, model.secret.data.id],
|
||||
},
|
||||
{
|
||||
label: 'Actions',
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import EditBase from './secret-edit';
|
|||
import KeymgmtKeyForm from 'vault/forms/keymgmt/key';
|
||||
import KeymgmtProviderForm from 'vault/forms/keymgmt/provider';
|
||||
import TotpKeyForm from 'vault/forms/totp/key';
|
||||
import TransitKeyForm from 'vault/forms/transit/key';
|
||||
import SshRoleForm from 'vault/forms/ssh/role';
|
||||
import AlphabetForm from 'vault/forms/transform/alphabet';
|
||||
import TemplateForm from 'vault/forms/transform/template';
|
||||
|
|
@ -65,6 +66,17 @@ export default EditBase.extend({
|
|||
);
|
||||
}
|
||||
|
||||
if (modelType === 'transit-key') {
|
||||
return new TransitKeyForm(
|
||||
{
|
||||
backend,
|
||||
type: 'aes256-gcm96',
|
||||
auto_rotate_period: '0s',
|
||||
},
|
||||
{ isNew: true }
|
||||
);
|
||||
}
|
||||
|
||||
if (modelType === 'role-ssh') {
|
||||
return new SshRoleForm(
|
||||
{ backend, key_type: 'ca', not_before_duration: '30s', port: 22 },
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { set } from '@ember/object';
|
||||
import { set, get } from '@ember/object';
|
||||
import Ember from 'ember';
|
||||
import { resolve } from 'rsvp';
|
||||
import { service } from '@ember/service';
|
||||
|
|
@ -17,6 +17,9 @@ import { isValidProvider } from 'vault/utils/keymgmt-provider-utils';
|
|||
import KeymgmtKeyForm from 'vault/forms/keymgmt/key';
|
||||
import KeymgmtProviderForm from 'vault/forms/keymgmt/provider';
|
||||
import TotpKeyForm from 'vault/forms/totp/key';
|
||||
import clamp from 'vault/utils/clamp';
|
||||
import TransitKeyForm from 'vault/forms/transit/key';
|
||||
|
||||
import SshRoleForm from 'vault/forms/ssh/role';
|
||||
import AlphabetForm from 'vault/forms/transform/alphabet';
|
||||
import TemplateForm from 'vault/forms/transform/template';
|
||||
|
|
@ -28,6 +31,50 @@ import {
|
|||
SecretsApiTransformListRolesListEnum,
|
||||
} from '@hashicorp/vault-client-typescript';
|
||||
|
||||
// TODO: Move this into a util file or class
|
||||
const ACTION_VALUES = {
|
||||
encrypt: {
|
||||
isSupported: 'supports_encryption',
|
||||
description: 'Looks up wrapping properties for the given token.',
|
||||
glyph: 'lock-fill',
|
||||
},
|
||||
decrypt: {
|
||||
isSupported: 'supports_decryption',
|
||||
description: 'Decrypts the provided ciphertext using this key.',
|
||||
glyph: 'mail-open',
|
||||
},
|
||||
datakey: {
|
||||
isSupported: 'supports_encryption',
|
||||
description: 'Generates a new key and value encrypted with this key.',
|
||||
glyph: 'key',
|
||||
},
|
||||
rewrap: {
|
||||
isSupported: 'supports_encryption',
|
||||
description: 'Rewraps the ciphertext using the latest version of the named key.',
|
||||
glyph: 'reload',
|
||||
},
|
||||
sign: {
|
||||
isSupported: 'supports_signing',
|
||||
description: 'Get the cryptographic signature of the given data.',
|
||||
glyph: 'pencil-tool',
|
||||
},
|
||||
hmac: {
|
||||
isSupported: true,
|
||||
description: 'Generate a data digest using a hash algorithm.',
|
||||
glyph: 'shuffle',
|
||||
},
|
||||
verify: {
|
||||
isSupported: true,
|
||||
description: 'Validate the provided signature for the given data.',
|
||||
glyph: 'check-circle',
|
||||
},
|
||||
export: {
|
||||
isSupported: 'exportable',
|
||||
description: 'Get the named key.',
|
||||
glyph: 'external-link',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @type Class
|
||||
*/
|
||||
|
|
@ -261,6 +308,104 @@ export default Route.extend({
|
|||
};
|
||||
},
|
||||
|
||||
async fetchTransitKey(name, backend) {
|
||||
const res = await this.api.secrets.transitReadKey(name, backend);
|
||||
|
||||
const transitModel = {
|
||||
backend,
|
||||
id: name,
|
||||
...res,
|
||||
...this.transitEncryptionKeyVersions(
|
||||
res.keys,
|
||||
res.min_decryption_version,
|
||||
res.min_encryption_version,
|
||||
res.latest_version,
|
||||
res.supports_signing,
|
||||
res.supports_encryption
|
||||
),
|
||||
};
|
||||
|
||||
return transitModel;
|
||||
},
|
||||
|
||||
async fetchTransitKeyCapabilities(backend, name, secretModel) {
|
||||
const rotatePath = this.capabilitiesService.pathFor('transitKeyRotate', { backend, name });
|
||||
const keyPath = this.capabilitiesService.pathFor('transitKey', { backend, name });
|
||||
|
||||
const capabilities = await this.capabilitiesService.fetch([rotatePath, keyPath]);
|
||||
|
||||
return {
|
||||
canRotate: capabilities[rotatePath]?.canUpdate,
|
||||
canRead: capabilities[keyPath]?.canRead,
|
||||
canUpdate: capabilities[keyPath]?.canUpdate,
|
||||
canDelete: capabilities[keyPath]?.canDelete !== false && secretModel.deletion_allowed !== false, // check deletion_allowed from the model in addition to the capability since both are required to allow deletion
|
||||
};
|
||||
},
|
||||
|
||||
// TODO: Move these into separate classes or utils
|
||||
transitEncryptionKeyVersions(
|
||||
keys,
|
||||
minDecryptionVersion,
|
||||
minEncryptionVersion,
|
||||
latestVersion,
|
||||
supportsSigning,
|
||||
supportsEncryption
|
||||
) {
|
||||
const validKeys = Object.keys(keys);
|
||||
const keyVersions = [];
|
||||
const keysForEncryption = [];
|
||||
|
||||
// get keyVersions
|
||||
let maxVersion = Math.max(...validKeys);
|
||||
while (maxVersion > 0) {
|
||||
keyVersions.unshift(maxVersion);
|
||||
maxVersion--;
|
||||
}
|
||||
|
||||
// get encryptionKeyVersions using keyVersions
|
||||
const encryptionKeyVersions = keyVersions
|
||||
.filter((version) => {
|
||||
return version >= minDecryptionVersion;
|
||||
})
|
||||
.reverse();
|
||||
|
||||
// get keysForEncryption
|
||||
const minVersion = clamp(minEncryptionVersion - 1, 0, latestVersion);
|
||||
while (latestVersion > minVersion) {
|
||||
keysForEncryption.push(latestVersion);
|
||||
latestVersion--;
|
||||
}
|
||||
|
||||
// get exportKeyTypes
|
||||
const exportKeyTypes = ['hmac'];
|
||||
if (supportsSigning) {
|
||||
exportKeyTypes.unshift('signing');
|
||||
}
|
||||
if (supportsEncryption) {
|
||||
exportKeyTypes.unshift('encryption');
|
||||
}
|
||||
|
||||
return {
|
||||
encryptionKeyVersions,
|
||||
keyVersions,
|
||||
keysForEncryption,
|
||||
exportKeyTypes,
|
||||
validKeyVersions: Object.keys(keys),
|
||||
};
|
||||
},
|
||||
|
||||
transitSupportedActions(secretModel) {
|
||||
return Object.keys(ACTION_VALUES)
|
||||
.filter((name) => {
|
||||
const { isSupported } = ACTION_VALUES[name];
|
||||
return typeof isSupported === 'boolean' || get(secretModel, isSupported);
|
||||
})
|
||||
.map((name) => {
|
||||
const { description, glyph } = ACTION_VALUES[name];
|
||||
return { name, description, glyph };
|
||||
});
|
||||
},
|
||||
|
||||
async fetchKeymgmtKey(backend, name) {
|
||||
const { data } = await this.api.secrets.keyManagementReadKey(name, backend);
|
||||
|
||||
|
|
@ -525,6 +670,12 @@ export default Route.extend({
|
|||
if (modelType === 'totp-key') {
|
||||
secretModel = await this.fetchTotpKey(backend, secret);
|
||||
capabilities = await this.fetchTotpKeyCapabilities(backend, secret);
|
||||
} else if (modelType === 'transit-key') {
|
||||
secretModel = await this.fetchTransitKey(secret, backend);
|
||||
capabilities = await this.fetchTransitKeyCapabilities(backend, secret, secretModel);
|
||||
secretModel.supportedActions = this.transitSupportedActions(secretModel);
|
||||
// replace secretModel with form
|
||||
secretModel = new TransitKeyForm(secretModel, { isNew: false });
|
||||
} else if (modelType === 'keymgmt/key') {
|
||||
secretModel = await this.fetchKeymgmtKey(backend, secret);
|
||||
capabilities = await this.fetchKeymgmtKeyCapabilities(backend, secret);
|
||||
|
|
|
|||
|
|
@ -125,8 +125,14 @@ export default class ApiService extends Service {
|
|||
}
|
||||
// if the requested path is locked by a control group we need to create a new error response
|
||||
const json = await this.readJson(response);
|
||||
|
||||
// derive from REQUEST, not response - transit export is making an api request but trying to also set vault header on the request, not the response - causing a control group error.
|
||||
const wasWrapTTLRequested = (context.init.headers as Headers)?.get?.('X-Vault-Wrap-TTL');
|
||||
const wrapTtl = headers.get('X-Vault-Wrap-TTL');
|
||||
const isLockedByControlGroup = this.controlGroup.isRequestedPathLocked(json, wrapTtl);
|
||||
const isLockedByControlGroup = this.controlGroup.isRequestedPathLocked(
|
||||
json,
|
||||
wasWrapTTLRequested ? wasWrapTTLRequested : wrapTtl
|
||||
);
|
||||
|
||||
if (isLockedByControlGroup && json) {
|
||||
const error = {
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@
|
|||
<:actions>
|
||||
<SecretLink
|
||||
class="is-inline has-text-info"
|
||||
@secret={{this.model.id}}
|
||||
@backend={{this.model.backend}}
|
||||
@secret={{this.model.data.id}}
|
||||
@backend={{this.model.data.backend}}
|
||||
@mode="show"
|
||||
@replace={{true}}
|
||||
@queryParams={{hash tab="actions"}}
|
||||
|
|
@ -28,12 +28,12 @@
|
|||
<div class="tabs-container box is-sideless is-fullwidth is-paddingless is-marginless">
|
||||
<nav class="tabs">
|
||||
<ul>
|
||||
{{#each this.model.supportedActions as |supportedAction|}}
|
||||
{{#each this.model.data.supportedActions as |supportedAction|}}
|
||||
<li class={{if (eq supportedAction.name this.selectedAction) "is-active"}}>
|
||||
<SecretLink
|
||||
@mode="actions"
|
||||
@backend={{this.model.backend}}
|
||||
@secret={{this.model.id}}
|
||||
@backend={{this.model.data.backend}}
|
||||
@secret={{this.model.data.id}}
|
||||
@queryParams={{hash action=supportedAction.name}}
|
||||
data-test-transit-action-link={{supportedAction.name}}
|
||||
>
|
||||
|
|
@ -50,6 +50,6 @@
|
|||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<TransitKeyActions @selectedAction={{this.selectedAction}} @key={{this.model}} />
|
||||
<TransitKeyActions @selectedAction={{this.selectedAction}} @key={{this.model.data}} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -90,6 +90,8 @@ export const PATH_MAP = {
|
|||
syncSetAssociation: apiPath`sys/sync/destinations/${'type'}/${'name'}/associations/set`,
|
||||
totpKey: apiPath`${'backend'}/keys/${'name'}`,
|
||||
totpKeys: apiPath`${'backend'}/keys`,
|
||||
transitKeyRotate: apiPath`${'backend'}/keys/${'name'}/rotate`,
|
||||
transitKey: apiPath`${'backend'}/keys/${'name'}`,
|
||||
transformAlphabet: apiPath`${'backend'}/alphabet/${'name'}`,
|
||||
transformAlphabets: apiPath`${'backend'}/alphabet`,
|
||||
transformRole: apiPath`${'backend'}/role/${'name'}`,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,17 @@
|
|||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { click, fillIn, find, currentURL, settled, visit, findAll, waitFor } from '@ember/test-helpers';
|
||||
import {
|
||||
click,
|
||||
fillIn,
|
||||
find,
|
||||
currentURL,
|
||||
settled,
|
||||
visit,
|
||||
findAll,
|
||||
waitFor,
|
||||
waitUntil,
|
||||
} from '@ember/test-helpers';
|
||||
import { module, test } from 'qunit';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
|
@ -233,14 +243,15 @@ module('Acceptance | transit', function (hooks) {
|
|||
const name = `test-generate-${this.uid}`;
|
||||
await click(SES.createSecretLink);
|
||||
|
||||
await fillIn(SELECTORS.form('name'), name);
|
||||
await fillIn(SELECTORS.form('type'), type);
|
||||
await click(SELECTORS.form('exportable'));
|
||||
await click(SELECTORS.form('derived'));
|
||||
await click(SELECTORS.form('convergent-encryption'));
|
||||
await fillIn(GENERAL.inputByAttr('name'), name);
|
||||
await fillIn(GENERAL.inputByAttr('type'), type);
|
||||
await click(GENERAL.inputByAttr('exportable'));
|
||||
await click(GENERAL.inputByAttr('derived'));
|
||||
await click(GENERAL.inputByAttr('convergent_encryption'));
|
||||
await click(GENERAL.ttl.toggle('Auto-rotation period'));
|
||||
await click(SELECTORS.form('create'));
|
||||
|
||||
await waitUntil(() => currentURL() === `/vault/secrets-engines/${this.path}/show/${name}?tab=details`);
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
`/vault/secrets-engines/${this.path}/show/${name}?tab=details`,
|
||||
|
|
@ -305,30 +316,30 @@ module('Acceptance | transit', function (hooks) {
|
|||
},
|
||||
{
|
||||
type: 'aes128-gcm96',
|
||||
'convergent-encryption': true,
|
||||
convergent_encryption: true,
|
||||
derived: true,
|
||||
exportable: true,
|
||||
},
|
||||
{
|
||||
type: 'aes256-gcm96',
|
||||
'convergent-encryption': true,
|
||||
convergent_encryption: true,
|
||||
derived: true,
|
||||
exportable: true,
|
||||
},
|
||||
{
|
||||
type: 'chacha20-poly1305',
|
||||
'convergent-encryption': true,
|
||||
convergent_encryption: true,
|
||||
derived: true,
|
||||
exportable: true,
|
||||
},
|
||||
];
|
||||
for (const key of KEY_OPTIONS) {
|
||||
const { type } = key;
|
||||
await fillIn(SELECTORS.form('type'), type);
|
||||
await fillIn(GENERAL.inputByAttr('type'), type);
|
||||
|
||||
for (const checkbox of ['exportable', 'derived', 'convergent-encryption']) {
|
||||
for (const checkbox of ['exportable', 'derived', 'convergent_encryption']) {
|
||||
const assertion = key[checkbox] ? 'exists' : 'doesNotExist';
|
||||
assert.dom(SELECTORS.form(checkbox))[assertion](`${type} ${checkbox} ${assertion}`);
|
||||
assert.dom(GENERAL.inputByAttr(checkbox))[assertion](`${type} ${checkbox} ${assertion}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { click, fillIn, render } from '@ember/test-helpers';
|
|||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { capabilitiesStub } from 'vault/tests/helpers/stubs';
|
||||
import TransitKeyForm from 'vault/forms/transit/key';
|
||||
|
||||
const SELECTORS = {
|
||||
createForm: '[data-test-transit-create-form]',
|
||||
|
|
@ -25,20 +26,20 @@ module('Integration | Component | transit-edit', function (hooks) {
|
|||
this.server.post('/sys/capabilities-self', () =>
|
||||
capabilitiesStub('transit-backend/keys/some-key', ['sudo'])
|
||||
);
|
||||
this.model = this.store.createRecord('transit-key', { backend: 'transit-backend', id: 'some-key' });
|
||||
this.model = { backend: 'transit', id: 'some-key' };
|
||||
this.backendCrumb = {
|
||||
label: 'transit',
|
||||
text: 'transit',
|
||||
path: 'vault.cluster.secrets.backend.list-root',
|
||||
model: 'transit',
|
||||
};
|
||||
this.form = new TransitKeyForm(this.model, { isNew: false });
|
||||
});
|
||||
|
||||
test('it renders in create mode and updates model', async function (assert) {
|
||||
await render(hbs`
|
||||
<TransitEdit
|
||||
@key={{this.model}}
|
||||
@model={{this.model}}
|
||||
@form={{this.form}}
|
||||
@mode="create"
|
||||
@root={{this.backendCrumb}}
|
||||
@preferAdvancedEdit={{false}}
|
||||
|
|
@ -48,25 +49,24 @@ module('Integration | Component | transit-edit', function (hooks) {
|
|||
assert.dom(SELECTORS.ttlToggle).isNotChecked();
|
||||
|
||||
// confirm model params update when ttl changes
|
||||
assert.strictEqual(this.model.autoRotatePeriod, '0');
|
||||
assert.strictEqual(this.form.data.auto_rotate_period, undefined);
|
||||
await click(SELECTORS.ttlToggle);
|
||||
|
||||
assert.dom(SELECTORS.ttlValue).hasValue('30'); // 30 days
|
||||
assert.strictEqual(this.model.autoRotatePeriod, '720h');
|
||||
assert.strictEqual(this.form.data.auto_rotate_period, '720h');
|
||||
|
||||
await fillIn(SELECTORS.ttlValue, '10'); // 10 days
|
||||
assert.strictEqual(this.model.autoRotatePeriod, '240h');
|
||||
assert.strictEqual(this.form.data.auto_rotate_period, '240h');
|
||||
});
|
||||
|
||||
test('it renders edit form correctly when key has autoRotatePeriod=0', async function (assert) {
|
||||
this.model.autoRotatePeriod = 0;
|
||||
this.model.keys = {
|
||||
this.form.data.auto_rotate_period = 0;
|
||||
this.form.data.keys = {
|
||||
1: 1684882652000,
|
||||
};
|
||||
await render(hbs`
|
||||
<TransitEdit
|
||||
@key={{this.model}}
|
||||
@model={{this.model}}
|
||||
@form={{this.form}}
|
||||
@mode="edit"
|
||||
@root={{this.backendCrumb}}
|
||||
@preferAdvancedEdit={{false}}
|
||||
|
|
@ -74,26 +74,25 @@ module('Integration | Component | transit-edit', function (hooks) {
|
|||
assert.dom(SELECTORS.editForm).exists();
|
||||
assert.dom(SELECTORS.ttlToggle).isNotChecked();
|
||||
|
||||
assert.strictEqual(this.model.autoRotatePeriod, 0);
|
||||
assert.strictEqual(this.form.data.auto_rotate_period, 0);
|
||||
|
||||
await click(SELECTORS.ttlToggle);
|
||||
assert.dom(SELECTORS.ttlToggle).isChecked();
|
||||
assert.dom(SELECTORS.ttlValue).hasValue('30');
|
||||
assert.strictEqual(this.model.autoRotatePeriod, '720h', 'model value changes with toggle');
|
||||
assert.strictEqual(this.form.data.auto_rotate_period, '720h', 'model value changes with toggle');
|
||||
|
||||
await click(SELECTORS.ttlToggle);
|
||||
assert.strictEqual(this.model.autoRotatePeriod, 0); // reverts to original value when toggled back on
|
||||
assert.strictEqual(this.form.data.auto_rotate_period, '0s'); // reverts to original value when toggled back on
|
||||
});
|
||||
|
||||
test('it renders edit form correctly when key has non-zero rotation period', async function (assert) {
|
||||
this.model.autoRotatePeriod = '5h';
|
||||
this.model.keys = {
|
||||
this.form.data.auto_rotate_period = '5h';
|
||||
this.form.data.keys = {
|
||||
1: 1684882652000,
|
||||
};
|
||||
await render(hbs`
|
||||
<TransitEdit
|
||||
@key={{this.model}}
|
||||
@model={{this.model}}
|
||||
@form={{this.form}}
|
||||
@mode="edit"
|
||||
@root={{this.backendCrumb}}
|
||||
@preferAdvancedEdit={{false}}
|
||||
|
|
@ -104,12 +103,20 @@ module('Integration | Component | transit-edit', function (hooks) {
|
|||
|
||||
await click(SELECTORS.ttlToggle);
|
||||
assert.dom(SELECTORS.ttlToggle).isNotChecked();
|
||||
assert.strictEqual(this.model.autoRotatePeriod, 0, 'model value changes back to 0 when toggled off');
|
||||
assert.strictEqual(
|
||||
this.form.data.auto_rotate_period,
|
||||
'0s',
|
||||
'model value changes back to 0 when toggled off'
|
||||
);
|
||||
|
||||
await click(SELECTORS.ttlToggle);
|
||||
assert.strictEqual(this.model.autoRotatePeriod, '5h'); // reverts to original value when toggled back on
|
||||
assert.strictEqual(
|
||||
this.form.data.auto_rotate_period,
|
||||
'5h',
|
||||
'model value changes to original value when toggled back on'
|
||||
);
|
||||
|
||||
await fillIn(SELECTORS.ttlValue, '10');
|
||||
assert.strictEqual(this.model.autoRotatePeriod, '10h');
|
||||
assert.strictEqual(this.form.data.auto_rotate_period, '10h');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,52 +3,19 @@
|
|||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { run } from '@ember/runloop';
|
||||
import { resolve } from 'rsvp';
|
||||
import Service from '@ember/service';
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'vault/tests/helpers';
|
||||
import { render, click, find, fillIn, blur, triggerEvent, waitFor } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import { encodeString } from 'vault/utils/b64';
|
||||
import waitForError from 'vault/tests/helpers/wait-for-error';
|
||||
import codemirror, { getCodeEditorValue, setCodeEditorValue } from 'vault/tests/helpers/codemirror';
|
||||
import codemirror, { setCodeEditorValue } from 'vault/tests/helpers/codemirror';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
|
||||
const storeStub = Service.extend({
|
||||
callArgs: null,
|
||||
keyActionReturnVal: null,
|
||||
rootKeyActionReturnVal: null,
|
||||
adapterFor() {
|
||||
const self = this;
|
||||
return {
|
||||
keyAction(action, { backend, id, payload }, options) {
|
||||
self.set('callArgs', { action, backend, id, payload });
|
||||
self.set('callArgsOptions', options);
|
||||
const rootResp = { ...self.get('rootKeyActionReturnVal') };
|
||||
const resp =
|
||||
Object.keys(rootResp).length > 0
|
||||
? rootResp
|
||||
: {
|
||||
data: { ...self.get('keyActionReturnVal') },
|
||||
};
|
||||
return resolve(resp);
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
import sinon from 'sinon';
|
||||
|
||||
module('Integration | Component | transit key actions', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
run(() => {
|
||||
this.owner.unregister('service:store');
|
||||
this.owner.register('service:store', storeStub);
|
||||
this.storeService = this.owner.lookup('service:store');
|
||||
});
|
||||
});
|
||||
|
||||
test('it requires `key`', async function (assert) {
|
||||
const promise = waitForError();
|
||||
render(hbs`
|
||||
|
|
@ -141,120 +108,144 @@ module('Integration | Component | transit key actions', function (hooks) {
|
|||
});
|
||||
|
||||
async function doEncrypt(assert, actions = [], keyattrs = {}) {
|
||||
const keyDefaults = { backend: 'transit', id: 'akey', supportedActions: ['encrypt'].concat(actions) };
|
||||
const keyDefaults = {
|
||||
backend: 'transit',
|
||||
id: 'akey',
|
||||
supportedActions: ['encrypt'].concat(actions),
|
||||
};
|
||||
|
||||
const key = { ...keyDefaults, ...keyattrs };
|
||||
this.set('key', key);
|
||||
this.set('selectedAction', 'encrypt');
|
||||
this.set('storeService.keyActionReturnVal', { ciphertext: 'secret' });
|
||||
|
||||
this.apiStub = sinon.stub(this.owner.lookup('service:api').secrets, 'transitEncrypt').resolves({
|
||||
data: { ciphertext: 'secret' },
|
||||
});
|
||||
|
||||
await render(hbs`
|
||||
<TransitKeyActions @selectedAction={{this.selectedAction}} @key={{this.key}} />`);
|
||||
<TransitKeyActions
|
||||
@selectedAction={{this.selectedAction}}
|
||||
@key={{this.key}}
|
||||
/>
|
||||
`);
|
||||
|
||||
let editor;
|
||||
await waitFor('.cm-editor');
|
||||
editor = codemirror('#plaintext-control');
|
||||
setCodeEditorValue(editor, 'plaintext');
|
||||
|
||||
await click('button[type="submit"]');
|
||||
assert.deepEqual(
|
||||
this.storeService.callArgs,
|
||||
{
|
||||
action: 'encrypt',
|
||||
backend: 'transit',
|
||||
id: 'akey',
|
||||
payload: {
|
||||
plaintext: encodeString('plaintext'),
|
||||
},
|
||||
},
|
||||
'passes expected args to the adapter'
|
||||
|
||||
assert.true(this.apiStub.calledOnce, 'calls the API to encrypt');
|
||||
|
||||
assert.true(
|
||||
this.apiStub.calledWith('akey', 'transit', {
|
||||
plaintext: encodeString('plaintext'),
|
||||
}),
|
||||
'passes expected args to transitEncrypt'
|
||||
);
|
||||
|
||||
assert.strictEqual(find('[data-test-encrypted-value="ciphertext"]').innerText, 'secret');
|
||||
|
||||
// exit modal
|
||||
await click('dialog button');
|
||||
|
||||
// Encrypt again, with pre-encoded value and checkbox selected
|
||||
const preEncodedValue = encodeString('plaintext');
|
||||
|
||||
await waitFor('.cm-editor');
|
||||
editor = codemirror('#plaintext-control');
|
||||
setCodeEditorValue(editor, preEncodedValue);
|
||||
|
||||
await click('input[data-test-transit-input="encodedBase64"]');
|
||||
await click('button[type="submit"]');
|
||||
|
||||
assert.deepEqual(
|
||||
this.storeService.callArgs,
|
||||
{
|
||||
action: 'encrypt',
|
||||
backend: 'transit',
|
||||
id: 'akey',
|
||||
payload: {
|
||||
plaintext: preEncodedValue,
|
||||
},
|
||||
},
|
||||
'passes expected args to the adapter'
|
||||
assert.strictEqual(this.apiStub.callCount, 2, 'calls the API to encrypt again');
|
||||
|
||||
assert.true(
|
||||
this.apiStub.secondCall.calledWith('akey', 'transit', {
|
||||
plaintext: preEncodedValue,
|
||||
}),
|
||||
'passes pre-encoded value without re-encoding'
|
||||
);
|
||||
|
||||
await click('dialog button');
|
||||
}
|
||||
|
||||
test('it encrypts', doEncrypt);
|
||||
|
||||
test('it shows key version selection', async function (assert) {
|
||||
const keyDefaults = { backend: 'transit', id: 'akey', supportedActions: ['encrypt'].concat([]) };
|
||||
const keyattrs = { keysForEncryption: [3, 2, 1], latestVersion: 3 };
|
||||
const keyDefaults = {
|
||||
backend: 'transit',
|
||||
id: 'akey',
|
||||
supportedActions: ['encrypt'],
|
||||
};
|
||||
const keyattrs = {
|
||||
keysForEncryption: [3, 2, 1],
|
||||
latestVersion: 3,
|
||||
};
|
||||
|
||||
const key = { ...keyDefaults, ...keyattrs };
|
||||
this.set('key', key);
|
||||
this.set('storeService.keyActionReturnVal', { ciphertext: 'secret' });
|
||||
const encryptStub = sinon.stub(this.owner.lookup('service:api').secrets, 'transitEncrypt').resolves({
|
||||
data: { ciphertext: 'secret' },
|
||||
});
|
||||
|
||||
await render(hbs`
|
||||
<TransitKeyActions @selectedAction="encrypt" @key={{this.key}} />`);
|
||||
<TransitKeyActions
|
||||
@selectedAction="encrypt"
|
||||
@key={{this.key}}
|
||||
/>
|
||||
`);
|
||||
|
||||
await waitFor('.cm-editor');
|
||||
|
||||
const editor = codemirror();
|
||||
setCodeEditorValue(editor, 'plaintext');
|
||||
|
||||
assert.dom('#key_version').exists({ count: 1 }, 'it renders the key version selector');
|
||||
|
||||
await triggerEvent('#key_version', 'change');
|
||||
await click('button[type="submit"]');
|
||||
assert.deepEqual(
|
||||
this.storeService.callArgs,
|
||||
{
|
||||
action: 'encrypt',
|
||||
backend: 'transit',
|
||||
id: 'akey',
|
||||
payload: {
|
||||
plaintext: encodeString('plaintext'),
|
||||
key_version: '0',
|
||||
},
|
||||
},
|
||||
|
||||
assert.true(encryptStub.calledOnce, 'calls transitEncrypt');
|
||||
|
||||
assert.true(
|
||||
encryptStub.calledWith('akey', 'transit', {
|
||||
plaintext: encodeString('plaintext'),
|
||||
key_version: '0',
|
||||
}),
|
||||
'includes key_version in the payload'
|
||||
);
|
||||
});
|
||||
|
||||
test('it hides key version selection', async function (assert) {
|
||||
const keyDefaults = { backend: 'transit', id: 'akey', supportedActions: ['encrypt'].concat([]) };
|
||||
const keyDefaults = {
|
||||
backend: 'transit',
|
||||
id: 'akey',
|
||||
supportedActions: ['encrypt'],
|
||||
};
|
||||
|
||||
const keyattrs = { keysForEncryption: [1] };
|
||||
const key = { ...keyDefaults, ...keyattrs };
|
||||
|
||||
this.set('key', key);
|
||||
this.set('storeService.keyActionReturnVal', { ciphertext: 'secret' });
|
||||
|
||||
await render(hbs`
|
||||
<TransitKeyActions @selectedAction="encrypt" @key={{this.key}} />`);
|
||||
<TransitKeyActions
|
||||
@selectedAction="encrypt"
|
||||
@key={{this.key}}
|
||||
/>
|
||||
`);
|
||||
|
||||
await waitFor('.cm-editor');
|
||||
|
||||
const editor = codemirror('#plaintext-control');
|
||||
setCodeEditorValue(editor, 'plaintext');
|
||||
|
||||
assert.dom('#key_version').doesNotExist('it does not render the selector when there is only one key');
|
||||
});
|
||||
|
||||
test('it does not carry ciphertext value over to decrypt', async function (assert) {
|
||||
assert.expect(4);
|
||||
const plaintext = 'not so secret';
|
||||
await doEncrypt.call(this, assert, ['decrypt']);
|
||||
|
||||
this.set('storeService.keyActionReturnVal', { plaintext });
|
||||
this.set('selectedAction', 'decrypt');
|
||||
await waitFor('.cm-editor');
|
||||
const editor = codemirror('#ciphertext-control');
|
||||
assert.strictEqual(getCodeEditorValue(editor), '', 'does not prefill ciphertext value');
|
||||
});
|
||||
|
||||
const setupExport = async function () {
|
||||
this.set('key', {
|
||||
backend: 'transit',
|
||||
|
|
@ -268,23 +259,30 @@ module('Integration | Component | transit key actions', function (hooks) {
|
|||
};
|
||||
|
||||
test('it can export a key:default behavior', async function (assert) {
|
||||
this.set('storeService.rootKeyActionReturnVal', { wrap_info: { token: 'wrapped-token' } });
|
||||
const exportStub = sinon.stub(this.owner.lookup('service:api').secrets, 'transitExportKey').resolves({
|
||||
wrap_info: { token: 'wrapped-token' },
|
||||
});
|
||||
|
||||
await setupExport.call(this);
|
||||
await click('button[type="submit"]');
|
||||
|
||||
assert.true(exportStub.calledOnce);
|
||||
assert.deepEqual(
|
||||
this.storeService.callArgs,
|
||||
{
|
||||
action: 'export',
|
||||
backend: 'transit',
|
||||
id: 'akey',
|
||||
payload: {
|
||||
param: ['encryption'],
|
||||
exportStub.firstCall.args,
|
||||
[
|
||||
'akey',
|
||||
'encryption-key',
|
||||
'transit',
|
||||
{
|
||||
headers: {
|
||||
'X-Vault-Wrap-TTL': '30m',
|
||||
},
|
||||
wrapTTL: '30m',
|
||||
},
|
||||
},
|
||||
'passes expected args to the adapter'
|
||||
],
|
||||
'passes expected args to api service'
|
||||
);
|
||||
assert.strictEqual(this.storeService.callArgsOptions.wrapTTL, '30m', 'passes value for wrapTTL');
|
||||
|
||||
assert.strictEqual(
|
||||
find('[data-test-encrypted-value="export"]').innerText,
|
||||
'wrapped-token',
|
||||
|
|
@ -293,77 +291,112 @@ module('Integration | Component | transit key actions', function (hooks) {
|
|||
});
|
||||
|
||||
test('it can export a key:unwrapped behavior', async function (assert) {
|
||||
const response = { keys: { a: 'key' } };
|
||||
this.set('storeService.keyActionReturnVal', response);
|
||||
const response = {
|
||||
data: {
|
||||
keys: { a: 'key' },
|
||||
type: 'encryption',
|
||||
name: 'akey',
|
||||
},
|
||||
};
|
||||
sinon.stub(this.owner.lookup('service:api').secrets, 'transitExportKey').resolves(response);
|
||||
|
||||
await setupExport.call(this);
|
||||
await click('[data-test-toggle-label="Wrap response"]');
|
||||
await click(GENERAL.submitButton);
|
||||
|
||||
assert.dom('#transit-export-modal').exists('Modal opens after export');
|
||||
assert.deepEqual(
|
||||
JSON.parse(find('[data-test-encrypted-value="export"]').innerText),
|
||||
response,
|
||||
{
|
||||
keys: { a: 'key' },
|
||||
type: 'encryption',
|
||||
name: 'akey',
|
||||
},
|
||||
'prints json response'
|
||||
);
|
||||
});
|
||||
|
||||
test('it can export a key: unwrapped, single version', async function (assert) {
|
||||
const response = { keys: { a: 'key' } };
|
||||
this.set('storeService.keyActionReturnVal', response);
|
||||
const response = {
|
||||
data: {
|
||||
keys: { a: 'key' },
|
||||
type: 'encryption',
|
||||
name: 'akey',
|
||||
},
|
||||
};
|
||||
const exportVersionStub = sinon
|
||||
.stub(this.owner.lookup('service:api').secrets, 'transitExportKeyVersion')
|
||||
.resolves(response);
|
||||
|
||||
await setupExport.call(this);
|
||||
await click('[data-test-toggle-label="Wrap response"]');
|
||||
await click('#exportVersion');
|
||||
await triggerEvent('#exportVersion', 'change');
|
||||
await click(GENERAL.submitButton);
|
||||
|
||||
assert.dom('#transit-export-modal').exists('Modal opens after export');
|
||||
assert.deepEqual(
|
||||
JSON.parse(find('[data-test-encrypted-value="export"]').innerText),
|
||||
response,
|
||||
{
|
||||
keys: { a: 'key' },
|
||||
type: 'encryption',
|
||||
name: 'akey',
|
||||
},
|
||||
'prints json response'
|
||||
);
|
||||
|
||||
assert.true(exportVersionStub.calledOnce);
|
||||
assert.deepEqual(
|
||||
this.storeService.callArgs,
|
||||
{
|
||||
action: 'export',
|
||||
backend: 'transit',
|
||||
id: 'akey',
|
||||
payload: {
|
||||
param: ['encryption', 1],
|
||||
},
|
||||
},
|
||||
'passes expected args to the adapter'
|
||||
exportVersionStub.firstCall.args,
|
||||
['akey', 'encryption-key', 1, 'transit', {}],
|
||||
'passes expected args to api service'
|
||||
);
|
||||
});
|
||||
|
||||
test('it includes algorithm param for HMAC', async function (assert) {
|
||||
// Return mocked data so a11y-testing doesn't get mad about empty copy button contents
|
||||
this.set('storeService.rootKeyActionReturnVal', { data: { hmac: 'vault:v1:hmac-token' } });
|
||||
const hmacStub = sinon.stub(this.owner.lookup('service:api').secrets, 'transitGenerateHmac').resolves({
|
||||
data: {
|
||||
hmac: 'vault:v1:hmac-token',
|
||||
},
|
||||
});
|
||||
|
||||
this.set('key', {
|
||||
backend: 'transit',
|
||||
id: 'akey',
|
||||
supportedActions: ['hmac'],
|
||||
validKeyVersions: [1],
|
||||
});
|
||||
|
||||
await render(hbs`
|
||||
<TransitKeyActions @key={{this.key}} @selectedAction="hmac" />`);
|
||||
<TransitKeyActions
|
||||
@key={{this.key}}
|
||||
@selectedAction="hmac"
|
||||
/>
|
||||
`);
|
||||
|
||||
await fillIn('#algorithm', 'sha2-384');
|
||||
await blur('#algorithm');
|
||||
|
||||
await waitFor('.cm-editor');
|
||||
const editor = codemirror();
|
||||
setCodeEditorValue(editor, 'plaintext');
|
||||
|
||||
await click('input[data-test-transit-input="encodedBase64"]');
|
||||
await click(GENERAL.submitButton);
|
||||
|
||||
assert.true(hmacStub.calledOnce, 'calls transitGenerateHmac');
|
||||
|
||||
assert.deepEqual(
|
||||
this.storeService.callArgs,
|
||||
{
|
||||
action: 'hmac',
|
||||
backend: 'transit',
|
||||
id: 'akey',
|
||||
payload: {
|
||||
hmacStub.firstCall.args,
|
||||
[
|
||||
'akey',
|
||||
'transit',
|
||||
{
|
||||
algorithm: 'sha2-384',
|
||||
input: 'plaintext',
|
||||
},
|
||||
},
|
||||
'passes expected args to the adapter'
|
||||
],
|
||||
'passes expected args to the API'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue