changeset 6845ead3108d in trytond:default
details: https://hg.tryton.org/trytond?cmd=changeset;node=6845ead3108d
description:
Allow authentication methods to be combined
issue9303
review291621002
diffstat:
CHANGELOG | 1 +
doc/topics/configuration.rst | 19 ++++++++++++++-----
trytond/res/user.py | 27 ++++++++++++++++-----------
trytond/tests/test_user.py | 41 ++++++++++++++++++++++++++++++++++++++++-
4 files changed, 71 insertions(+), 17 deletions(-)
diffs (159 lines):
diff -r b0684e2aa5b0 -r 6845ead3108d CHANGELOG
--- a/CHANGELOG Tue May 19 19:36:09 2020 +0200
+++ b/CHANGELOG Thu May 21 09:06:10 2020 +0200
@@ -1,3 +1,4 @@
+* Allow combining authentication methods together
* Add context to export CSV route
Version 5.6.0 - 2020-05-04
diff -r b0684e2aa5b0 -r 6845ead3108d doc/topics/configuration.rst
--- a/doc/topics/configuration.rst Tue May 19 19:36:09 2020 +0200
+++ b/doc/topics/configuration.rst Thu May 21 09:06:10 2020 +0200
@@ -283,11 +283,19 @@
authentications
~~~~~~~~~~~~~~~
-A comma separated list of login methods to use to authenticate the user.
-By default, Tryton supports only the `password` method which compare the
-password entered by the user against a stored hash. But other modules can
-define new methods (please refers to their documentation).
-The methods are tested following the order of the list.
+A comma separated list of the authentication methods to try when attempting to
+verify a user's identity. Each method is tried in turn, following the order of
+the list, until one succeeds. In order to allow `multi-factor authentication`_,
+individual methods can be combined together using a plus (`+`) symbol.
+
+Example::
+
+ authentications = password+sms,ldap
+
+By default, Tryton only supports the `password` method. This method compares
+the password entered by the user against a stored hash of the user's password.
+Other modules can define additional authentication methods, please refer to
+their documentation for more information.
Default: `password`
@@ -496,3 +504,4 @@
.. _SSL-CERT: https://docs.python.org/library/ssl.html#ssl.wrap_socket
.. _STARTTLS: http://en.wikipedia.org/wiki/STARTTLS
.. _WSGI middleware:
https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface#Specification_overview
+.. _`multi-factor authentication`:
https://en.wikipedia.org/wiki/Multi-factor_authentication
diff -r b0684e2aa5b0 -r 6845ead3108d trytond/res/user.py
--- a/trytond/res/user.py Tue May 19 19:36:09 2020 +0200
+++ b/trytond/res/user.py Thu May 21 09:06:10 2020 +0200
@@ -11,6 +11,7 @@
import logging
import uuid
import mmap
+import re
try:
import secrets
except ImportError:
@@ -59,8 +60,8 @@
'UserConfigStart', 'UserConfig',
]
logger = logging.getLogger(__name__)
-_has_password = 'password' in config.get(
- 'session', 'authentications', default='password').split(',')
+_has_password = 'password' in re.split('[,+]', config.get(
+ 'session', 'authentications', default='password'))
passlib_path = config.get('password', 'passlib')
if passlib_path:
@@ -625,17 +626,21 @@
LoginAttempt.add(login)
raise RateLimitException()
Transaction().atexit(time.sleep, random.randint(0, 2 ** count - 1))
- for method in config.get(
+ for methods in config.get(
'session', 'authentications', default='password').split(','):
- try:
- func = getattr(cls, '_login_%s' % method)
- except AttributeError:
- logger.info('Missing login method: %s', method)
- continue
- user_id = func(login, parameters)
- if user_id:
+ user_ids = set()
+ for method in methods.split('+'):
+ try:
+ func = getattr(cls, '_login_%s' % method)
+ except AttributeError:
+ logger.info('Missing login method: %s', method)
+ break
+ user_ids.add(func(login, parameters))
+ if len(user_ids) != 1 or not all(user_ids):
+ break
+ if len(user_ids) == 1 and all(user_ids):
LoginAttempt.remove(login)
- return user_id
+ return user_ids.pop()
LoginAttempt.add(login)
@classmethod
diff -r b0684e2aa5b0 -r 6845ead3108d trytond/tests/test_user.py
--- a/trytond/tests/test_user.py Tue May 19 19:36:09 2020 +0200
+++ b/trytond/tests/test_user.py Thu May 21 09:06:10 2020 +0200
@@ -3,7 +3,8 @@
import datetime
import os
import unittest
-from unittest.mock import patch, ANY
+from contextlib import contextmanager
+from unittest.mock import patch, ANY, Mock
from trytond.tests.test_tryton import activate_module, with_transaction
from trytond.pool import Pool
@@ -15,6 +16,16 @@
FROM = '[email protected]'
+@contextmanager
+def set_authentications(methods):
+ saved_methods = config.get('session', 'authentications')
+ config.set('session', 'authentications', methods)
+ try:
+ yield
+ finally:
+ config.set('session', 'authentications', saved_methods)
+
+
class UserTestCase(unittest.TestCase):
'Test User'
@@ -205,6 +216,34 @@
'password': user.password_reset,
}))
+ @with_transaction()
+ def test_authentications(self):
+ "Test authentications"
+ pool = Pool()
+ User = pool.get('res.user')
+
+ user = User(login='user')
+ user.save()
+
+ User._login_always = Mock(return_value=user.id)
+ User._login_different = Mock(return_value=user.id + 1)
+ User._login_never = Mock(return_value=None)
+
+ for methods, result in (
+ ('never,never', None),
+ ('never,always', user.id),
+ ('always,never', user.id),
+ ('always,always', user.id),
+ ('never+never', None),
+ ('never+always', None),
+ ('always+never', None),
+ ('always+always', user.id),
+ ('always+different', None),
+ ):
+ with self.subTest(methods=methods, result=result):
+ with set_authentications(methods):
+ self.assertEqual(User.get_login('user', {}), result)
+
def suite():
return unittest.TestLoader().loadTestsFromTestCase(UserTestCase)