Fork me on GitHub

Object-Level Security

This demo builds on Group-Level Security by looking at how to implement more fine-grained permissions. We’ll be using Pyramid’s default authorization policy, the ACLAuthorizationPolicy.

Goals

  1. Maintain the previous group-level associations.
  2. Allow editing of pages by the creator of the page.
  3. Allow viewing of user-related pages by their respective users.

Why A Resource “Tree”?

Trees provide a natural way to describe hierarchical data. This happens to be, very often, how people think about permissions. If a user has the “edit” permission on the node containing all Page objects in the tree, then they usually also have the “edit” permission on the Page objects themselves, unless explicitly denied by a specific Page instance.

Our demo is focused on a resource tree similar to the following:

root (/)                   (Root)
|- users                   (UserFactory)
|  `- {login}              (User)
`- pages                   (PageFactory)
   `- {page}               (Page)

Except that in order to illustrate the usage of the multiple factories, the individual branches are broken into their own individual trees. The downside to breaking up the tree is that there is no longer a common location to provide a default ACL, for example to simply give full access to “admin” users. On the flip side, the trees become much simpler to understand as they are more focused.

Location-Aware Resources and Hierarchical Permissions

The ACLAuthorizationPolicy works on this idea by supporting cascading ACLs. Remember that the ACLAuthorizationPolicy operates on 3 parameters, the principals, the permission and the context. The context is the last node traversed in the resource tree and it is the first place the policy checks for an ACL. If no matching entry is found, the policy will then recursively check the parent of the context for an ACL and matching entry, bubbling up the tree until it hits the root. In this way, hierarchical permissions are automatically handled when organizing resources and ACLs into a resource tree.

The ACLAuthorizationPolicy can recursively travel through the lineage from the context to the root due to what Pyramid calls location-aware resources, meaning they implement the __parent__ and __name__ attributes.

Disabling Hierarchical Permissions

The cascading nature of the tree can be prevented in 2 simple ways without replacing the ACLAuthorizationPolicy with a different implementation.

  1. Add an entry at the end of each ACL for pyramid.security.DENY_ALL which is effectively an alias for the entry (Deny, Everyone, ALL_PERMISSIONS). If no matching entry is found in the ACL prior to this entry, then this one will match effectively preventing the policy from going any further.
  2. Do not create location-aware resources. i.e. Do not add __parent__ properties to your resources.

Resource Factories

From the Group-Level Security demo we saw how to create a single-node resource tree that could map a user’s list of principal identifiers to the __acl__ property of the Root, resulting in either being allowed or denied access to a resource based on the required permission.

It’s actually possible to have many resource trees in a single Pyramid application. The root factory, supplied with the request object, can return the root of any tree it wishes. Each route, when using URL Dispatch, can also define its own factory. It’s this last point that we’ll explore further.

As Pyramid processes an incoming request, the router first finds a matching route for the requested URL, then performs traversal on the resource tree returned from the root factory. If the route itself does not define a factory, the default root factory will be used to compute the resource tree. There are actually 4 possibilities for the path used in traversal.

  1. If no route is matched, traversal occurs on the URL path.
  2. If a route is matched and the route defines a *traverse pattern, the matching part of the URL will be used.
  3. If a route is matched and the route defines a traverse argument then the supplied path will be traversed.
  4. If a route is matched and no traversal path has been defined then traversal occurs effectively on the / path, returning the root of the tree as the context. Note that this is how the Group-Level Security demo is configured.

Why Is This Interesting?

An URL in a “REST”-y world (to beat a dead term) is intended to focus on a single resource. This maps very well to Pyramid’s concept of traversing a tree of resources. Object-level security can be thought of in this way, where the resource in question is modifiable by specific users (actually principals). We can define a root factory that knows how to load a specific type of resource and attach it to only relevant URLs. Examples of this idea are shown below.

Securing the Views

The only change between the object-level demo and the group-level demo from the previous section are how the ACLs are computed. The permission on each view remains unchanged.

Note

Because traversal is loading the actual objects from the model to determine the ACL, the views no longer need to explicitly load those objects. Instead they can just use the context which will be a User or Page object. This of course only works for loading of simple objects, more complex queries would still need to be done within the views themselves.

User Pages

For user-related pages in our application, our goal was to secure them based on the relationship between the currently logged in user and the requested user’s page. We’ll use a simple resource tree with only one branch, traversed via the path '/{login}'.

User Factory

For these pages we can use the UserFactory to provide default ACL for any user page.

class UserFactory(object):
    __acl__ = [
        (Allow, 'g:admin', ALL_PERMISSIONS),
    ]

    def __init__(self, request):
        self.request = request

    def __getitem__(self, key):
        user = USERS[key]
        user.__parent__ = self
        user.__name__ = key
        return user

User Resources

A User object is created and returned by the UserFactory based on the login property. A User resource should only be accessible by the user itself, or an “admin” (which we’ve covered already in the UserFactory). To enable this object-level access to the specific resource, the User implements a dynamic __acl__ by turning the class variable into a property which can be computed per-instance of the object. The new ACL contains an entry for a principal matching the login property of the object.

class User(object):
    @property
    def __acl__(self):
        return [
            (Allow, self.login, 'view'),
        ]

Defining the Routes

Traversal of the new tree is done by configuring the user-related routes to use the new UserFactory instead of the default Root factory.

    config.add_route('users', '/users', factory=UserFactory)
    config.add_route('user', '/user/{login}', factory=UserFactory,
                     traverse='/{login}')

The 'user' route also overrides the traverse parameter to load the User object for that URL. The matched login in the route will be substituted into the traversal path. For example, a request for the '/user/michael' URL, will cause the traversal path to be '/michael'.

Wiki Pages

The biggest change to the wiki pages is giving the creator of the page the ability to edit it, even if they aren’t in the “editor” group. The resource tree is a single branch traversed via the path '/{title}'.

Page Factory

For these pages we can use the PageFactory to provide default ACL for any wiki page.

class PageFactory(object):
    __acl__ = [
        (Allow, Everyone, 'view'),
        (Allow, Authenticated, 'create'),
    ]

    def __init__(self, request):
        self.request = request

    def __getitem__(self, key):
        page = PAGES[key]
        page.__parent__ = self
        page.__name__ = key
        return page

Page Resources

A Page object is created and returned by the PageFactory based on the uri property. A Page resource should only be editable by the creator, an “editor” or an “admin” (which we’ve covered already in the PageFactory). To enable this object-level access to the specific resource, the Page implements a dynamic __acl__ by turning the class variable into a property which can be computed per-instance of the object. The new ACL contains an entry for a principal matching the owner property of the object.

class Page(object):
    @property
    def __acl__(self):
        return [
            (Allow, self.owner, 'edit'),
            (Allow, 'g:editor', 'edit'),
        ]

Defining the Routes

Traversal of the new tree is done by configuring the wiki-related routes to use the new PageFactory instead of the default Root factory.

    config.add_route('pages', '/pages', factory=PageFactory)
    config.add_route('create_page', '/create_page', factory=PageFactory)
    config.add_route('page', '/page/{title}', factory=PageFactory,
                     traverse='/{title}')
    config.add_route('edit_page', '/page/{title}/edit', factory=PageFactory,
                     traverse='/{title}')

The 'page' and 'edit_page' routes also override the traverse parameter to load the Page object for that URL. The matched title in the route will be substituted into the traversal path. For example, a request for the '/page/hello' URL, will cause the traversal path to be '/hello'.

Summary

We’ve seen how to build different resource trees into different parts of our site, as well as how to configure hierarchical permissions via the structure of the tree and location-aware resources.

Object or row-level security on User and Page objects was achieved via dynamic ACLs on the respective objects.

Isn’t This Just Traversal? Why Even Bother With URL Dispatch?

It’s very close to traversal. The traversal machinery is executed whether using URL Dispatch or Traversal. The difference is simply how view lookup occurs. When using Traversal, the computed context and view name are the key factors in determining which view is executed. In URL Dispatch it is dependent on which route matched and the context is used in the background almost purely for security purposes.

A lot of times we may not have full control over our URLs due to legacy code, or just disagreements on design. In those cases it’s still important to provide a way to map those routes to resources in our system and the custom factory and traverse arguments allow for that level of customization.

Where To Go From Here?

Pyramid’s entire auth system is pluggable. Almost all of the examples of authorization depend on the ACLAuthorizationPolicy because it is so flexible. However, it could be replaced with something that did group or object lookup independent of the actual traversal context, or it could give an entirely new meaning to the context in terms of security.