Some initial plumbing for sfatables
authorSapan Bhatia <sapanb@cs.princeton.edu>
Wed, 12 Aug 2009 14:44:11 +0000 (14:44 +0000)
committerSapan Bhatia <sapanb@cs.princeton.edu>
Wed, 12 Aug 2009 14:44:11 +0000 (14:44 +0000)
sfatables/README [new file with mode: 0644]
sfatables/commands/__init__.py [new file with mode: 0644]
sfatables/commands/commands.py [new file with mode: 0644]
sfatables/commands/moo.py [new file with mode: 0644]
sfatables/matches/example_sfa_input.xml [new file with mode: 0644]
sfatables/matches/hrn.xml [new file with mode: 0644]
sfatables/sfatables.py [new file with mode: 0755]

diff --git a/sfatables/README b/sfatables/README
new file mode 100644 (file)
index 0000000..ed7b7ac
--- /dev/null
@@ -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 (file)
index 0000000..34cd92e
--- /dev/null
@@ -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 (file)
index 0000000..063caab
--- /dev/null
@@ -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 (file)
index 0000000..6564e9b
--- /dev/null
@@ -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 (file)
index 0000000..b702a21
--- /dev/null
@@ -0,0 +1,3 @@
+<record xmlns="http://www.planet-lab.org/sfa/records/user/2009/8">
+    <hrn>plc.princeton.foo</hrn>
+</record>
diff --git a/sfatables/matches/hrn.xml b/sfatables/matches/hrn.xml
new file mode 100644 (file)
index 0000000..a4473c8
--- /dev/null
@@ -0,0 +1,18 @@
+<!-- 
+"sfa-input" specifies the subset of the requestor context that this match needs to see. It is specified as an xpath expression.
+For this simple match, we just need to look at sfa-input. 
+
+"rule" specifies an xpath/xquery expression that evaluates the match. In this case, we just check if the requestor's HRN is the same
+as the one matched by this rule.
+
+-->
+
+<match xmlns="http://www.planet-lab.org/sfa/sfatables/match/2009/8">
+    <sfa-input>
+        //request/user/hrn
+    </sfa-input>
+
+    <rule>
+        (//request/user/hrn eq //current/usr/hrn)
+    </rule>
+</match>
diff --git a/sfatables/sfatables.py b/sfatables/sfatables.py
new file mode 100755 (executable)
index 0000000..dbfa712
--- /dev/null
@@ -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()