You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@qpid.apache.org by ch...@apache.org on 2015/12/01 00:10:30 UTC

qpid-dispatch git commit: Add policy engine and a test policy

Repository: qpid-dispatch
Updated Branches:
  refs/heads/crolke-DISPATCH-188-1 e621de2a1 -> 9d1dcb112


Add policy engine and a test policy


Project: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/repo
Commit: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/commit/9d1dcb11
Tree: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/tree/9d1dcb11
Diff: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/diff/9d1dcb11

Branch: refs/heads/crolke-DISPATCH-188-1
Commit: 9d1dcb1120c693c16440f50183641b12a2502669
Parents: e621de2
Author: Chuck Rolke <cr...@redhat.com>
Authored: Mon Nov 30 18:10:10 2015 -0500
Committer: Chuck Rolke <cr...@redhat.com>
Committed: Mon Nov 30 18:10:10 2015 -0500

----------------------------------------------------------------------
 .../qpid_dispatch_internal/management/policy.py | 363 +++++++++++++++++++
 tests/policy-1/policy-photoserver.conf          | 148 ++++++++
 2 files changed, 511 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/9d1dcb11/python/qpid_dispatch_internal/management/policy.py
----------------------------------------------------------------------
diff --git a/python/qpid_dispatch_internal/management/policy.py b/python/qpid_dispatch_internal/management/policy.py
new file mode 100644
index 0000000..7009dd4
--- /dev/null
+++ b/python/qpid_dispatch_internal/management/policy.py
@@ -0,0 +1,363 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License
+#
+
+"""
+Utilities for command-line programs.
+"""
+
+import sys, optparse, os
+import ConfigParser
+from collections import Sequence, Mapping
+from qpid_dispatch_site import VERSION
+import pdb #; pdb.set_trace()
+#from traceback import format_exc
+import ast
+
+
+
+"""Entity implementing the business logic of user connection/access policy.
+
+Reading configuration files is treated as a set of CREATE operations.
+
+Provides interfaces for per-listener policy lookup:
+
+- Listener accept
+- AMQP Open
+"""
+
+class PolicyError(Exception):
+    def __init__(self, value):
+        self.value = value
+    def __str__(self):
+        return repr(self.value)
+
+
+class PolicyValidator():
+    """
+    Validate incoming configuration for legal schema.
+    - Warn about section options that go unused.
+    - Disallow negative max connection numbers.
+    - Check that connectionOrigins resolve to IP hosts
+    """
+    schema_version = 1
+
+    schema_allowed_options = [(), (
+        'connectionAllowUnrestricted',
+        'connectionOrigins',
+        'connectionPolicy',
+        'maximumConnections',
+        'maximumConnectionsPerHost',
+        'maximumConnectionsPerUser',
+        'policies',
+        'policyVersion',
+        'roles',
+        'schemaVersion')
+        ]
+    schema_disallowed_options = [(),
+        ()
+        ]
+
+    allowed_opts = ()
+    disallowed_opts = ()
+    validator = None
+
+    def __init__(self, schema_version=1):
+        """
+        Create a validator for the given schema version.
+        @param[in] schema_version version selector
+        """
+        if schema_version != 1:
+            raise PolicyError(
+                "Illegal policy schema version %s. Must be '1'." % schema_version)
+        self.schema_version = schema_version
+        self.allowed_opts = self.schema_allowed_options[schema_version]
+        self.disallowed_opts = self.schema_disallowed_options[schema_version]
+        self.validator = self.validate_v1
+
+
+    def validateNumber(self, val, v_min, v_max, errors):
+        """
+        Range check a numeric int policy value
+        @param[in] val policy value to check
+        @param[in] v_min minumum value
+        @param[in] v_max maximum value. zero disables check
+        @param[out] errors failure message
+        @return v_min <= val <= v_max
+        """
+        error = ""
+        try:
+            v_int = int(val)
+        except Exception, e:
+            errors.append("Value '%s' does not resolve to an integer." % val)
+            return False
+        if v_int < v_min:
+            errors.append("Value '%s' is below minimum '%s'." % (val, v_min))
+            return False
+        if v_max > 0 and v_int > v_max:
+            errors.append("Value '%s' is above maximum '%s'." % (val, v_max))
+            return False
+        return True
+
+
+    def validate_v1(self, name, policy_in, policy_out, warnings, errors):
+        """
+        Validate a schema.
+        @param[in] name - application name
+        @param[in] policy_in - section from ConfigParser as a list of tuples
+        @param[out] policy_out - validated policy as nested map
+        @param[out] warnings - nonfatal irregularities observed
+        @param[out] errors - descriptions of failure
+        @return - policy is usable
+        """
+        cerror = []
+        # validate the options
+        for (key, val) in policy_in:
+            if key not in self.allowed_opts:
+                warnings.append("Application '%s' option '%s' is ignored." %
+                                (name, key))
+            if key in self.disallowed_opts:
+                errors.append("Application '%s' option '%s' is disallowed." %
+                              (name, key))
+                return False
+            if key == "schemaVersion":
+                if not int(self.schema_version) == int(val):
+                    errors.append("Application '%s' expected schema version '%s' but is '%s'." %
+                                  (name, self.schema_version, val))
+                    return False
+                policy_out[key] = val
+            if key == "policyVersion":
+                if not self.validateNumber(val, 0, 0, cerror):
+                    errors.append("Application '%s' option '%s' must resolve to a positive integer: '%s'." %
+                                    (name, key, cerror[0]))
+                    return False
+                policy_out[key] = val
+            elif key in ['maximumConnections',
+                         'maximumConnectionsPerHost',
+                         'maximumConnectionsPerUser'
+                         ]:
+                if not self.validateNumber(val, 0, 65535, cerror):
+                    msg = ("Application '%s' option '%s' has error '%s'." % 
+                           (name, key, cerror[0]))
+                    errors.append(msg)
+                    return False
+                policy_out[key] = val
+            elif key in ['connectionOrigins',
+                         'connectionPolicy',
+                         'policies',
+                         'roles'
+                         ]:
+                try:
+                    submap = ast.literal_eval(val)
+                    if not type(submap) is dict:
+                        errors.append("Application '%s' option '%s' must be of type 'dict' but is '%s'" %
+                                      (name, key, type(submap)))
+                        return False
+                    if key == "policies":
+                        for pname in submap:
+                            for setting in submap[pname]:
+                                sval = submap[pname][setting]
+                                if setting in ['max_frame_size',
+                                               'max_message_size',
+                                               'max_receivers',
+                                               'max_senders',
+                                               'max_session_window',
+                                               'max_sessions'
+                                               ]:
+                                    if not self.validateNumber(sval, 0, 0, cerror):
+                                        errors.append("Application '%s' option '%s' policy '%s' setting '%s' has error '%s'." %
+                                                      (name, key, pname, setting, cerror[0]))
+                                        return False
+                                elif setting in ['allow_anonymous_sender',
+                                                 'allow_dynamic_src'
+                                                 ]:
+                                    if not type(sval) is bool:
+                                        errors.append("Application '%s' option '%s' policy '%s' setting '%s' has illegal boolean value '%s'." %
+                                                      (name, key, pname, setting, sval))
+                                        return False
+                                elif setting in ['sources',
+                                                 'targets'
+                                                 ]:
+                                    if not type(sval) is list:
+                                        errors.append("Application '%s' option '%s' policy '%s' setting '%s' must be type 'list' but is '%s'." %
+                                                      (name, key, pname, setting, type(sval)))
+                                        return False
+                                else:
+                                    warnings.append("Application '%s' option '%s' policy '%s' setting '%s' is ignored." %
+                                                      (name, key, pname, setting))
+                    policy_out[key] = submap
+                except Exception, e:
+                    errors.append("Application '%s' option '%s' error processing  %s map: %s" %
+                                  (name, key, e))
+                    return False
+        return True
+
+
+class Policy():
+    """The policy database."""
+
+    data = {}
+    folder = "."
+    schema_version = 1
+    validator = None
+
+    def __init__(self, folder=".", schema_version=1):
+        """
+        Create instance
+        @params folder: relative path from __file__ to conf file folder
+        """
+        self.folder = folder
+        self.schema_version = schema_version
+        self.validator = PolicyValidator(schema_version)
+        self.policy_io_read_files()
+
+    #
+    # Policy file I/O
+    #
+    def policy_io_read_files(self):
+        """
+        Read all conf files and create the policies they contain.
+        """
+        apath = os.path.abspath(os.path.dirname(__file__))
+        apath = os.path.join(apath, self.folder)
+        for i in os.listdir(apath):
+            if i.endswith(".conf"):
+                self.policy_io_read_file(os.path.join(apath, i))
+
+    def policy_io_read_file(self, fn):
+        """
+        Read a single policy config file.
+        A file may hold multiple policies in separate ConfigParser sections.
+        All policies validated before any are committed.
+        Create each policy in db.
+        @param fn: absolute path to file
+        """
+        try:
+            cp = ConfigParser.ConfigParser()
+            cp.optionxform = str
+            cp.read(fn)
+
+        except Exception, e:
+            raise PolicyError( 
+                "Error processing policy configuration file '%s' : %s" % (fn, e))
+        newpolicies = {}
+        for policy in cp.sections():
+            warnings = []
+            diag = []
+            candidate = {}
+            if not self.validator.validator(policy, cp.items(policy), candidate, warnings, diag):
+                msg = "Policy file '%s' is invalid: %s" % (fn, diag[0])
+                raise PolicyError( msg )
+            if len(warnings) > 0:
+                print ("LogMe: Policy file '%s' application '%s' has warnings: %s" %
+                       (fn, policy, warnings))
+            newpolicies[policy] = candidate
+        for newpol in newpolicies:
+            self.data[newpol] = newpolicies[newpol]
+
+    #
+    # CRUD interface
+    #
+    def policy_create(self, name, policy, validate=True):
+        """
+        Create named policy
+        @param name: policy name
+        @param policy: policy data
+        """
+        warnings = []
+        diag = []
+        candidate = {}
+        result = self.validator.validator(name, policy, candidate, warnings, diag)
+        if validate and not result:
+            raise PolicyError( "Policy '%s' is invalid: %s" % (name, diag[0]) )
+        if len(warnings) > 0:
+            print ("LogMe: Application '%s' has warnings: %s" %
+                   (name, warnings))
+        self.data[name] = candidate
+
+    def policy_read(self, name):
+        """Read named policy"""
+        return self.data[name]
+
+    def policy_update(self, name, policy):
+        """Update named policy"""
+        pass
+
+    def policy_delete(self, name):
+        """Delete named policy"""
+        del self.data[name]
+
+    #
+    # db enumerator
+    #
+    def policy_db_get_names(self):
+        """Return a list of policy names."""
+        return self.data.keys()
+
+
+#
+# HACK ALERT: Temporary
+# Functions related to main
+#
+class ExitStatus(Exception):
+    """Raised if a command wants a non-0 exit status from the script"""
+    def __init__(self, status): self.status = status
+
+def main_except(argv):
+
+    usage = "usage: %prog [options]\nRead and print all conf files in a folder."
+    parser = optparse.OptionParser(usage=usage)
+    parser.set_defaults(folder="../../../tests/policy-1")
+    parser.add_option("-f", "--folder", action="store", type="string", dest="folder",
+                      help="Use named folder instead of policy-1")
+    parser.add_option("-d", "--dump", action="store_true", dest="dump",
+                      help="Dump policy details")
+
+    (options, args) = parser.parse_args()
+
+    policy = Policy(options.folder)
+
+    print("policy names: %s" % policy.policy_db_get_names())
+
+    if options.dump:
+        print("Policy details:")
+        for pname in policy.policy_db_get_names():
+            print("policy : %s" % pname)
+            p = ("%s" % policy.policy_read(pname))
+            print(p.replace('\\n', '\n'))
+
+    newpolicy = [('versionId', 3), ('maximumConnections', '20')]
+    policy.policy_create('test', newpolicy)
+
+    print("policy names with test: %s" % policy.policy_db_get_names())
+
+    print("policy test data:")
+    print(policy.policy_read('test'))
+
+def main(argv):
+    try:
+        main_except(argv)
+        return 0
+    except ExitStatus, e:
+        return e.status
+    except Exception, e:
+        print "%s: %s"%(type(e).__name__, e)
+        return 1
+
+if __name__ == "__main__":
+    sys.exit(main(sys.argv))

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/9d1dcb11/tests/policy-1/policy-photoserver.conf
----------------------------------------------------------------------
diff --git a/tests/policy-1/policy-photoserver.conf b/tests/policy-1/policy-photoserver.conf
new file mode 100644
index 0000000..621d417
--- /dev/null
+++ b/tests/policy-1/policy-photoserver.conf
@@ -0,0 +1,148 @@
+##
+## Licensed to the Apache Software Foundation (ASF) under one
+## or more contributor license agreements.  See the NOTICE file
+## distributed with this work for additional information
+## regarding copyright ownership.  The ASF licenses this file
+## to you under the Apache License, Version 2.0 (the
+## "License"); you may not use this file except in compliance
+## with the License.  You may obtain a copy of the License at
+##
+##   http://www.apache.org/licenses/LICENSE-2.0
+##
+## Unless required by applicable law or agreed to in writing,
+## software distributed under the License is distributed on an
+## "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+## KIND, either express or implied.  See the License for the
+## specific language governing permissions and limitations
+## under the License
+##
+
+# Definitions for photoserver application
+[photoserver]
+
+# policy schema used in this conf file
+schemaVersion            : 1
+
+# a version number to resolve multiple instances of this policy
+policyVersion            : 1
+
+# Aggregate connection limits
+maximumConnections        : 10
+maximumConnectionsPerUser : 5
+maximumConnectionsPerHost : 5
+
+# roles is a map.
+# key   = role name
+# value = list of authid names assigned to the role
+roles: {
+  'anonymous'       : ['anonymous'],
+  'users'           : ['u1', 'u2'],
+  'paidsubscribers' : ['p1', 'p2'],
+  'test'            : ['zeke', 'ynot'],
+  'admin'           : ['alice', 'bob', 'ellen'],
+  'superuser'       : ['ellen']
+  }
+
+# connectionOrigins is a map.
+# key   = origin name
+# value = list of host addresses or host address ranges
+connectionOrigins: {
+  'Ten18':      ['10.18.0.0,10.18.255.255'],
+  'EllensWS':   ['72.135.2.9'],
+  'TheLabs':    ['10.48.0.0,10.48.255.255','192.168.100.0,192.168.100.255'],
+  'Localhost':  ['127.0.0.1','::1'],
+  }
+
+# connectionPolicy is a map.
+# key   = role name
+# value = list of connection origin names
+connectionPolicy: {
+  'admin'      : ['Ten18', 'TheLabs', 'Localhost'],
+  'test'       : ['TheLabs'],
+  'superuser'  : ['Localhost', 'EllensWS']
+  }
+
+# connectionAllowUnrestricted - If a user is not restricted by a connectionPolicy
+#                               then is this user allowed to connect?
+connectionAllowUnrestricted : True
+
+# policy is a map.
+# key   = role name or authid name
+# value = policy containing:
+#         - values passed in AMQP Open and Attach performatives
+#         - allowed source and target names in AMQP Attach
+#
+policies: {
+  'anonymous' : {
+    'max_frame_size'         : 111111,
+    'max_message_size'       : 111111,
+    'max_session_window'     : 111111,
+    'max_sessions'           : 1,
+    'max_senders'            : 11,
+    'max_receivers'          : 11,
+    'allow_dynamic_src'      : False,
+    'allow_anonymous_sender' : False,
+    'sources'                : ['public'],
+    'targets'                : []
+    },
+  'users' : {
+    'max_frame_size'         : 222222,
+    'max_message_size'       : 222222,
+    'max_session_window'     : 222222,
+    'max_sessions'           : 2,
+    'max_senders'            : 22,
+    'max_receivers'          : 22,
+    'allow_dynamic_src'      : False,
+    'allow_anonymous_sender' : False,
+    'sources'                : ['public', 'private'],
+    'targets'                : ['public']
+    },
+  'paidsubscribers' : {
+    'max_frame_size'         : 333333,
+    'max_message_size'       : 333333,
+    'max_session_window'     : 333333,
+    'max_sessions'           : 3,
+    'max_senders'            : 33,
+    'max_receivers'          : 33,
+    'allow_dynamic_src'      : True,
+    'allow_anonymous_sender' : False,
+    'sources'                : ['public', 'private'],
+    'targets'                : ['public', 'private']
+    },
+  'test' : {
+    'max_frame_size'         : 444444,
+    'max_message_size'       : 444444,
+    'max_session_window'     : 444444,
+    'max_sessions'           : 4,
+    'max_senders'            : 44,
+    'max_receivers'          : 44,
+    'allow_dynamic_src'      : True,
+    'allow_anonymous_sender' : True,
+    'sources'                : ['private'],
+    'targets'                : ['private']
+    },
+  'admin' : {
+    'max_frame_size'         : 555555,
+    'max_message_size'       : 555555,
+    'max_session_window'     : 555555,
+    'max_sessions'           : 5,
+    'max_senders'            : 55,
+    'max_receivers'          : 55,
+    'allow_dynamic_src'      : True,
+    'allow_anonymous_sender' : True,
+    'sources'                : ['public', 'private', 'management'],
+    'targets'                : ['public', 'private', 'management']
+    },
+  'superuser' : {
+    'max_frame_size'         : 666666,
+    'max_message_size'       : 666666,
+    'max_session_window'     : 666666,
+    'max_sessions'           : 6,
+    'max_senders'            : 66,
+    'max_receivers'          : 66,
+    'allow_dynamic_src'      : False,
+    'allow_anonymous_sender' : False,
+    'sources'                : ['public', 'private', 'management', 'root'],
+    'targets'                : ['public', 'private', 'management', 'root']
+    }
+  }


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@qpid.apache.org
For additional commands, e-mail: commits-help@qpid.apache.org