2012
27
Oct

Pyramid, Persona & Group-Level Auth

Georges Dubus has created pyramid_persona, a nice library to integrate Mozilla Persona authentication into Pyramid projects. He also wrote a tutorial blog post explaining the basics of how to use the library. In principle it should be easy to go from what Georges has written to getting Persona-based authentication fully integrated with group or object level authorization in a Pyramid project, but it took some head scratching for me to do that. What better reason to blog about what I came up with and build on Georges' excellent work...

By the way, pyramid_persona is also a really nice example of advanced configuration tactics in Pyramid that show off the framework's plug-in friendliness.

Before diving is, I should also mention that Michael Merickel's authentication and authorization tutorial is an indispensable reference.

The Goal: The example I'm going to use is a very simple group-level authorization one. We'll create a site with a public side that anyone can interact with, and a private side that only allows access by users who have authenticated via Persona, and are known to the app as members of the admin group. We'll use the alchemy scaffold, so the app will have URL mapping via URL dispatch and persistence via SQLAlchemy.

So, let's get started.

Create a virtualenv, install Pyramid, and create an alchemy scaffold app:

(ppga)$ pcreate -s alchemy pyramid_persona_group_auth_demo

Add pyramid_persona to the requires list in setup.py, and add some settings to development.ini:

persona.secret = some secret string
persona.audiences = http://localhost:6543
persona.siteName = Pyramid Persona Group Auth Demo

mako.directories = pyramid_persona_group_auth_demo:templates

The persona settings are described in the Configuration section of the pyramid_persona README. The mako.directories setting is needed because we're going to use Mako for our login and admin page templates.

Next up, we add an Administrator class to our models.py file:

class Administrator(Base):
    __tablename__ = 'admins'
    id = Column(Integer, primary_key=True)
    persona_email = Column(Text, index=True, unique=True)

    def __init__(self, persona_email):
        self.persona_email = persona_email

We also need to bootstrap our Persona email address into the database so that we can sign-in to our app when we first start it up. To do that, import Administrator into scripts/initializedb.py and add a couple of lines to the end of the main function in that file:

from ..models import Administrator
...
with transaction.manager:
    model = MyModel(name='one', value=1)
    DBSession.add(model)
    admin = Administrator(persona_email='me@example.com')
    DBSession.add(admin)

Obviously, you'll want to use an email address that you have registered with Persona in place of me@example.com.

Now it's time to create some templates and views for the admin site of our site. These pages are almost verbatim from Georges' blog post or the pyramid_persona README - he really has done most of the heavy lifting! We start with a stub for the admin home page in templates/admin_home.mako:

<!DOCTYPE html>
<html>
<head>
  <script type="text/javascript"
    src="//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js">
  </script>
  <script src="https://login.persona.org/include.js"></script>
  <script >${request.persona_js}</script>
</head>
<body>
  <h1>Pyramid Persona Group Auth Demo Admin</h1>
  Welcome to the admin side, ${userid}
  ${request.persona_button}
</body>
</html>

All we've done here is hooked into Persona, and set up a welcome message and a sign-out button in lieu of any real admin interface content and funtionality.

The request.person_js and request.persona_button attributes are available thanks to config.set_request_property() calls in pyramid_persona.__init__.py. Recall that I mentioned what a nice example of advanced Pyramid configuration pyramid_persona is - it's well worth reading Georges' code to learn how he has made things so easy for the rest of us.

We'll also create a very similar looking template for our sign-in page in templates/admin_signin.mako:

<!DOCTYPE html>
<html>
<head>
  <script type="text/javascript"
    src="//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js">
  </script>
  <script src="https://login.persona.org/include.js"></script>
  <script >${request.persona_js}</script>
</head>
<body>
<h1>Pyramid Persona Group Auth Demo Admin</h1>
  ${request.persona_button}
</body>
</html>

There are obviously some opportunities to use template inheritance here, to say nothing of the total absence of styling in those templates.

Moving along to the views that render those templates, we create admin_views.py containing:

