Limit authentication attempt per IP network
ip_address and ip_network are added to LoginAttempt. issue7110 review41031002
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
* Limit authentication attempt per IP network
|
||||
* Manage 'X-Forwarded' headers from proxies
|
||||
* Add '_request' attribute to Transaction context
|
||||
* Delete translations on deletion only when model has translatable field
|
||||
|
||||
@@ -262,6 +262,31 @@ attempts over a period of `timeout`.
|
||||
|
||||
Default: `5`
|
||||
|
||||
max_attempt_ip_network
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The maximum authentication attempt from the same network before the server
|
||||
answers unconditionally `Too Many Requests` for any other attempts. The
|
||||
counting is done on all attempts over a period of `timeout`.
|
||||
|
||||
Default: `300`
|
||||
|
||||
ip_network_4
|
||||
~~~~~~~~~~~~
|
||||
|
||||
The network prefix to apply on IPv4 address for counting the authentication
|
||||
attempts.
|
||||
|
||||
Default: `32`
|
||||
|
||||
ip_network_6
|
||||
~~~~~~~~~~~~
|
||||
|
||||
The network prefix to apply on IPv6 address for counting the authentication
|
||||
attempts.
|
||||
|
||||
Default: `56`
|
||||
|
||||
password
|
||||
--------
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ Prerequisites
|
||||
* python-dateutil (http://labix.org/python-dateutil)
|
||||
* polib (https://bitbucket.org/izi/polib/wiki/Home)
|
||||
* python-sql 0.4 or later (http://code.google.com/p/python-sql/)
|
||||
* ipaddress (https://github.com/phihag/ipaddress)
|
||||
* Optional: psycopg 2.5.0 or later (http://www.initd.org/)
|
||||
* Optional: psycopg2cffi 2.5.0 or later
|
||||
(http://github.com/chtd/psycopg2cffi)
|
||||
|
||||
@@ -100,6 +100,7 @@ setup(name=name,
|
||||
'python-sql >= 0.4',
|
||||
'werkzeug',
|
||||
'wrapt',
|
||||
'ipaddress',
|
||||
],
|
||||
extras_require={
|
||||
'PostgreSQL': pg_require,
|
||||
|
||||
@@ -67,6 +67,9 @@ class TrytonConfigParser(ConfigParser.RawConfigParser):
|
||||
self.set('session', 'authentications', 'password')
|
||||
self.set('session', 'timeout', 600)
|
||||
self.set('session', 'max_attempt', 5)
|
||||
self.set('session', 'max_attempt_ip_network', 300)
|
||||
self.set('session', 'ip_network_4', 32)
|
||||
self.set('session', 'ip_network_6', 56)
|
||||
self.add_section('password')
|
||||
self.set('password', 'length', 8)
|
||||
self.set('password', 'entropy', 0.75)
|
||||
|
||||
+40
-2
@@ -11,6 +11,7 @@ import datetime
|
||||
import logging
|
||||
import uuid
|
||||
import mmap
|
||||
import ipaddress
|
||||
from functools import wraps
|
||||
from itertools import groupby, ifilter
|
||||
from operator import attrgetter
|
||||
@@ -540,6 +541,11 @@ class User(ModelSQL, ModelView):
|
||||
Return user id if password matches
|
||||
'''
|
||||
LoginAttempt = Pool().get('res.user.login.attempt')
|
||||
count_ip = LoginAttempt.count_ip()
|
||||
if count_ip > config.getint(
|
||||
'session', 'max_attempt_ip_network', default=300):
|
||||
# Do not add attempt as the goal is to prevent flooding
|
||||
raise RateLimitException()
|
||||
count = LoginAttempt.count(login)
|
||||
if count > config.getint('session', 'max_attempt', default=5):
|
||||
LoginAttempt.add(login)
|
||||
@@ -634,6 +640,8 @@ class LoginAttempt(ModelSQL):
|
||||
"""
|
||||
__name__ = 'res.user.login.attempt'
|
||||
login = fields.Char('Login', size=512)
|
||||
ip_address = fields.Char("IP Address")
|
||||
ip_network = fields.Char("IP Network")
|
||||
|
||||
@classmethod
|
||||
def __register__(cls, module_name):
|
||||
@@ -649,6 +657,21 @@ class LoginAttempt(ModelSQL):
|
||||
return (datetime.datetime.now()
|
||||
- datetime.timedelta(seconds=config.getint('session', 'timeout')))
|
||||
|
||||
@classmethod
|
||||
def ipaddress(cls):
|
||||
context = Transaction().context
|
||||
ip_address = ''
|
||||
ip_network = ''
|
||||
if context.get('_request') and context['_request'].get('remote_addr'):
|
||||
ip_address = ipaddress.ip_address(
|
||||
unicode(context['_request']['remote_addr']))
|
||||
prefix = config.getint(
|
||||
'session', 'ip_network_%s' % ip_address.version)
|
||||
ip_network = ipaddress.ip_network(
|
||||
unicode(context['_request']['remote_addr']))
|
||||
ip_network = ip_network.supernet(new_prefix=prefix)
|
||||
return ip_address, ip_network
|
||||
|
||||
def _login_size(func):
|
||||
@wraps(func)
|
||||
def wrapper(cls, login, *args, **kwargs):
|
||||
@@ -662,7 +685,12 @@ class LoginAttempt(ModelSQL):
|
||||
table = cls.__table__()
|
||||
cursor.execute(*table.delete(where=table.create_date < cls.delay()))
|
||||
|
||||
cls.create([{'login': login}])
|
||||
ip_address, ip_network = cls.ipaddress()
|
||||
cls.create([{
|
||||
'login': login,
|
||||
'ip_address': str(ip_address),
|
||||
'ip_network': str(ip_network),
|
||||
}])
|
||||
|
||||
@classmethod
|
||||
@_login_size
|
||||
@@ -676,11 +704,21 @@ class LoginAttempt(ModelSQL):
|
||||
def count(cls, login):
|
||||
cursor = Transaction().connection.cursor()
|
||||
table = cls.__table__()
|
||||
cursor.execute(*table.select(Count(Literal(1)),
|
||||
cursor.execute(*table.select(Count(Literal('*')),
|
||||
where=(table.login == login)
|
||||
& (table.create_date >= cls.delay())))
|
||||
return cursor.fetchone()[0]
|
||||
|
||||
@classmethod
|
||||
def count_ip(cls):
|
||||
cursor = Transaction().connection.cursor()
|
||||
table = cls.__table__()
|
||||
_, ip_network = cls.ipaddress()
|
||||
cursor.execute(*table.select(Count(Literal('*')),
|
||||
where=(table.ip_network == str(ip_network))
|
||||
& (table.create_date >= cls.delay())))
|
||||
return cursor.fetchone()[0]
|
||||
|
||||
del _login_size
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user