7. Authorization

At this point we have a fully working application, but you may have noticed that anyone can alter our entries. We should change that by introducing user authorization, where we assign security statements to resources (e.g., blog entries) describing the permissions required to perform an operation (e.g., add or edit a blog entry).

For the sake of simplicity, in this tutorial we will assume that every user can edit every blog entry as long as they are signed in to our application.

Pyramid provides some ready-made policies for this, as well as mechanisms for writing custom policies.

We will use the policies provided by the framework:

  • AuthTktAuthenticationPolicy

    Obtains user data from a Pyramid “auth ticket” cookie.

  • ACLAuthorizationPolicy

    An authorization policy which consults an ACL object attached to a context to determine authorization information about a principal or multiple principals.

OK, so the description for ACLAuthorizationPolicy has a lot of scary words in it, but in practice it’s a simple concept that allows for great flexibility when defining permission systems.

The policy basically checks if a user has a permission to the specific context of a view based on Access Control Lists.

What does this mean? What is a context?

A context could be anything. Imagine you are building a forum application, and you want to add a feature where only moderators will be able to edit a specific topic in a specific forum. In this case, our context would be the forum object; it would have info attached to it about who has specific permissions to this resource.

Or something simpler, who can access admin pages? In this case, a context would be an arbitrary object that has information attached to it about who is an administrator of the site.

How does this relate to our application?

Since our application does not track who owns blog entries, we will assume the latter scenario: any authenticated (logged in) user has authorization to administer the blog entries. We will make the most trivial context factory object. As its name implies, the factory will return the context object (in our case, an arbitrary class). It will say that everyone logged in to our application can create and edit blog entries.

Create a context factory

In the root of our application package, let’s create a new file called security.py with the following content.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from pyramid.security import Allow, Everyone, Authenticated


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

    def __init__(self, request):
        pass

This is the object that was mentioned a moment ago, a context factory. It’s not tied to any specific entity in a database, and it returns an __acl__ property which says that everyone has a 'view' permission, and users that are logged in also have 'create' and 'edit' permissions.

Create authentication and authorization policies

Now it’s time to tell Pyramid about the policies we want to register with our application.

Let’s open our configuration __init__.py at the root of our project, and add the following imports as indicated by the emphasized lines.

1
2
3
from pyramid.config import Configurator
from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy

Now it’s time to update our configuration. We need to create our policies, and pass them to the configurator. Add or edit the emphasized lines.

 9
10
11
12
13
14
15
    authentication_policy = AuthTktAuthenticationPolicy('somesecret')
    authorization_policy = ACLAuthorizationPolicy()
    with Configurator(settings=settings,
                      authentication_policy=authentication_policy,
                      authorization_policy=authorization_policy) as config:
        config.include('.models')
        config.include('pyramid_jinja2')

The string “somesecret” passed into the policy will be a secret string used for cookie signing, so that our authentication cookie is secure.

The last thing we need to add is to assign our context factory to our routes. We want this to be the route responsible for entry creation and updates. Modify the following emphasized lines.

1
2
3
4
5
6
7
def includeme(config):
    config.add_static_view('static', 'static', cache_max_age=3600)
    config.add_route('home', '/')
    config.add_route('blog', '/blog/{id:\d+}/{slug}')
    config.add_route('blog_action', '/blog/{action}',
                     factory='pyramid_blogr.security.BlogRecordFactory')
    config.add_route('auth', '/sign/{action}')

Now for the finishing touch. We set “create” and “edit” permissions on our views. Open views/blog.py, and change our @view_config decorators as shown by the following emphasized lines.

18
19
20
@view_config(route_name='blog_action', match_param='action=create',
             renderer='pyramid_blogr:templates/edit_blog.jinja2',
             permission='create')
31
32
33
@view_config(route_name='blog_action', match_param='action=edit',
             renderer='pyramid_blogr:templates/edit_blog.jinja2',
             permission='create')

Current state of our application

For convenience here are the two files you have edited in their entirety up to this point (security.py was already rendered above).

__init__.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from pyramid.config import Configurator
from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy


def main(global_config, **settings):
    """ This function returns a Pyramid WSGI application.
    """
    authentication_policy = AuthTktAuthenticationPolicy('somesecret')
    authorization_policy = ACLAuthorizationPolicy()
    with Configurator(settings=settings,
                      authentication_policy=authentication_policy,
                      authorization_policy=authorization_policy) as config:
        config.include('.models')
        config.include('pyramid_jinja2')
        config.include('.routes')
        config.scan()
    return config.make_wsgi_app()

views/blog.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from pyramid.view import view_config
from pyramid.httpexceptions import HTTPNotFound, HTTPFound
from ..models.blog_record import BlogRecord
from ..services.blog_record import BlogRecordService
from ..forms import BlogCreateForm, BlogUpdateForm


@view_config(route_name='blog',
             renderer='pyramid_blogr:templates/view_blog.jinja2')
def blog_view(request):
    blog_id = int(request.matchdict.get('id', -1))
    entry = BlogRecordService.by_id(blog_id, request)
    if not entry:
        return HTTPNotFound()
    return {'entry': entry}


@view_config(route_name='blog_action', match_param='action=create',
             renderer='pyramid_blogr:templates/edit_blog.jinja2',
             permission='create')
def blog_create(request):
    entry = BlogRecord()
    form = BlogCreateForm(request.POST)
    if request.method == 'POST' and form.validate():
        form.populate_obj(entry)
        request.dbsession.add(entry)
        return HTTPFound(location=request.route_url('home'))
    return {'form': form, 'action': request.matchdict.get('action')}


@view_config(route_name='blog_action', match_param='action=edit',
             renderer='pyramid_blogr:templates/edit_blog.jinja2',
             permission='create')
def blog_update(request):
    blog_id = int(request.params.get('id', -1))
    entry = BlogRecordService.by_id(blog_id, request)
    if not entry:
        return HTTPNotFound()
    form = BlogUpdateForm(request.POST, entry)
    if request.method == 'POST' and form.validate():
        del form.id  # SECURITY: prevent overwriting of primary key
        form.populate_obj(entry)
        return HTTPFound(
            location=request.route_url('blog', id=entry.id,slug=entry.slug))
    return {'form': form, 'action': request.matchdict.get('action')}

Now if you try to visit the links to create or update entries, you will see that they respond with a 403 HTTP status because Pyramid detects that there is no user object that has edit or create permissions.

Our views are secured!

Next: 8. Authentication.