~landscape/zope3/newer-from-ztk

« back to all changes in this revision

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

  • Committer: Thomas Hervé
  • Date: 2009-07-08 13:52:04 UTC
  • Revision ID: thomas@canonical.com-20090708135204-df5eesrthifpylf8
Remove twisted copy

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