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

  1. Define 2 groups, “editor” and “admin”.

  2. Restrict creation of pages to authenticated users.

  3. Restrict editing of existing pages to users in the “editor” or “admin” groups.

  4. 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.