From 0b3c55399bc12eeab1025742da616f493342d5b8 Mon Sep 17 00:00:00 2001 From: Fernando Giorgetti Date: Fri, 25 May 2018 17:02:52 -0300 Subject: [PATCH] DISPATCH-1013 - Enable vhost policies to be defined on router config --- .../management/config.py | 18 ++++- .../policy/policy_local.py | 5 +- tests/system_test.py | 17 ++++- tests/system_tests_policy.py | 76 ++++++++++++++++++- 4 files changed, 107 insertions(+), 9 deletions(-) diff --git a/python/qpid_dispatch_internal/management/config.py b/python/qpid_dispatch_internal/management/config.py index 82814b525b..8f48746e7e 100644 --- a/python/qpid_dispatch_internal/management/config.py +++ b/python/qpid_dispatch_internal/management/config.py @@ -32,6 +32,9 @@ class Config(object): """Load config entities from qdrouterd.conf and validated against L{QdSchema}.""" + # static property to control depth level while reading the entities + child_level = 0 + def __init__(self, filename=None, schema=QdSchema(), raw_json=False): self.schema = schema self.config_types = [et for et in schema.entity_types.itervalues() @@ -58,9 +61,10 @@ def transform_sections(sections): @staticmethod def _parse(lines): """Parse config file format into a section list""" - begin = re.compile(r'([\w-]+)[ \t]*{') # WORD { - end = re.compile(r'}') # } - attr = re.compile(r'([\w-]+)[ \t]*:[ \t]*(.+)') # WORD1: VALUE + begin = re.compile(r'([\w-]+)[ \t]*{[ \t]*($|#)') # WORD { + end = re.compile(r'^}') # } + attr = re.compile(r'([\w-]+)[ \t]*:[ \t]*(.+)') # WORD1: VALUE + child = re.compile(r'([\$]*[\w-]+)[ \t]*:[ \t]*{[ \t]*($|#)') # WORD: { # The 'pattern:' and 'bindingKey:' attributes in the schema are special # snowflakes. They allow '#' characters in their value, so they cannot @@ -75,6 +79,14 @@ def sub(line): return "" if line.split(':')[0].strip() in special_snowflakes: line = re.sub(hash_ok, r'"\1": "\2",', line) + elif child.search(line): + line = line.split('#')[0].strip() + line = re.sub(child, r'"\1": {', line) + Config.child_level += 1 + elif end.search(line) and Config.child_level > 0: + line = line.split('#')[0].strip() + line = re.sub(end, r'},', line) + Config.child_level -= 1 else: line = line.split('#')[0].strip() line = re.sub(begin, r'["\1", {', line) diff --git a/python/qpid_dispatch_internal/policy/policy_local.py b/python/qpid_dispatch_internal/policy/policy_local.py index f651dcc293..91cd8fc8f2 100644 --- a/python/qpid_dispatch_internal/policy/policy_local.py +++ b/python/qpid_dispatch_internal/policy/policy_local.py @@ -253,7 +253,7 @@ def compile_app_settings(self, vhostname, usergroup, policy_in, policy_out, warn errors.append("Policy vhost '%s' user group '%s' option '%s' has error '%s'." % (vhostname, usergroup, key, cerror[0])) return False - policy_out[key] = val + policy_out[key] = int(val) elif key == PolicyKeys.KW_REMOTE_HOSTS: # Conection groups are lists of IP addresses that need to be # converted into binary structures for comparisons. @@ -265,6 +265,8 @@ def compile_app_settings(self, vhostname, usergroup, policy_in, policy_out, warn PolicyKeys.KW_ALLOW_DYNAMIC_SRC, PolicyKeys.KW_ALLOW_USERID_PROXY ]: + if type(val) in [unicode, str] and val.lower() in ['true', 'false']: + val = True if val == 'true' else False if not type(val) is bool: errors.append("Policy vhost '%s' user group '%s' option '%s' has illegal boolean value '%s'." % (vhostname, usergroup, key, val)) @@ -336,6 +338,7 @@ def compile_access_ruleset(self, name, policy_in, policy_out, warnings, errors): # validate the options for key, val in policy_in.iteritems(): + if key not in self.allowed_ruleset_options: warnings.append("Policy vhost '%s' option '%s' is ignored." % (name, key)) diff --git a/tests/system_test.py b/tests/system_test.py index d8baed760f..78ce5ac85d 100755 --- a/tests/system_test.py +++ b/tests/system_test.py @@ -302,10 +302,21 @@ def defaults(self): def __str__(self): """Generate config file content. Calls default() first.""" - def props(p): - return "".join([" %s: %s\n"%(k, v) for k, v in p.iteritems()]) + def tabs(level): + return " " * level + + def sub_elem(l, level): + return "".join(["%s%s: {\n%s%s}\n" % (tabs(level), n, props(p, level + 1), tabs(level)) for n, p in l]) + + def child(v, level): + return "{\n%s%s}" % (sub_elem(v, level), tabs(level - 1)) + + def props(p, level): + return "".join( + ["%s%s: %s\n" % (tabs(level), k, v if not isinstance(v, list) else child(v, level + 1)) for k, v in + p.iteritems()]) self.defaults() - return "".join(["%s {\n%s}\n"%(n, props(p)) for n, p in self]) + return "".join(["%s {\n%s}\n"%(n, props(p, 1)) for n, p in self]) def __init__(self, name=None, config=Config(), pyinclude=None, wait=True, perform_teardown=True): """ diff --git a/tests/system_tests_policy.py b/tests/system_tests_policy.py index 92555d03ee..5ead8cc453 100644 --- a/tests/system_tests_policy.py +++ b/tests/system_tests_policy.py @@ -21,8 +21,8 @@ import os, json from system_test import TestCase, Qdrouterd, main_module, Process, TIMEOUT, DIR from subprocess import PIPE, STDOUT -from proton import ConnectionException -from proton.utils import BlockingConnection, LinkDetached +from proton import ConnectionException, Timeout +from proton.utils import BlockingConnection, LinkDetached, SyncRequestResponse from qpid_dispatch_internal.policy.policy_util import is_ipv6_enabled class AbsoluteConnectionCountLimit(TestCase): @@ -820,5 +820,77 @@ def test_hostname_pattern_01_denied_add(self): self.assertFalse("222222" in qdm_out) +class VhostPolicyFromRouterConfig(TestCase): + """ + Verify that connections beyond the vhost limit are denied. + Differently than global maxConnections, opening a connection + does not raise a ConnectionException, but when an attempt to + create a sync request and response client is made after limit + is reached, the connection times out. + """ + @classmethod + def setUpClass(cls): + """Start the router""" + super(VhostPolicyFromRouterConfig, cls).setUpClass() + config = Qdrouterd.Config([ + ('router', {'mode': 'standalone', 'id': 'QDR.Policy'}), + ('listener', {'port': cls.tester.get_port()}), + ('policy', {'maxConnections': 100, 'enableVhostPolicy': 'true'}), + ('vhost', { + 'hostname': '0.0.0.0', 'maxConnections': 2, + 'allowUnknownUser': 'true', + 'groups': [( + '$default', { + 'users': '*', 'remoteHosts': '*', + 'sources': '*', 'targets': '*', + 'allowDynamicSource': 'true' + } + ), ( + 'anonymous', { + 'users': 'anonymous', 'remoteHosts': '*', + 'sources': '*', 'targets': '*', + 'allowDynamicSource': 'true', + 'allowAnonymousSender': 'true' + } + )] + }) + ]) + + cls.router = cls.tester.qdrouterd('vhost-conn-limit-router', config, wait=True) + + def address(self): + return self.router.addresses[0] + + def test_verify_vhost_maximum_connections(self): + addr = "%s/$management" % self.address() + timeout = 5 + + # two connections should be ok + denied = False + try: + bc1 = SyncRequestResponse(BlockingConnection(addr, timeout=timeout)) + bc2 = SyncRequestResponse(BlockingConnection(addr, timeout=timeout)) + except ConnectionException: + denied = True + except Timeout: + denied = True + + self.assertFalse(denied) # assert connections were opened + + # third connection should be denied + denied = False + try: + bc3 = SyncRequestResponse(BlockingConnection(addr, timeout=timeout)) + except ConnectionException: + denied = True + except Timeout: + denied = True + + self.assertTrue(denied) # assert if connection that should not open did open + + bc1.connection.close() + bc2.connection.close() + + if __name__ == '__main__': unittest.main(main_module())