From b00d9660541a25b8d7d6f4cc20bb43a8b5c7b2ed Mon Sep 17 00:00:00 2001 From: Chelsea Shaw <82459713+hashishaw@users.noreply.github.com> Date: Wed, 9 Feb 2022 10:56:49 -0600 Subject: [PATCH] UI/transit auto rotate interval (#13970) * Add format-ttl helper * Add autoRotateInterval to model and serializer for transit key * Add goSafeTimeString to object returned from TtlPicker2 component * Add auto rotate interval to transit key components * clean up unit calculator on ttl-picker, with tests * Fix tests, cleanup * Add changelog --- changelog/13970.txt | 3 + ui/app/components/transit-edit.js | 10 +++ ui/app/helpers/format-ttl.js | 22 +++++ ui/app/models/transit-key.js | 6 ++ ui/app/serializers/transit-key.js | 2 + ui/app/templates/components/transit-edit.hbs | 4 + .../components/transit-form-create.hbs | 11 +++ .../components/transit-form-edit.hbs | 11 +++ .../components/transit-form-show.hbs | 4 + ui/lib/core/addon/components/ttl-picker2.js | 27 ++++-- .../components/ttl-picker2-test.js | 83 ++++++++++++------- .../integration/helpers/format-ttl-test.js | 67 +++++++++++++++ 12 files changed, 213 insertions(+), 37 deletions(-) create mode 100644 changelog/13970.txt create mode 100644 ui/app/helpers/format-ttl.js create mode 100644 ui/tests/integration/helpers/format-ttl-test.js diff --git a/changelog/13970.txt b/changelog/13970.txt new file mode 100644 index 000000000..0c15ae07d --- /dev/null +++ b/changelog/13970.txt @@ -0,0 +1,3 @@ +```release-note:feature +ui: Add support for time-based key autorotation in transit secrets engine. +``` \ No newline at end of file diff --git a/ui/app/components/transit-edit.js b/ui/app/components/transit-edit.js index a317dacdc..a24712bf1 100644 --- a/ui/app/components/transit-edit.js +++ b/ui/app/components/transit-edit.js @@ -18,6 +18,7 @@ export default Component.extend(FocusOnInsertMixin, { onDataChange() {}, onRefresh() {}, key: null, + autoRotateInvalid: false, requestInFlight: or('key.isLoading', 'key.isReloading', 'key.isSaving'), willDestroyElement() { @@ -92,6 +93,15 @@ export default Component.extend(FocusOnInsertMixin, { set(this.key, key, event.target.checked); }, + handleAutoRotateChange(ttlObj) { + if (ttlObj.enabled) { + set(this.key, 'autoRotateInterval', ttlObj.goSafeTimeString); + this.set('autoRotateInvalid', ttlObj.seconds < 3600); + } else { + set(this.key, 'autoRotateInterval', 0); + } + }, + derivedChange(val) { this.key.setDerived(val); }, diff --git a/ui/app/helpers/format-ttl.js b/ui/app/helpers/format-ttl.js new file mode 100644 index 000000000..8f0dd47ca --- /dev/null +++ b/ui/app/helpers/format-ttl.js @@ -0,0 +1,22 @@ +import { helper } from '@ember/component/helper'; + +export default helper(function formatTtl([timestring], { removeZero = false }) { + // Expects a number followed by one of s, m, or h + // eg. 40m or 1h20m0s + let matches = timestring?.match(/([0-9]+[h|m|s])/g); + if (!matches) { + return timestring; + } + return matches + .map((set) => { + // eslint-disable-next-line no-unused-vars + let [_, number, unit] = set.match(/([0-9]+)(h|m|s)/); + if (removeZero && number === '0') { + return null; + } + const word = { h: 'hour', m: 'minute', s: 'second' }[unit]; + return `${number} ${number === '1' ? word : word + 's'}`; + }) + .filter((s) => null !== s) + .join(' '); +}); diff --git a/ui/app/models/transit-key.js b/ui/app/models/transit-key.js index a89bf1925..048646843 100644 --- a/ui/app/models/transit-key.js +++ b/ui/app/models/transit-key.js @@ -56,6 +56,12 @@ export default Model.extend({ fieldValue: 'id', readOnly: true, }), + autoRotateInterval: attr({ + defaultValue: '0', + defaultShown: 'Key is not automatically rotated', + editType: 'ttl', + label: 'Auto-rotation interval', + }), deletionAllowed: attr('boolean'), derived: attr('boolean'), exportable: attr('boolean'), diff --git a/ui/app/serializers/transit-key.js b/ui/app/serializers/transit-key.js index d044e69a7..fec0f28f0 100644 --- a/ui/app/serializers/transit-key.js +++ b/ui/app/serializers/transit-key.js @@ -50,10 +50,12 @@ export default RESTSerializer.extend({ const min_decryption_version = snapshot.attr('minDecryptionVersion'); const min_encryption_version = snapshot.attr('minEncryptionVersion'); const deletion_allowed = snapshot.attr('deletionAllowed'); + const auto_rotate_interval = snapshot.attr('autoRotateInterval'); return { min_decryption_version, min_encryption_version, deletion_allowed, + auto_rotate_interval, }; } else { snapshot.id = snapshot.attr('name'); diff --git a/ui/app/templates/components/transit-edit.hbs b/ui/app/templates/components/transit-edit.hbs index da9389d95..bc3dba93d 100644 --- a/ui/app/templates/components/transit-edit.hbs +++ b/ui/app/templates/components/transit-edit.hbs @@ -25,6 +25,8 @@ Name +
+ +
diff --git a/ui/app/templates/components/transit-form-edit.hbs b/ui/app/templates/components/transit-form-edit.hbs index c6d5b1abb..dae5a0a23 100644 --- a/ui/app/templates/components/transit-form-edit.hbs +++ b/ui/app/templates/components/transit-form-edit.hbs @@ -16,6 +16,17 @@
+
+ +
diff --git a/ui/app/templates/components/transit-form-show.hbs b/ui/app/templates/components/transit-form-show.hbs index d163c98ed..09d94fef7 100644 --- a/ui/app/templates/components/transit-form-show.hbs +++ b/ui/app/templates/components/transit-form-show.hbs @@ -170,6 +170,10 @@ {{/if}} {{else}} + {{#if @key.derived}} diff --git a/ui/lib/core/addon/components/ttl-picker2.js b/ui/lib/core/addon/components/ttl-picker2.js index e85074ecb..48699d6d7 100644 --- a/ui/lib/core/addon/components/ttl-picker2.js +++ b/ui/lib/core/addon/components/ttl-picker2.js @@ -8,7 +8,7 @@ * ```js * * ``` - * @param onChange {Function} - This function will be passed a TTL object, which includes enabled{bool}, seconds{number}, timeString{string}. + * @param onChange {Function} - This function will be passed a TTL object, which includes enabled{bool}, seconds{number}, timeString{string}, goSafeTimeString{string}. * @param label="Time to live (TTL)" {String} - Label is the main label that lives next to the toggle. * @param helperTextDisabled="Allow tokens to be used indefinitely" {String} - This helper text is shown under the label when the toggle is switched off * @param helperTextEnabled="Disable the use of the token after" {String} - This helper text is shown under the label when the toggle is switched on @@ -33,10 +33,14 @@ const secondsMap = { h: 3600, d: 86400, }; -const validUnits = ['s', 'm', 'h', 'd']; const convertFromSeconds = (seconds, unit) => { return seconds / secondsMap[unit]; }; +const goSafeConvertFromSeconds = (seconds, unit) => { + // Go only accepts s, m, or h units + let u = unit === 'd' ? 'h' : unit; + return convertFromSeconds(seconds, u) + u; +}; export default TtlForm.extend({ layout, @@ -74,12 +78,18 @@ export default TtlForm.extend({ } else { try { const seconds = Duration.parse(value).seconds(); - const lastDigit = value.toString().substring(value.length - 1); - if (validUnits.indexOf(lastDigit) >= 0 && lastDigit !== 's') { - time = convertFromSeconds(seconds, lastDigit); - unit = lastDigit; - } else { - time = seconds; + time = seconds; + // get largest unit with no remainder + if (seconds % secondsMap.d === 0) { + unit = 'd'; + } else if (seconds % secondsMap.h === 0) { + unit = 'h'; + } else if (seconds % secondsMap.m === 0) { + unit = 'm'; + } + + if (unit !== 's') { + time = convertFromSeconds(seconds, unit); } } catch (e) { // if parsing fails leave as default 30s @@ -111,6 +121,7 @@ export default TtlForm.extend({ enabled: enableTTL, seconds, timeString: time + unit, + goSafeTimeString: goSafeConvertFromSeconds(seconds, unit), }; this.onChange(ttl); }, diff --git a/ui/tests/integration/components/ttl-picker2-test.js b/ui/tests/integration/components/ttl-picker2-test.js index 33f2127ff..11ff4f85a 100644 --- a/ui/tests/integration/components/ttl-picker2-test.js +++ b/ui/tests/integration/components/ttl-picker2-test.js @@ -7,9 +7,11 @@ import hbs from 'htmlbars-inline-precompile'; module('Integration | Component | ttl-picker2', function (hooks) { setupRenderingTest(hooks); + hooks.beforeEach(function () { + this.set('onChange', sinon.spy()); + }); + test('it renders time and unit inputs when TTL enabled', async function (assert) { - let changeSpy = sinon.spy(); - this.set('onChange', changeSpy); await render(hbs` `); await click('[data-test-toggle-input="clicktest"]'); - assert.ok(changeSpy.calledOnce, 'it calls the passed onChange'); + assert.ok(this.onChange.calledOnce, 'it calls the passed onChange'); assert.ok( - changeSpy.calledWith({ + this.onChange.calledWith({ enabled: true, seconds: 600, timeString: '10m', + goSafeTimeString: '10m', }), 'Passes the default values back to onChange' ); }); test('it keeps seconds value when unit is changed', async function (assert) { - let changeSpy = sinon.spy(); - this.set('onChange', changeSpy); await render(hbs` `); await click('[data-test-toggle-input="clicktest"]'); - assert.ok(changeSpy.calledOnce, 'it calls the passed onChange'); + assert.ok(this.onChange.calledOnce, 'it calls the passed onChange'); assert.ok( - changeSpy.calledWith({ + this.onChange.calledWith({ enabled: true, seconds: 360, timeString: '360s', + goSafeTimeString: '360s', }), 'Changes enabled to true on click' ); await fillIn('[data-test-select="ttl-unit"]', 'm'); assert.ok( - changeSpy.calledWith({ + this.onChange.calledWith({ enabled: true, seconds: 360, timeString: '6m', + goSafeTimeString: '6m', }), 'Units and time update without changing seconds value' ); @@ -93,8 +92,6 @@ module('Integration | Component | ttl-picker2', function (hooks) { }); test('it recalculates seconds when unit is changed and recalculateSeconds is on', async function (assert) { - let changeSpy = sinon.spy(); - this.set('onChange', changeSpy); await render(hbs` + `); + await click('[data-test-toggle-input="clicktest"]'); + assert.ok(this.onChange.calledOnce, 'it calls the passed onChange'); + assert.ok( + this.onChange.calledWith({ + enabled: true, + seconds: 172800, + timeString: '2d', + goSafeTimeString: '48h', + }), + 'Converts day unit to go safe time' + ); + }); + + test('it calls onChange on init when rendered if changeOnInit is true', async function (assert) { await render(hbs` `); assert.ok( - changeSpy.calledWith({ + this.onChange.calledWith({ enabled: true, seconds: 6000, timeString: '100m', + goSafeTimeString: '100m', }), 'Seconds value is recalculated based on time and unit' ); - assert.ok(changeSpy.calledOnce, 'it calls the passed onChange after render'); + assert.ok(this.onChange.calledOnce, 'it calls the passed onChange after render'); + }); + + test('it converts to the largest round unit on init', async function (assert) { + await render(hbs` + + `); + assert.dom('[data-test-ttl-value]').hasValue('1000', 'time value is converted'); + assert.dom('[data-test-select="ttl-unit"]').hasValue('m', 'unit value is m (minutes)'); }); }); diff --git a/ui/tests/integration/helpers/format-ttl-test.js b/ui/tests/integration/helpers/format-ttl-test.js new file mode 100644 index 000000000..14aa7fed9 --- /dev/null +++ b/ui/tests/integration/helpers/format-ttl-test.js @@ -0,0 +1,67 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Helper | format-ttl', function (hooks) { + setupRenderingTest(hooks); + + test('it renders the input if no match found', async function (assert) { + this.set('inputValue', '1234'); + + await render(hbs`{{format-ttl inputValue}}`); + + assert.equal(this.element.textContent.trim(), '1234'); + }); + + test('it parses hours correctly', async function (assert) { + this.set('inputValue', '12h'); + + await render(hbs`{{format-ttl inputValue}}`); + assert.equal(this.element.textContent.trim(), '12 hours'); + + this.set('inputValue', '1h'); + assert.equal(this.element.textContent.trim(), '1 hour'); + }); + + test('it parses minutes correctly', async function (assert) { + this.set('inputValue', '30m'); + + await render(hbs`{{format-ttl inputValue}}`); + assert.equal(this.element.textContent.trim(), '30 minutes'); + + this.set('inputValue', '1m'); + assert.equal(this.element.textContent.trim(), '1 minute'); + }); + + test('it parses seconds correctly', async function (assert) { + this.set('inputValue', '45s'); + + await render(hbs`{{format-ttl inputValue}}`); + assert.equal(this.element.textContent.trim(), '45 seconds'); + + this.set('inputValue', '1s'); + assert.equal(this.element.textContent.trim(), '1 second'); + }); + + test('it parses multiple matches correctly', async function (assert) { + this.set('inputValue', '1h30m0s'); + + await render(hbs`{{format-ttl inputValue}}`); + assert.equal(this.element.textContent.trim(), '1 hour 30 minutes 0 seconds'); + }); + + test('it removes 0 values if removeZero true', async function (assert) { + this.set('inputValue', '1h30m0s'); + + await render(hbs`{{format-ttl inputValue removeZero=true}}`); + assert.equal(this.element.textContent.trim(), '1 hour 30 minutes'); + }); + + test('returns empty string if all values 0 and removeZero true', async function (assert) { + this.set('inputValue', '0h0m0s'); + + await render(hbs`{{format-ttl inputValue removeZero=true}}`); + assert.equal(this.element.textContent.trim(), ''); + }); +});