From 498cf150f6eb694610f9dbdd868dc1c47e5ad05d Mon Sep 17 00:00:00 2001 From: Sapan Bhatia Date: Wed, 12 Aug 2009 14:44:11 +0000 Subject: [PATCH] Some initial plumbing for sfatables --- sfatables/README | 17 ++ sfatables/commands/__init__.py | 5 + sfatables/commands/commands.py | 18 ++ sfatables/commands/moo.py | 210 ++++++++++++++++++++++++ sfatables/matches/example_sfa_input.xml | 3 + sfatables/matches/hrn.xml | 18 ++ sfatables/sfatables.py | 64 ++++++++ 7 files changed, 335 insertions(+) create mode 100644 sfatables/README create mode 100644 sfatables/commands/__init__.py create mode 100644 sfatables/commands/commands.py create mode 100644 sfatables/commands/moo.py create mode 100644 sfatables/matches/example_sfa_input.xml create mode 100644 sfatables/matches/hrn.xml create mode 100755 sfatables/sfatables.py diff --git a/sfatables/README b/sfatables/README new file mode 100644 index 00000000..ed7b7ac1 --- /dev/null +++ b/sfatables/README @@ -0,0 +1,17 @@ +Examples: + +Add rules: + +e.g. +* sfatables -A INCOMING --requestor-hrn ple.emaniacs.* -j ACCEPT +* sfatables -A INCOMING --requestor-hrn ple.* -j RESTRICT_NODES --include-only ple.emaniacs.pool_ple + +or + +* sfatables -A INCOMING --requestor-hrn=plc.princeton.coblitz requested=plc.tp.*[tp_coblitz=true] -> result=true +requester=plc.princeton.other_whitelisted_slice requested=plc.tp.*[tp_coblitz=true] -> result=true +requester=* requested=plc.tp.*[tp_coblitz=true] -> result=false + +Default policy: + +* sfatables -P INCOMING REJECT diff --git a/sfatables/commands/__init__.py b/sfatables/commands/__init__.py new file mode 100644 index 00000000..34cd92ea --- /dev/null +++ b/sfatables/commands/__init__.py @@ -0,0 +1,5 @@ +all = """ +add +delete +list +""".split() diff --git a/sfatables/commands/commands.py b/sfatables/commands/commands.py new file mode 100644 index 00000000..063caab9 --- /dev/null +++ b/sfatables/commands/commands.py @@ -0,0 +1,18 @@ +import os, time + +class Command: + options = [] + help = '' + key='' + matches = False + targets = False + + def __init__(self): + return + + def call(self): + # Override this function + return True + + def __call__(self, option, opt_str, value, parser, *args, **kwargs): + return self.call(option) diff --git a/sfatables/commands/moo.py b/sfatables/commands/moo.py new file mode 100644 index 00000000..6564e9b2 --- /dev/null +++ b/sfatables/commands/moo.py @@ -0,0 +1,210 @@ +import os, time + +class Command: + commandline_options = [] + help = "Add a new rule" + + def __init__(self): + if (len(commandline_options!=2)): + raise Exception("Internal error: each command must supply 2 command line options") + + + def __call__(self, option, opt_str, value, parser, *args, **kwargs): + return True + + + + def help(self, indent = " "): + """ + Text documentation for the method. + """ + + (min_args, max_args, defaults) = self.args() + + text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns)) + + text += "Description:\n\n" + lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")] + text += "\n".join(lines) + "\n\n" + + def param_text(name, param, indent, step): + """ + Format a method parameter. + """ + + text = indent + + # Print parameter name + if name: + param_offset = 32 + text += name.ljust(param_offset - len(indent)) + else: + param_offset = len(indent) + + # Print parameter type + param_type = python_type(param) + text += xmlrpc_type(param_type) + "\n" + + # Print parameter documentation right below type + if isinstance(param, Parameter): + wrapper = textwrap.TextWrapper(width = 70, + initial_indent = " " * param_offset, + subsequent_indent = " " * param_offset) + text += "\n".join(wrapper.wrap(param.doc)) + "\n" + param = param.type + + text += "\n" + + # Indent struct fields and mixed types + if isinstance(param, dict): + for name, subparam in param.iteritems(): + text += param_text(name, subparam, indent + step, step) + elif isinstance(param, Mixed): + for subparam in param: + text += param_text(name, subparam, indent + step, step) + elif isinstance(param, (list, tuple, set)): + for subparam in param: + text += param_text("", subparam, indent + step, step) + + return text + + text += "Parameters:\n\n" + for name, param in zip(max_args, self.accepts): + text += param_text(name, param, indent, indent) + + text += "Returns:\n\n" + text += param_text("", self.returns, indent, indent) + + return text + + def args(self): + """ + Returns a tuple: + + ((arg1_name, arg2_name, ...), + (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...), + (None, None, ..., optional1_default, optional2_default, ...)) + + That represents the minimum and maximum sets of arguments that + this function accepts and the defaults for the optional arguments. + """ + + # Inspect call. Remove self from the argument list. + max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount] + defaults = self.call.func_defaults + if defaults is None: + defaults = () + + min_args = max_args[0:len(max_args) - len(defaults)] + defaults = tuple([None for arg in min_args]) + defaults + + return (min_args, max_args, defaults) + + def type_check(self, name, value, expected, args): + """ + Checks the type of the named value against the expected type, + which may be a Python type, a typed value, a Parameter, a + Mixed type, or a list or dictionary of possibly mixed types, + values, Parameters, or Mixed types. + + Extraneous members of lists must be of the same type as the + last specified type. For example, if the expected argument + type is [int, bool], then [1, False] and [14, True, False, + True] are valid, but [1], [False, 1] and [14, True, 1] are + not. + + Extraneous members of dictionaries are ignored. + """ + + # If any of a number of types is acceptable + if isinstance(expected, Mixed): + for item in expected: + try: + self.type_check(name, value, item, args) + return + except GeniInvalidArgument, fault: + pass + raise fault + + # If an authentication structure is expected, save it and + # authenticate after basic type checking is done. + #if isinstance(expected, Auth): + # auth = expected + #else: + # auth = None + + # Get actual expected type from within the Parameter structure + if isinstance(expected, Parameter): + min = expected.min + max = expected.max + nullok = expected.nullok + expected = expected.type + else: + min = None + max = None + nullok = False + + expected_type = python_type(expected) + + # If value can be NULL + if value is None and nullok: + return + + # Strings are a special case. Accept either unicode or str + # types if a string is expected. + if expected_type in StringTypes and isinstance(value, StringTypes): + pass + + # Integers and long integers are also special types. Accept + # either int or long types if an int or long is expected. + elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)): + pass + + elif not isinstance(value, expected_type): + raise GeniInvalidArgument("expected %s, got %s" % \ + (xmlrpc_type(expected_type), + xmlrpc_type(type(value))), + name) + + # If a minimum or maximum (length, value) has been specified + if expected_type in StringTypes: + if min is not None and \ + len(value.encode(self.api.encoding)) < min: + raise GeniInvalidArgument, "%s must be at least %d bytes long" % (name, min) + if max is not None and \ + len(value.encode(self.api.encoding)) > max: + raise GeniInvalidArgument, "%s must be at most %d bytes long" % (name, max) + elif expected_type in (list, tuple, set): + if min is not None and len(value) < min: + raise GeniInvalidArgument, "%s must contain at least %d items" % (name, min) + if max is not None and len(value) > max: + raise GeniInvalidArgument, "%s must contain at most %d items" % (name, max) + else: + if min is not None and value < min: + raise GeniInvalidArgument, "%s must be > %s" % (name, str(min)) + if max is not None and value > max: + raise GeniInvalidArgument, "%s must be < %s" % (name, str(max)) + + # If a list with particular types of items is expected + if isinstance(expected, (list, tuple, set)): + for i in range(len(value)): + if i >= len(expected): + j = len(expected) - 1 + else: + j = i + self.type_check(name + "[]", value[i], expected[j], args) + + # If a struct with particular (or required) types of items is + # expected. + elif isinstance(expected, dict): + for key in value.keys(): + if key in expected: + self.type_check(name + "['%s']" % key, value[key], expected[key], args) + for key, subparam in expected.iteritems(): + if isinstance(subparam, Parameter) and \ + subparam.optional is not None and \ + not subparam.optional and key not in value.keys(): + raise GeniInvalidArgument("'%s' not specified" % key, name) + + #if auth is not None: + # auth.check(self, *args) diff --git a/sfatables/matches/example_sfa_input.xml b/sfatables/matches/example_sfa_input.xml new file mode 100644 index 00000000..b702a212 --- /dev/null +++ b/sfatables/matches/example_sfa_input.xml @@ -0,0 +1,3 @@ + + plc.princeton.foo + diff --git a/sfatables/matches/hrn.xml b/sfatables/matches/hrn.xml new file mode 100644 index 00000000..a4473c86 --- /dev/null +++ b/sfatables/matches/hrn.xml @@ -0,0 +1,18 @@ + + + + + //request/user/hrn + + + + (//request/user/hrn eq //current/usr/hrn) + + diff --git a/sfatables/sfatables.py b/sfatables/sfatables.py new file mode 100755 index 00000000..dbfa7126 --- /dev/null +++ b/sfatables/sfatables.py @@ -0,0 +1,64 @@ +#!/usr/bin/python +# SFAtables is a tool for restricting access to an SFA aggregate in a generic +# and extensible way. + +# It is modeled using abstractions in iptables. Specifically, 'matches' specify +# criteria for matching certain requests, 'targets' specify actions that treat +# requests in a certain way, and 'chains' are used to group related +# match-action pairs. + +import sys +import os +import pdb +from optparse import OptionParser + +def load_extensions(module): + command_dict={} + commands = __import__(module,fromlist=[".".join(module.split('.')[:-1])]) + + for command_name in commands.all: + command_module = getattr(commands, command_name) + command = getattr(command_module, command_name) + command_dict[command.key]=command() + + return command_dict + +def create_parser(command_dict): + parser = OptionParser(usage="sfatables [command] [chain] [match] [target]", + description='See "man sfatables" for more detail.') + + for k in command_dict.keys(): + command = command_dict[k] + for (short_option,long_option) in command.options: + parser.add_option(short_option,long_option,dest=command.key,help=command.help,metavar=command.help.upper()) + + return parser + + +def main(): + command_dict = load_extensions("sfa.sfatables.commands") + command_parser = create_parser(command_dict) + (options, args) = command_parser.parse_args() + + if (len(options.keys() != 1): + raise Exception("sfatables takes one command at a time.\n") + + selected_command = command_dict[options.keys()[0]] + + match_options = None + target_options = None + + if (selected_command.matches): + match_dict = load_extensions("sfa.sfatables.matches") + match_parser = create_parser(match_dict) + (options, args) = match_parser.parse_args(args[2:]) # Change to next location of -- + + if (selected_command.targets): + match_dict = load_extensions("sfa.sfatables.targets") + target_parser = create_parser(match_dict) + (options, args) = target_parser.parse_args(args[5:]) # Change to next location of -- + + command(options, match_options, target_options) + +if __name__=='__main__': + main() -- 2.43.0