Pyramid Auth with Akhet and SQLAlchemy

This recipe assumes you’re using the Akhet package, which makes it easy to configure SQL Alchemy and uses many of the Pylons conventions in your Pyramid project. Although you can apply these ideas to any Pyramid project, if you’re following step by step you’ll want first install Akhet via easy_install or pip.

Our ultimate goal here is to create a small working application that demonstrates basic Pyramid auth features supported by a SQL Alchemy model. Here’s an overview of what we’ll need to make it happen:

  1. Create a root factory in your model that associates allow/deny directives with groups and permissions
  2. Create users and groups in your model
  3. Create a callback function to retrieve a list of groups a user is subscribed to based on their user ID
  4. Make a “forbidden view” that will be invoked when a Forbidden exception is raised.
  5. Create a login action that will check the username/password and remember the user if successful
  6. Restrict access to handler actions by passing in a permission='somepermission' argument to the @action decorator
  7. Wire it all together in your config

Getting Started

Let’s create a new Pyramid site called simpleauth:

$ paster create -t simpleauth

When prompted with “Include SQLAlchemy configuration?”, enter “y”.

Switch to the newly created simpleauth directory and run:

$ python setup.py develop
$ paster serve --reload development.ini

Now you’re up and running on http://127.0.0.1:5000.

Defining the Model

The only models file we’ll need for this project is simpleauth/simpleauth/models/__init__.py, but in a more complex application it might be better to use a separate file (like auth.py) to hold the models we’re going to create. You can get started by adding import statements and a custom root factory:

import os
from hashlib import sha1
from sqlalchemy.exc import IntegrityError
from pyramid.security import Allow, Everyone

class RootFactory(object):
    __acl__ = [ (Allow, Everyone, 'everybody'),
                (Allow, 'basic', 'entry'),
                (Allow, 'secured', ('entry', 'topsecret'))
              ]
    def __init__(self, request):
        pass

The custom root factory generates objects that will be used as the context of requests sent to your web application. The first attribute of the root factory is the ACL, or access control list. It’s a list of tuples that contain a directive to handle the request (such as Allow or Deny), the group that is granted or denied access to the resource, and a permission (or optionally a tuple of permissions) to be associated with that group.

The example access control list above indicates that we will allow everyone to view pages with the ‘everybody’ permission, members of the basic group to view pages restricted with the ‘entry’ permission, and members of the secured group to view pages restricted with either the ‘entry’ or ‘topsecret’ permissions. The special principal ‘Everyone’ is a built-in feature that allows any person visiting your site (known as a principal) access to a given resource.

Although we’ve defined the permissions declaratively as part of the RootFactory class, we’d like to persist our groups and users in a database. We’re going to use a SQLite database for the purposes of this example, using the default configuration supplied by Akhet, but you can specify a different database in your development.ini file. Here is the required code for our users, groups, and user_group tables, which you can now add to models/__init__.py:

def groupfinder(userid, request):
    user = Users.by_id(userid)
    return [g.groupname for g in user.mygroups]

class Groups(Base):
    __tablename__ = 'groups'
    id = sa.Column(sa.Integer,
                   sa.Sequence('groups_seq_id', optional=True),
                   primary_key=True)
    groupname = sa.Column(sa.Unicode(255), unique=True)

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

class Users(Base):
    __tablename__ = 'users'
    id = sa.Column(sa.Integer,
                   sa.Sequence('users_seq_id', optional=True),
                   primary_key=True)
    username = sa.Column(sa.Unicode(80), nullable=False, unique=True)
    password = sa.Column(sa.Unicode(80), nullable=False)
    mygroups = orm.relationship(Groups, secondary='user_group')

    def __init__(self, user, password):
        self.username = user
        self._set_password(password)

    @classmethod
    def by_id(cls, userid):
        return Session.query(Users).filter(Users.id==userid).first()

    @classmethod
    def by_username(cls, username):
        return Session.query(Users).filter(Users.username==username).first()

    def _set_password(self, password):
        hashed_password = password

        if isinstance(password, unicode):
            password_8bit = password.encode('UTF-8')
        else:
            password_8bit = password

        salt = sha1()
        salt.update(os.urandom(60))
        hash = sha1()
        hash.update(password_8bit + salt.hexdigest())
        hashed_password = salt.hexdigest() + hash.hexdigest()

        if not isinstance(hashed_password, unicode):
            hashed_password = hashed_password.decode('UTF-8')

        self.password = hashed_password

    def validate_password(self, password):
        hashed_pass = sha1()
        hashed_pass.update(password + self.password[:40])
        return self.password[40:] == hashed_pass.hexdigest()

