[Zope-CVS] CVS: Products/CookieCrumbler - CookieCrumbler.py:1.16

Shane Hathaway shane@zope.com
Fri, 6 Jun 2003 16:41:14 -0400


Update of /cvs-repository/Products/CookieCrumbler
In directory cvs.zope.org:/tmp/cvs-serv18734

Modified Files:
	CookieCrumbler.py 
Log Message:
Tidied up CookieCrumbler.  Wrote unit tests (yay!).  Changed the
"redir_always" option into an "unauth_page" option, making it possible
to show a different page when an authenticated user is unauthorized
(as opposed to the login page, which is meant for unauthenticated
users.)  Replaced ATTEMPT_DISABLED with a special exception class.



=== Products/CookieCrumbler/CookieCrumbler.py 1.15 => 1.16 ===
--- Products/CookieCrumbler/CookieCrumbler.py:1.15	Fri Jun  6 11:15:36 2003
+++ Products/CookieCrumbler/CookieCrumbler.py	Fri Jun  6 16:40:44 2003
@@ -29,13 +29,19 @@
 
 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):
     '''
@@ -45,11 +51,9 @@
     meta_type = 'Cookie Crumbler'
 
     security = ClassSecurityInfo()
-    security.declareProtected(ModifyCookieCrumblers,
-                              'manage_editProperties',
-                              'manage_changeProperties')
-    security.declareProtected(Permissions.view_management_screens,
-                              'manage_propertiesForm')
+    security.declareProtected(ModifyCookieCrumblers, 'manage_editProperties')
+    security.declareProtected(ModifyCookieCrumblers, 'manage_changeProperties')
+    security.declareProtected(ViewManagementScreens, 'manage_propertiesForm')
 
 
     _properties = ({'id':'auth_cookie', 'type': 'string', 'mode':'w',
@@ -61,11 +65,11 @@
                    {'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':'redir_always', 'type': 'boolean', 'mode':'w',
-                    'label':'Always redirect to login page when unauthorized'},
+                   {'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'},
                    )
@@ -75,8 +79,8 @@
     pw_cookie = '__ac_password'
     persist_cookie = '__ac_persistent'
     auto_login_page = 'login_form'
+    unauth_page = ''
     logout_page = 'logged_out'
-    redir_always = 0
     local_cookie_path = 0
 
     security.declarePrivate('delRequestVar')
@@ -117,28 +121,36 @@
 
     security.declarePrivate('modifyRequest')
     def modifyRequest(self, req, resp):
-        # Returns flags indicating what the user is trying to do.
+        """Copies cookie-supplied credentials to the basic auth fields.
 
-        if req.__class__ is not HTTPRequest:
-            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 not req[ 'REQUEST_METHOD' ] in ( 'HEAD', 'GET', 'PUT', 'POST' ):
-            return ATTEMPT_DISABLED
-
-        if req.environ.has_key( 'WEBDAV_SOURCE_PORT' ):
-            return ATTEMPT_DISABLED
-
-        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)
@@ -155,8 +167,9 @@
                 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])
                 if ac and ac != 'deleted':
@@ -166,32 +179,33 @@
                         # Not a valid auth header.
                         pass
                     else:
+                        attempt = ATTEMPT_RESUME
                         req._auth = 'Basic %s' % ac
-                        req._cookie_auth = 1
                         resp._auth = 1
                         self.delRequestVar(req, self.auth_cookie)
-                        return ATTEMPT_RESUME
-            if getattr(req, '_cookie_auth', 0):
-                # A higher cookie crumbler already did the work of
-                # moving the cookie to _auth, but the inner CC
-                # should have the opportunity to override logout forms, etc.
-                return ATTEMPT_RESUME
-            return ATTEMPT_NONE
+
+        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 not req.get('disable_cookie_login__', 0):
-            if (self.redir_always 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 req.get('disable_cookie_login__', 0):
+            return
+
+        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.
@@ -231,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.
@@ -243,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.
@@ -251,19 +265,30 @@
         # 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']
@@ -272,6 +297,9 @@
                 return url
         return None
 
+    # backward compatible alias
+    getLoginURL = getUnauthorizedURL
+
     security.declarePublic('logout')
     def logout(self):
         '''
@@ -282,17 +310,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.