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