user_group_table = sa.Table('user_group', Base.metadata,
    sa.Column('user_id', sa.Integer, sa.ForeignKey(Users.id)),
    sa.Column('group_id', sa.Integer, sa.ForeignKey(Groups.id)),
)

When we configure our ACL, we’ll need a callback function that will check the user ID of an authenticated user and return a list of the groups they belong to. It will then be able to compare the user’s group memberships to the required group for a specified resource and either allow or deny access. The groupfinder function fills this purpose.

The Groups and Users classes, combined with the many-to-many user_group association table, define the methods we will need for user authentication and authorization. Note the following methods defined in the Users class, which we will be employing shortly:

  1. by_id and by_username: these will query the user table by id or username and return your user as an instance of Users or will return None. We will use the by_username method when the user initially logs in with a username, and we will use by_id for authenticated users to check if they have the appropriate permissions for restricted pages.
  2. _set_password and validate_password: these will let us save the user’s password in hashed form in the database and give us a way to validate it against the plain-text password supplied through the webform. Keep in mind that although storing the password in its hashed form can offer some protection at the database level, the password sent to your application will be sent in clear text and could potentially be monitored. If you are securing sensitive information, consider using HTTPS or an alternate security method to encrypt the password enroute to your site.

The last addition to our models/__init__.py file is a load_database function that will create our groups, a user named genericuser with password basic123 belonging to the basic group, and a user named superuser with password secret123 belonging to the secured group. This is not an ideal way to add users and groups to your database, but we use it here to get up and running quickly:

def load_database(db_string):
    engine = sa.create_engine(db_string, echo=True)
    Session.configure(bind=engine)
    Base.metadata.bind = engine
    Base.metadata.create_all(engine)
    try:
        session = Session()
        user1 = Users('genericuser', 'basic123')
        user2 = Users('superuser', 'secret123')
        group1 = Groups('basic')
        group2 = Groups('secured')
        session.add(group1)
        session.add(group2)
        user1.mygroups.append(group1)
        user2.mygroups.append(group2)
        session.add(user1)
        session.add(user2)
        transaction.commit()
    except IntegrityError:
        pass

We will later include this function in our main application so that it will run each time the application is started. It either creates the users and groups for the first time, or relies on SQLAlchemy to raise an IntegrityError (indicating we’ve violated the unique constraints we set on usernames and groupnames) to avoid adding the same users and groups each time we start our application. However, note that it is only a convenience function to give us test users for example purposes, not something you would want to run each time you restarted a production application.

Templates

Akhet allows us to render Mako templates with .html extensions. We will need 3 templates for our demo, which should be saved in your templates directory

home.html – the landing page for authenticated users, requiring ‘entry’ level access:

<html>
<body>

<p>Welcome back, ${currentuser}!</p>

<p>If you are viewing this page, it means you have 'entry' access.</p>

<p><a href="${url('secret')}">Click here</a> for Top Secret access (authorized users only)</p>

<p><a href="${url('logout')}">Click here</a> to logout.</p>

</body>
</html>

login.html – unauthenticated users will be redirected here to login:

<html>
<body>

<form name="login_form" method="POST" action="${request.route_url('login')}">
  <label>Login:</label>
  <input type="text" name="login" value="${login}" /><br />
  <label>Password:</label>
  <input type="password" name="password" value="${password}" /> <br />
  <input type="submit" name="login_submitted" value="Login" />
</form>

${message}

</body>
</html>

