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