diff --git a/authserv/app_common.py b/authserv/app_common.py index 58677d2d813e9f3a3bffb4adc0cab1f5cec89b4f..7e316c43828afe5400f720c7eece16f98fda7572 100644 --- a/authserv/app_common.py +++ b/authserv/app_common.py @@ -16,7 +16,7 @@ def check_ratelimit(request, username, source_ip): period=current_app.config.get('RATELIMIT_USER_PERIOD', 60)): abort(503) if (source_ip - and not whitelisted(source_ip, current_app.config.get('SOURCE_IP_WHITELIST')) + and not whitelisted(source_ip) and not ratelimit_http_request( request, source_ip, tag='ip', count=current_app.config.get('RATELIMIT_SOURCEIP_COUNT', 10), @@ -66,8 +66,8 @@ def _do_auth(mc, username, service, shard, password, otp_token, source_ip, if current_app.config.get('ENABLE_BLACKLIST'): if bl.is_blacklisted('u', username): return _user_blacklisted - if (source_ip - and not whitelisted(source_ip, current_app.config.get('SOURCE_IP_WHITELIST')) + is_whitelisted = whitelisted(source_ip) + if (source_ip and not is_whitelisted and bl.is_blacklisted('ip', source_ip)): return _ip_blacklisted @@ -94,8 +94,7 @@ def _do_auth(mc, username, service, shard, password, otp_token, source_ip, if user: if bl.auth_failure('u', username): current_app.logger.info('blacklisted user %s', username) - if (source_ip - and not whitelisted(source_ip, current_app.config.get('SOURCE_IP_WHITELIST'))): + if source_ip and not is_whitelisted: if bl.auth_failure('ip', source_ip): current_app.logger.info('blacklisted IP %s', source_ip) diff --git a/authserv/ratelimit.py b/authserv/ratelimit.py index d549ccacf4482ab33402343909d972f9d1a14d46..267706bf5878d439a1e143202a7774690e459a11 100644 --- a/authserv/ratelimit.py +++ b/authserv/ratelimit.py @@ -1,4 +1,4 @@ -import functools +import ipaddr import re import threading from flask import abort, request, current_app @@ -10,36 +10,37 @@ key_sep = '/' DEFAULT_WHITELIST = [ '127.0.0.1', '::1', - '172.16.1.*', + '172.16.1.0/24', ] -def _tostr(s): - if isinstance(s, unicode): - return s.encode('utf-8') - return s +def init_whitelist(app): + wl = app.config.get('SOURCE_IP_WHITELIST', DEFAULT_WHITELIST) + app.whitelist = [ipaddr.IPNetwork(x) for x in wl] -_rxcache = {} -_rxcache_lock = threading.Lock() +_fake_v6_re = re.compile(r'^::\d+\.\d+\.\d+\.\d+$') - -def _rxcompile(s): - with _rxcache_lock: - if s not in _rxcache: - _rxcache[s] = re.compile('^%s$' % s.replace('.', r'\.').replace('*', '.*')) - return _rxcache[s] - - -def whitelisted(value, wl): - if not wl: - wl = DEFAULT_WHITELIST - for rx in wl: - if _rxcompile(rx).search(value): +def whitelisted(s): + if not s: + return False + # Some endpoints like to pass IPv4 addresses as ::addr, which + # isn't really entirely correct and confuses ipaddr. + if _fake_v6_re.match(s): + s = s[2:] + ip = ipaddr.IPAddress(s) + for net in current_app.whitelist: + if (ip in net) or (hasattr(ip, 'ipv4_mapped') and ip.ipv4_mapped and ip.ipv4_mapped in net): return True return False +def _tostr(s): + if isinstance(s, unicode): + return s.encode('utf-8') + return s + + class RateLimit(object): """Keep track of request rates using Memcache.""" @@ -95,6 +96,7 @@ class BlackList(object): return result is None def incr(self, mc, key): + # Returns True if the key was added to the blacklist. key = _tostr(self.prefix + key) if not self.rl.check(mc, key): mc.set(key, 'true', time=self.ttl) @@ -111,11 +113,13 @@ class AuthBlackList(object): def is_blacklisted(self, tag, value): if not value: return False - key = key_sep.join([tag, value]) + key = key_sep.join((tag, value)) return not self.blacklist.check(self.mc, key) def auth_failure(self, tag, value): + # Returns True if the key was added to the blacklist. if not value: - return - key = key_sep.join([tag, value]) + return False + key = key_sep.join((tag, value)) return self.blacklist.incr(self.mc, key) + diff --git a/authserv/server.py b/authserv/server.py index 7a18c8618ad556d174dc0a7ff2a6bd90a2274a2b..d7955b968a298bdb864f0e4525eea3c79ef0a272 100644 --- a/authserv/server.py +++ b/authserv/server.py @@ -12,10 +12,13 @@ import signal import sys from authserv import app_main from authserv import app_nginx +from authserv import ratelimit -def create_app(app, userdb=None, mc=None): +def create_app(app, userdb=None, mc=None, extra_config=None): app.config.from_envvar('APP_CONFIG', silent=True) + if extra_config: + app.config.update(extra_config) if not userdb: from authserv import ldap_model @@ -33,6 +36,8 @@ def create_app(app, userdb=None, mc=None): mc = pylibmc.ThreadMappedPool(client) app.memcache = mc + ratelimit.init_whitelist(app) + return app diff --git a/authserv/test/test_app_main.py b/authserv/test/test_app_main.py index bb746259b145e99c5f243fb406a73f2676abfe61..4877502b544e2040594b4444f2e86bc20ae24e7c 100644 --- a/authserv/test/test_app_main.py +++ b/authserv/test/test_app_main.py @@ -21,13 +21,15 @@ class ServerTest(unittest.TestCase): } app = server.create_app(app_main.app, userdb=FakeUserDb(self.users), - mc=FakeMemcachePool(_time)) - app.config.update({ - 'TESTING': True, - 'DEBUG': True, - 'ENABLE_BLACKLIST': True, - 'ENABLE_RATELIMIT': True, - }) + mc=FakeMemcachePool(_time), + extra_config={ + 'TESTING': True, + 'DEBUG': True, + 'ENABLE_BLACKLIST': True, + 'ENABLE_RATELIMIT': True, + 'BLACKLIST_COUNT': 5, + 'BLACKLIST_PERIOD': 10, + }) self.app = app.test_client() def test_auth_simple_ok(self): diff --git a/authserv/test/test_app_nginx.py b/authserv/test/test_app_nginx.py index 1021f051a6f9182a0f80a6f852f5e36b2ef8c1da..20ccb25e5b528c3a581222bafe15d1e5db3eac9b 100644 --- a/authserv/test/test_app_nginx.py +++ b/authserv/test/test_app_nginx.py @@ -17,11 +17,11 @@ class ServerTest(unittest.TestCase): } app = server.create_app(app_nginx.app, userdb=FakeUserDb(self.users), - mc=FakeMemcachePool(_time)) - app.config.update({ - 'TESTING': True, - 'DEBUG': True, - }) + mc=FakeMemcachePool(_time), + extra_config={ + 'TESTING': True, + 'DEBUG': True, + }) self.app = app.test_client() def test_nginx_http_auth_ok(self): diff --git a/authserv/test/test_integration.py b/authserv/test/test_integration.py index 03203341d5c3e325afdb67579d90184f16af0c1e..4047a7cf6c1521bdac5c1a1bf38cfc67fcd97341 100644 --- a/authserv/test/test_integration.py +++ b/authserv/test/test_integration.py @@ -63,11 +63,11 @@ class SSLServerTest(unittest.TestCase): def _runserver(): app = server.create_app(app_main.app, userdb=FakeUserDb(cls.users), - mc=FakeMemcachePool(time.time)) - app.config.update({ - 'TESTING': True, - 'DEBUG': True, - }) + mc=FakeMemcachePool(time.time), + extra_config={ + 'TESTING': True, + 'DEBUG': True, + }) print >>sys.stderr, 'starting HTTP server on port', cls.port server.run( diff --git a/authserv/test/test_ratelimit.py b/authserv/test/test_ratelimit.py index 78536369dac73c08a8092389119ee849f4e23d68..780d6b6668ad5d49a1c73ea9a4097fc4c37f70ac 100644 --- a/authserv/test/test_ratelimit.py +++ b/authserv/test/test_ratelimit.py @@ -1,5 +1,7 @@ from authserv.test import * from authserv.ratelimit import * +from authserv import server +from authserv import app_main class RateLimitTest(unittest.TestCase): @@ -55,9 +57,32 @@ class BlackListTest(unittest.TestCase): class WhitelistTest(unittest.TestCase): - def test_default_whitelist(self): - self.assertTrue(whitelisted('127.0.0.1', None)) - self.assertTrue(whitelisted('172.16.1.5', None)) - self.assertTrue(whitelisted('::1', None)) + def setUp(self): + app = server.create_app(app_main.app, + userdb=FakeUserDb({}), + mc=FakeMemcachePool(), + extra_config={ + 'TESTING': True, + 'DEBUG': True, + 'SOURCE_IP_WHITELIST': [ + '127.0.0.1', + '::1', + '172.16.1.0/24', + ], + }) + #self.app = app.test_client() + self.app = app + + def test_whitelist(self): + with self.app.app_context(): + self.assertTrue(whitelisted('127.0.0.1')) + self.assertTrue(whitelisted('::127.0.0.1')) + self.assertTrue(whitelisted('::ffff:127.0.0.1')) + self.assertTrue(whitelisted('::1')) + self.assertTrue(whitelisted('172.16.1.5')) + self.assertTrue(whitelisted('::172.16.1.5')) + self.assertTrue(whitelisted('::ffff:172.16.1.5')) - self.assertFalse(whitelisted('1.2.3.4', None)) + self.assertFalse(whitelisted('1.2.3.4')) + self.assertFalse(whitelisted('::1.2.3.4')) + self.assertFalse(whitelisted('2001:abcd:ef01::14')) diff --git a/debian/control b/debian/control index 20e223e42c02b3826487072d3b8a4e52af471e1a..6e76b9b28971a49311f55ed5d26ace7b37634a78 100644 --- a/debian/control +++ b/debian/control @@ -16,6 +16,6 @@ Description: PAM module for the A/I authentication protocol. Package: ai-auth-server Architecture: all Depends: ${python:Depends}, ${misc:Depends}, python-gevent, python-pylibmc, - memcached + python-ipaddr, memcached Description: Auth server package. Centralized authentication server with OTP support. diff --git a/setup.py b/setup.py index 5671e434a9713819cb2265b8ffbf58a8825601eb..e6e7fd87ba4fdebd4f6bedac63cab5a5f6178778 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( author="Autistici/Inventati", author_email="info@autistici.org", url="https://git.autistici.org/ai/authserv", - install_requires=["gevent", "python-ldap", "Flask", "pylibmc"], + install_requires=["gevent", "python-ldap", "Flask", "pylibmc", "ipaddr"], setup_requires=[], zip_safe=False, packages=find_packages(),