Commit 4bb60e73 by ale

Use proper IP address/network matching for whitelists

Replaces regexps with the "ipaddr" module.
parent 4bb2fcb3
Pipeline #352 passed with stages
in 2 minutes 48 seconds
......@@ -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)
......
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)
......@@ -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
......
......@@ -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):
......
......@@ -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):
......
......@@ -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(
......
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'))
......@@ -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.
......@@ -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(),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment