1
# -*- test-case-name: twisted.web.test.test_woven -*-
3
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
4
# See LICENSE for details.
8
"""Resource protection for Woven. If you wish to use twisted.cred to protect
9
your Woven application, you are probably most interested in
10
L{UsernamePasswordWrapper}.
13
from __future__ import nested_scopes
15
__version__ = "$Revision: 1.34 $"[11:-2]
24
from twisted.python import log, components
25
from twisted.web.resource import Resource, IResource
26
from twisted.web.util import redirectTo, Redirect, DeferredResource
27
from twisted.web.static import addSlash
28
from twisted.internet import reactor
29
from twisted.cred.error import LoginFailed, UnauthorizedLogin
32
return md5.new("%s_%s" % (str(random.random()) , str(time.time()))).hexdigest()
34
class GuardSession(components.Componentized):
35
"""A user's session with a system.
37
This utility class contains no functionality, but is used to
40
def __init__(self, guard, uid):
41
"""Initialize a session with a unique ID for that session.
43
components.Componentized.__init__(self)
46
self.expireCallbacks = []
47
self.checkExpiredID = None
53
def _getSelf(self, interface=None):
58
return self.getComponent(interface)
60
# Old Guard interfaces
62
def clientForService(self, service):
63
x = self.services.get(service)
69
def setClientForService(self, ident, perspective, client, service):
70
if self.services.has_key(service):
71
p, c, i = self.services[service]
73
del self.services[service]
75
self.services[service] = perspective, client, ident
76
perspective.attached(client, ident)
77
# this return value is useful for services that need to do asynchronous
81
# New Guard Interfaces
83
def resourceForPortal(self, port):
84
return self.portals.get(port)
86
def setResourceForPortal(self, rsrc, port, logout):
87
self.portalLogout(port)
88
self.portals[port] = rsrc, logout
91
def portalLogout(self, port):
92
p = self.portals.get(port)
97
del self.portals[port]
99
# timeouts and expiration
101
def setLifetime(self, lifetime):
102
"""Set the approximate lifetime of this session, in seconds.
104
This is highly imprecise, but it allows you to set some general
105
parameters about when this session will expire. A callback will be
106
scheduled each 'lifetime' seconds, and if I have not been 'touch()'ed
107
in half a lifetime, I will be immediately expired.
109
self.lifetime = lifetime
111
def notifyOnExpire(self, callback):
112
"""Call this callback when the session expires or logs out.
114
self.expireCallbacks.append(callback)
117
"""Expire/logout of the session.
119
log.msg("expired session %s" % self.uid)
120
del self.guard.sessions[self.uid]
121
for c in self.expireCallbacks:
126
self.expireCallbacks = []
127
if self.checkExpiredID:
128
self.checkExpiredID.cancel()
129
self.checkExpiredID = None
132
self.lastModified = time.time()
134
def checkExpired(self):
135
self.checkExpiredID = None
136
# If I haven't been touched in 15 minutes:
137
if time.time() - self.lastModified > self.lifetime / 2:
138
if self.guard.sessions.has_key(self.uid):
141
log.msg("no session to expire: %s" % self.uid)
143
log.msg("session given the will to live for %s more seconds" % self.lifetime)
144
self.checkExpiredID = reactor.callLater(self.lifetime,
146
def __getstate__(self):
147
d = self.__dict__.copy()
148
if d.has_key('checkExpiredID'):
149
del d['checkExpiredID']
152
def __setstate__(self, d):
153
self.__dict__.update(d)
157
INIT_SESSION = 'session-init'
159
def _setSession(wrap, req, cook):
160
req.session = wrap.sessions[cook]
161
req.getSession = req.session._getSelf
163
def urlToChild(request, *ar, **kw):
164
pp = request.prepath.pop()
165
orig = request.prePathURL()
166
request.prepath.append(pp)
169
# this SHOULD only happen in the case where the URL is just the hostname
173
args = request.args.copy()
176
ret += '?'+urllib.urlencode(args)
179
def redirectToSession(request, garbage):
180
rd = Redirect(urlToChild(request, *request.postpath, **{garbage:1}))
184
SESSION_KEY='__session_key__'
186
class SessionWrapper(Resource):
188
sessionLifetime = 1800
190
def __init__(self, rsrc, cookieKey=None):
191
Resource.__init__(self)
193
if cookieKey is None:
194
cookieKey = "woven_session_" + _sessionCookie()
195
self.cookieKey = cookieKey
198
def render(self, request):
199
return redirectTo(addSlash(request), request)
201
def getChild(self, path, request):
202
if not request.prepath:
204
cookie = request.getCookie(self.cookieKey)
205
setupURL = urlToChild(request, INIT_SESSION, *([path]+request.postpath))
206
request.setupSessionURL = setupURL
207
request.setupSession = lambda: Redirect(setupURL)
208
if path.startswith(SESSION_KEY):
209
key = path[len(SESSION_KEY):]
210
if key not in self.sessions:
211
return redirectToSession(request, '__start_session__')
212
self.sessions[key].setLifetime(self.sessionLifetime)
214
# /sessionized-url/${SESSION_KEY}aef9c34aecc3d9148/foo
216
# we are this getChild
217
# with a matching cookie
218
return redirectToSession(request, '__session_just_started__')
220
# We attempted to negotiate the session but failed (the user
221
# probably has cookies disabled): now we're going to return the
222
# resource we contain. In general the getChild shouldn't stop
224
# /sessionized-url/${SESSION_KEY}aef9c34aecc3d9148/foo
225
# ^ we are this getChild
226
# without a cookie (or with a mismatched cookie)
227
_setSession(self, request, key)
229
elif cookie in self.sessions:
230
# /sessionized-url/foo
231
# ^ we are this getChild
233
_setSession(self, request, cookie)
234
return getResource(self.resource, path, request)
235
elif path == INIT_SESSION:
236
# initialize the session
237
# /sessionized-url/session-init
240
newCookie = _sessionCookie()
241
request.addCookie(self.cookieKey, newCookie, path="/")
242
sz = self.sessions[newCookie] = GuardSession(self, newCookie)
244
rd = Redirect(urlToChild(request, SESSION_KEY+newCookie,
249
# /sessionized-url/foo
250
# ^ we are this getChild
252
request.getSession = lambda interface=None: None
253
return getResource(self.resource, path, request)
255
def getResource(resource, path, request):
257
request.postpath.insert(0, request.prepath.pop())
260
return resource.getChildWithDefault(path, request)
262
INIT_PERSPECTIVE = 'perspective-init'
263
DESTROY_PERSPECTIVE = 'perspective-destroy'
265
from twisted.python import formmethod as fm
266
from twisted.web.woven import form
269
newLoginSignature = fm.MethodSignature(
270
fm.String("username", "",
271
"Username", "Your user name."),
272
fm.Password("password", "",
273
"Password", "Your password."),
274
fm.Submit("submit", choices=[("Login", "", "")], allowNone=1),
277
from twisted.cred.credentials import UsernamePassword, Anonymous
279
class UsernamePasswordWrapper(Resource):
280
"""I bring a C{twisted.cred} Portal to the web. Use me to provide different Resources
281
(usually entire pages) based on a user's authentication details.
283
A C{UsernamePasswordWrapper} is a
284
L{Resource<twisted.web.resource.Resource>}, and is usually wrapped in a
285
L{SessionWrapper} before being inserted into the site tree.
287
The L{Realm<twisted.cred.portal.IRealm>} associated with your
288
L{Portal<twisted.cred.portal.Portal>} should be prepared to accept a
289
request for an avatar that implements the L{twisted.web.resource.IResource}
290
interface. This avatar should probably be something like a Woven
291
L{Page<twisted.web.woven.page.Page>}. That is, it should represent a whole
292
web page. Once you return this avatar, requests for it's children do not go
295
If you want to determine what unauthenticated users see, make sure your
296
L{Portal<twisted.cred.portal.Portal>} has a checker associated that allows
297
anonymous access. (See L{twisted.cred.checkers.AllowAnonymousAccess})
301
def __init__(self, portal, callback=None, errback=None):
302
"""Constructs a UsernamePasswordWrapper around the given portal.
304
@param portal: A cred portal for your web application. The checkers
305
associated with this portal must be able to accept username/password
307
@type portal: L{twisted.cred.portal.Portal}
309
@param callback: Gets called after a successful login attempt.
310
A resource that redirects to "." will display the avatar resource.
311
If this parameter isn't provided, defaults to a standard Woven
313
@type callback: A callable that accepts a Woven
314
L{model<twisted.web.woven.interfaces.IModel>} and returns a
315
L{IResource<twisted.web.resource.Resource>}.
317
@param errback: Gets called after a failed login attempt.
318
If this parameter is not provided, defaults to a the standard Woven
319
form error (i.e. The original form on a page of its own, with
321
@type errback: A callable that accepts a Woven
322
L{model<twisted.web.woven.interfaces.IModel>} and returns a
323
L{IResource<twisted.web.resource.Resource>}.
325
Resource.__init__(self)
327
self.callback = callback
328
self.errback = errback
330
def _ebFilter(self, f):
331
f.trap(LoginFailed, UnauthorizedLogin)
332
raise fm.FormException(password="Login failed, please enter correct username and password.")
334
def getChild(self, path, request):
335
s = request.getSession()
337
return request.setupSession()
338
if path == INIT_PERSPECTIVE:
339
def loginSuccess(result):
340
interface, avatarAspect, logout = result
341
s.setResourceForPortal(avatarAspect, self.portal, logout)
343
def triggerLogin(username, password, submit=None):
344
return self.portal.login(
345
UsernamePassword(username, password),
354
return form.FormProcessor(
355
newLoginSignature.method(
358
callback=self.callback,
361
elif path == DESTROY_PERSPECTIVE:
362
s.portalLogout(self.portal)
365
r = s.resourceForPortal(self.portal)
367
## Delegate our getChild to the resource our portal says is the right one.
368
return getResource(r[0], path, request)
370
return DeferredResource(
371
self.portal.login(Anonymous(), None, IResource
373
lambda (interface, avatarAspect, logout):
374
getResource(s.setResourceForPortal(avatarAspect,
375
self.portal, logout),
380
from twisted.web.woven import interfaces, utils
381
## Dumb hack until we have an ISession and use interface-to-interface adaption
382
components.registerAdapter(utils.WovenLivePage, GuardSession, interfaces.IWovenLivePage)