[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)