26 Aug 2008 nbm   » (Journeyer)

Simple Routes-based authentication with Pylons

Some of the services in the SynthaSite service layer use Python, WSGI, and the Pylons web application framework (with some TurboGears 2...). Particular functions require authentication, while others do not. We had a few simple working parameters:

  • We only currently need a single user name and password for these particular services, since we are authenticating the connecting application, not a particular user.
  • The authentication details must live in deploy configuration, not code.
  • We would like to easily be able to see a list of all entry points into the application, and see whether they require authentication.
  • If we do not specify otherwise, assume that our functions require authentication.
  • To simplify testing and development, be able to easily turn off the authentication requirement in deploy configuration.

We already have a list of all entry points into these applications, since they use Routes. In the Pylons layout, these live in config/routing.py, and look like this:

from pylons import config
from routes import Mapper

def make_map():
"""Create, configure and return the routes Mapper"""
map = Mapper(directory=config['pylons.paths']['controllers'],
always_scan=config['debug'])

# The ErrorController route (handles 404/500 error pages); it should
# likely stay at the top, ensuring it can always be resolved
map.connect('error/:action/:id', controller='error')

# CUSTOM ROUTES HERE

map.connect('users', '/users', controller='users', action='index')
map.connect('user', '/users/:user_id', controller='users', action='user')
# ...

While one can use Routes with controllers and actions that depend on the URL, I prefer being explicit.  This creates a single list of all the accessible URLs and their accompanying controllers and actions.

If you attach additional keywords to the map.connect method, then they are added to the defaults attribute of the Route object created for each of those routes.  The RoutesMiddleware WSGI middleware places the route that matches the incoming request into the routes.route key in the environ dictionary that drives WSGI.  So, we can just add _auth = True to routes that require auth and _auth = False to those that don't, and create our own simple authentication middleware.  It would look something like this:

from paste.auth.basic import AuthBasicHandler

class LocalAuthenticationMiddleware(object):
def __init__(self, app, config):
realm = config.get('localauthentication.realm', None)
username = config.get('localauthentication.username', None)
password = config.get('localauthentication.password', None)
if realm:
def authfunc(environ, username, password,
_wanted_username = username, _wanted_password = password):
if username == _wanted_username:
if password == _wanted_password:
return True
self.protected_app = AuthBasicHandler(app, realm, authfunc)
else:
self.protected_app = app
self.config = config
self.app = app

def __call__(self, environ, start_response):
route = environ.get('routes.route', None)
if not route:
return self.app(environ, start_response)

if route.defaults.get('_auth', 'False') == 'False':
return self.app(environ, start_response)

return self.protected_app(environ, start_response)

We use Paste's AuthBasicHandler WSGI middleware to optionally wrap our application.  We keep a reference to our application around, in case we don't want to apply authentication.  When our middleware is called, we check whether we want the AuthBasicHandler-wrapped application, or the plain application, and call the one we want as per standard WSGI middleware.

Specifying _auth = True and _auth = False for every route is going to be painful.  Instead, we created a simple wrapper function around map.connect that we use instead, and it does the defaulting to requiring authentication for us (amongst other things):

    def connect(route_name, route_url, *args, **kw):
if 'method' in kw:
method = kw.pop('method')
if 'conditions' in kw:
kw['conditions']['method'] = method
else:
kw['conditions'] = dict(method=method)

# Unless otherwise specified, require authentication
kw['_auth'] = kw.get('_auth', True)

return map.connect(route_name, route_url, *args, **kw)

Syndicated 2008-08-22 14:58:10 from Neil Blakey-Milner

Latest blog entries     Older blog entries

New Advogato Features

New HTML Parser: The long-awaited libxml2 based HTML parser code is live. It needs further work but already handles most markup better than the original parser.

Keep up with the latest Advogato features by reading the Advogato status blog.

If you're a C programmer with some spare time, take a look at the mod_virgule project page and help us with one of the tasks on the ToDo list!