from pyramid.renderers import render
from pyramid.response import Response
from pyramid.security import authenticated_userid
from pyramid.view import (
    forbidden_view_config,
    view_config,
    )

@forbidden_view_config()
def admin_signin(request):
    body = render('admin_signin.mako', {}, request=request)
    return Response(body, status='403 Forbidden')

@view_config(
    route_name='admin.home',
    renderer='admin_home.mako',
    permission='admin')
def admin_home(request):
    userid = authenticated_userid(request)
    return {'userid': userid}

The admin_signin function renders our admin_signin.mako template and packages it up as a 403 Forbidden response. That function is decorated with @forbidden_view_config() so that it will replace the default forbidden view that pyramid_persona supplies. That's how we get our sign-in page to appear when an unauthenticated user hits the admin URL.

The admin_home function renders our admin home page stub. The important thing to note here is the permission='admin' argument in the @view_config decorator.

Finally, we add our security policy to pyramid_persona_group_auth_demo/__init__.py and add our admin home page view to the route map. We need some new imports:

from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.security import ALL_PERMISSIONS
from pyramid.security import Allow
from sqlalchemy.orm.exc import NoResultFound
from .models import Administrator

Then we add a dirt simple resource tree to provide the access control list that implements our security policy; i.e. any user in the admin group has full access to admin resources:

class Root(object):
    """Simplest possible resource tree to map groups to permissions.
    """
    __acl__ = [
        (Allow, 'g:admin', ALL_PERMISSIONS),
    ]

    def __init__(self, request):
        self.request = request

We also need a groupfinder function to provide to the authentication policy constructor so that it can identify whether or not an authenticated user is in the admin group:

def groupfinder(userid, request):
    query = DBSession.query(Administrator).\
                filter(Administrator.persona_email == userid)
    try:
        query.one()
        return ['g:admin']
    except NoResultFound:
        return None

Now let's hook everything together via the configuration in the main function. We add our resource tree to the Configurator constructor call:

config = Configurator(settings=settings, root_factory=Root)

as well as including the pyramid_persona configuration, and overriding its authentication policy setting with our own that hooks in the groupfinder function:

config.include('pyramid_persona')
authn_policy = AuthTktAuthenticationPolicy(
    settings['persona.secret'],
    callback=groupfinder)
config.set_authentication_policy(authn_policy)

Last but not least, we add a route to map /admin to our admin home page:

config.add_route('admin.home', '/admin')

It's time to test!

Install our app to get all of the dependencies installed, initialize the database with the handy initialize_pyramid_persona_group_auth_demo_db command that Pyramid creates for us during installation of an alchemy scaffold app, and start the server:

(ppga)$ python setup.py develop
(ppga)$ # lots of output
(ppga)$ initialize_pyramid_persona_group_auth_demo_db development.ini
(ppga)$ pserve --reload development.ini

Browsing to http://localhost:6543 should show you the default Pyramid app page - that's the public side of our site.

Going to http://localhost:6543/admin should show you the admin sign-in page:

../../../../../images/2012-10-27-admin_signin.png

and, if you inspect the requests and responses with your browser's web dev tools, you'll see that we got the expected 403 Forbidden response status.

Clicking the sign-in button pops the login.persona.org site in a new window with our site name and audience domain displayed:

../../../../../images/2012-10-27-persona.png

And, upon successfully signing in at Persona, we are redirected to the admin home page of our site:

../../../../../images/2012-10-27-admin_page.png

And that's all there is to it! Extending the site security to a more fine-grained group-level hierachy, or to object-level security should be relatively easy with the guidance in Michael Merickel's authentication and authorization tutorial.

The source code for the demo I've created here is available on Bitbucket at https://bitbucket.org/douglatornell/pyramid_persona_group_auth_demo

Thanks again to Georges Dubus for making this easy with the pyramid_persona library, and to Mozilla for creating Persona.

Updated 2012-11-02

  • Fixed typo in import statement for Administrator model. Thanks ppaez!
  • Added index on persona_email column of Administrator model. Thanks Georges Dubus!
Read and Post Comments