secret.html – requires authenticated users to have ‘topsecret’ access:

<html>
<body>

You've reached the secret page! The secret is: ${secret}

</body>
</html>

These templates are intentionally kept simple so we can focus on the functionality of our demo. If you are familiar with Mako, you will recognize variables being referenced in the form of ${variable} in the html. In the next section we will show how to pass variables to the templates. You will also see that we generate URLs in the form: ${url('<named route>', request)}, where the url function is automatically made available to the template.

Handlers

By this point we’ve prepared our model and our html templates. In order to interact with the model and render the templates, we use handlers. The handlers/main.py file should look like this:

import logging
from pyramid_handlers import action
from pyramid.security import authenticated_userid
from simpleauth.models import Users
import simpleauth.handlers.base as base

log = logging.getLogger(__name__)

class MainHandler(base.Handler):

    @action(renderer='home.html', permission='entry')
    def index(self):
        userid = authenticated_userid(self.request)
        username = Users.by_id(userid).username
        return { 'currentuser':username }


    @action(renderer='secret.html', permission='topsecret')
    def secret(self):
        return {'secret':'42'}

Handlers are covered in detail in other tutorials, but to give a quick overview, here are the components of interest to us:

  1. The @action() call lets us define a renderer (in our case, the name of the html templates we saved earlier), and the name of a permission defined in our ACL
  2. The functions defined within the handler (called actions) return a dictionary that will be made available to the template. For instance, in the definition of index, we include a key ‘currentuser’ with the value of the authenticated user’s username, which will be available to the template as ${currentuser}.

In order for all of this to work, we need to register the handlers and match them to URLs in handlers/__init__.py, which should look like this:

def includeme(config):
    config.add_handler("main", "/{action}", "simpleauth.handlers.main:MainHandler",
        path_info=r"/(?!favicon\.ico|robots\.txt|w3c)")

    # simpleauth additions
    config.add_handler('login', '/login/', 'simpleauth.handlers.auth:Auth', action='login')
    config.add_handler('logout', '/logout/', 'simpleauth.handlers.auth:Auth', action='logout')

    config.add_handler('home', '/', 'simpleauth.handlers.main:MainHandler', action='index')
    config.add_handler('secret', '/secret/', 'simpleauth.handlers.main:MainHandler', action='secret')

Although you could define all of your handlers and actions in handlers/main.py, we’re going to create a new file called handlers/auth.py for our login and logout actions:

from pyramid.httpexceptions import HTTPFound
from pyramid.security import remember
from pyramid.security import forget
from pyramid.url import route_url
from pyramid_handlers import action
from pyramid.view import view_config
from pyramid.response import Response
import simpleauth.handlers.base as base
from simpleauth.models import Users

class Auth(base.Handler):

    @view_config(renderer='simpleauth:templates/login.html',context='pyramid.exceptions.Forbidden')
    @action(renderer='login.html')
    def login(self):
        if 'submitlogin' in self.request.params:
            login = self.request.params['login']
            password = self.request.params['password']
            user = Users.by_username(login)
            if user and user.validate_password(password):
                headers = remember(self.request, user.id)
                home = route_url('home', self.request)
                return HTTPFound(location=home, headers=headers)
            message = 'Please check your username or password'
            return dict(message=message, login=login, password=password)
        else:
            return {'login':'', 'password':'', 'message':''}

    def logout(self):
        headers = forget(self.request)
        loginpage = route_url('login', self.request)
        return HTTPFound(location=loginpage, headers=headers)

The login function will check to see if the form has been submitted, and will attempt to authenticate the user. If the username is found in the database and the password validates, it will set the cookie, remember the user, and redirect them to the home page. If the user cannot be authenticated, it will display the login page again with a message reminding them to check their username or password. The logout function will remove the cookie, forget the user, and redirect them to the login page.

In our auth.py file when we defined the login action, on successful login we asked Pyramid to remember the user’s ID with the call to remember(self.request, user.id). It’s this user ID that will be passed into the groupfinder function (from models/__init__.py) on any requests made by an authenticated user to a restricted resource. We use the by_id method defined in our model to find the user, and a list comprehension to return a list of all the group names associated with the user.

