diff --git a/ui/app/routes/vault/cluster/access/reset-password.js b/ui/app/routes/vault/cluster/access/reset-password.js
deleted file mode 100644
index f66a2ce150..0000000000
--- a/ui/app/routes/vault/cluster/access/reset-password.js
+++ /dev/null
@@ -1,43 +0,0 @@
-/**
- * Copyright IBM Corp. 2016, 2025
- * SPDX-License-Identifier: BUSL-1.1
- */
-
-import Route from '@ember/routing/route';
-import { service } from '@ember/service';
-import { encodePath } from 'vault/utils/path-encoding-helpers';
-
-const ERROR_UNAVAILABLE = 'Password reset is not available for your current auth mount.';
-const ERROR_NO_ACCESS =
- 'You do not have permissions to update your password. If you think this is a mistake ask your administrator to update your policy.';
-export default class VaultClusterAccessResetPasswordRoute extends Route {
- @service auth;
- @service store;
-
- async model() {
- // Password reset is only available on userpass type auth mounts
- if (this.auth.authData?.authMethodType !== 'userpass') {
- throw new Error(ERROR_UNAVAILABLE);
- }
- const { authMountPath, displayName } = this.auth.authData;
- if (!authMountPath || !displayName) {
- throw new Error(ERROR_UNAVAILABLE);
- }
- try {
- const capabilities = await this.store.findRecord(
- 'capabilities',
- `auth/${encodePath(authMountPath)}/users/${encodePath(displayName)}/password`
- );
- // Check that the user has ability to update password
- if (!capabilities.canUpdate) {
- throw new Error(ERROR_NO_ACCESS);
- }
- } catch (e) {
- // If capabilities can't be queried, default to letting the API decide
- }
- return {
- backend: authMountPath,
- username: displayName,
- };
- }
-}
diff --git a/ui/app/routes/vault/cluster/access/reset-password.ts b/ui/app/routes/vault/cluster/access/reset-password.ts
new file mode 100644
index 0000000000..9f78fe771c
--- /dev/null
+++ b/ui/app/routes/vault/cluster/access/reset-password.ts
@@ -0,0 +1,45 @@
+/**
+ * Copyright IBM Corp. 2016, 2025
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import Route from '@ember/routing/route';
+import { service } from '@ember/service';
+
+import type AuthService from 'vault/vault/services/auth';
+import type CapabilitiesService from 'vault/services/capabilities';
+
+const ERROR_UNAVAILABLE = 'Password reset is not available for the current user.';
+const ERROR_NO_ACCESS =
+ 'You do not have permissions to update your password. If you think this is a mistake ask your administrator to update your policy.';
+
+export default class VaultClusterAccessResetPasswordRoute extends Route {
+ @service declare readonly auth: AuthService;
+ @service declare readonly capabilities: CapabilitiesService;
+
+ async model() {
+ const { authMethodType, authMountPath, displayName } = this.auth.authData;
+ // Password reset is only available on userpass type auth mounts
+ if (authMethodType !== 'userpass') {
+ throw new Error(ERROR_UNAVAILABLE);
+ }
+
+ // Both of these are necessary to build the reset password URL
+ if (!authMountPath || !displayName) {
+ throw new Error(ERROR_UNAVAILABLE);
+ }
+
+ const capabilities = await this.capabilities.fetchPathCapabilities(
+ `auth/${authMountPath}/users/${displayName}/password`
+ );
+
+ // Throw an error if we know for certain the user doesn't have permission
+ if (!capabilities.canUpdate) {
+ throw new Error(ERROR_NO_ACCESS);
+ }
+ return {
+ backend: authMountPath,
+ username: displayName,
+ };
+ }
+}
diff --git a/ui/app/templates/vault/cluster/access/reset-password-error.hbs b/ui/app/templates/vault/cluster/access/reset-password-error.hbs
index 487be1758f..70eaa1a5ab 100644
--- a/ui/app/templates/vault/cluster/access/reset-password-error.hbs
+++ b/ui/app/templates/vault/cluster/access/reset-password-error.hbs
@@ -5,15 +5,13 @@
-
-
-
-
-
+ <:customFooter>
+
-
-
\ No newline at end of file
+
+
\ No newline at end of file
diff --git a/ui/lib/core/addon/components/page/error.hbs b/ui/lib/core/addon/components/page/error.hbs
index d739fbe909..5d61c52a70 100644
--- a/ui/lib/core/addon/components/page/error.hbs
+++ b/ui/lib/core/addon/components/page/error.hbs
@@ -30,4 +30,8 @@
{{/each}}
{{/if}}
+
+ {{#if (has-block "customFooter")}}
+ {{yield to="customFooter"}}
+ {{/if}}
\ No newline at end of file
diff --git a/ui/lib/core/addon/components/sidebar/user-menu.hbs b/ui/lib/core/addon/components/sidebar/user-menu.hbs
index 123ca67f20..69e2f60cd2 100644
--- a/ui/lib/core/addon/components/sidebar/user-menu.hbs
+++ b/ui/lib/core/addon/components/sidebar/user-menu.hbs
@@ -17,13 +17,17 @@
{{/if}}
{{#if this.hasEntityId}}
-
+
Multi-factor authentication
{{/if}}
{{#if this.isUserpass}}
- Reset password
+ Reset password
{{/if}}
diff --git a/ui/tests/acceptance/mfa-setup-test.js b/ui/tests/acceptance/mfa-setup-test.js
index 62f54300c5..cdc5c99f2a 100644
--- a/ui/tests/acceptance/mfa-setup-test.js
+++ b/ui/tests/acceptance/mfa-setup-test.js
@@ -58,6 +58,12 @@ module('Acceptance | mfa-setup', function (hooks) {
await click('[data-test-user-menu-item="mfa"]');
});
+ test('it closes the dropdown after navigating', async function (assert) {
+ assert
+ .dom(GENERAL.button('user-menu-trigger'))
+ .hasAttribute('aria-expanded', 'false', 'dropdown closes after navigating to MFA');
+ });
+
test('it should login through MFA and post to generate and be able to restart the setup', async function (assert) {
assert.expect(5);
// the network requests required in this test
diff --git a/ui/tests/acceptance/reset-password-test.js b/ui/tests/acceptance/reset-password-test.js
index 998d73ae0f..44a94f8b61 100644
--- a/ui/tests/acceptance/reset-password-test.js
+++ b/ui/tests/acceptance/reset-password-test.js
@@ -4,12 +4,13 @@
*/
import { module, test } from 'qunit';
-import { currentURL, click, fillIn, settled, waitFor } from '@ember/test-helpers';
+import { currentURL, click, fillIn, settled, waitFor, currentRouteName, visit } from '@ember/test-helpers';
import { setupApplicationTest } from 'vault/tests/helpers';
import { login, loginMethod } from 'vault/tests/helpers/auth/auth-helpers';
import { createPolicyCmd, deleteAuthCmd, mountAuthCmd, runCmd } from '../helpers/commands';
import { v4 as uuidv4 } from 'uuid';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
+import sinon from 'sinon';
const SUCCESS_MESSAGE = 'Successfully reset password';
@@ -70,4 +71,66 @@ module('Acceptance | reset password', function (hooks) {
assert.dom('[data-test-flash-message]').hasText(`Success ${SUCCESS_MESSAGE}`);
assert.dom('[data-test-input="reset-password"]').hasValue('', 'Resets input after save');
});
+
+ test('renders error template when user lacks update permission', async function (assert) {
+ await login();
+ // Create a user with just the default policy
+ await runCmd([
+ mountAuthCmd('userpass', this.userpass),
+ `write auth/${this.userpass}/users/no-access password=password`,
+ ]);
+
+ await loginMethod(
+ { username: 'no-access', password: 'password', path: this.userpass },
+ { authType: 'userpass', toggleOptions: true }
+ );
+
+ await click(GENERAL.button('user-menu-trigger'));
+ await click('[data-test-user-menu-item="reset-password"]');
+ assert
+ .dom(GENERAL.button('user-menu-trigger'))
+ .hasAttribute('aria-expanded', 'false', 'dropdown closes after navigating');
+ assert.dom(GENERAL.pageError.title(403)).hasText('403 Not Authorized');
+ assert
+ .dom(GENERAL.pageError.message)
+ .hasText(
+ 'You do not have permissions to update your password. If you think this is a mistake ask your administrator to update your policy.'
+ );
+ assert.strictEqual(
+ currentRouteName(),
+ 'vault.cluster.access.reset-password_error',
+ 'redirects to reset password route'
+ );
+ });
+
+ test('renders error if auth data is unavailable', async function (assert) {
+ const authStub = sinon.stub(this.owner.lookup('service:auth'), 'authData');
+ await login();
+ authStub.value({});
+ // Create a user with just the default policy
+ await runCmd([
+ mountAuthCmd('userpass', this.userpass),
+ `write auth/${this.userpass}/users/no-access password=password`,
+ ]);
+
+ await loginMethod(
+ { username: 'no-access', password: 'password', path: this.userpass },
+ { authType: 'userpass', toggleOptions: true }
+ );
+
+ // Have to visit the route directly since user menu option is hidden when auth data unavailable
+ await visit('/vault/access/reset-password');
+ assert.strictEqual(
+ currentRouteName(),
+ 'vault.cluster.access.reset-password_error',
+ 'redirects to reset password route'
+ );
+ assert.dom(GENERAL.pageError.title(403)).hasText('403 Not Authorized');
+ assert.dom(GENERAL.pageError.message).hasText('Password reset is not available for the current user.');
+ assert
+ .dom(`${GENERAL.pageError.error} a`)
+ .exists()
+ .hasText('Update password API docs', 'it renders doc link');
+ authStub.restore();
+ });
});
diff --git a/ui/types/vault/services/auth.d.ts b/ui/types/vault/services/auth.d.ts
index f57e344db5..c450d6316f 100644
--- a/ui/types/vault/services/auth.d.ts
+++ b/ui/types/vault/services/auth.d.ts
@@ -12,6 +12,8 @@ import type { NormalizedAuthData, NormalizeAuthResponseKeys } from 'vault/auth/f
import type { AuthResponseAuthKey, AuthResponseDataKey } from 'vault/auth/methods';
interface AuthData {
+ authMethodType: string;
+ authMountPath: string;
userRootNamespace: string;
token: string;
policies: string[];