From e6736aaed7e9a8352e91f0098d44a91fa584d69b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Tue, 7 Nov 2023 16:52:06 +0100 Subject: [PATCH] Allow resetting password of user via RPC Closes #10629 --- trytond/CHANGELOG | 1 + trytond/trytond/protocols/dispatcher.py | 19 ++++++++++++++ trytond/trytond/security.py | 33 +++++++++++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/trytond/CHANGELOG b/trytond/CHANGELOG index 017a54caba..57989fd0e7 100644 --- a/trytond/CHANGELOG +++ b/trytond/CHANGELOG @@ -1,3 +1,4 @@ +* Allow resetting password of user via RPC * Support cached_property on object with __slots__ * Allow creating indexes concurrently * Allow skipping index creation diff --git a/trytond/trytond/protocols/dispatcher.py b/trytond/trytond/protocols/dispatcher.py index 9185cf7357..b10460d7a4 100644 --- a/trytond/trytond/protocols/dispatcher.py +++ b/trytond/trytond/protocols/dispatcher.py @@ -36,6 +36,7 @@ def rpc(request, database_name): methods = { 'common.db.login': login, 'common.db.logout': logout, + 'common.db.reset_password': reset_password, 'system.listMethods': list_method, 'system.methodHelp': help_method, 'system.methodSignature': lambda *a: 'signatures not supported', @@ -72,6 +73,24 @@ def logout(request, database_name): context={'_request': request.context}) +def reset_password(request, database_name, user, language=None): + authentications = config.get( + 'session', 'authentications', default='password').split(',') + if not any('password' in m.split('+') for m in authentications): + abort(HTTPStatus.FORBIDDEN) + context = { + 'language': language, + '_request': request.context, + } + try: + security.reset_password(database_name, user, context=context) + except backend.DatabaseOperationalError: + logger.error('fail to connect to %s', database_name, exc_info=True) + abort(HTTPStatus.NOT_FOUND) + except RateLimitException: + abort(HTTPStatus.TOO_MANY_REQUESTS) + + @app.route('/', methods=['POST']) def root(request, *args): methods = { diff --git a/trytond/trytond/security.py b/trytond/trytond/security.py index 57a16646ba..0c77f412e8 100644 --- a/trytond/trytond/security.py +++ b/trytond/trytond/security.py @@ -1,6 +1,9 @@ # This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. +import datetime as dt import logging +import random +import time from trytond import backend from trytond.config import config @@ -76,6 +79,36 @@ def logout(dbname, user, session, context=None): user, _get_remote_addr(context), dbname) +def reset_password(dbname, user, context=None): + now = dt.datetime.now() + # Prevent guessing code execution path + time.sleep(random.random()) + for count in range(config.getint('database', 'retry'), -1, -1): + with Transaction().start(dbname, 0, context=context): + pool = _get_pool(dbname) + User = pool.get('res.user') + try: + users = User.search([ + ('login', '=', user), + ]) + if not users: + logger.info("Reset password for unknown user: %s", user) + break + else: + user, = users + if user.password_reset and user.password_reset_expire > now: + logger.info( + "Password reset already exists for user: %s", user) + else: + user.reset_password() + logger.info("Password reset for user: %s", user) + break + except backend.DatabaseOperationalError: + if count: + continue + raise + + def check(dbname, user, session, context=None): for count in range(config.getint('database', 'retry'), -1, -1): with Transaction().start(dbname, user, context=context) as transaction: