mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-04 20:06:27 +02:00
* fix reset password error substate not rendering * fix user dropdown not closing after route click * add test coverage for stuck user menu; Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com>
This commit is contained in:
parent
d8788f7792
commit
422acb5e1f
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
45
ui/app/routes/vault/cluster/access/reset-password.ts
Normal file
45
ui/app/routes/vault/cluster/access/reset-password.ts
Normal file
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -5,15 +5,13 @@
|
||||
|
||||
<Page::Header @title="Reset password" />
|
||||
|
||||
<Hds::ApplicationState @align="center" class="top-padding-32" as |A|>
|
||||
<A.Header @title="No password reset access" @titleTag="h2" />
|
||||
<A.Body @text={{this.model.message}} />
|
||||
<A.Footer as |F|>
|
||||
<F.LinkStandalone
|
||||
<Page::Error @error={{hash httpStatus=403 message=this.model.message}} @isFullPage={{true}}>
|
||||
<:customFooter>
|
||||
<Hds::Link::Standalone
|
||||
@iconPosition="trailing"
|
||||
@icon="docs-link"
|
||||
@text="Update password API docs"
|
||||
@href={{doc-link "/vault/api-docs/auth/userpass#update-password-on-user"}}
|
||||
/>
|
||||
</A.Footer>
|
||||
</Hds::ApplicationState>
|
||||
</:customFooter>
|
||||
</Page::Error>
|
||||
@ -30,4 +30,8 @@
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if (has-block "customFooter")}}
|
||||
{{yield to="customFooter"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
@ -17,13 +17,17 @@
|
||||
{{/if}}
|
||||
|
||||
{{#if this.hasEntityId}}
|
||||
<D.Interactive @route="vault.cluster.mfa-setup" data-test-user-menu-item="mfa">
|
||||
<D.Interactive @route="vault.cluster.mfa-setup" {{on "click" D.close}} data-test-user-menu-item="mfa">
|
||||
Multi-factor authentication
|
||||
</D.Interactive>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.isUserpass}}
|
||||
<D.Interactive @route="vault.cluster.access.reset-password" data-test-user-menu-item="reset-password">Reset password</D.Interactive>
|
||||
<D.Interactive
|
||||
@route="vault.cluster.access.reset-password"
|
||||
{{on "click" D.close}}
|
||||
data-test-user-menu-item="reset-password"
|
||||
>Reset password</D.Interactive>
|
||||
{{/if}}
|
||||
|
||||
<D.Generic id="container">
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
2
ui/types/vault/services/auth.d.ts
vendored
2
ui/types/vault/services/auth.d.ts
vendored
@ -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[];
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user