Parcourir la source

[trac983] implemented RequestACL.execute()

JINMEI Tatuya il y a 14 ans
Parent
commit
6ec7cbb997

+ 3 - 0
src/lib/acl/acl.h

@@ -88,8 +88,11 @@ public:
      * the context against conditions and if it matches, returns the
      * action that belongs to the first matched entry or default action
      * if nothing matches.
+     *
      * \param context The thing that should be checked. It is directly
      *     passed to the checks.
+     *
+     * \return The action for the ACL entry that first matches the context.
      */
     const Action& execute(const Context& context) const {
         const typename Entries::const_iterator end(entries_.end());

+ 52 - 0
src/lib/python/isc/acl/dns_requestacl_inc.cc

@@ -0,0 +1,52 @@
+namespace {
+const char* const RequestACL_doc = "\
+The ACL itself.\n\
+\n\
+It holds bunch of ordered entries, each one consisting of a check ( of\n\
+any kind, it might be even compound) and an action that is returned\n\
+whenever the action matches. They are tested in the order and first\n\
+match counts.\n\
+\n\
+This is non-copyable. It seems that there's no need to copy them (even\n\
+when it would be technically possible), so we forbid it just to\n\
+prevent copying it by accident. If there really is legitimate use,\n\
+this restriction can be removed.\n\
+\n\
+The class is template. It is possible to specify on which context the\n\
+checks match and which actions it returns. The actions must be\n\
+copyable for this to work and it is expected to be something small,\n\
+usually an enum (but other objects are also possible).\n\
+\n\
+There are protected functions. In fact, you should consider them\n\
+private, they are protected so tests can get inside. This class is not\n\
+expected to be subclassed in real applications.\n\
+\n\
+ACL(default_action)\n\
+\n\
+    Constructor.\n\
+\n\
+    Parameters:\n\
+      default_action It is the action that is returned when the\n\
+                 checked things \"falls off\" the end of the list\n\
+                 (when no rule matched).\n\
+\n\
+";
+
+const char* const RequestACL_execute_doc = "\
+execute(context) ->  Action \n\
+\n\
+The actual main function that decides.\n\
+\n\
+This is the function that takes the entries one by one, checks the\n\
+context against conditions and if it matches, returns the action that\n\
+belongs to the first matched entry or default action if nothing\n\
+matches.\n\
+\n\
+Parameters:\n\
+  context    The thing that should be checked. It is directly passed\n\
+             to the checks.\n\
+\n\
+Return Value(s): The action for the ACL entry that first matches the\n\
+context.\n\
+";
+} // unnamed namespace

+ 40 - 18
src/lib/python/isc/acl/dns_requestacl_python.cc

@@ -28,11 +28,14 @@
 #include <acl/acl.h>
 #include <acl/dns.h>
 
+#include "acl.h"
 #include "dns_requestacl_python.h"
+#include "dns_requestcontext_python.h"
 
 using namespace std;
 using namespace isc::util::python;
 using namespace isc::acl;
+using namespace isc::acl::python;
 using namespace isc::acl::dns;
 using namespace isc::acl::dns::python;
 
@@ -51,6 +54,9 @@ using namespace isc::acl::dns::python;
 // Trivial constructor.
 s_RequestACL::s_RequestACL() {}
 
+// Import pydoc text
+#include "dns_requestacl_inc.cc"
+
 namespace {
 // Shortcut type which would be convenient for adding class variables safely.
 typedef CPPPyObjectContainer<s_RequestACL, RequestACL> RequestACLContainer;
@@ -60,26 +66,9 @@ typedef CPPPyObjectContainer<s_RequestACL, RequestACL> RequestACLContainer;
 // the type definition of the object, since both can use the other
 //
 
-// General creation and destruction
-int RequestACL_init(s_RequestACL* self, PyObject* args);
-void RequestACL_destroy(s_RequestACL* self);
-
 // These are the functions we export
 // For a minimal support, we don't need them.
 
-// This list contains the actual set of functions we have in
-// python. Each entry has
-// 1. Python method name
-// 2. Our static function here
-// 3. Argument type
-// 4. Documentation
-PyMethodDef RequestACL_methods[] = {
-    { NULL, NULL, 0, NULL }
-};
-
-// This is a template of typical code logic of python class initialization
-// with C++ backend.  You'll need to adjust it according to details of the
-// actual C++ class.
 int
 RequestACL_init(s_RequestACL* self, PyObject* /*args*/) {
     // maybe we should prohibit direct creation of the ACL
@@ -118,6 +107,39 @@ RequestACL_destroy(s_RequestACL* const self) {
     self->cppobj.reset();
     Py_TYPE(self)->tp_free(self);
 }
+
+PyObject*
+RequestACL_execute(PyObject* po_self, PyObject* args) {
+    s_RequestACL* const self = static_cast<s_RequestACL*>(po_self);
+
+    try {
+        const s_RequestContext* po_context;
+        if (PyArg_ParseTuple(args, "O!", &requestcontext_type, &po_context)) {
+            const BasicAction action =
+                self->cppobj->execute(*po_context->cppobj);
+            return (Py_BuildValue("I", action));
+        }
+    } catch (const exception& ex) {
+        const string ex_what = "Failed to execute ACL: " + string(ex.what());
+        PyErr_SetString(po_ACLError, ex_what.c_str());
+    } catch (...) {
+        PyErr_SetString(PyExc_RuntimeError,
+                        "Unexpected exception in executing ACL");
+    }
+
+    return (NULL);
+}
+
+// This list contains the actual set of functions we have in
+// python. Each entry has
+// 1. Python method name
+// 2. Our static function here
+// 3. Argument type
+// 4. Documentation
+PyMethodDef RequestACL_methods[] = {
+    { "execute", RequestACL_execute, METH_VARARGS, RequestACL_execute_doc },
+    { NULL, NULL, 0, NULL }
+};
 } // end of unnamed namespace
 
 namespace isc {
@@ -148,7 +170,7 @@ PyTypeObject requestacl_type = {
     NULL,                               // tp_setattro
     NULL,                               // tp_as_buffer
     Py_TPFLAGS_DEFAULT,                 // tp_flags
-    "The RequestACL class objects is...(COMPLETE THIS)",
+    RequestACL_doc,
     NULL,                               // tp_traverse
     NULL,                               // tp_clear
     NULL,				 // tp_richcompare

+ 71 - 4
src/lib/python/isc/acl/tests/dns_test.py

@@ -15,7 +15,7 @@
 
 import unittest
 import socket
-from isc.acl.acl import LoaderError, Error
+from isc.acl.acl import LoaderError, Error, ACCEPT, REJECT, DROP
 from isc.acl.dns import *
 
 def get_sockaddr(address, port):
@@ -23,6 +23,24 @@ def get_sockaddr(address, port):
     ai = socket.getaddrinfo(address, port, 0, 0, 0, socket.AI_NUMERICHOST)[0]
     return ai[4]
 
+def get_acl(prefix):
+    '''This is a simple shortcut for creating an ACL containing single rule
+    that accepts addresses for the given IP prefix (and reject any others
+    by default)
+    '''
+    return load_request_acl('[{"action": "ACCEPT", "from": "' + prefix + '"}]')
+
+def get_context(address):
+    '''This is a simple shortcut wrapper for creating a RequestContext
+    object with a given IP address.  Port number doesn't matter in the test
+    (as of the initial implementation), so it's fixed for simplicity.
+    '''
+    return RequestContext(get_sockaddr(address, 53000))
+
+# These are commonly used RequestContext object
+CONTEXT4 = get_context('192.0.2.1')
+CONTEXT6 = get_context('2001:db8::1')
+
 class RequestContextTest(unittest.TestCase):
 
     def test_construct(self):
@@ -74,6 +92,9 @@ class RequestContextTest(unittest.TestCase):
 
 class RequestACLTest(unittest.TestCase):
 
+    def test_direct_construct(self):
+        acl = RequestACL()
+
     def test_request_loader(self):
         # these shouldn't raise an exception
         load_request_acl('[{"action": "DROP"}]')
@@ -88,7 +109,7 @@ class RequestACLTest(unittest.TestCase):
                           '[{"action": "DROP"}]', 0)
 
     def test_bad_acl_syntax(self):
-        # this test is derived from loader_test.cc
+        # the following are derived from loader_test.cc
         self.assertRaises(LoaderError, load_request_acl, '{}');
         self.assertRaises(LoaderError, load_request_acl, '42');
         self.assertRaises(LoaderError, load_request_acl, 'true');
@@ -101,6 +122,18 @@ class RequestACLTest(unittest.TestCase):
         self.assertRaises(LoaderError, load_request_acl, '[null]');
         self.assertRaises(LoaderError, load_request_acl, '[{}]');
 
+        # the following are derived from dns_test.cc
+        self.assertRaises(LoaderError, load_request_acl,
+                          '[{"action": "ACCEPT", "bad": "192.0.2.1"}]')
+        self.assertRaises(LoaderError, load_request_acl,
+                          '[{"action": "ACCEPT", "from": 4}]')
+        self.assertRaises(LoaderError, load_request_acl,
+                          '[{"action": "ACCEPT", "from": []}]')
+        self.assertRaises(LoaderError, load_request_acl,
+                          '[{"action": "ACCEPT", "from": "bad"}]')
+        self.assertRaises(LoaderError, load_request_acl,
+                          '[{"action": "ACCEPT", "from": null}]')
+
     def test_bad_acl_ipsyntax(self):
         # this test is derived from ip_check_unittest.cc
         self.assertRaises(LoaderError, load_request_acl,
@@ -124,8 +157,42 @@ class RequestACLTest(unittest.TestCase):
         self.assertRaises(LoaderError, load_request_acl,
                           '[{"action": "DROP", "from": "::1/129"')
 
-    def test_construct(self):
-        RequestACL()
+    def test_execute(self):
+        # tests derived from dns_test.cc.  We don't directly expose checks
+        # in the python wrapper, so we test it via execute().
+        self.assertEqual(ACCEPT, get_acl('192.0.2.1').execute(CONTEXT4))
+        self.assertEqual(REJECT, get_acl('192.0.2.53').execute(CONTEXT4))
+        self.assertEqual(ACCEPT, get_acl('192.0.2.0/24').execute(CONTEXT4))
+        self.assertEqual(REJECT, get_acl('192.0.1.0/24').execute(CONTEXT4))
+        self.assertEqual(REJECT, get_acl('192.0.1.0/24').execute(CONTEXT4))
+
+        self.assertEqual(ACCEPT, get_acl('2001:db8::1').execute(CONTEXT6))
+        self.assertEqual(REJECT, get_acl('2001:db8::53').execute(CONTEXT6))
+        self.assertEqual(ACCEPT, get_acl('2001:db8::/64').execute(CONTEXT6))
+        self.assertEqual(REJECT, get_acl('2001:db8:1::/64').execute(CONTEXT6))
+        self.assertEqual(REJECT, get_acl('32.1.13.184').execute(CONTEXT6))
+
+        # A bit more complicated example, derived from resolver_config_unittest
+        acl = load_request_acl('[ {"action": "ACCEPT", ' +
+                               '     "from": "192.0.2.1"},' +
+                               '    {"action": "REJECT",' +
+                               '     "from": "192.0.2.0/24"},' +
+                               '    {"action": "DROP",' +
+                               '     "from": "2001:db8::1"},' +
+                               '] }')
+        self.assertEqual(ACCEPT, acl.execute(CONTEXT4))
+        self.assertEqual(REJECT, acl.execute(get_context('192.0.2.2')))
+        self.assertEqual(DROP, acl.execute(get_context('2001:db8::1')))
+        self.assertEqual(REJECT, acl.execute(get_context('2001:db8::2')))
+
+    def test_bad_execute(self):
+        acl = get_acl('192.0.2.1')
+        # missing parameter
+        self.assertRaises(TypeError, acl.execute)
+        # too many parameters
+        self.assertRaises(TypeError, acl.execute, get_context('192.0.2.2'), 0)
+        # type mismatch
+        self.assertRaises(TypeError, acl.execute, 'bad parameter')
 
 if __name__ == '__main__':
     unittest.main()