[CMF-checkins] CVS: CMF/CMFCore - CookieCrumbler.py:1.21

Shane Hathaway shane@zope.com
Mon, 16 Jun 2003 13:39:09 -0400


Update of /cvs-repository/CMF/CMFCore
In directory cvs.zope.org:/tmp/cvs-serv21067

Modified Files:
	CookieCrumbler.py 
Log Message:
Brought CookieCrumbler up to date with standalone version.


=== CMF/CMFCore/CookieCrumbler.py 1.20 => 1.21 ===
--- CMF/CMFCore/CookieCrumbler.py:1.20	Fri Jun 13 14:13:30 2003
+++ CMF/CMFCore/CookieCrumbler.py	Mon Jun 16 13:39:08 2003
@@ -15,27 +15,33 @@
 $Id$
 """
 
-from base64 import encodestring
+from base64 import encodestring, decodestring
 from urllib import quote, unquote
+from Acquisition import aq_inner, aq_parent
 from DateTime import DateTime
 from utils import SimpleItemWithProperties
-from AccessControl import ClassSecurityInfo
+from AccessControl import ClassSecurityInfo, Permissions
 from ZPublisher import BeforeTraverse
 import Globals
-from CMFCorePermissions import ModifyCookieCrumblers
-from CMFCorePermissions import ViewManagementScreens
 from Globals import HTMLFile
 from zLOG import LOG, ERROR
 import sys
 
 from ZPublisher.HTTPRequest import HTTPRequest
 
+
 # Constants.
-ATTEMPT_DISABLED = -1  # Disable cookie crumbler
 ATTEMPT_NONE = 0       # No attempt at authentication
 ATTEMPT_LOGIN = 1      # Attempt to log in
 ATTEMPT_RESUME = 2     # Attempt to resume session
 
+ModifyCookieCrumblers = 'Modify Cookie Crumblers'
+ViewManagementScreens = Permissions.view_management_screens
+
+
+class CookieCrumblerDisabled (Exception):
+    """Cookie crumbler should not be used for a certain request"""
+
 
 class CookieCrumbler (SimpleItemWithProperties):
     '''
@@ -59,9 +65,13 @@
                    {'id':'persist_cookie', 'type': 'string', 'mode':'w',
                     'label':'User name persistence form variable'},
                    {'id':'auto_login_page', 'type': 'string', 'mode':'w',
-                    'label':'Auto-login page ID'},
+                    'label':'Login page ID'},
                    {'id':'logout_page', 'type': 'string', 'mode':'w',
                     'label':'Logout page ID'},
+                   {'id':'unauth_page', 'type': 'string', 'mode':'w',
+                    'label':'Failed authorization page ID'},
+                   {'id':'local_cookie_path', 'type': 'boolean', 'mode':'w',
+                    'label':'Use cookie paths to limit scope'},
                    )
 
     auth_cookie = '__ac'
@@ -69,7 +79,9 @@
     pw_cookie = '__ac_password'
     persist_cookie = '__ac_persistent'
     auto_login_page = 'login_form'
+    unauth_page = ''
     logout_page = 'logged_out'
+    local_cookie_path = 0
 
     security.declarePrivate('delRequestVar')
     def delRequestVar(self, req, name):
@@ -84,6 +96,16 @@
         try: del req.environ[name]
         except: pass
 
+    security.declarePublic('getCookiePath')
+    def getCookiePath(self):
+        if not self.local_cookie_path:
+            return '/'
+        parent = aq_parent(aq_inner(self))
+        if parent is not None:
+            return '/' + parent.absolute_url(1)
+        else:
+            return '/'
+
     # Allow overridable cookie set/expiration methods.
     security.declarePrivate('getCookieMethod')
     def getCookieMethod( self, name='setAuthCookie', default=None ):
@@ -91,75 +113,103 @@
 
     security.declarePrivate('defaultSetAuthCookie')
     def defaultSetAuthCookie( self, resp, cookie_name, cookie_value ):
-        resp.setCookie( cookie_name, cookie_value, path='/')
+        resp.setCookie( cookie_name, cookie_value, path=self.getCookiePath())
 
     security.declarePrivate('defaultExpireAuthCookie')
     def defaultExpireAuthCookie( self, resp, cookie_name ):
-        resp.expireCookie( cookie_name, path='/')
+        resp.expireCookie( cookie_name, path=self.getCookiePath())
 
     security.declarePrivate('modifyRequest')
     def modifyRequest(self, req, resp):
-        # Returns flags indicating what the user is trying to do.
-
-        if req.__class__ is not HTTPRequest:
-            return ATTEMPT_DISABLED
-
-        if not req[ 'REQUEST_METHOD' ] in ( 'HEAD', 'GET', 'PUT', 'POST' ):
-            return ATTEMPT_DISABLED
+        """Copies cookie-supplied credentials to the basic auth fields.
 
