9

I've been reading on the ways to implement authorization (and authentication) to my newly created Pyramid application. I keep bumping into the concept called "Resource". I am using python-couchdb in my application and not using RDBMS at all, hence no SQLAlchemy. If I create a Product object like so:

class Product(mapping.Document):
  item = mapping.TextField()
  name = mapping.TextField()
  sizes = mapping.ListField()

Can someone please tell me if this is also called the resource? I've been reading the entire documentation of Pyramids, but no where does it explain the term resource in plain simple english (maybe I'm just stupid). If this is the resource, does this mean I just stick my ACL stuff in here like so:

class Product(mapping.Document):
  __acl__ = [(Allow, AUTHENTICATED, 'view')]
  item = mapping.TextField()
  name = mapping.TextField()
  sizes = mapping.ListField()

  def __getitem__(self, key):
      return <something>

If I were to also use Traversal, does this mean I add the getitem function in my python-couchdb Product class/resource?

Sorry, it's just really confusing with all the new terms (I came from Pylons 0.9.7).

Thanks in advance.

tshepang
  • 12,111
  • 21
  • 91
  • 136
Mark
  • 2,137
  • 4
  • 27
  • 42

2 Answers2

6

I think the piece you are missing is the traversal part. Is Product the resource? Well it depends on what your traversal produces, it could produce products.....

Perhaps it might be best to walk this through from the view back to how it gets configured when the application is created...

Here's a typical view.

  @view_config(context=Product, permission="view")
  def view_product(context, request):
      pass # would do stuff  

So this view gets called when context is an instance of Product. AND if the acl attribute of that instance has the "view" permission. So how would an instance of Product become context?

This is where the magic of traversal comes in. The very logic of traversal is simply a dictionary of dictionaries. So one way that this could work for you is if you had a url like

/product/1

Somehow, some resource needs to be traversed by the segments of the url to determine a context so that a view can be determined. What if we had something like...

  class ProductContainer(object):
      """
      container = ProductContainer()
      container[1]
      >>> <Product(1)>
      """
      def __init__(self, request, name="product", parent=None):
          self.__name__ = name
          self.__parent__ = parent
          self._request = request

      def __getitem__(self, key):
          p = db.get_product(id=key)

          if not p:
              raise KeyError(key)
          else:
              p.__acl__ = [(Allow, Everyone,"view")]
              p.__name__ = key
              p.__parent__ = self
              return p

Now this is covered in the documentation and I'm attempting to boil it down to the basics you need to know. The ProductContainer is an object that behaves like a dictionary. The "name" and "parent" attributes are required by pyramid in order for the url generation methods to work right.

So now we have a resource that can be traversed. How do we tell pyramid to traverse ProductContainer? We do that through the Configurator object.

  config = Configurator()
  config.add_route(name="product",
                   path="/product/*traverse",
                   factory=ProductContainer)
  config.scan()
  application = config.make_wsgi_app()

The factory parameter expects a callable and it hands it the current request. It just so happens that ProductContainer.init will do that just fine.

This might seem a little much for such a simple example, but hopefully you can imagine the possibilities. This pattern allows for very granular permission models.

If you don't want/need a very granular permission model such as row level acl's you probably don't need traversal, instead you can use routes with a single root factory.

  class RootFactory(object):
      def __init__(self, request):
          self._request = request
          self.__acl__ = [(Allow, Everyone, "view")]  # todo: add more acls


  @view_config(permission="view", route_name="orders")
  def view_product(context, request):
      order_id, product_id = request.matchdict["order_id"], request.matchdict["product_id"]
      pass # do what you need to with the input, the security check already happened

  config = Configurator(root_factory=RootFactory)

  config.add_route(name="orders",
                   path="/order/{order_id}/products/{product_id}")

  config.scan()
  application = config.make_wsgi_app()

note: I did the code example from memory, obviously you need all the necessary imports etc. in other words this isn't going to work as a copy/paste

