diff --git a/src/middlewared/middlewared/plugins/account.py b/src/middlewared/middlewared/plugins/account.py index 71390268dad30..ebe333eff191a 100644 --- a/src/middlewared/middlewared/plugins/account.py +++ b/src/middlewared/middlewared/plugins/account.py @@ -1537,16 +1537,19 @@ async def set_password(self, app, data): password changes will be rejected if the payload does not match the currently-authenticated user. - API keys granting access to this endpoint will be able to reset - the password of any user. + NOTE: users authenticated with a one-time password will be able + to change the password without submitting a second time. """ verrors = ValidationErrors() is_full_admin = credential_has_full_admin(app.authenticated_credentials) + is_otp_login = False authenticated_user = None if app.authenticated_credentials.is_user_session: authenticated_user = app.authenticated_credentials.user['username'] + if 'OTPW' in app.authenticated_credentials.user['account_attributes']: + is_otp_login = True username = data['username'] password = data['new_password'] @@ -1580,7 +1583,9 @@ async def set_password(self, app, data): f'{username}: user is not local to the TrueNAS server.' ) - if not is_full_admin: + # Require submitting password twice if this is not a full admin session + # and does not have a one-time password. + if not is_full_admin and not is_otp_login: if data['old_password'] is None: verrors.add( 'user.set_password.old_password', diff --git a/tests/api2/test_password_reset.py b/tests/api2/test_password_reset.py index 7141d373ba073..2f9149c453871 100644 --- a/tests/api2/test_password_reset.py +++ b/tests/api2/test_password_reset.py @@ -138,3 +138,23 @@ def test_restricted_user_set_password(): 'old_password': TEST_PASSWORD2, 'new_password': TEST_PASSWORD2_2 }) + + +def test_password_reset_via_onetime_password(): + with unprivileged_user( + username=TEST_USERNAME, + group_name=TEST_GROUPNAME, + privilege_name='TEST_PASSWD_RESET_PRIVILEGE', + web_shell=False, + roles=['READONLY_ADMIN'] + ) as acct: + otp = call('auth.generate_onetime_password', {'username': acct.username}) + with client(auth=(acct.username, otp)) as c: + c.call('user.set_password', { + 'username': TEST_USERNAME, + 'new_password': TEST_PASSWORD2_2 + }) + + # Verify that password change worked + with client(auth=(acct.username, TEST_PASSWORD2_2)): + pass