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¶
Maintain the previous group-level associations.
Allow editing of pages by the creator of the page.
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.
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.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.
If no route is matched, traversal occurs on the URL path.
If a route is matched and the route defines a
*traverse
pattern, the matching part of the URL will be used.If a route is matched and the route defines a
traverse
argument then the supplied path will be traversed.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.