[Zope3-checkins] SVN: Zope3/trunk/ added third party ClientId management

Jodok Batlogg jodok.batlogg at lovelysystems.com
Wed May 2 10:13:19 EDT 2007


Log message for revision 75004:
  added third party ClientId management

Changed:
  U   Zope3/trunk/doc/CHANGES.txt
  U   Zope3/trunk/src/zope/app/session/http.py
  U   Zope3/trunk/src/zope/app/session/tests.py

-=-
Modified: Zope3/trunk/doc/CHANGES.txt
===================================================================
--- Zope3/trunk/doc/CHANGES.txt	2007-05-02 14:05:04 UTC (rev 75003)
+++ Zope3/trunk/doc/CHANGES.txt	2007-05-02 14:13:19 UTC (rev 75004)
@@ -10,6 +10,10 @@
 
     New features
 
+      - Added support for third party ClientId Cookie Managers. Reverse
+        Proxies like Apache with mod_id or Nginx with ngx_http_userid_module
+        are able to handle client identification. zope.app.session
+
       - Added salt to MD5/SHA1 password managers. (Thanks Mark Giovannetti for
         the patch)
 

Modified: Zope3/trunk/src/zope/app/session/http.py
===================================================================
--- Zope3/trunk/src/zope/app/session/http.py	2007-05-02 14:05:04 UTC (rev 75003)
+++ Zope3/trunk/src/zope/app/session/http.py	2007-05-02 14:13:19 UTC (rev 75004)
@@ -16,6 +16,7 @@
 $Id$
 """
 import hmac
+import logging
 import random
 import re
 import sha
@@ -33,16 +34,22 @@
 
 from zope.app.session.i18n import ZopeMessageFactory as _
 from zope.app.session.interfaces import IClientIdManager
+from zope.schema.fieldproperty import FieldProperty
 from zope.app.http.httpdate import build_http_date
 
 __docformat__ = 'restructuredtext'
 
 cookieSafeTrans = string.maketrans("+/", "-.")
 
+logger = logging.getLogger()
+
 def digestEncode(s):
     """Encode SHA digest for cookie."""
     return s.encode("base64")[:-2].translate(cookieSafeTrans)
 
+class MissingClientIdException(Exception):
+    """No ClientId found in Request"""
+
 class ICookieClientIdManager(IClientIdManager):
     """Manages sessions using a cookie"""
 
@@ -72,15 +79,31 @@
             missing_value=None,
             )
 
+    thirdparty = schema.Bool(
+            title=_('Third party cookie'),
+            description=_(
+                "Is a third party issuing the identification cookie? "
+                "Servers like Apache or Nginx have capabilities to issue "
+                "identification cookies too. If Third party cookies are "
+                "beeing used, Zope will never send a cookie back, just check "
+                "for them."
+                ),
+            required=False,
+            default=False,
+            )
+
+
 class CookieClientIdManager(zope.location.Location, Persistent):
     """Session utility implemented using cookies."""
 
     implements(IClientIdManager, ICookieClientIdManager, IAttributeAnnotatable)
 
+    thirdparty = FieldProperty(ICookieClientIdManager['thirdparty'])
+    cookieLifetime = FieldProperty(ICookieClientIdManager['cookieLifetime'])
+
     def __init__(self):
         self.namespace = "zope3_cs_%x" % (int(time.time()) - 1000000000)
         self.secret = "%.20f" % random.random()
-        self.cookieLifetime = None
 
     def getClientId(self, request):
         """Get the client id
@@ -113,21 +136,42 @@
           >>> type(id) == type('')
           True
 
+        It's also possible to use third-party cookies. E.g. Apache `mod_uid`
+        or Nginx `ngx_http_userid_module` are able to issue user tracking
+        cookies in front of Zope. In case thirdparty is activated Zope may
+        not set a cookie.
+        
+          >>> bim.thirdparty = True
+          >>> request3 = HTTPRequest(StringIO(''), {}, None)
+          >>> bim.getClientId(request3)
+          Traceback (most recent call last):
+          ...
+          MissingClientIdException          
+          >>> cookie = request3.response.getCookie(bim.namespace)
+          >>> cookie is None
+          True
+          
         """
         sid = self.getRequestId(request)
         if sid is None:
-            sid = self.generateUniqueId()
-        self.setRequestId(request, sid)
+            if self.thirdparty:
+                raise MissingClientIdException
+            else:
+                sid = self.generateUniqueId()
+                
+        if not self.thirdparty:
+            self.setRequestId(request, sid)
+
         return sid
 
     def generateUniqueId(self):
         """Generate a new, random, unique id.
 
-            >>> bim = CookieClientIdManager()
-            >>> id1 = bim.generateUniqueId()
-            >>> id2 = bim.generateUniqueId()
-            >>> id1 != id2
-            True
+             >>> bim = CookieClientIdManager()
+             >>> id1 = bim.generateUniqueId()
+             >>> id2 = bim.generateUniqueId()
+             >>> id1 != id2
+             True
 
         """
         data = "%.20f%.20f%.20f" % (random.random(), time.time(), time.clock())
@@ -145,57 +189,70 @@
 
         For example:
 
-            >>> from zope.publisher.http import HTTPRequest
-            >>> request = HTTPRequest(StringIO(''), {}, None)
-            >>> bim = CookieClientIdManager()
+          >>> from zope.publisher.http import HTTPRequest
+          >>> request = HTTPRequest(StringIO(''), {}, None)
+          >>> bim = CookieClientIdManager()
 
         Because no cookie has been set, we get no id:
 
-            >>> bim.getRequestId(request) is None
-            True
+          >>> bim.getRequestId(request) is None
+          True
 
         We can set an id:
 
-            >>> id1 = bim.generateUniqueId()
-            >>> bim.setRequestId(request, id1)
+          >>> id1 = bim.generateUniqueId()
+          >>> bim.setRequestId(request, id1)
 
         And get it back:
 
-            >>> bim.getRequestId(request) == id1
-            True
+          >>> bim.getRequestId(request) == id1
+          True
 
         When we set the request id, we also set a response cookie.  We
         can simulate getting this cookie back in a subsequent request:
 
-            >>> request2 = HTTPRequest(StringIO(''), {}, None)
-            >>> request2._cookies = dict(
-            ...   [(name, cookie['value'])
-            ...    for (name, cookie) in request.response._cookies.items()
-            ...   ])
+          >>> request2 = HTTPRequest(StringIO(''), {}, None)
+          >>> request2._cookies = dict(
+          ...   [(name, cookie['value'])
+          ...    for (name, cookie) in request.response._cookies.items()
+          ...   ])
 
         And we get the same id back from the new request:
 
-            >>> bim.getRequestId(request) == bim.getRequestId(request2)
-            True
+          >>> bim.getRequestId(request) == bim.getRequestId(request2)
+          True
 
+        If another server is managing the ClientId cookies (Apache, Nginx)
+        we're returning their value without checking:
+        
+          >>> bim.namespace = 'uid'
+          >>> bim.thirdparty = True
+          >>> request3 = HTTPRequest(StringIO(''), {}, None)
+          >>> request3._cookies = {'uid': 'AQAAf0Y4gjgAAAQ3AwMEAg=='}
+          >>> bim.getRequestId(request3)
+          'AQAAf0Y4gjgAAAQ3AwMEAg=='
+          
         """
-        # If there is an id set on the response, use that but don't trust it.
-        # We need to check the response in case there has already been a new
-        # session created during the course of this request.
         response_cookie = request.response.getCookie(self.namespace)
         if response_cookie:
             sid = response_cookie['value']
         else:
             request = IHTTPApplicationRequest(request)
             sid = request.getCookies().get(self.namespace, None)
-        if sid is None or len(sid) != 54:
-            return None
-        s, mac = sid[:27], sid[27:]
-        if (digestEncode(hmac.new(s, self.secret, digestmod=sha).digest())
-            != mac):
-            return None
+        if self.thirdparty:
+            return sid
         else:
-            return sid
+            # If there is an id set on the response, use that but don't trust it.
+            # We need to check the response in case there has already been a new
+            # session created during the course of this request.
+            if sid is None or len(sid) != 54:
+                return None
+            s, mac = sid[:27], sid[27:]
+            if (digestEncode(hmac.new(s, self.secret, digestmod=sha).digest())
+                != mac):
+                return None
+            else:
+                return sid
 
     def setRequestId(self, request, id):
         """Set cookie with id on request.
@@ -247,7 +304,15 @@
             >>> expires = time.mktime(rfc822.parsedate(cookie['expires']))
             >>> expires > time.mktime(time.gmtime()) + 55*60
             True
-
+                        
+        If another server in front of Zope (Apache, Nginx) is managing the
+        cookies we won't set any ClientId cookies:
+        
+          >>> request = HTTPRequest(StringIO(''), {}, None)
+          >>> bim.thirdparty = True
+          >>> bim.setRequestId(request, '1234')
+          >>> cookie = request.response.getCookie(bim.namespace)
+          >>> cookie
         """
         # TODO: Currently, the path is the ApplicationURL. This is reasonable,
         #     and will be adequate for most purposes.
