Skip to content
Snippets Groups Projects
Commit 92d47106 authored by Cédric Krier's avatar Cédric Krier :atom:
Browse files

Add ip_address and device_cookie login method options

issue10987
review360491008
parent 0fc174ad
No related branches found
No related tags found
1 merge request!114Return an empty list when the treeview has no selection
* Add ip_address and device_cookie login method options
* Add support for Python 3.10
* Remove support for Python 3.6
* Add creatable attribute on tree and form views
......
......@@ -405,5 +405,15 @@
authentications = password+sms,ldap
Each combined method can have options to skip them if they are met except for
the first method.
They are defined by appending their name to the method name after a question
mark (``?``) and separated by colons (``:``).
Example::
authentications = password+sms?ip_address:device_cookie
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.
......@@ -408,7 +418,11 @@
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.
By default, Tryton supports the ``ip_address`` and ``device_cookie`` options.
The ``ip_address`` compares the client IP address with the known network list
defined in `authentication_ip_network`_.
The ``device_cookie`` checks the client device is a known device of the user.
Other modules can define additional authentication methods and options, please
refer to their documentation for more information.
Default: ``password``
......@@ -412,6 +426,14 @@
Default: ``password``
authentication_ip_network
~~~~~~~~~~~~~~~~~~~~~~~~~
A comma separated list of known IP networks used to check for ``ip_address``
authentication method option.
Default: ``''``
max_age
~~~~~~~
......
......@@ -670,6 +670,7 @@
pool = Pool()
LoginAttempt = pool.get('res.user.login.attempt')
UserDevice = pool.get('res.user.device')
parameters = parameters.copy()
count_ip = LoginAttempt.count_ip()
if count_ip > config.getint(
......@@ -678,6 +679,7 @@
raise RateLimitException()
device_cookie = UserDevice.get_valid_cookie(
login, parameters.get('device_cookie'))
parameters['device_cookie'] = device_cookie
count = LoginAttempt.count(login, device_cookie)
if count > config.getint('session', 'max_attempt', default=5):
LoginAttempt.add(login, device_cookie)
......@@ -687,6 +689,15 @@
'session', 'authentications', default='password').split(','):
user_ids = set()
for method in methods.split('+'):
if user_ids:
try:
method, options = method.split('?', 1)
except ValueError:
options = []
else:
options = options.split(':')
if cls._check_login_options(options, login, parameters):
continue
try:
func = getattr(cls, '_login_%s' % method)
except AttributeError:
......@@ -701,6 +712,37 @@
LoginAttempt.add(login, device_cookie)
@classmethod
def _check_login_options(cls, options, login, parameters):
for option in options:
try:
func = getattr(cls, '_check_login_options_%s' % option)
except AttributeError:
logger.info("Missing login option: %s", option)
continue
if func(login, parameters):
return True
else:
return False
@classmethod
def _check_login_options_ip_address(cls, login, parameters):
context = Transaction().context
if context.get('_request') and context['_request'].get('remote_addr'):
ip_address = ipaddress.ip_address(
str(context['_request']['remote_addr']))
network_list = config.get('session', 'authentication_ip_network')
if network_list:
for network in network_list.split(','):
ip_network = ipaddress.ip_network(network)
if ip_address in ip_network:
return True
return False
@classmethod
def _check_login_options_device_cookie(cls, login, parameters):
return bool(parameters.get('device_cookie'))
@classmethod
def _login_password(cls, login, parameters):
if 'password' not in parameters:
msg = gettext('res.msg_user_password', login=login)
......
......@@ -291,6 +291,68 @@
self.assertEqual(LoginAttempt.count('user', None), 1)
self.assertEqual(LoginAttempt.count('user', cookie), 0)
@with_transaction()
def test_authentication_option_ip_address(self):
"Test authentication with ip_address option"
pool = Pool()
User = pool.get('res.user')
user = User(login='user')
user.save()
ip_network = config.get(
'session', 'authentication_ip_network', default='')
config.set(
'session', 'authentication_ip_network',
'192.168.0.0/16,127.0.0.0/8')
self.addCleanup(
config.set, 'session', 'authentication_ip_network', ip_network)
with patch.object(User, '_login_always', create=True) as always, \
patch.object(User, '_login_never', create=True) as never:
always.return_value = user.id
never.return_value = None
with set_authentications('always+never?ip_address'):
for address, result in [
('192.168.0.1', user.id),
('172.17.0.1', None),
('127.0.0.1', user.id),
]:
with self.subTest(address=address):
with Transaction().set_context(_request={
'remote_addr': address,
}):
self.assertEqual(
User.get_login('user', {}), result)
@with_transaction()
def test_authentication_option_device_cookie(self):
"Test authentication with device cookie option"
pool = Pool()
User = pool.get('res.user')
UserDevice = pool.get('res.user.device')
user = User(login='user')
user.save()
with Transaction().set_user(user.id):
cookie = UserDevice.renew(None)
with patch.object(User, '_login_always', create=True) as always, \
patch.object(User, '_login_never', create=True) as never:
always.return_value = user.id
never.return_value = None
with set_authentications('always+never?device_cookie'):
for value, result in [
(cookie, user.id),
('not cookie', None),
]:
with self.subTest(cookie=value):
self.assertEqual(
User.get_login('user', {'device_cookie': value}),
result)
def suite():
return unittest.TestLoader().loadTestsFromTestCase(UserTestCase)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment