Guard In Depth

    Note:

    This HOWTO documents Woven Guard, part of the Woven framework. The Woven framework should not be used for new projects. The newer Nevow framework, available as part of the Quotient project, is a simpler framework with consistent semantics and better testing and is strongly recommended over Woven.

    Woven guard is founded on the twisted.cred framework, and it is important to be somewhat familiar with cred to work with guard. As always, when using this framework, start by writing a IRealm. The guard module expects the realm to respond to the resource.IResource interace.

    Here is an example of a simple realm which generates different resources for logged-in and non-logged-in users:

    from twisted.cred import portal, checkers
    from twisted.web import resource, static
    
    def noLogout():
        pass
    
    class Realm:
        __implements__ = portal.IRealm
        def requestAvatar(self, avatarId, mind, *interfaces):
            if resource.IResource not in interfaces:
                raise NotImplementedError
            if avatarId is checkers.ANONYMOUS:
                avatar = static.Data("Who are you?", 'text/html')
            else:
                avatar = static.Data("The answer is 42", 'text/html')
            return (resource.IResource, avatar, noLogout)
    

    This realm makes sure that only non-anonymous users will know the answer to life, the universe and everything.

    Usually, one or more custom resource classes will be written when using guard, but it is not necessary. For example, a guard intended to protect a certain directory can use a static.File resource. Note that the IRealm should be prepared to get the ANONYMOUS avatar ID, and handle it correctly. From whatever resource the anonymous avatar gets, a link should point to guard.INIT_PERSPECTIVE relative to the guard root. This is the only way users can log in. From whatever resources the non-anonymous avatars get it is strongly recommended to link to guard.DESTROY_PERSPECTIVE. This is the only way users can log out.

    from twisted.cred import portal, checkers
    from twisted.web import resource, static
    from twisted.web.woven import guard
    
    login='<a href="%s">login</a>' % guard.PERSPECTIVE_INIT
    logout='<a href="%s">logout</a>' % guard.PERSPECTIVE_DESTROY
    
    def noLogout():
        pass
    
    class Realm:
        __implements__ = portal.IRealm
        def requestAvatar(self, avatarId, mind, *interfaces):
            if resource.IResource not in interfaces:
                raise NotImplementedError
            if avatarId is checkers.ANONYMOUS:
                avatar = static.Data("Who are you?<br>"+login, 'text/html')
            else:
                avatar = static.Data("The answer is 42, %s<br>%s" % (avatarId,
                                     logout), 'text/html')
            return (resource.IResource, avatar, noLogout)
    

    Once the realm is written, it is possible to generate a resource which will wrap it with appropriate code to manage users, sessions and authentication. But, as always, nothing deals with the realm directly -- all the rest of the code deals with a Portal which wraps the realm.

    Note: You will almost always want to put checkers.AllowAnonymousAccess in the checkers registered for the Portal, otherwise it will be impossible to log in.

    The canonical thing is to use resource=guard.SessionWrapper(guard.UsernamePasswordWrapper(portal, callback=callback). The callback is used to redirect the request to the appropriate place after a successful login. Usually, you will want to redirect to the parent:

    def parentRedirect(_):
        return util.ParentRedirect()
    

    When a client first reaches a guarded resource, it is redirected to session-init/. From there, it will be redirected to a URL containing a long magic hex string, where a cookie will be set, and then to the original URL with ?__session_just_started__=1 tucked at the end. The addition is to guarantee that the client will not think it is in a redirection loop (wget, for example, has that problem).

    Note that in resources which are children of the guarded resources, request.getSession automatically returns the Woven session. Since it is a Componentized, it is possible to use getComponent and setComponent to keep state related to a user in the session.

    For simple cases, the approach described here leads to quite a bit of boiler-plate code (about 30 lines or so). If a web application has simple authentication needs, it is possible to use simpleguard, which allows you to skip implementing a realm yourself.

    The important function in simpleguard is resource=guardResource(resource, checkers, nonauthenticated=None). checkers should be a list of ICredentialCheckers. It is not necessary to put AllowAnonymousAccess here -- it will be added automatically. This allow you to differentiate in resources only based on authenticated/anonymous users, without finer distinction. However, in the given resources, and their children, it is possible to use request.getComponent(simpleguard.Authenticated). This will return None if the request is anonymous, or an instance with a .name attribute, which is the avatar ID. This can allow the application to personalize parts of it.

    The way the login page and error-on-login page look can be customized when creating the guarded resource. Here is an example:

    from twisted.web.woven import simpleguard, page, guard
    from twisted.web import resource, util, microdom
    from twisted.cred import checkers, portal
    
    class Authenticated(page.Page):
    
        template='<html>Hello <span model="name"/>!</html>'
    
        def wmfactory_name(self, request):
            return request.getComponent(simpleguard.Authenticated).name
    
    class LoginPage(page.Page):
        """This is the page that is shown to non-logged in users."""
    
        isLeaf = True
        addSlash = 0
        template = '''<html>
        <head>
            <title>Login</title>
            <style type="text/css">
    .formDescription, .formError {
        /* fixme - inherit */
        font-size: smaller;
        font-family: sans-serif;
        margin-bottom: 1em;
    }
    
    .formDescription {
        color: green;
    }
    
    .formError {
        color: red;
    }
    </style>
        </head>
        <body>
        <h1>Please Log In</h1>
        <div class="shell">
        <div class="loginform" view="loginform" />
        </div>
    
        </body>
    </html>'''          
    
        def __init__(self, formModel=None):
            page.Page.__init__(self)
            self.formModel = formModel
    
        def wvupdate_loginform(self, request, widget, model):
            microdom.lmx(widget.node).form(action=guard.INIT_PERSPECTIVE,
                                           model="form")
    
        def wmfactory_form(self, request):
            if self.formModel:
                return self.formModel
            else:
                return guard.newLoginSignature.method(None)
    
    
    def callback(_):
        return util.Redirect(".")
    
    def buildGuardedResource():
        return simpleguard.guardResource(
                   Authenticated(),
                   [checkers.InMemoryUsernamePasswordDatabaseDontUse(bob="12345")],
                   nonauthenticated=LoginPage(),
                   callback=callback, errback=LoginPage)
    
    listings/guard/login.py - listings/guard/login.py

    The trick here is that, of course, we define the original login page: while guard.PERSPECTIVE_INIT gives a default form, it is quite all right to use a different one. In this example, we just use the home page of non-authenticated users to show the login form. There is one other case where the default form will be shown: if the attempt to login did not succeed. However, this is only what happens by default: we can override this by supplying an errback. This will be called with a model describing the form, and all we have to do is to use woven to display it.

    Note that this example uses simpleguard, but equivalent code can be developed using guard -- the errback should be passed to the UsernamePasswordWrapper constructor in that case.

    Index

    Version: 1.3.0