Tom Willis
  • 5,250
  • 23
  • 34
  • Damn...I still can't grasp this abstract concept. Does the __name__ value have to be the same as the one in config.add_route(name="")? Is that how Traversal figures out which which Resource to use when matching an incoming URL like /photo/1? Can I try to illustrate an example of my app? My app handles orders of products. Each order may have 1 or many products in them. Does this mean that the __parent__ attribute should be set to the Order resource and the __name__ attribute should be the name of the Product resource? – Mark Mar 03 '12 at 10:29
  • Sorry, the above was not long enough. Looking at the ProductConatiner, is that a "Factory" like what is mentioned in the docs? Does this mean I have to create a factory for each resource, given that my resource could very well be the python-couchdb object called Product? – Mark Mar 03 '12 at 10:30
  • the name in add_route has to be unique for all routes in the system. but it doesn't have to match a segment in the path. I could just as well have named it "products". – Tom Willis Mar 03 '12 at 13:51
  • 1
    this is moving away from the original question I was answering but I will try to be helpful here. A factory is merely a function that produces something. A contstructor/__init__ is a factory by that definition. what pyramid wants in the add_route function is a function that will return something when called with 1 parameter which is the current request. ... – Tom Willis Mar 03 '12 at 14:57
  • 1
    if I were implementing your app and I wanted something equivalent to row level acl's I would have an OrderContainer and a ProductContainer. If you don't need row level acl's, you don't need an OrderContainer/ProductContainer in my opinion. You could instead pass in a root_factory to the Configurator.__init__ and set your acls there. and then use routes like you are used to – Tom Willis Mar 03 '12 at 15:05
  • I added an example that uses url dispatch instead of traversal which may make more sense to you coming from pyramid. it's the simplest example I could come up with hope that helps. – Tom Willis Mar 03 '12 at 16:23
  • 1
    As this is related directly to dispatch+traversal I'd suggest reading http://michael.merickel.org/projects/pyramid_auth_demo/ and looking at the code if you need something different from Tom's excellent answer. – Michael Merickel Mar 03 '12 at 19:48
  • Hmm thanks. The thing is I need traversal because I am required to perform very granular permissions. Trying to understand this whole traversal monster has given me a headache for the past two days. Looking through the docs, is your example of the ProductContainer another way of saying Resource Interface? I'll definitely try out the pyramid_auth_demo – Mark Mar 04 '12 at 17:51
  • Yeah I think ProductContainer is probably what would be referred to as a Resource Interface. Traversal is just using the segments of a url to determine a context object for a view by by consulting a nested dict like objects. It's a foreign concept but it's not a complext concept, after all they are just python objects. :) – Tom Willis Mar 04 '12 at 19:33
  • Ok having read through some more examples and documentation and reading other people's code, I realized the Root class is referring to the url "/". If my application has urls that go like: "/product or /order or /production or /customer", does this mean that in my Root class, I have to implement "if key == 'customer' or if key == 'production' or if key == 'order'" in the __getitem__ function? – Mark Mar 05 '12 at 15:39
  • Thank you so much Tom & not forgetting Michael. I just got my "a-ha" moment 3 hours ago. Now I am truly amazed with this whole zope/traversal mechanism; makes me feel like taking another look at Plone/Zope. I feeling a "high" that can only be expressed in code. – Mark Mar 05 '12 at 19:25
  • Now that I understand how this whole thing works, I'll be writing a blog-post this weekend to re-explain some of the concepts that were mentioned in the docs that I feel could be expressed in more "lay-man" terms. One thing that just struck me though: Does this mean that as a developer, my first step in creating a traversal application would be to have a good and thorough understanding of the possible URL structure for all pages in my application? – Mark Mar 05 '12 at 19:28
  • I would say knowing what urls your app will have is critical. But that may be my opinion. It's always step one for me :) – Tom Willis Mar 05 '12 at 21:54
  • I know this question is kinda old, but you just helped me understand Traversal and use it in a problem I was stuck in for a long time. Thanks. – RedBaron Aug 20 '15 at 12:03
  • @RedBaron glad it helped. I dont do pyramid for work anymore(ruby instead), and boy do I miss having traversal in my tool belt. – Tom Willis Aug 21 '15 at 19:51
0

Have you worked through http://michael.merickel.org/projects/pyramid_auth_demo/ ? If not, I suspect it may help. The last section http://michael.merickel.org/projects/pyramid_auth_demo/object_security.html implements the pattern you're after (note the example "model" classes inherit from nothing more complex than object).

thruflo
  • 668
  • 1
  • 7
  • 9