diff --git a/changelog/_14430.txt b/changelog/_14430.txt new file mode 100644 index 0000000000..a69ac0a8b3 --- /dev/null +++ b/changelog/_14430.txt @@ -0,0 +1,3 @@ +```release-note:bug +ui: Update KV max_version validation to disallow negative values. +``` diff --git a/changelog/_14445.txt b/changelog/_14445.txt new file mode 100644 index 0000000000..ef4c67d3ed --- /dev/null +++ b/changelog/_14445.txt @@ -0,0 +1,3 @@ +```release-note:bug +ui: Fix secrets to secrets-engines redirect for bookmarked URLs. +``` \ No newline at end of file diff --git a/ui/app/forms/secrets/engine.ts b/ui/app/forms/secrets/engine.ts index a1dcb4f48d..24b3814cee 100644 --- a/ui/app/forms/secrets/engine.ts +++ b/ui/app/forms/secrets/engine.ts @@ -20,8 +20,12 @@ export default class SecretsEngineForm extends MountForm // path validation is already defined on the MountForm class // add validation for kv max versions this.validations['kv_config.max_versions'] = [ - { type: 'number', message: 'Maximum versions must be a number.' }, + { type: 'number', options: { min: 0 }, message: 'Maximum versions must be a non-negative number.' }, { type: 'length', options: { min: 1, max: 16 }, message: 'You cannot go over 16 characters.' }, + { + validator: (data: SecretsEngineFormData) => !data?.kv_config?.max_versions?.toString().includes('.'), + message: 'Maximum versions must be a whole number.', + }, ]; // add validation for plugin_version when mounting external plugins this.validations['config.plugin_version'] = [ diff --git a/ui/app/forms/secrets/kv.ts b/ui/app/forms/secrets/kv.ts index b7d241b03f..429a35a8d0 100644 --- a/ui/app/forms/secrets/kv.ts +++ b/ui/app/forms/secrets/kv.ts @@ -43,8 +43,12 @@ export default class KvForm extends Form { }, ], max_versions: [ - { type: 'number', message: 'Maximum versions must be a number.' }, + { type: 'number', options: { min: 0 }, message: 'Maximum versions must be a non-negative number.' }, { type: 'length', options: { min: 1, max: 16 }, message: 'You cannot go over 16 characters.' }, + { + validator: ({ max_versions }: KvForm['data']) => !max_versions?.toString().includes('.'), + message: 'Maximum versions must be a whole number.', + }, ], }; diff --git a/ui/app/router.js b/ui/app/router.js index 7ac7838656..af00bd8a72 100644 --- a/ui/app/router.js +++ b/ui/app/router.js @@ -168,6 +168,7 @@ Router.map(function () { }); }); this.route('secrets-redirect', { path: '/secrets' }); // legacy redirect + this.route('secrets-redirect-with-path', { path: '/secrets/*path' }); // legacy redirect with wildcard to capture full path this.route('secrets', { path: '/secrets-engines' }, function () { this.route('enable', function () { this.route('create', { path: '/:mount_type' }); diff --git a/ui/app/routes/vault/cluster/secrets-redirect-with-path.js b/ui/app/routes/vault/cluster/secrets-redirect-with-path.js new file mode 100644 index 0000000000..ad4a1b3b12 --- /dev/null +++ b/ui/app/routes/vault/cluster/secrets-redirect-with-path.js @@ -0,0 +1,27 @@ +/** + * Copyright IBM Corp. 2016, 2025 + * SPDX-License-Identifier: BUSL-1.1 + */ +import Route from '@ember/routing/route'; +import { service } from '@ember/service'; + +export default class SecretsRedirectWithPathRoute extends Route { + @service router; + + beforeModel(transition) { + // Redirect to secrets page under /secrets-engines + // if the user navigates to the legacy path /secrets + // Preserve the full path after /secrets (e.g., /secrets/kv/kv/list -> /secrets-engines/kv/kv/list) + const params = transition.to.params; + const path = params?.path; + + if (path) { + // Construct the new URL with full path including /vault/secrets-engines/*path + const newUrl = `/vault/secrets-engines/${path}`; + this.router.replaceWith(newUrl); + } else { + // If no path, just redirect to the base secrets page + this.router.replaceWith('vault.cluster.secrets'); + } + } +} diff --git a/ui/app/utils/forms/validators.js b/ui/app/utils/forms/validators.js index 4c37c51796..6be134cbf6 100644 --- a/ui/app/utils/forms/validators.js +++ b/ui/app/utils/forms/validators.js @@ -26,10 +26,12 @@ export const length = (value, { nullable = false, min, max } = {}) => { return nullable; }; -export const number = (value, { nullable = false } = {}) => { +export const number = (value, { nullable = false, min, max } = {}) => { // since 0 is falsy, !value returns true even though 0 is a valid number if (!value && value !== 0) return nullable; - return !isNaN(value); + const underMin = isPresent(min) ? value < min : false; + const overMax = isPresent(max) ? value > max : false; + return !isNaN(value) && !underMin && !overMax; }; export const containsWhiteSpace = (value) => { diff --git a/ui/tests/acceptance/cluster-test.js b/ui/tests/acceptance/cluster-test.js index d01e8e64bd..84f103953a 100644 --- a/ui/tests/acceptance/cluster-test.js +++ b/ui/tests/acceptance/cluster-test.js @@ -10,7 +10,7 @@ import { v4 as uuidv4 } from 'uuid'; import { login, loginMethod, logout } from 'vault/tests/helpers/auth/auth-helpers'; import { mountBackend } from 'vault/tests/helpers/components/mount-backend-form-helpers'; -import { runCmd } from 'vault/tests/helpers/commands'; +import { mountEngineCmd, runCmd, deleteEngineCmd } from 'vault/tests/helpers/commands'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; const tokenWithPolicy = async function (name, policy) { @@ -120,4 +120,15 @@ module('Acceptance | cluster', function (hooks) { 'Navigating to /secrets redirects to /secrets-engines' ); }); + + test('redirects to /secrets-engines/kv/kv/list from legacy /secrets/kv/kv/list path', async function (assert) { + await runCmd(mountEngineCmd('kv', this.backend), false); + await visit('/vault/secrets/kv/kv/list'); + assert.strictEqual( + currentURL(), + '/vault/secrets-engines/kv/kv/list', + 'Navigating to /secrets redirects to /secrets-engines' + ); + await runCmd(deleteEngineCmd('kv')); + }); }); diff --git a/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-create-test.js b/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-create-test.js index b8963d7c0f..46fe32d4ef 100644 --- a/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-create-test.js +++ b/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-create-test.js @@ -180,10 +180,17 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook // max_versions validation await fillIn(FORM.inputByAttr('max_versions'), 'seven'); await click(FORM.saveBtn); - assert.dom(GENERAL.validationErrorByAttr('max_versions')).hasText('Maximum versions must be a number.'); + assert + .dom(GENERAL.validationErrorByAttr('max_versions')) + .hasText('Maximum versions must be a non-negative number.'); await fillIn(FORM.inputByAttr('max_versions'), '99999999999999999'); await click(FORM.saveBtn); assert.dom(GENERAL.validationErrorByAttr('max_versions')).hasText('You cannot go over 16 characters.'); + await fillIn(FORM.inputByAttr('max_versions'), '1.23'); + await click(FORM.saveBtn); + assert + .dom(GENERAL.validationErrorByAttr('max_versions')) + .hasText('Maximum versions must be a whole number.'); await fillIn(FORM.inputByAttr('max_versions'), '7'); // Fill in other metadata @@ -436,10 +443,17 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook // max_versions validation await fillIn(FORM.inputByAttr('max_versions'), 'seven'); await click(FORM.saveBtn); - assert.dom(GENERAL.validationErrorByAttr('max_versions')).hasText('Maximum versions must be a number.'); + assert + .dom(GENERAL.validationErrorByAttr('max_versions')) + .hasText('Maximum versions must be a non-negative number.'); await fillIn(FORM.inputByAttr('max_versions'), '99999999999999999'); await click(FORM.saveBtn); assert.dom(GENERAL.validationErrorByAttr('max_versions')).hasText('You cannot go over 16 characters.'); + await fillIn(FORM.inputByAttr('max_versions'), '1.23'); + await click(FORM.saveBtn); + assert + .dom(GENERAL.validationErrorByAttr('max_versions')) + .hasText('Maximum versions must be a whole number.'); await fillIn(FORM.inputByAttr('max_versions'), '7'); // Fill in other metadata @@ -583,10 +597,17 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook // max_versions validation await fillIn(FORM.inputByAttr('max_versions'), 'seven'); await click(FORM.saveBtn); - assert.dom(GENERAL.validationErrorByAttr('max_versions')).hasText('Maximum versions must be a number.'); + assert + .dom(GENERAL.validationErrorByAttr('max_versions')) + .hasText('Maximum versions must be a non-negative number.'); await fillIn(FORM.inputByAttr('max_versions'), '99999999999999999'); await click(FORM.saveBtn); assert.dom(GENERAL.validationErrorByAttr('max_versions')).hasText('You cannot go over 16 characters.'); + await fillIn(FORM.inputByAttr('max_versions'), '1.23'); + await click(FORM.saveBtn); + assert + .dom(GENERAL.validationErrorByAttr('max_versions')) + .hasText('Maximum versions must be a whole number.'); await fillIn(FORM.inputByAttr('max_versions'), '7'); // Fill in other metadata @@ -750,10 +771,17 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook // max_versions validation await fillIn(FORM.inputByAttr('max_versions'), 'seven'); await click(FORM.saveBtn); - assert.dom(GENERAL.validationErrorByAttr('max_versions')).hasText('Maximum versions must be a number.'); + assert + .dom(GENERAL.validationErrorByAttr('max_versions')) + .hasText('Maximum versions must be a non-negative number.'); await fillIn(FORM.inputByAttr('max_versions'), '99999999999999999'); await click(FORM.saveBtn); assert.dom(GENERAL.validationErrorByAttr('max_versions')).hasText('You cannot go over 16 characters.'); + await fillIn(FORM.inputByAttr('max_versions'), '1.23'); + await click(FORM.saveBtn); + assert + .dom(GENERAL.validationErrorByAttr('max_versions')) + .hasText('Maximum versions must be a whole number.'); await fillIn(FORM.inputByAttr('max_versions'), '7'); // Fill in other metadata @@ -976,10 +1004,17 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook // max_versions validation await fillIn(FORM.inputByAttr('max_versions'), 'seven'); await click(FORM.saveBtn); - assert.dom(GENERAL.validationErrorByAttr('max_versions')).hasText('Maximum versions must be a number.'); + assert + .dom(GENERAL.validationErrorByAttr('max_versions')) + .hasText('Maximum versions must be a non-negative number.'); await fillIn(FORM.inputByAttr('max_versions'), '99999999999999999'); await click(FORM.saveBtn); assert.dom(GENERAL.validationErrorByAttr('max_versions')).hasText('You cannot go over 16 characters.'); + await fillIn(FORM.inputByAttr('max_versions'), '1.23'); + await click(FORM.saveBtn); + assert + .dom(GENERAL.validationErrorByAttr('max_versions')) + .hasText('Maximum versions must be a whole number.'); await fillIn(FORM.inputByAttr('max_versions'), '7'); // Fill in other metadata diff --git a/ui/tests/integration/components/kv/page/kv-page-metadata-edit-test.js b/ui/tests/integration/components/kv/page/kv-page-metadata-edit-test.js index 69c7c408a9..2a443f8583 100644 --- a/ui/tests/integration/components/kv/page/kv-page-metadata-edit-test.js +++ b/ui/tests/integration/components/kv/page/kv-page-metadata-edit-test.js @@ -122,7 +122,10 @@ module('Integration | Component | kv-v2 | Page::Secret::Metadata::Edit', functio await click(FORM.saveBtn); assert .dom(FORM.validationError('max_versions')) - .hasText('Maximum versions must be a number.', 'Validation message is shown for max_versions'); + .hasText( + 'Maximum versions must be a non-negative number.', + 'Validation message is shown for max_versions' + ); await click(FORM.cancelBtn); assert.true(this.onCancel.called, 'onCancel action is called');