# -*- test-case-name: twisted.test.test_woven -*- # Copyright (c) 2001-2004 Twisted Matrix Laboratories. # See LICENSE for details. # """Resource protection for Woven. If you wish to use twisted.cred to protect your Woven application, you are probably most interested in L{UsernamePasswordWrapper}. """ from __future__ import nested_scopes __version__ = "$Revision: 1.34 $"[11:-2] import random import time import md5 import warnings import urllib # Twisted Imports from twisted.python import log, components from twisted.web.resource import Resource, IResource from twisted.web.util import redirectTo, Redirect, DeferredResource from twisted.web.static import addSlash from twisted.internet import reactor from twisted.cred.error import Unauthorized, LoginFailed, UnauthorizedLogin def _sessionCookie(): return md5.new("%s_%s" % (str(random.random()) , str(time.time()))).hexdigest() class GuardSession(components.Componentized): """A user's session with a system. This utility class contains no functionality, but is used to represent a session. """ def __init__(self, guard, uid): """Initialize a session with a unique ID for that session. """ components.Componentized.__init__(self) self.guard = guard self.uid = uid self.expireCallbacks = [] self.checkExpiredID = None self.setLifetime(60) self.services = {} self.portals = {} self.touch() def _getSelf(self, interface=None): self.touch() if interface is None: return self else: return self.getComponent(interface) # Old Guard interfaces def clientForService(self, service): x = self.services.get(service) if x: return x[1] else: return x def setClientForService(self, ident, perspective, client, service): if self.services.has_key(service): p, c, i = self.services[service] p.detached(c, ident) del self.services[service] else: self.services[service] = perspective, client, ident perspective.attached(client, ident) # this return value is useful for services that need to do asynchronous # stuff. return client # New Guard Interfaces def resourceForPortal(self, port): return self.portals.get(port) def setResourceForPortal(self, rsrc, port, logout): self.portalLogout(port) self.portals[port] = rsrc, logout return rsrc def portalLogout(self, port): p = self.portals.get(port) if p: r, l = p try: l() except: log.err() del self.portals[port] # timeouts and expiration def setLifetime(self, lifetime): """Set the approximate lifetime of this session, in seconds. This is highly imprecise, but it allows you to set some general parameters about when this session will expire. A callback will be scheduled each 'lifetime' seconds, and if I have not been 'touch()'ed in half a lifetime, I will be immediately expired. """ self.lifetime = lifetime def notifyOnExpire(self, callback): """Call this callback when the session expires or logs out. """ self.expireCallbacks.append(callback) def expire(self): """Expire/logout of the session. """ log.msg("expired session %s" % self.uid) del self.guard.sessions[self.uid] for c in self.expireCallbacks: try: c() except: log.err() self.expireCallbacks = [] if self.checkExpiredID: self.checkExpiredID.cancel() self.checkExpiredID = None def touch(self): self.lastModified = time.time() def checkExpired(self): self.checkExpiredID = None # If I haven't been touched in 15 minutes: if time.time() - self.lastModified > self.lifetime / 2: if self.guard.sessions.has_key(self.uid): self.expire() else: log.msg("no session to expire: %s" % self.uid) else: log.msg("session given the will to live for %s more seconds" % self.lifetime) self.checkExpiredID = reactor.callLater(self.lifetime, self.checkExpired) def __getstate__(self): d = self.__dict__.copy() if d.has_key('checkExpiredID'): del d['checkExpiredID'] return d def __setstate__(self, d): self.__dict__.update(d) self.touch() self.checkExpired() INIT_SESSION = 'session-init' def _setSession(wrap, req, cook): req.session = wrap.sessions[cook] req.getSession = req.session._getSelf def urlToChild(request, *ar, **kw): pp = request.prepath.pop() orig = request.prePathURL() request.prepath.append(pp) c = '/'.join(ar) if orig[-1] == '/': # this SHOULD only happen in the case where the URL is just the hostname ret = orig + c else: ret = orig + '/' + c args = request.args.copy() args.update(kw) if args: ret += '?'+urllib.urlencode(args) return ret def redirectToSession(request, garbage): rd = Redirect(urlToChild(request, *request.postpath, **{garbage:1})) rd.isLeaf = 1 return rd SESSION_KEY='__session_key__' class SessionWrapper(Resource): sessionLifetime = 1800 def __init__(self, rsrc, cookieKey=None): Resource.__init__(self) self.resource = rsrc if cookieKey is None: cookieKey = "woven_session_" + _sessionCookie() self.cookieKey = cookieKey self.sessions = {} def render(self, request): return redirectTo(addSlash(request), request) def getChild(self, path, request): if not request.prepath: return None cookie = request.getCookie(self.cookieKey) setupURL = urlToChild(request, INIT_SESSION, *([path]+request.postpath)) request.setupSessionURL = setupURL request.setupSession = lambda: Redirect(setupURL) if path.startswith(SESSION_KEY): key = path[len(SESSION_KEY):] if key not in self.sessions: return redirectToSession(request, '__start_session__') self.sessions[key].setLifetime(self.sessionLifetime) if cookie == key: # /sessionized-url/${SESSION_KEY}aef9c34aecc3d9148/foo # ^ # we are this getChild # with a matching cookie return redirectToSession(request, '__session_just_started__') else: # We attempted to negotiate the session but failed (the user # probably has cookies disabled): now we're going to return the # resource we contain. In general the getChild shouldn't stop # there. # /sessionized-url/${SESSION_KEY}aef9c34aecc3d9148/foo # ^ we are this getChild # without a cookie (or with a mismatched cookie) _setSession(self, request, key) return self.resource elif cookie in self.sessions: # /sessionized-url/foo # ^ we are this getChild # with a session _setSession(self, request, cookie) return getResource(self.resource, path, request) elif path == INIT_SESSION: # initialize the session # /sessionized-url/session-init # ^ this getChild # without a session newCookie = _sessionCookie() request.addCookie(self.cookieKey, newCookie, path="/") sz = self.sessions[newCookie] = GuardSession(self, newCookie) sz.checkExpired() rd = Redirect(urlToChild(request, SESSION_KEY+newCookie, *request.postpath)) rd.isLeaf = 1 return rd else: # /sessionized-url/foo # ^ we are this getChild # without a session request.getSession = lambda interface=None: None return getResource(self.resource, path, request) def getResource(resource, path, request): if resource.isLeaf: request.postpath.insert(0, request.prepath.pop()) return resource else: return resource.getChildWithDefault(path, request) INIT_PERSPECTIVE = 'perspective-init' DESTROY_PERSPECTIVE = 'perspective-destroy' from twisted.python import formmethod as fm from twisted.web.woven import form loginSignature = fm.MethodSignature( fm.String("identity", "", "Identity", "The unique name of your account."), fm.Password("password", "", "Password", "The creative name of your password."), fm.String("perspective", None, "Perspective", "(Optional) The name of the role within your account " "you wish to perform.")) class PerspectiveWrapper(Resource): """DEPRECATED. I am a wrapper that will restrict access to Resources based on a C{twisted.cred.service.Service}'s 'authorizer' and perspective list. Please note that I must be in turn wrapped by a SessionWrapper, since my login functionality requires a session to be established. """ def __init__(self, service, noAuthResource, authResourceFactory, callback=None): """Create a PerspectiveWrapper. @type service: C{twisted.cred.service.Service} @type noAuthResource: C{Resource} @type authResourceFactory: a callable object @param authResourceFactory: This should be a function which takes as an argument perspective from 'service' and returns a C{Resource} instance. @param noAuthResource: This parameter is the C{Resource} that is used when the user is browsing this site anonymously. Somewhere accessible from this should be a link to 'perspective-init', which will display a C{form.FormProcessor} that allows the user to log in. """ warnings.warn("Please use UsernamePasswordWrapper instead", DeprecationWarning, 2) Resource.__init__(self) self.service = service self.noAuthResource = noAuthResource self.authResourceFactory = authResourceFactory self.callback = callback def getChild(self, path, request): s = request.getSession() if s is None: return request.setupSession() if path == INIT_PERSPECTIVE: def loginMethod(identity, password, perspective=None): idfr = self.service.authorizer.getIdentityRequest(identity) idfr.addCallback( lambda ident: ident.verifyPlainPassword(password). addCallback(lambda ign: ident.requestPerspectiveForService(self.service.serviceName)) .addCallback(lambda psp: s.setClientForService(ident, psp, self.authResourceFactory(psp), self.service))) def loginFailure(f): if f.trap(Unauthorized): raise fm.FormException(str(f.value)) raise f idfr.addErrback(loginFailure) return idfr return form.FormProcessor( loginSignature.method(loginMethod), callback=self.callback) elif path == DESTROY_PERSPECTIVE: s.setClientForService(None, None, None, self.service) return Redirect(".") else: sc = s.clientForService(self.service) if sc: return getResource(sc, path, request) return getResource(self.noAuthResource, path, request) newLoginSignature = fm.MethodSignature( fm.String("username", "", "Username", "Your user name."), fm.Password("password", "", "Password", "Your password."), fm.Submit("submit", choices=[("Login", "", "")], allowNone=1), ) from twisted.cred.credentials import UsernamePassword, Anonymous class UsernamePasswordWrapper(Resource): """I bring a C{twisted.cred} Portal to the web. Use me to provide different Resources (usually entire pages) based on a user's authentication details. A C{UsernamePasswordWrapper} is a L{Resource}, and is usually wrapped in a L{SessionWrapper} before being inserted into the site tree. The L{Realm} associated with your L{Portal} should be prepared to accept a request for an avatar that implements the L{twisted.web.resource.IResource} interface. This avatar should probably be something like a Woven L{Page}. That is, it should represent a whole web page. Once you return this avatar, requests for it's children do not go through guard. If you want to determine what unauthenticated users see, make sure your L{Portal} has a checker associated that allows anonymous access. (See L{twisted.cred.checkers.AllowAnonymousAccess}) """ def __init__(self, portal, callback=None, errback=None): """Constructs a UsernamePasswordWrapper around the given portal. @param portal: A cred portal for your web application. The checkers associated with this portal must be able to accept username/password credentials. @type portal: L{twisted.cred.portal.Portal} @param callback: Gets called after a successful login attempt. A resource that redirects to "." will display the avatar resource. If this parameter isn't provided, defaults to a standard Woven "Thank You" page. @type callback: A callable that accepts a Woven L{model} and returns a L{IResource}. @param errback: Gets called after a failed login attempt. If this parameter is not provided, defaults to a the standard Woven form error (i.e. The original form on a page of its own, with errors noted.) @type errback: A callable that accepts a Woven L{model} and returns a L{IResource}. """ Resource.__init__(self) self.portal = portal self.callback = callback self.errback = errback def _ebFilter(self, f): f.trap(LoginFailed, UnauthorizedLogin) raise fm.FormException(password="Login failed, please enter correct username and password.") def getChild(self, path, request): s = request.getSession() if s is None: return request.setupSession() if path == INIT_PERSPECTIVE: def loginSuccess(result): interface, avatarAspect, logout = result s.setResourceForPortal(avatarAspect, self.portal, logout) def triggerLogin(username, password, submit=None): return self.portal.login( UsernamePassword(username, password), None, IResource ).addCallback( loginSuccess ).addErrback( self._ebFilter ) return form.FormProcessor( newLoginSignature.method( triggerLogin ), callback=self.callback, errback=self.errback ) elif path == DESTROY_PERSPECTIVE: s.portalLogout(self.portal) return Redirect(".") else: r = s.resourceForPortal(self.portal) if r: ## Delegate our getChild to the resource our portal says is the right one. return getResource(r[0], path, request) else: return DeferredResource( self.portal.login(Anonymous(), None, IResource ).addCallback( lambda (interface, avatarAspect, logout): getResource(s.setResourceForPortal(avatarAspect, self.portal, logout), path, request))) from twisted.web.woven import interfaces, utils ## Dumb hack until we have an ISession and use interface-to-interface adaption components.registerAdapter(utils.WovenLivePage, GuardSession, interfaces.IWovenLivePage)