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 and the entry 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: cherrypy.request.login = username for condition in conditions: # A condition is just a callable that returns true or false if not condition(): raise cherrypy.HTTPRedirect("/auth/login") 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 return True # if the user fulfills the conditions they define, False otherwise # # They can access the current username as cherrypy.request.login # # Define those at will however suits the application. def member_of(groupname): def check(): # replace with actual check if <username> is in <groupname> return cherrypy.request.login == 'joe' and groupname == 'admin' return check def name_is(reqd_username): return lambda: reqd_username == cherrypy.request.login # These might be handy def any_of(*conditions): """Returns True if any of the conditions match""" def check(): for c in conditions: if c(): 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(): for c in conditions: if not c(): 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 self.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 returns True or False depending on if the user meets the condition. A condition can get the current username from cherrypy.request.login. 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")) # equivalent: @require(name_is("joe"), 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
Here is slight modification of code so that after loging user is redirected to page he requested
import urllib 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 and the entry is evaluated as alist of conditions that the user must fulfill""" conditions = cherrypy.request.config.get('auth.require', None) # format GET params get_parmas = urllib.quote(cherrypy.request.request_line.split()[1]) if conditions is not None: username = cherrypy.session.get(SESSION_KEY) if username: cherrypy.request.login = username for condition in conditions: # A condition is just a callable that returns true orfalse if not condition(): # Send old page as from_page parameter raise cherrypy.HTTPRedirect("/auth/login?from_page=%s" % get_parmas) else: # Send old page as from_page parameter raise cherrypy.HTTPRedirect("/auth/login?from_page=%s" %get_parmas)
-- Ivan