@@ -258,20 +323,24 @@
         #     Seeing as this utility instance has a unique namespace for its
         #     cookie, using ApplicationURL shouldn't be a problem.
 
-        if self.cookieLifetime is not None:
-            if self.cookieLifetime:
-                expires = build_http_date(time.time() + self.cookieLifetime)
+        if self.thirdparty:
+            logger.warning('ClientIdManager is using thirdparty cookies, '
+                'ignoring setIdRequest call')            
+        else:
+            if self.cookieLifetime is not None:
+                if self.cookieLifetime:
+                    expires = build_http_date(time.time() + self.cookieLifetime)
+                else:
+                    expires = 'Tue, 19 Jan 2038 00:00:00 GMT'
+                request.response.setCookie(
+                        self.namespace, id, expires=expires,
+                        path=request.getApplicationURL(path_only=True)
+                        )
             else:
-                expires = 'Tue, 19 Jan 2038 00:00:00 GMT'
-            request.response.setCookie(
-                    self.namespace, id, expires=expires,
-                    path=request.getApplicationURL(path_only=True)
-                    )
-        else:
-            request.response.setCookie(
-                    self.namespace, id,
-                    path=request.getApplicationURL(path_only=True)
-                    )
+                request.response.setCookie(
+                        self.namespace, id,
+                        path=request.getApplicationURL(path_only=True)
+                        )
 
 def notifyVirtualHostChanged(event):
     """Adjust cookie paths when IVirtualHostRequest information changes.
@@ -283,6 +352,7 @@
         >>> class DummyManager(object):
         ...     implements(ICookieClientIdManager)
         ...     namespace = 'foo'
+        ...     thirdparty = False
         ...     request_id = None
         ...     def setRequestId(self, request, id):
         ...         self.request_id = id
@@ -307,7 +377,23 @@
         >>> notifyVirtualHostChanged(event)
         >>> manager.request_id
         'bar'
+    
+    If a server in front of Zope manages the ClientIds (Apache, Nginx), we
+    don't need to take care about the cookies
+
+        >>> manager2 = DummyManager()
+        >>> manager2.thirdparty = True
+        >>> event2 = DummyEvent()
         
+    However, when a cookie *has* been set, the manager is called so it can
+    update the cookie if need be:
+    
+        >>> event2.request.response.setCookie('foo2', 'bar2')
+        >>> notifyVirtualHostChanged(event2)
+        >>> id = manager2.request_id
+        >>> id is None
+        True
+
     Cleanup of the utility registration:
     
         >>> import zope.component.testing
@@ -318,8 +404,12 @@
     # IHTTPRequest for the response attribute, and so does the cookie-
     # manager.
     request = IHTTPRequest(event.request, None)
+    if event.request is None:
+        return 
     for name, manager in component.getUtilitiesFor(IClientIdManager):
-        if manager and request and ICookieClientIdManager.providedBy(manager):
-            cookie = request.response.getCookie(manager.namespace)
-            if cookie:
-                manager.setRequestId(request, cookie['value'])
+        if manager and ICookieClientIdManager.providedBy(manager):
+            # Third party ClientId Managers need no modification at all 
+            if not manager.thirdparty:
+                cookie = request.response.getCookie(manager.namespace)
+                if cookie:
+                    manager.setRequestId(request, cookie['value'])
\ No newline at end of file

Modified: Zope3/trunk/src/zope/app/session/tests.py
===================================================================
--- Zope3/trunk/src/zope/app/session/tests.py	2007-05-02 14:05:04 UTC (rev 75003)
+++ Zope3/trunk/src/zope/app/session/tests.py	2007-05-02 14:13:19 UTC (rev 75004)
@@ -194,8 +194,10 @@
     suite.addTest(unittest.makeSuite(TestBootstrap))
     suite.addTest(doctest.DocTestSuite())
     suite.addTest(doctest.DocTestSuite('zope.app.session.session',
-                                       tearDown=tearDownTransaction))
-    suite.addTest(doctest.DocTestSuite('zope.app.session.http'))
+        tearDown=tearDownTransaction))
+    suite.addTest(doctest.DocTestSuite('zope.app.session.http',
+        optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,)
+        )
     suite.addTest(unittest.makeSuite(ZPTSessionTest))
     suite.addTest(unittest.makeSuite(VirtualHostSessionTest))
     return suite



More information about the Zope3-Checkins mailing list