Twisted Documentation: Guard In Depth

# 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 zope.interface import implements
    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 zope.interface import implements
    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 `ICredentialChecker`s. 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: 0.6.0