CherryPy Project Download

Many sites have both public and private pages, where anyone can access the public pages, but only logged-in, registered users can access the private pages.

Here's a simple implementation using CherryPy 2.0 and Python 2.4 decorators:

from cherrypy import cpg

def html(title):
    '''HTML decorator: wrap the results of the called function in HTML
    header and footer tags, setting the page title in the HTML HEADer.

    Because this decorator takes arguments, it has to return _another_
    decorator that actually uses the arguments (see PEP 318 for
    details).
    
    '''
    def _wrapper(fn):
        def _innerWrapper(*args, **kwargs):
            '''The .join(list)() nonsense is just in case you're using
            the generator filter (since the filter doesn't get a
            chance to act inside the decorator.'''
            return '<HTML><HEAD><TITLE>%s</TITLE></HEAD><BODY>%s</BODY></HTML>' % (title, 
            "".join(list(fn(*args, **kwargs))))
        return _innerWrapper
    return _wrapper    

def needsLogin(fn):
    '''Login decorator: if the user is not logged in, send up a login
    rather than the page content.  Point the login form back to the
    same page to avoid a redirect when the user logs in correctly.
    '''
    def _wrapper(*args, **kwargs):
        if 'userid' in cpg.request.sessionMap:
            # User is logged in; allow access
            return fn(*args, **kwargs)
        else:
            # User isn't logged in yet.
            
            # See if the user just tried to log in
            try:
                submit = kwargs['login']
                email = kwargs['loginEmail']
                password = kwargs['loginPassword']
            except KeyError:
                # No, this wasn't a login attempt.  Send the user to
                # the login "page".
                return loginPage(cpg.request.path)

            # Now look up the user id by the email and password
            userid = getUserId(email, password)
            if userid is None:
                # Bad login attempt
                return loginPage(cpg.request.path, 'Invalid email address or password.')
            # User is now logged in, so retain the userid and show the content
            cpg.request.sessionMap['userid'] = userid

            # If you don't want the 'login', 'loginEmail' and 'loginPassword'
            # values to be passed to the original method that was interrupted
            # by the login challenge, uncomment the following
            # del kwargs['login']
            # del kwargs['loginEmail']
            # del kwargs['loginPassword']

            return fn(*args, **kwargs)
    return _wrapper


def getUserId(email, password):
    '''Simple function to look up a user id from email and password.
    Naturally, this would be stored in a database rather than
    hardcoded, and the password would be stored in a hashed format
    rather than in cleartext.

    Returns the userid on success, or None on failure.
    '''
    
    accounts = {('tim@lesher.ws', 'foo'): 'tim'}

    return accounts.get((email,password), None)

@html('Sitename: Log In')
def loginPage(targetPage, message=None):
    '''Return a login "pagelet" that replaces the regular content if
    the user is not logged in.'''
    result = []
    result.append('<h1>Sitename Login</h1>')
    if message is not None:
        result.append('<p>%s</p>' % message)
    result.append('<form action=%s method=post>' % targetPage)
    result.append('<p>Email Address: <input type=text name="loginEmail"></p>')
    result.append('<p>Password: <input type=password name="loginPassword"></p>')
    result.append('<p><input type="submit" name="login" value="Log In"></p>')
    result.append('</form>')
    return '\n'.join(result)

class Site:
    @cpg.expose
    @html("Sitename: Home Page")
    def index(self, *args, **kwargs):
        return '''<h1>SiteName</h1>
        <h2>Home Page</h2>
        <p><a href="public">Public Page</a></p>
        <p><a href="private">Private Page</a> <i>(registered users only)</i></p>
        '''

    @cpg.expose
    @html("Sitename: Public Page")
    def public(self, *args, **kwargs):
        return '''<h1>SiteName</h1>
        <h2>Public Page</h2>
        <p><a href="/">Go back home</a></p>'''
    
    @cpg.expose
    @needsLogin
    @html("Sitename: Private Page")
    def private(self, *args, **kwargs):
        return '''<h1>SiteName</h1>
        <h2>Private Page</h2>
        <p><a href="/">Go back home</a></p>'''

        
cpg.root = Site()

cpg.server.start(configFile = 'site.conf')

This example uses two decorators:

  1. An html decorator that wraps the return value of its function in HTML tags, including a <TITLE> tag in the HTML <HEAD>.
  2. A needsLogin decorator that checks whether a user is logged in before providing "private" content.

I originally implemented this using cherrypy.lib.aspect, but I ran into some limitations of that class:

  1. You get precisely one _before and _after method for each Aspect-derived class, so you can't easily mix-and-match multiple aspects.
  2. If your _before handler returns STOP, your _after handler never gets called. This means that if you're trying to implement header and footer aspects, your header aspect has to do the same work as the footer aspect if it halts processing.
  3. The handlers don't get access to either the self object or the arguments of the method to be called.

I started implementing a version of Aspect that corrected these problems, but I soon realized I was re-implementing decorators, so I switched to a decorator-based implementation.

Attachments

Hosted by WebFaction

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