~certify-web-dev/twisted/certify-trunk

« back to all changes in this revision

Viewing changes to twisted/web/woven/guard.py

  • Committer: Bazaar Package Importer
  • Author(s): Matthias Klose
  • Date: 2007-01-17 14:52:35 UTC
  • mfrom: (1.1.5 upstream) (2.1.2 etch)
  • Revision ID: james.westby@ubuntu.com-20070117145235-btmig6qfmqfen0om
Tags: 2.5.0-0ubuntu1
New upstream version, compatible with python2.5.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- test-case-name: twisted.web.test.test_woven -*-
 
2
 
 
3
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
 
4
# See LICENSE for details.
 
5
 
 
6
#
 
7
 
 
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}.
 
11
"""
 
12
 
 
13
from __future__ import nested_scopes
 
14
 
 
15
__version__ = "$Revision: 1.34 $"[11:-2]
 
16
 
 
17
import random
 
18
import time
 
19
import md5
 
20
import urllib
 
21
 
 
22
# Twisted Imports
 
23
 
 
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
 
30
 
 
31
def _sessionCookie():
 
32
    return md5.new("%s_%s" % (str(random.random()) , str(time.time()))).hexdigest()
 
33
 
 
34
class GuardSession(components.Componentized):
 
35
    """A user's session with a system.
 
36
 
 
37
    This utility class contains no functionality, but is used to
 
38
    represent a session.
 
39
    """
 
40
    def __init__(self, guard, uid):
 
41
        """Initialize a session with a unique ID for that session.
 
42
        """
 
43
        components.Componentized.__init__(self)
 
44
        self.guard = guard
 
45
        self.uid = uid
 
46
        self.expireCallbacks = []
 
47
        self.checkExpiredID = None
 
48
        self.setLifetime(60)
 
49
        self.services = {}
 
50
        self.portals = {}
 
51
        self.touch()
 
52
 
 
53
    def _getSelf(self, interface=None):
 
54
        self.touch()
 
55
        if interface is None:
 
56
            return self
 
57
        else:
 
58
            return self.getComponent(interface)
 
59
 
 
60
    # Old Guard interfaces
 
61
 
 
62
    def clientForService(self, service):
 
63
        x = self.services.get(service)
 
64
        if x:
 
65
            return x[1]
 
66
        else:
 
67
            return x
 
68
 
 
69
    def setClientForService(self, ident, perspective, client, service):
 
70
        if self.services.has_key(service):
 
71
            p, c, i = self.services[service]
 
72
            p.detached(c, ident)
 
73
            del self.services[service]
 
74
        else:
 
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
 
78
        # stuff.
 
79
        return client
 
80
 
 
81
    # New Guard Interfaces
 
82
 
 
83
    def resourceForPortal(self, port):
 
84
        return self.portals.get(port)
 
85
 
 
86
    def setResourceForPortal(self, rsrc, port, logout):
 
87
        self.portalLogout(port)
 
88
        self.portals[port] = rsrc, logout
 
89
        return rsrc
 
90
 
 
91
    def portalLogout(self, port):
 
92
        p = self.portals.get(port)
 
93
        if p:
 
94
            r, l = p
 
95
            try: l()
 
96
            except: log.err()
 
97
            del self.portals[port]
 
98
 
 
99
    # timeouts and expiration
 
100
 
 
101
    def setLifetime(self, lifetime):
 
102
        """Set the approximate lifetime of this session, in seconds.
 
103
 
 
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.
 
108
        """
 
109
        self.lifetime = lifetime
 
110
 
 
111
    def notifyOnExpire(self, callback):
 
112
        """Call this callback when the session expires or logs out.
 
113
        """
 
114
        self.expireCallbacks.append(callback)
 
115
 
 
116
    def expire(self):
 
117
        """Expire/logout of the session.
 
