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.
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.
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.
The cascading nature of the tree can be prevented in 2 simple ways without replacing the ACLAuthorizationPolicy with a different implementation.
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.
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.
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.
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}'.
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
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'),
]
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'.
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}'.
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
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'),
]
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'.
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.
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.
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.