Commits

Paul J. Davis  committed 71074d5

Filter Python access with a function.

JavaScript requests for data from the Python VM can now be filtered by
specifying an access callback function on the Context instance. See the
README or test-access.py tests for API usage.

Thanks to Richard Boulton for the initial patch.

  • Participants
  • Parent commits 6c25f53

Comments (0)

Files changed (8)

     >>> fruit = Orange()
     >>> cx.add_global("apple", fruit)
     >>> cx.execute('"Show me the " + apple.is_ripe("raisin");')
-    Show me the ripe raisin
+    u'Show me the ripe raisin'
+
 
 Playing with Classes
 --------------------
     ...
     >>> rt = spidermonkey.Runtime()
     >>> cx = rt.new_context()
-    >>> cx.add_global(Monkey)
+    >>> cx.add_global("Monkey", Monkey)
     >>> monkey = cx.execute('var x = new Monkey(); x.baz = "schmammo"; x;')
     >>> monkey.baz
-    'schmammo'
+    u'schmammo'
     >>> monkey.__class__.__name__
     'Monkey'
 
+
 JavaScript Functions
 --------------------
 
     >>> cx = rt.new_context()
     >>> func = cx.execute('function(val) {return "whoosh: " + val;}')
     >>> func("zipper!");
-    'whoosh: zipper!'
+    u'whoosh: zipper!'
+
+
+Filtering access to Python
+--------------------------
+
+    >>> import spidermonkey
+    >>> rt = spidermonkey.Runtime()
+    >>> def checker(obj, name):
+    ...     return not name.startswith("_")
+    ...
+    >>> cx = rt.new_context(access=checker)
+    >>> # Alternatively:
+    >>> cx.set_access()                                     #doctest: +ELLIPSIS
+    <function checker at ...>
+    >>> cx.set_access(checker)                              #doctest: +ELLIPSIS
+    <function checker at ...>
+    >>> cx.add_global("fish", {"gold": "gone", "_old_lady": "huzza"})
+    >>> cx.execute('fish["_old_lady"];')
+    Traceback (most recent call last):
+            ...
+    JSError: Error executing JavaScript.
+
 
 Previous Authors
 ================
 
 Keiji Costantini
     * Bug report on the memory limit test.
+
+Richard Boulton
+    * Initial patch for filtering Python access.

File spidermonkey/context.c

 #include <jsobj.h>
 #include <jscntxt.h>
 
+// Forward decl for add_prop
+JSBool set_prop(JSContext* jscx, JSObject* jsobj, jsval key, jsval* rval);
+
 JSBool
 add_prop(JSContext* jscx, JSObject* jsobj, jsval key, jsval* rval)
 {
         ret = JS_TRUE;
         goto done;
     }
+    
+    // Check access to python land.
+    if(Context_has_access(pycx, jscx, pycx->global, pykey) <= 0) goto done;
 
     // Bail if the global doesn't have a __delitem__
     if(!PyObject_HasAttrString(pycx->global, "__delitem__"))
     pykey = js2py(pycx, key);
     if(pykey == NULL) goto done;
 
