CherryPy Project Download

Simple authentication and access restrictions helpers

This small module provides CherryPy apps with facilities to specify in configuration and/or with decorators that specfic access restrictions apply to parts of the site. If those restrictions are not met, a user is redirect to a login page.

This is not meant to be a standalone module, just an example that you would include in your project and modify/extend to suit your needs.

Usage example is included below.

# -*- encoding: UTF-8 -*-
#
# Form based authentication for CherryPy. Requires the
# Session tool to be loaded.
#

import cherrypy

SESSION_KEY = '_cp_username'

def check_credentials(username, password):
    """Verifies credentials for username and password.
    Returns None on success or a string describing the error on failure"""
    # Adapt to your needs
    if username in ('joe', 'steve') and password == 'secret':
        return None
    else:
        return u"Incorrect username or password."
    
    # An example implementation which uses an ORM could be:
    # u = User.get(username)
    # if u is None:
    #     return u"Username %s is unknown to me." % username
    # if u.password != md5.new(password).hexdigest():
    #     return u"Incorrect password"

def check_auth(*args, **kwargs):
    """A tool that looks in config for 'auth.require'. If found and it
    is not None, a login is required it is evaluated as a list of conditions
    that the user must fulfill"""
    conditions = cherrypy.request.config.get('auth.require', None)
    if conditions is not None:
        username = cherrypy.session.get(SESSION_KEY)
        if username:
            for condition in conditions:
                # A condition is just a callable that takes a username and
                # returns True or False
                if not condition(username):
                    raise cherrypy.HTTPRedirect("/auth/login")
            cherrypy.request.login = username
        else:
            raise cherrypy.HTTPRedirect("/auth/login")
    
cherrypy.tools.auth = cherrypy.Tool('before_handler', check_auth)

def require(*conditions):
    """A decorator that appends conditions to the auth.require config
    variable."""
    def decorate(f):
        if not hasattr(f, '_cp_config'):
            f._cp_config = dict()
        if 'auth.require' not in f._cp_config:
            f._cp_config['auth.require'] = []
        f._cp_config['auth.require'].extend(conditions)
        return f
    return decorate


# Conditions are callables that take a username and return True
# if the username fulfills the conditions, False otherwise
#
# Define those at will however suits the application.

def member_of(groupname):
    def check(username):
        # replace with actual check if <username> is in <groupname>
        return username == 'joe' and groupname == 'admin'
    return check

def name_is(reqd_username):
    def check(username):
        return reqd_username == username
    return check

# These might be handy

def any_of(*conditions):
    """Returns True if any of the conditions match"""
    def check(username):
        for c in conditions:
            if c(username):
                return True
        return False
    return check

# By default all conditions are required, but this might still be
# needed if you want to use it inside of an any_of(...) condition
def all_of(*conditions):
    """Returns True if all of the conditions match"""
    def check(username):
        for c in conditions:
            if not c(username):
                return False
        return True
    return check


# Controller to provide login and logout actions

class AuthController(object):
    
    def on_login(self, username):
        """Called on successful login"""
    
    def on_logout(self, username):
        """Called on logout"""
    
    def get_loginform(self, username, msg="Enter login information", from_page="/"):
        return """<html><body>
            <form method="post" action="/auth/login">
            <input type="hidden" name="from_page" value="%(from_page)s" />
            %(msg)s<br />
            Username: <input type="text" name="username" value="%(username)s" /><br />
            Password: <input type="password" name="password" /><br />
            <input type="submit" value="Log in" />
        </body></html>""" % locals()
    
    @cherrypy.expose
    def login(self, username=None, password=None, from_page="/"):
        if username is None or password is None:
            return self.get_loginform("", from_page=from_page)
        
        error_msg = check_credentials(username, password)
        if error_msg:
            return get_loginform(username, error_msg, from_page)
        else:
            cherrypy.session[SESSION_KEY] = cherrypy.request.login = username
            self.on_login(username)
            raise cherrypy.HTTPRedirect(from_page or "/")
    
    @cherrypy.expose
    def logout(self, from_page="/"):
        sess = cherrypy.session
        username = sess.get(SESSION_KEY, None)
        sess[SESSION_KEY] = None
        if username:
            cherrypy.request.login = None
            self.on_logout(username)
        raise cherrypy.HTTPRedirect(from_page or "/")

The developer must at least modify check_credentials to suit whatever means your applications has for matching usernames to passwords. The member_of condition also should be altered in lieu with your user/groups model if you have one.

You'll also probably want to customize the methods in AuthController.

How it works

The check_auth function runs for every request before the handler, but after the handler has been choosen by the dispatcher. It examines cherrypy.request.config for an entry named auth.require. If that entry is not None, a valid login is required. Furthermore, it is evaluated as a list of conditions.

Each condition is a callable that takes the username as an argument and returns True or False depending on if the user meets the condition. If any of the conditions are not met, the user is redirected to the login page.

Here is an example of how to use this, given that the above is saved in auth.py:

import cherrypy
from auth import AuthController, require, member_of, name_is

class RestrictedArea:
    
    # all methods in this controller (and subcontrollers) is
    # open only to members of the admin group
    
    _cp_config = {
        'auth.require': [member_of('admin')]
    }
    
    @cherrypy.expose
    def index(self):
        return """This is the admin only area."""


class Root:
    
    _cp_config = {
        'tools.sessions.on': True,
        'tools.auth.on': True
    }
    
    auth = AuthController()
    
    restricted = RestrictedArea()
    
    @cherrypy.expose
    @require()
    def index(self):
        return """This page only requires a valid login."""
    
    @cherrypy.expose
    def open(self):
        return """This page is open to everyone"""
    
    @cherrypy.expose
    @require(name_is("joe"))
    def only_for_joe(self):
        return """Hello Joe - this page is available to you only"""

    # This is only available if the user name is joe _and_ he's in group admin
    @cherrypy.expose
    @require(name_is("joe"))
    @require(member_of("admin"))
    def only_for_joe_admin(self):
        return """Hello Joe Admin - this page is available to you only"""


if __name__ == '__main__':
    cherrypy.quickstart(Root())

Hope someone finds it useful..

For any questions, you'll find me on #cherrypy or at arnarbi -at- gmail

-- Arnar Birgisson

Hosted by WebFaction

Log in as guest/cherrypy to create/edit wiki pages