Back to index

plone3  3.1.7
CookieCrumbler.py
Go to the documentation of this file.
00001 ##############################################################################
00002 #
00003 # Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
00004 # 
00005 # This software is subject to the provisions of the Zope Public License,
00006 # Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
00007 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
00008 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
00009 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
00010 # FOR A PARTICULAR PURPOSE.
00011 # 
00012 ##############################################################################
00013 """ Cookie Crumbler: Enable cookies for non-cookie user folders.
00014 
00015 $Id: CookieCrumbler.py 76996 2007-06-24 00:18:49Z hannosch $
00016 """
00017 
00018 from base64 import encodestring, decodestring
00019 from urllib import quote, unquote
00020 
00021 from Acquisition import aq_inner, aq_parent
00022 from DateTime import DateTime
00023 from AccessControl import ClassSecurityInfo, Permissions
00024 from ZPublisher import BeforeTraverse
00025 import Globals
00026 from Globals import HTMLFile
00027 from ZPublisher.HTTPRequest import HTTPRequest
00028 from OFS.Folder import Folder
00029 from zExceptions import Redirect
00030 from zope.interface import implements
00031 from zope.app.container.interfaces import IObjectMovedEvent
00032 from OFS.interfaces import IObjectWillBeMovedEvent
00033 
00034 from interfaces import ICookieCrumbler
00035 
00036 
00037 # Constants.
00038 ATTEMPT_NONE = 0       # No attempt at authentication
00039 ATTEMPT_LOGIN = 1      # Attempt to log in
00040 ATTEMPT_RESUME = 2     # Attempt to resume session
00041 
00042 ModifyCookieCrumblers = 'Modify Cookie Crumblers'
00043 ViewManagementScreens = Permissions.view_management_screens
00044 
00045 
00046 class CookieCrumblerDisabled(Exception):
00047 
00048     """Cookie crumbler should not be used for a certain request.
00049     """
00050 
00051 
00052 class CookieCrumbler(Folder):
00053 
00054     """Reads cookies during traversal and simulates the HTTP auth headers.
00055     """
00056 
00057     implements(ICookieCrumbler)
00058 
00059     meta_type = 'Cookie Crumbler'
00060 
00061     security = ClassSecurityInfo()
00062     security.declareProtected(ModifyCookieCrumblers, 'manage_editProperties')
00063     security.declareProtected(ModifyCookieCrumblers, 'manage_changeProperties')
00064     security.declareProtected(ViewManagementScreens, 'manage_propertiesForm')
00065 
00066     # By default, anonymous users can view login/logout pages.
00067     _View_Permission = ('Anonymous',)
00068 
00069 
00070     _properties = ({'id':'auth_cookie', 'type': 'string', 'mode':'w',
00071                     'label':'Authentication cookie name'},
00072                    {'id':'name_cookie', 'type': 'string', 'mode':'w',
00073                     'label':'User name form variable'},
00074                    {'id':'pw_cookie', 'type': 'string', 'mode':'w',
00075                     'label':'User password form variable'},
00076                    {'id':'persist_cookie', 'type': 'string', 'mode':'w',
00077                     'label':'User name persistence form variable'},
00078                    {'id':'auto_login_page', 'type': 'string', 'mode':'w',
00079                     'label':'Login page ID'},
00080                    {'id':'logout_page', 'type': 'string', 'mode':'w',
00081                     'label':'Logout page ID'},
00082                    {'id':'unauth_page', 'type': 'string', 'mode':'w',
00083                     'label':'Failed authorization page ID'},
00084                    {'id':'local_cookie_path', 'type': 'boolean', 'mode':'w',
00085                     'label':'Use cookie paths to limit scope'},
00086                    {'id':'cache_header_value', 'type': 'string', 'mode':'w',
00087                     'label':'Cache-Control header value'},
00088                    {'id':'log_username', 'type':'boolean', 'mode': 'w',
00089                     'label':'Log cookie auth username to access log'}
00090                    )
00091 
00092     auth_cookie = '__ac'
00093     name_cookie = '__ac_name'
00094     pw_cookie = '__ac_password'
00095     persist_cookie = '__ac_persistent'
00096     auto_login_page = 'login_form'
00097     unauth_page = ''
00098     logout_page = 'logged_out'
00099     local_cookie_path = False
00100     cache_header_value = 'private'
00101     log_username = True
00102 
00103     security.declarePrivate('delRequestVar')
00104     def delRequestVar(self, req, name):
00105         # No errors of any sort may propagate, and we don't care *what*
00106         # they are, even to log them.
00107         try: del req.other[name]
00108         except: pass
00109         try: del req.form[name]
00110         except: pass
00111         try: del req.cookies[name]
00112         except: pass
00113         try: del req.environ[name]
00114         except: pass
00115 
00116     security.declarePublic('getCookiePath')
00117     def getCookiePath(self):
00118         if not self.local_cookie_path:
00119             return '/'
00120         parent = aq_parent(aq_inner(self))
00121         if parent is not None:
00122             return '/' + parent.absolute_url(1)
00123         else:
00124             return '/'
00125 
00126     # Allow overridable cookie set/expiration methods.
00127     security.declarePrivate('getCookieMethod')
00128     def getCookieMethod(self, name, default=None):
00129         return getattr(self, name, default)
00130 
00131     security.declarePrivate('defaultSetAuthCookie')
00132     def defaultSetAuthCookie(self, resp, cookie_name, cookie_value):
00133         # XXX: this method violates the rules for tools/utilities:
00134         # it depends on self.REQUEST
00135         kw = {}
00136         req = getattr(self, 'REQUEST', None)
00137         if req is not None and req.get('SERVER_URL', '').startswith('https:'):
00138             # Ask the client to send back the cookie only in SSL mode
00139             kw['secure'] = 'y'
00140         resp.setCookie(cookie_name, cookie_value,
00141                        path=self.getCookiePath(), **kw)
00142 
00143     security.declarePrivate('defaultExpireAuthCookie')
00144     def defaultExpireAuthCookie(self, resp, cookie_name):
00145         resp.expireCookie(cookie_name, path=self.getCookiePath())
00146     
00147     def _setAuthHeader(self, ac, request, response):
00148         """Set the auth headers for both the Zope and Medusa http request
00149         objects.
00150         """
00151         request._auth = 'Basic %s' % ac
00152         response._auth = 1
00153         if self.log_username:
00154             # Set the authorization header in the medusa http request
00155             # so that the username can be logged to the Z2.log
00156             try:
00157                 # Put the full-arm latex glove on now...
00158                 medusa_headers = response.stdout._request._header_cache
00159             except AttributeError:
00160                 pass
00161             else:
00162                 medusa_headers['authorization'] = request._auth
00163 
00164     security.declarePrivate('modifyRequest')
00165     def modifyRequest(self, req, resp):
00166         """Copies cookie-supplied credentials to the basic auth fields.
00167 
00168         Returns a flag indicating what the user is trying to do with
00169         cookies: ATTEMPT_NONE, ATTEMPT_LOGIN, or ATTEMPT_RESUME.  If
00170         cookie login is disabled for this request, raises
00171         CookieCrumblerDisabled.
00172         """
00173         if (req.__class__ is not HTTPRequest
00174             or not req['REQUEST_METHOD'] in ('HEAD', 'GET', 'PUT', 'POST')
00175             or req.environ.has_key('WEBDAV_SOURCE_PORT')):
00176             raise CookieCrumblerDisabled
00177 
00178         # attempt may contain information about an earlier attempt to
00179         # authenticate using a higher-up cookie crumbler within the
00180         # same request.
00181         attempt = getattr(req, '_cookie_auth', ATTEMPT_NONE)
00182 
00183         if attempt == ATTEMPT_NONE:
00184             if req._auth:
00185                 # An auth header was provided and no cookie crumbler
00186                 # created it.  The user must be using basic auth.
00187                 raise CookieCrumblerDisabled
00188 
00189             if req.has_key(self.pw_cookie) and req.has_key(self.name_cookie):
00190                 # Attempt to log in and set cookies.
00191                 attempt = ATTEMPT_LOGIN
00192                 name = req[self.name_cookie]
00193                 pw = req[self.pw_cookie]
00194                 ac = encodestring('%s:%s' % (name, pw)).rstrip()
00195                 self._setAuthHeader(ac, req, resp)
00196                 if req.get(self.persist_cookie, 0):
00197                     # Persist the user name (but not the pw or session)
00198                     expires = (DateTime() + 365).toZone('GMT').rfc822()
00199                     resp.setCookie(self.name_cookie, name,
00200                                    path=self.getCookiePath(),
00201                                    expires=expires)
00202                 else:
00203                     # Expire the user name
00204                     resp.expireCookie(self.name_cookie,
00205                                       path=self.getCookiePath())
00206                 method = self.getCookieMethod( 'setAuthCookie'
00207                                              , self.defaultSetAuthCookie )
00208                 method( resp, self.auth_cookie, quote( ac ) )
00209                 self.delRequestVar(req, self.name_cookie)
00210                 self.delRequestVar(req, self.pw_cookie)
00211 
00212             elif req.has_key(self.auth_cookie):
00213                 # Attempt to resume a session if the cookie is valid.
00214                 # Copy __ac to the auth header.
00215                 ac = unquote(req[self.auth_cookie])
00216                 if ac and ac != 'deleted':
00217                     try:
00218                         decodestring(ac)
00219                     except:
00220                         # Not a valid auth header.
00221                         pass
00222                     else:
00223                         attempt = ATTEMPT_RESUME
00224                         self._setAuthHeader(ac, req, resp)
00225                         self.delRequestVar(req, self.auth_cookie)
00226                         method = self.getCookieMethod(
00227                             'twiddleAuthCookie', None)
00228                         if method is not None:
00229                             method(resp, self.auth_cookie, quote(ac))
00230 
00231         req._cookie_auth = attempt
00232         return attempt
00233 
00234 
00235     def __call__(self, container, req):
00236         '''The __before_publishing_traverse__ hook.'''
00237         resp = req['RESPONSE']
00238         try:
00239             attempt = self.modifyRequest(req, resp)
00240         except CookieCrumblerDisabled:
00241             return
00242         if req.get('disable_cookie_login__', 0):
00243             return
00244 
00245         if (self.unauth_page or
00246             attempt == ATTEMPT_LOGIN or attempt == ATTEMPT_NONE):
00247             # Modify the "unauthorized" response.
00248             req._hold(ResponseCleanup(resp))
00249             resp.unauthorized = self.unauthorized
00250             resp._unauthorized = self._unauthorized
00251         if attempt != ATTEMPT_NONE:
00252             # Trying to log in or resume a session
00253             if self.cache_header_value:
00254                 # we don't want caches to cache the resulting page
00255                 resp.setHeader('Cache-Control', self.cache_header_value)
00256                 # demystify this in the response.
00257                 resp.setHeader('X-Cache-Control-Hdr-Modified-By',
00258                                'CookieCrumbler')
00259             phys_path = self.getPhysicalPath()
00260             if self.logout_page:
00261                 # Cookies are in use.
00262                 page = getattr(container, self.logout_page, None)
00263                 if page is not None:
00264                     # Provide a logout page.
00265                     req._logout_path = phys_path + ('logout',)
00266             req._credentials_changed_path = (
00267                 phys_path + ('credentialsChanged',))
00268 
00269     security.declarePublic('credentialsChanged')
00270     def credentialsChanged(self, user, name, pw):
00271         # XXX: this method violates the rules for tools/utilities:
00272         # it depends on self.REQUEST
00273         ac = encodestring('%s:%s' % (name, pw)).rstrip()
00274         method = self.getCookieMethod( 'setAuthCookie'
00275                                        , self.defaultSetAuthCookie )
00276         resp = self.REQUEST['RESPONSE']
00277         method( resp, self.auth_cookie, quote( ac ) )
00278 
00279     def _cleanupResponse(self):
00280         # XXX: this method violates the rules for tools/utilities:
00281         # it depends on self.REQUEST
00282         resp = self.REQUEST['RESPONSE']
00283         # No errors of any sort may propagate, and we don't care *what*
00284         # they are, even to log them.
00285         try: del resp.unauthorized
00286         except: pass
00287         try: del resp._unauthorized
00288         except: pass
00289         return resp
00290 
00291     security.declarePrivate('unauthorized')
00292     def unauthorized(self):
00293         resp = self._cleanupResponse()
00294         # If we set the auth cookie before, delete it now.
00295         if resp.cookies.has_key(self.auth_cookie):
00296             del resp.cookies[self.auth_cookie]
00297         # Redirect if desired.
00298         url = self.getUnauthorizedURL()
00299         if url is not None:
00300             raise Redirect, url
00301         # Fall through to the standard unauthorized() call.
00302         resp.unauthorized()
00303 
00304     def _unauthorized(self):
00305         resp = self._cleanupResponse()
00306         # If we set the auth cookie before, delete it now.
00307         if resp.cookies.has_key(self.auth_cookie):
00308             del resp.cookies[self.auth_cookie]
00309         # Redirect if desired.
00310         url = self.getUnauthorizedURL()
00311         if url is not None:
00312             resp.redirect(url, lock=1)
00313             # We don't need to raise an exception.
00314             return
00315         # Fall through to the standard _unauthorized() call.
00316         resp._unauthorized()
00317 
00318     security.declarePublic('getUnauthorizedURL')
00319     def getUnauthorizedURL(self):
00320         '''
00321         Redirects to the login page.
00322         '''
00323         # XXX: this method violates the rules for tools/utilities:
00324         # it depends on self.REQUEST
00325         req = self.REQUEST
00326         resp = req['RESPONSE']
00327         attempt = getattr(req, '_cookie_auth', ATTEMPT_NONE)
00328         if attempt == ATTEMPT_NONE:
00329             # An anonymous user was denied access to something.
00330             page_id = self.auto_login_page
00331             retry = ''
00332         elif attempt == ATTEMPT_LOGIN:
00333             # The login attempt failed.  Try again.
00334             page_id = self.auto_login_page
00335             retry = '1'
00336         else:
00337             # An authenticated user was denied access to something.
00338             page_id = self.unauth_page
00339             retry = ''
00340         if page_id:
00341             page = self.restrictedTraverse(page_id, None)
00342             if page is not None:
00343                 came_from = req.get('came_from', None)
00344                 if came_from is None:
00345                     came_from = req.get('VIRTUAL_URL', None)
00346                     if came_from is None:
00347                         came_from = '%s%s%s' % ( req['SERVER_URL'].strip(),
00348                                                  req['SCRIPT_NAME'].strip(),
00349                                                  req['PATH_INFO'].strip() )
00350                     query = req.get('QUERY_STRING')
00351                     if query:
00352                         # Include the query string in came_from
00353                         if not query.startswith('?'):
00354                             query = '?' + query
00355                         came_from = came_from + query
00356                 url = '%s?came_from=%s&retry=%s&disable_cookie_login__=1' % (
00357                     page.absolute_url(), quote(came_from), retry)
00358                 return url
00359         return None
00360 
00361     # backward compatible alias
00362     getLoginURL = getUnauthorizedURL
00363 
00364     security.declarePublic('logout')
00365     def logout(self):
00366         '''
00367         Logs out the user and redirects to the logout page.
00368         '''
00369         # XXX: this method violates the rules for tools/utilities:
00370         # it depends on self.REQUEST
00371         req = self.REQUEST
00372         resp = req['RESPONSE']
00373         method = self.getCookieMethod( 'expireAuthCookie'
00374                                      , self.defaultExpireAuthCookie )
00375         method( resp, cookie_name=self.auth_cookie )
00376         if self.logout_page:
00377             page = self.restrictedTraverse(self.logout_page, None)
00378             if page is not None:
00379                 resp.redirect('%s?disable_cookie_login__=1'
00380                               % page.absolute_url())
00381                 return ''
00382         # We should not normally get here.
00383         return 'Logged out.'
00384 
00385     security.declarePublic('propertyLabel')
00386     def propertyLabel(self, id):
00387         """Return a label for the given property id
00388         """
00389         for p in self._properties:
00390             if p['id'] == id:
00391                 return p.get('label', id)
00392         return id
00393 
00394 Globals.InitializeClass(CookieCrumbler)
00395 
00396 
00397 def handleCookieCrumblerEvent(ob, event):
00398     """ Event subscriber for (un)registering a CC as a before traverse hook.
00399     """
00400     if not ICookieCrumbler.providedBy(ob):
00401         return
00402 
00403     if IObjectMovedEvent.providedBy(event):
00404         if event.newParent is not None:
00405             # register before traverse hook
00406             handle = ob.meta_type + '/' + ob.getId()
00407             nc = BeforeTraverse.NameCaller(ob.getId())
00408             BeforeTraverse.registerBeforeTraverse(event.newParent, nc, handle)
00409     elif IObjectWillBeMovedEvent.providedBy(event):
00410         if event.oldParent is not None:
00411             # unregister before traverse hook
00412             handle = ob.meta_type + '/' + ob.getId()
00413             BeforeTraverse.unregisterBeforeTraverse(event.oldParent, handle)
00414 
00415 class ResponseCleanup:
00416     def __init__(self, resp):
00417         self.resp = resp
00418 
00419     def __del__(self):
00420         # Free the references.
00421         #
00422         # No errors of any sort may propagate, and we don't care *what*
00423         # they are, even to log them.
00424         try: del self.resp.unauthorized
00425         except: pass
00426         try: del self.resp._unauthorized
00427         except: pass
00428         try: del self.resp
00429         except: pass
00430 
00431 
00432 manage_addCCForm = HTMLFile('dtml/addCC', globals())
00433 manage_addCCForm.__name__ = 'addCC'
00434 
00435 def _create_forms(ob):
00436     ''' Create default forms inside ob '''
00437     import os
00438     from OFS.DTMLMethod import addDTMLMethod
00439     dtmldir = os.path.join(os.path.dirname(__file__), 'dtml')
00440     for fn in ('index_html', 'logged_in', 'logged_out', 'login_form',
00441                 'standard_login_footer', 'standard_login_header'):
00442         filename = os.path.join(dtmldir, fn + '.dtml')
00443         f = open(filename, 'rt')
00444         try: data = f.read()
00445         finally: f.close()
00446         addDTMLMethod(ob, fn, file=data)
00447 
00448 def manage_addCC(dispatcher, id, create_forms=0, REQUEST=None):
00449     ' '
00450     ob = CookieCrumbler()
00451     ob.id = id
00452     dispatcher._setObject(ob.getId(), ob)
00453     ob = getattr(dispatcher.this(), ob.getId())
00454     if create_forms:
00455         _create_forms(ob)
00456     if REQUEST is not None:
00457         return dispatcher.manage_main(dispatcher, REQUEST)