-        if req.environ.has_key( 'WEBDAV_SOURCE_PORT' ):
-            return ATTEMPT_DISABLED
+        Returns a flag indicating what the user is trying to do with
+        cookies: ATTEMPT_NONE, ATTEMPT_LOGIN, or ATTEMPT_RESUME.  If
+        cookie login is disabled for this request, raises
+        CookieCrumblerDisabled.
+        """
+        if (req.__class__ is not HTTPRequest
+            or not req['REQUEST_METHOD'] in ('HEAD', 'GET', 'PUT', 'POST')
+            or req.environ.has_key('WEBDAV_SOURCE_PORT')):
+            raise CookieCrumblerDisabled
+
+        # attempt may contain information about an earlier attempt to
+        # authenticate using a higher-up cookie crumbler within the
+        # same request.
+        attempt = getattr(req, '_cookie_auth', ATTEMPT_NONE)
+
+        if attempt == ATTEMPT_NONE:
+            if req._auth:
+                # An auth header was provided and no cookie crumbler
+                # created it.  The user must be using basic auth.
+                raise CookieCrumblerDisabled
 
-        if req._auth and not getattr(req, '_cookie_auth', 0):
-            # Using basic auth.
-            return ATTEMPT_DISABLED
-        else:
             if req.has_key(self.pw_cookie) and req.has_key(self.name_cookie):
                 # Attempt to log in and set cookies.
+                attempt = ATTEMPT_LOGIN
                 name = req[self.name_cookie]
                 pw = req[self.pw_cookie]
-                ac = encodestring('%s:%s' % (name, pw))
+                ac = encodestring('%s:%s' % (name, pw)).rstrip()
                 req._auth = 'Basic %s' % ac
-                req._cookie_auth = 1
                 resp._auth = 1
                 if req.get(self.persist_cookie, 0):
                     # Persist the user name (but not the pw or session)
                     expires = (DateTime() + 365).toZone('GMT').rfc822()
-                    resp.setCookie(self.name_cookie, name, path='/',
+                    resp.setCookie(self.name_cookie, name,
+                                   path=self.getCookiePath(),
                                    expires=expires)
                 else:
                     # Expire the user name
-                    resp.expireCookie(self.name_cookie, path='/')
+                    resp.expireCookie(self.name_cookie,
+                                      path=self.getCookiePath())
                 method = self.getCookieMethod( 'setAuthCookie'
                                              , self.defaultSetAuthCookie )
                 method( resp, self.auth_cookie, quote( ac ) )
                 self.delRequestVar(req, self.name_cookie)
                 self.delRequestVar(req, self.pw_cookie)
-                return ATTEMPT_LOGIN
+
             elif req.has_key(self.auth_cookie):
+                # Attempt to resume a session if the cookie is valid.
                 # Copy __ac to the auth header.
                 ac = unquote(req[self.auth_cookie])
-                req._auth = 'Basic %s' % ac
-                req._cookie_auth = 1
-                resp._auth = 1
-                self.delRequestVar(req, self.auth_cookie)
-                return ATTEMPT_RESUME
-            return ATTEMPT_NONE
+                if ac and ac != 'deleted':
+                    try:
+                        decodestring(ac)
+                    except:
+                        # Not a valid auth header.
+                        pass
+                    else:
+                        attempt = ATTEMPT_RESUME
+                        req._auth = 'Basic %s' % ac
+                        resp._auth = 1
+                        self.delRequestVar(req, self.auth_cookie)
+
+        req._cookie_auth = attempt
+        return attempt
+
 
     def __call__(self, container, req):
         '''The __before_publishing_traverse__ hook.'''
         resp = self.REQUEST['RESPONSE']
-        attempt = self.modifyRequest(req, resp)
-        if attempt == ATTEMPT_DISABLED:
+        try:
+            attempt = self.modifyRequest(req, resp)
+        except CookieCrumblerDisabled:
+            return
+        if req.get('disable_cookie_login__', 0):
             return
-        if not req.get('disable_cookie_login__', 0):
-            if attempt == ATTEMPT_LOGIN or attempt == ATTEMPT_NONE \
-                   or attempt == ATTEMPT_RESUME:
-                # Modify the "unauthorized" response.
-                req._hold(ResponseCleanup(resp))
-                resp.unauthorized = self.unauthorized
-                resp._unauthorized = self._unauthorized
+
+        if (self.unauth_page or
+            attempt == ATTEMPT_LOGIN or attempt == ATTEMPT_NONE):
+            # Modify the "unauthorized" response.
+            req._hold(ResponseCleanup(resp))
+            resp.unauthorized = self.unauthorized
+            resp._unauthorized = self._unauthorized
         if attempt != ATTEMPT_NONE:
+            # Trying to log in or resume a session
+            # we don't want caches to cache the resulting page
+            resp.setHeader('Cache-Control', 'no-cache')
+            # demystify this in the response.
+            resp.setHeader('X-Cache-Control-Hdr-Modified-By', 'CookieCrumbler')
             phys_path = self.getPhysicalPath()
             if self.logout_page:
                 # Cookies are in use.
@@ -195,7 +245,7 @@
         if resp.cookies.has_key(self.auth_cookie):
             del resp.cookies[self.auth_cookie]
         # Redirect if desired.
-        url = self.getLoginURL()
+        url = self.getUnauthorizedURL()
         if url is not None:
             raise 'Redirect', url
         # Fall through to the standard unauthorized() call.
@@ -207,7 +257,7 @@
         if resp.cookies.has_key(self.auth_cookie):
             del resp.cookies[self.auth_cookie]
         # Redirect if desired.
-        url = self.getLoginURL()
+        url = self.getUnauthorizedURL()
         if url is not None:
             resp.redirect(url, lock=1)
             # We don't need to raise an exception.
@@ -215,29 +265,47 @@
         # Fall through to the standard _unauthorized() call.
         resp._unauthorized()
 
-    security.declarePublic('getLoginURL')
-    def getLoginURL(self):
+    security.declarePublic('getUnauthorizedURL')
+    def getUnauthorizedURL(self):
         '''
         Redirects to the login page.
         '''
-        if self.auto_login_page:
-            req = self.REQUEST
-            resp = req['RESPONSE']
-            iself = getattr(self, 'aq_inner', self)
-            parent = getattr(iself, 'aq_parent', None)
-            page = getattr(parent, self.auto_login_page, None)
+        req = self.REQUEST
+        resp = req['RESPONSE']
+        attempt = getattr(req, '_cookie_auth', ATTEMPT_NONE)
+        if attempt == ATTEMPT_NONE:
+            # An anonymous user was denied access to something.
+            page_id = self.auto_login_page
+            retry = ''
+        elif attempt == ATTEMPT_LOGIN:
+            # The login attempt failed.  Try again.
+            page_id = self.auto_login_page
+            retry = '1'
+        else:
+            # An authenticated user was denied access to something.
+            page_id = self.unauth_page
+            retry = ''
+        if page_id:
+            parent = aq_parent(aq_inner(self))
+            page = getattr(parent, page_id, None)
             if page is not None:
-                retry = getattr(resp, '_auth', 0) and '1' or ''
                 came_from = req.get('came_from', None)
                 if came_from is None:
-                    came_from = req['URL']
-                    if req.get('QUERY_STRING'):
-                        came_from = '%s?%s' % (came_from, req['QUERY_STRING'])
+                    came_from = req.get('URL', '')
+                    query = req.get('QUERY_STRING')
+                    if query:
+                        # Include the query string in came_from
+                        if not query.startswith('?'):
+                            query = '?' + query
+                        came_from = came_from + query
                 url = '%s?came_from=%s&retry=%s&disable_cookie_login__=1' % (
                     page.absolute_url(), quote(came_from), retry)
                 return url
         return None
 
+    # backward compatible alias
+    getLoginURL = getUnauthorizedURL
+
     security.declarePublic('logout')
     def logout(self):
         '''
@@ -248,17 +316,15 @@
         method = self.getCookieMethod( 'expireAuthCookie'
                                      , self.defaultExpireAuthCookie )
         method( resp, cookie_name=self.auth_cookie )
-        redir = 0
         if self.logout_page:
-            iself = getattr(self, 'aq_inner', self)
-            parent = getattr(iself, 'aq_parent', None)
+            parent = aq_parent(aq_inner(self))
             page = getattr(parent, self.logout_page, None)
             if page is not None:
-                redir = 1
-                resp.redirect(page.absolute_url())
-        if not redir:
-            # Should not normally happen.
-            return 'Logged out.'
+                resp.redirect('%s?disable_cookie_login__=1'
+                              % page.absolute_url())
+                return ''
+        # We should not normally get here.
+        return 'Logged out.'
 
     # Installation and removal of traversal hooks.
 
@@ -297,11 +363,21 @@
 manage_addCCForm = HTMLFile('dtml/addCC', globals())
 manage_addCCForm.__name__ = 'addCC'
 
-def manage_addCC(self, id, REQUEST=None):
+def manage_addCC(self, id, create_forms=0, REQUEST=None):
     ' '
     ob = CookieCrumbler()
     ob.id = id
     self._setObject(id, ob)
+    if create_forms:
+        import os
+        from OFS.DTMLMethod import addDTMLMethod
+        dtmldir = os.path.join(os.path.dirname(__file__), 'dtml')
+        for fn in ('login_form', 'logged_in', 'logged_out'):
+            filename = os.path.join(dtmldir, fn + '.dtml')
+            f = open(filename, 'rt')
+            try: data = f.read()
+            finally: f.close()
+            addDTMLMethod(self, fn, file=data)
     if REQUEST is not None:
         return self.manage_main(self, REQUEST)