+    if(Context_has_access(pycx, jscx, pycx->global, pykey) <= 0) goto done;
+
     pyval = PyObject_GetItem(pycx->global, pykey);
     if(pyval == NULL)
     {
     pykey = js2py(pycx, key);
     if(pykey == NULL) goto done;
 
+    if(Context_has_access(pycx, jscx, pycx->global, pykey) <= 0) goto done;
+
     pyval = js2py(pycx, *rval);
     if(pyval == NULL) goto done;
 
 
     pykey = js2py(pycx, key);
     if(pykey == NULL) goto done;
+    
+    if(Context_has_access(pycx, jscx, pycx->global, pykey) <= 0) goto done;
 
     if(!PyMapping_HasKey(pycx->global, pykey))
     {
     Context* self = NULL;
     Runtime* runtime = NULL;
     PyObject* global = NULL;
+    PyObject* access = NULL;
 
-    if(!PyArg_ParseTuple(
-        args,
-        "O!|O",
+    char* keywords[] = {"runtime", "glbl", "access", NULL};
+
+    if(!PyArg_ParseTupleAndKeywords(
+        args, kwargs,
+        "O!|OO",
+        keywords,
         RuntimeType, &runtime,
-        &global
+        &global,
+        &access
     )) goto error;
 
+    if(global == Py_None) global = NULL;
+    if(access == Py_None) access = NULL;
+
     if(global != NULL && !PyMapping_Check(global))
     {
         PyErr_SetString(PyExc_TypeError,
         goto error;
     }
 
+    if(access != NULL && !PyCallable_Check(access))
+    {
+        PyErr_SetString(PyExc_TypeError,
+                            "Access handler must be callable.");
+        goto error;
+    }
+
     self = (Context*) type->tp_alloc(type, 0);
     if(self == NULL) goto error;
 
     if(global != NULL) Py_INCREF(global);
     self->global = global;
 
+    if(access != NULL) Py_INCREF(access);
+    self->access = access;
+
     // Setup counters for resource limits
     self->branch_count = 0;
     self->max_time = 0;
     }
 
     Py_XDECREF(self->global);
+    Py_XDECREF(self->access);
     Py_XDECREF(self->objects);
     Py_XDECREF(self->classes);
     Py_XDECREF(self->rt);
 }
 
 PyObject*
+Context_set_access(Context* self, PyObject* args, PyObject* kwargs)
+{
+    PyObject* ret = NULL;
+    PyObject* newval = NULL;
+
+    if(!PyArg_ParseTuple(args, "|O", &newval)) goto done;
+    if(newval != NULL && newval != Py_None)
+    {
+        if(!PyCallable_Check(newval))
+        {
+            PyErr_SetString(PyExc_TypeError,
+                                    "Access handler must be callable.");
+            ret = NULL;
+            goto done;
+        }
+    }
+
+    ret = self->access;
+
+    if(newval != NULL && newval != Py_None)
+    {
+        Py_INCREF(newval);
+        self->access = newval;
+    }
+
+    if(ret == NULL)
+    {
+        ret = Py_None;
+        Py_INCREF(ret);
+    }
+
+done:
+    return ret;
+}
+
+PyObject*
 Context_execute(Context* self, PyObject* args, PyObject* kwargs)
 {
     PyObject* obj = NULL;
         "Remove a global object in the JS VM."
     },
     {
+        "set_access",
+        (PyCFunction)Context_set_access,
+        METH_VARARGS,
+        "Set the access handler for wrapped python objects."
+    },
+    {
         "execute",
         (PyCFunction)Context_execute,
         METH_VARARGS,
     Context_new,                                /*tp_new*/
 };
 
+int
+Context_has_access(Context* pycx, JSContext* jscx, PyObject* obj, PyObject* key)
+{
+    PyObject* tpl = NULL;
+    PyObject* tmp = NULL;
+    int res = -1;
+
+    if(pycx->access == NULL)
+    {
+        res = 1;
+        goto done;
+    }
+
+    tpl = Py_BuildValue("(OO)", obj, key);
+    if(tpl == NULL) goto done;
+
+    tmp = PyObject_Call(pycx->access, tpl, NULL);
+    res = PyObject_IsTrue(tmp);
+
+done:
+    Py_XDECREF(tpl);
+    Py_XDECREF(tmp);
+
+    if(res < 0)
+    {
+        PyErr_Clear();
+        JS_ReportError(jscx, "Failed to check python access.");
+    }
+    else if(res == 0)
+    {
+        JS_ReportError(jscx, "Python access prohibited.");
+    }
+
+    return res;
+}
+
 PyObject*
 Context_get_class(Context* cx, const char* key)
 {

File spidermonkey/context.h

     PyObject_HEAD
     Runtime* rt;
     PyObject* global;
+    PyObject* access;
     JSContext* cx;
     JSObject* root;
     PyDictObject* classes;
 PyObject* Context_get_class(Context* cx, const char* key);
 int Context_add_class(Context* cx, const char* key, PyObject* val);
 
+int Context_has_access(Context*, JSContext*, PyObject*, PyObject*);
+
 int Context_has_object(Context* cx, PyObject* val);
 int Context_add_object(Context* cx, PyObject* val);
 int Context_rem_object(Context* cx, PyObject* val);

File spidermonkey/pyobject.c

     pykey = js2py(pycx, key);
     if(pykey == NULL) goto error;
 
+    if(Context_has_access(pycx, jscx, pyobj, pykey) <= 0) goto error;
+
     if(PyObject_DelItem(pyobj, pykey) < 0)
     {
         PyErr_Clear();
     pykey = js2py(pycx, key);
     if(pykey == NULL) goto done;
 
+    if(Context_has_access(pycx, jscx, pyobj, pykey) <= 0) goto done;
+    
     // Yeah. It's ugly as sin.
     if(PyString_Check(pykey) || PyUnicode_Check(pykey))
     {
         goto error;
     }
 
+    if(Context_has_access(pycx, jscx, pyobj, pykey) <= 0) goto error;
+
     pyval = js2py(pycx, *val);
     if(pyval == NULL)
     {
     PyObject* pyobj = NULL;
     PyObject* tpl = NULL;
     PyObject* ret = NULL;
+    PyObject* attrcheck = NULL;
     JSBool jsret = JS_FALSE;
     
     pycx = (Context*) JS_GetContextPrivate(jscx);
         goto error;
     }
 
+    // Use '__call__' as a notice that we want to execute a function.
+    attrcheck = PyString_FromString("__call__");
+    if(attrcheck == NULL) goto error;
+
+    if(Context_has_access(pycx, jscx, pyobj, attrcheck) <= 0) goto error;
+
     tpl = mk_args_tuple(pycx, jscx, argc, argv);
     if(tpl == NULL) goto error;
     
 success:
     Py_XDECREF(tpl);
     Py_XDECREF(ret);
+    Py_XDECREF(attrcheck);
     return jsret;
 }
 
     PyObject* pyobj = NULL;
     PyObject* tpl = NULL;
     PyObject* ret = NULL;
+    PyObject* attrcheck = NULL;
     JSBool jsret = JS_FALSE;
     
     pycx = (Context*) JS_GetContextPrivate(jscx);
         goto error;
     }
 
+    // Use '__init__' to signal use as a constructor.
+    attrcheck = PyString_FromString("__init__");
+    if(attrcheck == NULL) goto error;
+
+    if(Context_has_access(pycx, jscx, pyobj, attrcheck) <= 0) goto error;
+
     tpl = mk_args_tuple(pycx, jscx, argc, argv);
     if(tpl == NULL) goto error;
     

File spidermonkey/runtime.c

 {
     PyObject* cx = NULL;
     PyObject* tpl = NULL;
-    PyObject* global = NULL;
+    PyObject* global = Py_None;
+    PyObject* access = Py_None;
 
-    if(!PyArg_ParseTuple(args, "|O", &global)) goto error;
-    
-    if(global == NULL)
-    {
-        tpl = Py_BuildValue("(O)", self);
-    }
-    else
-    {
-        tpl = Py_BuildValue("(OO)", self, global);
-    }
+    char* keywords[] = {"glbl", "access", NULL};
+
+    if(!PyArg_ParseTupleAndKeywords(
+        args, kwargs,
+        "|OO",
+        keywords,
+        &global,
+        &access
+    )) goto error;
+
+    tpl = Py_BuildValue("OOO", self, global, access);
     if(tpl == NULL) goto error;
-    
+
     cx = PyObject_CallObject((PyObject*) ContextType, tpl);
     goto success;
 

File tests/test-access.py

+# Copyright 2009 Paul J. Davis <paul.joseph.davis@gmail.com>
+# Copyright 2009 Richard Boulton <richard@tartarus.org>
+#
+# This file is part of the python-spidermonkey package released
+# under the MIT license.
+import t
+
+@t.cx()
+def test_set_callback(cx):
+    t.eq(cx.set_access(), None)
+    t.raises(TypeError, cx.set_access, "foo")
+    t.raises(TypeError, cx.set_access, 1)
+    def cb(obj, name): return True
+    t.eq(cx.set_access(cb), None)
+    t.eq(cx.set_access(), cb)
+    # Check that we don't erase it.
+    t.eq(cx.set_access(), cb)
+
+@t.cx()
+def test_always_callback(cx):
+    names = []
+    def fn(obj, name):
+        names.append(name)
+        return True
+    cx.set_access(fn)
+    cx.add_global("foo", {"bar": "baz"})
+    t.eq(cx.execute('foo["bar"]'), "baz")
+    t.eq(names, ["bar"])
+
+@t.cx()
+def test_no_underscores(cx):
+    def fn(obj, name):
+        return name.strip()[:1] != "_"
+    cx.set_access(fn)
+    cx.add_global("foo", {"bar": "baz", "_bar": "_baz"})
+    t.eq(cx.execute('foo["bar"]'), "baz")
+    t.raises(t.JSError, cx.execute, 'foo["_bar"]')
+
+@t.cx()
+def test_no_set_invalid(cx):
+    def fn(obj, name):
+        return name.strip()[:1] != "_"
+    cx.set_access(fn)
+    glbl = {}
+    cx.add_global("foo", glbl)
+    cx.execute('foo["bar"] = "baz";')
+    t.raises(t.JSError, cx.execute, 'foo["_bar"] = "baz"')
+    t.eq(glbl, {"bar": "baz"})
+
+@t.rt()
+def test_access_in_ctor(rt):
+    def fn(obj, name):
+        return name != "bing"
+    cx = rt.new_context(access=fn)
+    cx.add_global("foo", {"bing": "boo"})
+    t.raises(t.JSError, cx.execute, 'foo["bing"];')
+
+@t.rt()
+def test_access_global(rt):
+    names = []
+    def fn(obj, name):
+        names.append(name)
+        return not name.startswith("_")
+    glbl = {"foo": "bar", "_bin": "zingle"}
+    cx = rt.new_context(glbl, fn)
+    t.eq(cx.execute('foo;'), "bar")
+    t.raises(t.JSError, cx.execute, '_bin;')
+    t.eq(names, ["foo", "foo", "_bin"])
+
+@t.cx()
+def test_dissallow_ctor(cx):
+    class DirtyCar(object):
+        def __init__(self):
+            self.zap = 2
+    def check(obj, name):
+        return name != "__init__"
+    cx.add_global("DirtyCar", DirtyCar)
+    cx.set_access(check)
+    t.raises(t.JSError, cx.execute, "DirtyCall();")
+
+@t.cx()
+def test_dissalow_call(cx):
+    class PepsiCan(object):
+        def __init__(self):
+            self.caffeine = "zaney!"
+        def __call__(self, arg):
+            return arg * 2
+    def check(obj, name):
+        return name != "__call__"
+    cx.add_global("PepsiCan", PepsiCan)
+    cx.set_access(check)
+    t.eq(cx.execute("var c = new PepsiCan(); c.caffeine;"), "zaney!")
+    t.raises(t.JSError, cx.execute, "c();")
+
+@t.cx()
+def test_on_wrapped_obj(cx):
+    class ShamWow(object):
+        def __init__(self):
+            self.bar = 2
+            self._bing = 3
+    def func():
+        return ShamWow()
+    cx.add_global("whee", func)
+
+    def check(obj, name):
+        return name in ["__call__", "__init__"] or not name.startswith("_")
+    cx.set_access(check);
+
+    t.eq(cx.execute("var f = whee(); f.bar;"), 2)
+    t.raises(t.JSError, cx.execute, "f._bing")
+
+@t.cx()
+def test_obj_method(cx):
+    class Checker(object):
+        def __init__(self):
+            self.names = []
+        def check(self, obj, name):
+            self.names.append(name)
+            return name != "kablooie"
+    c = Checker()
+    cx.set_access(c.check)
+    cx.add_global("bing", {"kablooie": "bar", "bing": 3})
+    t.eq(cx.execute('bing["bing"]'), 3)
+    t.raises(t.JSError, cx.execute, 'bing["kablooie"]')
+    t.eq(c.names, ["bing", "kablooie"])

File tests/test-global.py

     t.eq(cx.execute("foo;"), "bar")
 
 @t.rt()
+def test_py_global_by_kwarg(rt):
+    glbl = {"foo": "bar"}
+    cx = rt.new_context(glbl=glbl)
+    t.eq(cx.execute("foo;"), "bar")
+
+@t.rt()
 def test_py_set_global(rt):
     glbl = {}
     cx = rt.new_context(glbl)