Note that the use of the @view_config decorator is our way of telling Pyramid to load the login.html template when the Forbidden context is raised. It makes sense to add it here because it is associated with that action in terms of how we think about it, and later on we’ll run config.scan() in main() to register the view, but strictly speaking the view could be registered separately from the action.

Config

Our last step is to configure __init__.py (in the simpleauth/simpleauth/ directory). This is how we’ll finally tell our application about everything we’ve been doing. To make it easy to copy and paste, I’m including the full code for our __init__.py file below, and then we’ll review the parts that are relevant to this demo:

from pyramid.config import Configurator
import akhet
import pyramid_beaker
import sqlahelper
import sqlalchemy
from simpleauth.models import groupfinder, RootFactory, load_database
from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy


def main(global_config, **settings):
    """ This function returns a Pyramid WSGI application.
    """

    # Configure our authorization policy
    authentication = AuthTktAuthenticationPolicy('secretstring',
                                                 callback=groupfinder)
    authorization= ACLAuthorizationPolicy()

    # Create the Pyramid Configurator.
    config = Configurator(settings=settings, root_factory=RootFactory,
                          authentication_policy=authentication,
                          authorization_policy=authorization)
    config.scan()
    config.include("pyramid_handlers")
    config.include("akhet")

    # Initialize database
    engine = sqlalchemy.engine_from_config(settings, prefix="sqlalchemy.")
    sqlahelper.add_engine(engine)
    config.include("pyramid_tm")

    # Run load_database function
    dblocation = settings.get('sqlalchemy.url')
    load_database(dblocation)

    # Configure Beaker sessions and caching
    session_factory = pyramid_beaker.session_factory_from_settings(settings)
    config.set_session_factory(session_factory)
    pyramid_beaker.set_cache_regions_from_settings(settings)

    # Configure renderers and event subscribers
    config.add_renderer(".html", "pyramid.mako_templating.renderer_factory")
    config.add_subscriber("simpleauth.subscribers.create_url_generator",
        "pyramid.events.ContextFound")
    config.add_subscriber("simpleauth.subscribers.add_renderer_globals",
                          "pyramid.events.BeforeRender")

    # Set up view handlers
    config.include("simpleauth.handlers")

    config.add_static_route("simpleauth", "static", cache_max_age=3600)

    return config.make_wsgi_app()

Configuring the authorization policy is covered in detail in other tutorials and the Pyramid documentation, so I’ll only call your attention to the settings we customized:

  1. When instantiating AuthTktAuthenticationPolicy, the first argument should be a secret unique to your application (something more secret than the ‘secretstring’ used here), which will be used as an encryption key. The second argument references the groupfinder callback we created earlier in models/__init__.py.
  2. We next create the config, passing in the custom root factory we created in models/__init__.py, along with the authentication and authorization policies we just specified.

Testing your application

Now you can visit http://127.0.0.1:5000 to try it out! When you first go there it will see that you are not logged in and will route you to the login page. After logging in successfully, you will return to the main home page, where you can attempt to access the Top Secret link. If you login as genericuser (password ‘basic123’) you will be denied access to Top Secret, and will instead be redirected to the login page. If you login as superuser (password ‘secret123’), you will be granted access to Top Secret. However, you will see that either user can access the home page, which is protected by the ‘entry’ level permission that both users can access. A more sophisticated setup might distinguish between someone who is unauthenticated and someone who is unauthorized for a particular resource, but for now we’ll leave that as an exercise.

Final thoughts

Pyramid’s built-in security gives us a flexible and easy way to integrate security into our applications. It would only require a few additional steps to create pages that would let you create, edit, and delete users and groups, allowing users with the appropriate permissions to manage accounts. If you were to apply this to a blog or a wiki, you could move the Users and Groups classes to their own file and use models.py (or yet another filename) for the core parts of your application’s model. Pyramid allows for a great deal of freedom to structure these components in a way that will make sense for the application you are building.

Table Of Contents

This Page