Group-Level Security¶
Groups are a very common way of organizing users and mapping them to a
permission. It is not very fine-grained, but it is simple, and
enough for a lot of applications. It is also a good base to begin other
types of authorization. We’ll be using Pyramid’s default
authorization policy, the ACLAuthorizationPolicy
.
The demo application defines 2 groups: “editor” and “admin”. It also
supports a third principal pyramid.security.Authenticated
, which
is automatically added by Pyramid’s baked-in authentication policies.
Goals¶
Define 2 groups, “editor” and “admin”.
Restrict creation of pages to authenticated users.
Restrict editing of existing pages to users in the “editor” or “admin” groups.
Restrict viewing of user-related pages to only “admin” users.
Access Control Lists¶
In Pyramid, groups would be more accurately described as principal
identifiers, generated by the authentication policy. Pyramid’s
default authentication policies support a groupfinder callback which,
when passed the detected userid should return a list of principals or
None
if the userid is invalid (e.g. maybe it has been deleted).
From the Base Application, the User
object already has a list of
groups. Let’s add a groupfinder which can map the current user
to its groups:
def groupfinder(userid, request):
user = USERS.get(userid)
if user:
return ['g:%s' % g for g in user.groups]
The groups are prefixed with the “g:” to help distinguish them as principals related to the user’s groups.
Pyramid’s ACLAuthorizationPolicy
works by mapping the
effective principals from the groupfinder into an access control
list (ACL). Each entry in the list is a 3-tuple mapping a
principal to a permission. An entry can either allow
or deny access and the first entry matching one of the principals to
the permission wins.
Below is an example ACL that would allow any authenticated user the ability to create content, but disallow them from editing content unless they are an “editor” or an “admin”.
[
(Allow, Authenticated, 'create'),
(Allow, 'g:editor', 'edit'),
(Allow, 'g:admin', ALL_PERMISSIONS),
]
Notice how the principals in the ACL match up with the principals
returned from the groupfinder. Pyramid’s default authentication
policies will automatically add the pyramid.security.Everyone
principal to the list, and the pyramid.security.Authenticated
principal if the groupfinder returned anything other than None
.
Remember that None
would signify that the user is not authenticated.
The pyramid.security.ALL_PERMISSIONS
permission is a “magic”
identifier that matches any permission.
Root Factory¶
URL Dispatch’s use of the resource tree is fairly simple. By
default the traversal path is effectively '/'
, meaning that the
root of the tree will always be the context. As mentioned
briefly above, the ACLAuthorizationPolicy
uses the current
context to compute an ACL which maps principals to permissions.
This is done via the __acl__
property of the context.
class Root(object):
__acl__ = [
(Allow, Authenticated, 'create'),
(Allow, 'g:editor', 'edit'),
(Allow, 'g:admin', ALL_PERMISSIONS),
]
def __init__(self, request):
self.request = request
See how the __acl__
defines the ACL discussed previously. This
Root
object is also a root factory and the application can
be configured to use it as such. Root factories are explained in more
detail in The Resource Tree.
def main(global_settings, **settings):
config = Configurator(settings=settings)
authn_policy = AuthTktAuthenticationPolicy(
settings['auth.secret'],
callback=groupfinder,
)
authz_policy = ACLAuthorizationPolicy()
config.set_authentication_policy(authn_policy)
config.set_authorization_policy(authz_policy)
config.set_root_factory(Root)
Securing the Views¶
We’ve setup a single-node resource tree, we’ve configured our
authentication policy with a groupfinder, and we’ve set the
authorization policy to the ACLAuthorizationPolicy
. All
that’s left to do now is to start securing parts of our app.
Create Page View¶
As mentioned before, only authenticated users should be allowed to
create new pages in our wiki. Thus, our ACL maps the Authenticated
principal to the “create” permission. Now we can lock down the view
for '/create_page'
to require the “create” permission.
@view_config(
route_name='create_page',
permission='create',
renderer='edit_page.mako',
)
def create_page_view(request):
owner = request.authenticated_userid
Edit Page View¶
Editing of pages in the wiki via '/page/{title}/edit'
should only be
allowed by users in the “editor” or “admin” groups. We’ve already
mapped these groups to the “edit” permission, so simply add it to the
view.
route_name='edit_page',
permission='edit',
renderer='edit_page.mako',
)
def edit_page_view(request):
uri = request.matchdict['title']
User Views¶
The user-related pages '/users'
and '/user/{login}'
should only
be accessible by “admin” users. There isn’t an ACL explicitly mapping
the “admin” group to an permission for this, but the ALL_PERMISSIONS
magic permission is a catch-all, so we can make up any permission we want.
Here we’ll use the “admin” permission.
'/users'
:
@view_config( route_name='users', permission='admin', renderer='users.mako', ) def users_view(request):
'/user/{login}'
:
@view_config( route_name='user', permission='admin', renderer='user.mako', ) def user_view(request):
Simple Object-Level Authorization¶
We’ve added basic group-level access to our views. This scheme can actually do a lot more than simple groups, as hinted by the fact that the groups are actually a more general concept of principals.
Until now, we’ve treated each principal as a group and the
__acl__
property of the context as unchanging. Let’s instead
think of each principal as representing a resource identifier, and the
ACL as being dynamic depending on what resource is being accessed. For
example, the context knows that the current request is for a
Page
object with id == 1
. The context can make the
__acl__
property dynamic and populate it with a special entry giving
users with the principal 'page:1'
access. Now, all the
authentication policy must do is return a list of all pages
the user can access.
Below is some pseudocode for playing with this idea:
class Root(object):
@property
def __acl__(self):
if 'page' in self.request.matchdict:
page = self.request.matchdict['page']
return [(Allow, 'page:'+page, 'page')]
return []
def __init__(self, request):
self.request = request
Notice that the __acl__
on Root
is now dynamic, and changes
based on the request. The principal 'page:???'
must then match
one of the principals returned by the groupfinder below.
def groupfinder(userid, request):
user = User.get_by_id(userid)
if user is not None:
return ['page:'+page for page in user.pages]
This method suffers from certain drawbacks, such as needing to query the database for a list of all of the objects to which the user has access on every request. If this is a concern, the Object-Level Security demo provides a more efficient and flexible way to perform fine-grained security on the resources in the application.