[Grok-dev] Re: HTTP PUT and HTTP DELETE security support

Martijn Faassen faassen at startifact.com
Thu May 17 14:57:56 EDT 2007


Hey,

Christian Theune wrote:
> I'll have a look at the Zope 3 code in the train that I'm about to jump
> on.

Great! I've done a little bit of digging myself, today, so we should 
compare notes.

I've been hacking in grok.publication. First I had to go and add the 
following:

class GrokHTTPPublication(ZopePublicationSansProxy, HTTPPublication):
     def getDefaultTraversal(self, request, ob):
         obj, path = super(GrokHTTPPublication, self).getDefaultTraversal(
             request, ob)
         return removeSecurityProxy(obj), path

class GrokHTTPFactory(HTTPFactory):
     def __call__(self):
         request, publication = super(GrokHTTPFactory, self).__call__()
         return request, GrokHTTPPublication

and register it like this:

   <publisher
       name="HTTP"
       factory=".publication.GrokHTTPFactory"
       methods="*"
       mimetypes="*"
       priority="1"
       />

The priority 1 makes it kick in before the default one in Zope.

Next, I ran into a snag with this code:

class ZopePublicationSansProxy(object):
     ...

     def callObject(self, request, ob):
         checker = selectChecker(ob)
         if checker is not None:
             checker.check(ob, '__call__')
         return super(ZopePublicationSansProxy, 
self).callObject(request, ob)

This code makes the assumption that a method '__call__' is always the 
thing to check. This is true in case of the view, but that's not right 
in case of PUT. Looks like I'll have to introduce my own version of 
callObject for GrokHTTPPublication that doesn't make this assumption. I 
hacked around this for now by disabling the whole checker thing altogether.

BrowserPublication handles GET and POST, where this assumption makes 
sense. In case of PUT and DELETE, it'll try to call a method called PUT 
or DELETE on a view registered for IHTTPRequest.

The Zope 3 code does the following, in zope.app.publication.http:

class HTTPPublication(BaseHTTPPublication):
     """HTTP-specific publication"""

     def callObject(self, request, ob):
         # Exception handling, dont try to call request.method
         orig = ob
         if not IHTTPException.providedBy(ob):
             ob = zapi.queryMultiAdapter((ob, request), name=request.method)
             ob = getattr(ob, request.method, None)
             if ob is None:
                 raise MethodNotAllowed(orig, request)
         return mapply(ob, request.getPositionalArguments(), request)


Note that the request object passed in is a IHTTPRequest, not a 
IBrowserRequest. This means that registering a view for PUT doesn't 
work. Instead you need a multi adapter:

class PUT(grok.MultiAdapter):
     grok.adapts(IMyContent, IHTTPRequest)
     grok.name('PUT')
     grok.implements(Interface)

     def __init__(self, obj, request):
         pass

     def PUT(self):
         ...

The grok.implements is necessary as MultiAdapters don't allow 
registration otherwise. For HTTP-level views this makes little sense. 
Unfortunately the name of the view *and* the method name needs to be 
called PUT, so that's Repeating Yourself.

To support PUT on existing objects, we could define a new kind of PutView:

class MyPUT(grok.PutView):
     grok.context(IMyContent)

     def update(self, ...):
        pass

     def render(self):
        pass

One snag is that the name of the put view doesn't haven't any meaning, 
unlike the normal grok.View. PUT happens directly onto an object, not 
onto a view. The rest makes sense though: a PUT can get parameters just 
like a GET or a POST, which explains why you might want update(), and 
can return results as well which explains render(). So perhapss we call 
it grok.Put:

class MyPut(grok.Put):
    pass

Actually it might not make as much sense as we want. The normal 
grok.View does processing on the request arguments or POST data and 
parses them into request.form. For PUT you'd typically want to get the 
raw response body. We could have that be automatically be available on 
the object (self.context, self.request, self.data), or alternatively we 
can pass it as a data argument to 'update'.

class Put(grok.Put):
    grok.context(IMyContent)

    def update(self, data):
        pass

    def render(self):
        pass

grok.Delete should be very similar.

The Ruby on Rails talk discusses higher level systems to make an 
application get standard REST behavior. The pattern tends to be (also 
gleaned from the Atom Publishing Protocol that I was pointed to):

GET on a container: get list of items in container (in XML, JSON, etc)
GET on container item: get container item data
POST on a container: create new object
PUT on container item: overwrite container item data
DELETE on container item: delete container item

Once we implement something like what I sketched above, we have most of 
the ingredients to support this quite nicely in Grok, except for POST to 
a container. I can do this clunkily by putting a special handler for 
POST requests in a view on the container, but it'd be nicer to be able 
to do something like:

class Post(grok.Post):
    grok.context(IMyContainer)

    def update(self, data):
       pass

    def render(self):
       pass

It will take a bit of work to make this happen though, as I don't think 
the zope 3 publisher supports doing this out of the box. We'll have to 
to make some code that looks for this Post thing first, and if it's not 
there, fall back on the normal view behavior. This has some performance 
implications, however (an extra view lookup).

Ruby on Rails goes a step further in automating this by actually 
providing default behavior for the various handlers. This would be cool 
but probably would be in a framework on top of Grok for now, not in the 
core.

Comments? Ideas? Eager Zope 3 publisher hackers volunteering to start 
building this? :)

Regards,

Martijn



More information about the Grok-dev mailing list