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.
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)
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.