118
        """
 
119
        log.msg("expired session %s" % self.uid)
 
120
        del self.guard.sessions[self.uid]
 
121
        for c in self.expireCallbacks:
 
122
            try:
 
123
                c()
 
124
            except:
 
125
                log.err()
 
126
        self.expireCallbacks = []
 
127
        if self.checkExpiredID:
 
128
            self.checkExpiredID.cancel()
 
129
            self.checkExpiredID = None
 
130
 
 
131
    def touch(self):
 
132
        self.lastModified = time.time()
 
133
 
 
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):
 
139
                self.expire()
 
140
            else:
 
141
                log.msg("no session to expire: %s" % self.uid)
 
142
        else:
 
143
            log.msg("session given the will to live for %s more seconds" % self.lifetime)
 
144
            self.checkExpiredID = reactor.callLater(self.lifetime,
 
145
                                                    self.checkExpired)
 
146
    def __getstate__(self):
 
147
        d = self.__dict__.copy()
 
148
        if d.has_key('checkExpiredID'):
 
149
            del d['checkExpiredID']
 
150
        return d
 
151
 
 
152
    def __setstate__(self, d):
 
153
        self.__dict__.update(d)
 
154
        self.touch()
 
155
        self.checkExpired()
 
156
 
 
157
INIT_SESSION = 'session-init'
 
158
 
 
159
def _setSession(wrap, req, cook):
 
160
    req.session = wrap.sessions[cook]
 
161
    req.getSession = req.session._getSelf
 
162
 
 
163
def urlToChild(request, *ar, **kw):
 
164
    pp = request.prepath.pop()
 
165
    orig = request.prePathURL()
 
166
    request.prepath.append(pp)
 
167
    c = '/'.join(ar)
 
168
    if orig[-1] == '/':
 
169
        # this SHOULD only happen in the case where the URL is just the hostname
 
170
        ret = orig + c
 
171
    else:
 
172
        ret = orig + '/' + c
 
173
    args = request.args.copy()
 
174
    args.update(kw)
 
175
    if args:
 
176
        ret += '?'+urllib.urlencode(args)
 
177
    return ret
 
178
 
 
179
def redirectToSession(request, garbage):
 
180
    rd = Redirect(urlToChild(request, *request.postpath, **{garbage:1}))
 
181
    rd.isLeaf = 1
 
182
    return rd
 
183
 
 
184
SESSION_KEY='__session_key__'
 
185
 
 
186
class SessionWrapper(Resource):
 
187
 
 
188
    sessionLifetime = 1800
 
189
    
 
190
    def __init__(self, rsrc, cookieKey=None):
 
191
        Resource.__init__(self)
 
192
        self.resource = rsrc
 
193
        if cookieKey is None:
 
194
            cookieKey = "woven_session_" + _sessionCookie()
 
195
        self.cookieKey = cookieKey
 
196
        self.sessions = {}
 
197
 
 
198
    def render(self, request):
 
199
        return redirectTo(addSlash(request), request)
 
200
 
 
201
    def getChild(self, path, request):
 
202
        if not request.prepath:
 
203
            return None
 
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)
 
213
            if cookie == key:
 
214
                # /sessionized-url/${SESSION_KEY}aef9c34aecc3d9148/foo
 
215
                #                  ^
 
216
                #                  we are this getChild
 
217
                # with a matching cookie
 
218
                return redirectToSession(request, '__session_just_started__')
 
219
            else:
 
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
 
223
                # there.
 
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)
 
228
                return self.resource
 
229
        elif cookie in self.sessions:
 
230
            # /sessionized-url/foo
 
231
            #                 ^ we are this getChild
 
232
            # with a session
 
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
 
238
            #                  ^ this getChild
 
239
            # without a session
 
240
            newCookie = _sessionCookie()
 
241
            request.addCookie(self.cookieKey, newCookie, path="/")
 
242
            sz = self.sessions[newCookie] = GuardSession(self, newCookie)
 
243
            sz.checkExpired()
 
244
            rd = Redirect(urlToChild(request, SESSION_KEY+newCookie,
 
245
                                              *request.postpath))
 
246
            rd.isLeaf = 1
 
247
            return rd
 
248
        else:
 
249
            # /sessionized-url/foo
 
250
            #                 ^ we are this getChild
 
251
            # without a session
 
252
            request.getSession = lambda interface=None: None
 
253
            return getResource(self.resource, path, request)
 
254
 
 
255
def getResource(resource, path, request):
 
256
    if resource.isLeaf:
 
257
        request.postpath.insert(0, request.prepath.pop())
 
258
        return resource
 
259
    else:
 
260
        return resource.getChildWithDefault(path, request)
 
261
 
 
262
INIT_PERSPECTIVE = 'perspective-init'
 
263
DESTROY_PERSPECTIVE = 'perspective-destroy'
 
264
 
 
265
from twisted.python import formmethod as fm
 
266
from twisted.web.woven import form
 
267
 
 
268
 
 
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),
 
275
    )
 
276
 
 
277
from twisted.cred.credentials import UsernamePassword, Anonymous
 
278
 
 
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.
 
282
 
 
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.
 
286
 
 
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
 
293
    through guard.
 
294
 
 
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})
 
298
    
 
299
    """
 
300
    
 
301
    def __init__(self, portal, callback=None, errback=None):
 
302
        """Constructs a UsernamePasswordWrapper around the given portal.
 
303
 
 
304
        @param portal: A cred portal for your web application. The checkers
 
305
            associated with this portal must be able to accept username/password
 
306
            credentials.
 
307
        @type portal: L{twisted.cred.portal.Portal}
 
308
        
 
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
 
312
            "Thank You" page.
 
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>}.
 
316
 
 
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
 
320
            errors noted.)
 
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>}.
 
324
        """
 
325
        Resource.__init__(self)
 
326
        self.portal = portal
 
327
        self.callback = callback
 
328
        self.errback = errback
 
329
 
 
330
    def _ebFilter(self, f):
 
331
        f.trap(LoginFailed, UnauthorizedLogin)
 
332
        raise fm.FormException(password="Login failed, please enter correct username and password.")
 
333
 
 
334
    def getChild(self, path, request):
 
335
        s = request.getSession()
 
336
        if s is None:
 
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)
 
342
 
 
343
            def triggerLogin(username, password, submit=None):
 
344
                return self.portal.login(
 
345
                    UsernamePassword(username, password),
 
346
                    None, 
 
347
                    IResource
 
348
                ).addCallback(
 
349
                    loginSuccess
 
350
                ).addErrback(
 
351
                    self._ebFilter
 
352
                )
 
353
 
 
354
            return form.FormProcessor(
 
355
                newLoginSignature.method(
 
356
                    triggerLogin
 
357
                ),
 
358
                callback=self.callback,
 
359
                errback=self.errback
 
360
            )
 
361
        elif path == DESTROY_PERSPECTIVE:
 
362
            s.portalLogout(self.portal)
 
363
            return Redirect(".")
 
364
        else:
 
365
            r = s.resourceForPortal(self.portal)
 
366
            if r:
 
367
                ## Delegate our getChild to the resource our portal says is the right one.
 
368
                return getResource(r[0], path, request)
 
369
            else:
 
370
                return DeferredResource(
 
371
                    self.portal.login(Anonymous(), None, IResource
 
372
                                      ).addCallback(
 
373
                    lambda (interface, avatarAspect, logout):
 
374
                    getResource(s.setResourceForPortal(avatarAspect,
 
375
                                           self.portal, logout),
 
376
                                path, request)))
 
377
 
 
378
 
 
379
 
 
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)
 
383