diff --git a/ui/app/components/transit-form-show.js b/ui/app/components/transit-form-show.js index bde37c9276..17e8e83171 100644 --- a/ui/app/components/transit-form-show.js +++ b/ui/app/components/transit-form-show.js @@ -6,14 +6,18 @@ 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'); + export default class TransitFormShow extends Component { @service store; @service router; @service flashMessages; @action async rotateKey() { + const waiterToken = waiter.beginAsync(); const { backend, id } = this.args.key; try { await this.store.adapterFor('transit-key').keyAction('rotate', { backend, id }); @@ -22,6 +26,8 @@ export default class TransitFormShow extends Component { await this.router.refresh(); } catch (e) { this.flashMessages.danger(errorMessage(e)); + } finally { + waiter.endAsync(waiterToken); } } } diff --git a/ui/app/models/transit-key.js b/ui/app/models/transit-key.js index e304883e79..8cd8b1e4b9 100644 --- a/ui/app/models/transit-key.js +++ b/ui/app/models/transit-key.js @@ -4,102 +4,113 @@ */ import Model, { attr } from '@ember-data/model'; -import { alias } from '@ember/object/computed'; -import { set, get, computed } from '@ember/object'; +import { set, get } from '@ember/object'; import clamp from 'vault/utils/clamp'; import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities'; const ACTION_VALUES = { encrypt: { isSupported: 'supportsEncryption', - description: 'Looks up wrapping properties for the given token', + description: 'Looks up wrapping properties for the given token.', glyph: 'lock-fill', }, decrypt: { isSupported: 'supportsDecryption', - description: 'Decrypts the provided ciphertext using this key', + description: 'Decrypts the provided ciphertext using this key.', glyph: 'mail-open', }, datakey: { isSupported: 'supportsEncryption', - description: 'Generates a new key and value encrypted with this key', + description: 'Generates a new key and value encrypted with this key.', glyph: 'key', }, rewrap: { isSupported: 'supportsEncryption', - description: 'Rewraps the ciphertext using the latest version of the named key', + description: 'Rewraps the ciphertext using the latest version of the named key.', glyph: 'reload', }, sign: { isSupported: 'supportsSigning', - description: 'Get the cryptographic signature of the given data', + description: 'Get the cryptographic signature of the given data.', glyph: 'pencil-tool', }, hmac: { isSupported: true, - description: 'Generate a data digest using a hash algorithm', + description: 'Generate a data digest using a hash algorithm.', glyph: 'shuffle', }, verify: { isSupported: true, - description: 'Validate the provided signature for the given data', + description: 'Validate the provided signature for the given data.', glyph: 'check-circle', }, export: { isSupported: 'exportable', - description: 'Get the named key', + description: 'Get the named key.', glyph: 'external-link', }, }; -export default Model.extend({ - type: attr('string', { +export default class TransitKeyModel extends Model { + @attr('string') backend; + @attr('string', { defaultValue: 'aes256-gcm96', - }), - name: attr('string', { + }) + type; + + @attr('string', { label: 'Name', readOnly: true, - }), - autoRotatePeriod: attr({ + }) + name; + + @attr({ defaultValue: '0', defaultShown: 'Key is not automatically rotated', editType: 'ttl', label: 'Auto-rotation period', - }), - deletionAllowed: attr('boolean'), - derived: attr('boolean'), - exportable: attr('boolean'), - minDecryptionVersion: attr('number', { - defaultValue: 1, - }), - minEncryptionVersion: attr('number', { - defaultValue: 0, - }), - latestVersion: attr('number'), - keys: attr('object'), - convergentEncryption: attr('boolean'), - convergentEncryptionVersion: attr('number'), + }) + autoRotatePeriod; - supportsSigning: attr('boolean'), - supportsEncryption: attr('boolean'), - supportsDecryption: attr('boolean'), - supportsDerivation: attr('boolean'), + @attr('boolean') deletionAllowed; + @attr('boolean') derived; + @attr('boolean') exportable; + + @attr('number', { + defaultValue: 1, + }) + minDecryptionVersion; + + @attr('number', { + defaultValue: 0, + }) + minEncryptionVersion; + + @attr('number') latestVersion; + @attr('object') keys; + @attr('boolean') convergentEncryption; + @attr('number') convergentEncryptionVersion; + + @attr('boolean') supportsSigning; + @attr('boolean') supportsEncryption; + @attr('boolean') supportsDecryption; + @attr('boolean') supportsDerivation; setConvergentEncryption(val) { if (val === true) { set(this, 'derived', val); } set(this, 'convergentEncryption', val); - }, + } setDerived(val) { if (val === false) { set(this, 'convergentEncryption', val); } set(this, 'derived', val); - }, + } - supportedActions: computed('type', function () { + get supportedActions() { return Object.keys(ACTION_VALUES) .filter((name) => { const { isSupported } = ACTION_VALUES[name]; @@ -109,14 +120,14 @@ export default Model.extend({ const { description, glyph } = ACTION_VALUES[name]; return { name, description, glyph }; }); - }), + } - canDelete: computed('deletionAllowed', 'lastLoadTS', function () { + get canDelete() { const deleteAttrChanged = Boolean(this.changedAttributes().deletionAllowed); return this.deletionAllowed && deleteAttrChanged === false; - }), + } - keyVersions: computed('validKeyVersions', function () { + get keyVersions() { let maxVersion = Math.max(...this.validKeyVersions); const versions = []; while (maxVersion > 0) { @@ -124,25 +135,19 @@ export default Model.extend({ maxVersion--; } return versions; - }), + } - encryptionKeyVersions: computed( - 'keyVerisons', - 'keyVersions', - 'latestVersion', - 'minDecryptionVersion', - function () { - const { keyVersions, minDecryptionVersion } = this; + get encryptionKeyVersions() { + const { keyVersions, minDecryptionVersion } = this; - return keyVersions - .filter((version) => { - return version >= minDecryptionVersion; - }) - .reverse(); - } - ), + return keyVersions + .filter((version) => { + return version >= minDecryptionVersion; + }) + .reverse(); + } - keysForEncryption: computed('minEncryptionVersion', 'latestVersion', function () { + get keysForEncryption() { let { minEncryptionVersion, latestVersion } = this; const minVersion = clamp(minEncryptionVersion - 1, 0, latestVersion); const versions = []; @@ -151,13 +156,13 @@ export default Model.extend({ latestVersion--; } return versions; - }), + } - validKeyVersions: computed('keys', function () { + get validKeyVersions() { return Object.keys(this.keys); - }), + } - exportKeyTypes: computed('exportable', 'supportsEncryption', 'supportsSigning', 'type', function () { + get exportKeyTypes() { const types = ['hmac']; if (this.supportsSigning) { types.unshift('signing'); @@ -166,13 +171,17 @@ export default Model.extend({ types.unshift('encryption'); } return types; - }), + } + @lazyCapabilities(apiPath`${'backend'}/keys/${'id'}/rotate`, 'backend', 'id') rotatePath; + @lazyCapabilities(apiPath`${'backend'}/keys/${'id'}`, 'backend', 'id') secretPath; - backend: attr('string'), - - rotatePath: lazyCapabilities(apiPath`${'backend'}/keys/${'id'}/rotate`, 'backend', 'id'), - canRotate: alias('rotatePath.canUpdate'), - secretPath: lazyCapabilities(apiPath`${'backend'}/keys/${'id'}`, 'backend', 'id'), - canRead: alias('secretPath.canUpdate'), - canEdit: alias('secretPath.canUpdate'), -}); + get canRotate() { + return this.rotatePath.get('canUpdate') !== false; + } + get canRead() { + return this.secretPath.get('canUpdate') !== false; + } + get canEdit() { + return this.secretPath.get('canUpdate') !== false; + } +} diff --git a/ui/tests/acceptance/transit-test.js b/ui/tests/acceptance/transit-test.js index 4e1f51f423..ec66f2463e 100644 --- a/ui/tests/acceptance/transit-test.js +++ b/ui/tests/acceptance/transit-test.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { click, fillIn, find, currentURL, settled, visit, waitUntil, findAll } from '@ember/test-helpers'; +import { click, fillIn, find, currentURL, settled, visit, findAll } from '@ember/test-helpers'; import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; import { v4 as uuidv4 } from 'uuid'; @@ -345,17 +345,14 @@ 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); - // wait for rotate call - await waitUntil(() => find(SELECTORS.versionRow(2))); + assert.dom(SELECTORS.versionRow(2)).exists('two key versions after rotate'); // navigate back to actions tab await click(SELECTORS.actionsTab); - await waitUntil(() => find(SELECTORS.card('encrypt'))); assert.dom(SELECTORS.card('encrypt')).exists(`renders encrypt action card for ${name}`); await click(SELECTORS.card('encrypt')); assert @@ -367,11 +364,6 @@ module('Acceptance | transit (flaky)', function (hooks) { await testConvergentEncryption(assert, name); }); - /* - OLD FLAKY TESTS (skipped) - It's been a while since we've updated the transit engine - keeping these tests to run locally the next time we touch that secret engine - */ const KEY_TYPE_COMBINATIONS = [ { name: (uid) => `aes-${uid}`, @@ -459,7 +451,7 @@ module('Acceptance | transit (flaky)', function (hooks) { ]; for (const key of KEY_TYPE_COMBINATIONS) { - test.skip(`transit backend: ${key.type}`, async function (assert) { + test(`transit backend: ${key.type}`, async function (assert) { assert.expect(key.convergent ? 43 : 7); const name = await this.generateTransitKey(key); await visit(`vault/secrets/${this.path}/show/${name}`); @@ -473,12 +465,9 @@ module('Acceptance | transit (flaky)', function (hooks) { // wait for capabilities assert.dom('[data-test-transit-version]').exists({ count: 1 }, `${name}: only one key version`); - await waitUntil(() => find(SELECTORS.rotate.trigger)); await click(SELECTORS.rotate.trigger); await click(SELECTORS.rotate.confirm); - // wait for rotate call - await waitUntil(() => findAll('[data-test-transit-version]').length >= 2); assert .dom('[data-test-transit-version]') .exists({ count: 2 }, `${name}: two key versions after rotate`); @@ -491,7 +480,6 @@ module('Acceptance | transit (flaky)', function (hooks) { ); const keyAction = key.supportsEncryption ? 'encrypt' : 'sign'; - await waitUntil(() => find(`[data-test-transit-action-title=${keyAction}]`)); assert .dom(`[data-test-transit-action-title=${keyAction}]`)