vendor/CMF/1.6.3/CMFCore

view CookieCrumbler.py @ 2:4c712d7bd1d7

Added tag 1.6.3 for changeset 1babb9d61518
author Georges Racinet on purity.racinet.fr <georges@racinet.fr>
date Fri, 09 Sep 2011 12:44:00 +0200
parents
children
line source
1 ##############################################################################
2 #
3 # Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
4 #
5 # This software is subject to the provisions of the Zope Public License,
6 # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
7 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
8 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
9 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
10 # FOR A PARTICULAR PURPOSE.
11 #
12 ##############################################################################
13 """ Cookie Crumbler: Enable cookies for non-cookie user folders.
15 $Id$
16 """
18 from base64 import encodestring, decodestring
19 from urllib import quote, unquote
20 import sys
22 from Acquisition import aq_inner, aq_parent
23 from DateTime import DateTime
24 from AccessControl import getSecurityManager, ClassSecurityInfo, Permissions
25 from ZPublisher import BeforeTraverse
26 import Globals
27 from Globals import HTMLFile
28 from ZPublisher.HTTPRequest import HTTPRequest
29 from OFS.Folder import Folder
30 from zExceptions import Redirect
32 from zope.interface import implements
33 from interfaces import ICookieCrumbler
35 # Constants.
36 ATTEMPT_NONE = 0 # No attempt at authentication
37 ATTEMPT_LOGIN = 1 # Attempt to log in
38 ATTEMPT_RESUME = 2 # Attempt to resume session
40 ModifyCookieCrumblers = 'Modify Cookie Crumblers'
41 ViewManagementScreens = Permissions.view_management_screens
44 class CookieCrumblerDisabled (Exception):
45 """Cookie crumbler should not be used for a certain request"""
48 class CookieCrumbler (Folder):
49 '''
50 Reads cookies during traversal and simulates the HTTP
51 authentication headers.
52 '''
53 meta_type = 'Cookie Crumbler'
55 implements(ICookieCrumbler)
57 security = ClassSecurityInfo()
58 security.declareProtected(ModifyCookieCrumblers, 'manage_editProperties')
59 security.declareProtected(ModifyCookieCrumblers, 'manage_changeProperties')
60 security.declareProtected(ViewManagementScreens, 'manage_propertiesForm')
62 # By default, anonymous users can view login/logout pages.
63 _View_Permission = ('Anonymous',)
66 _properties = ({'id':'auth_cookie', 'type': 'string', 'mode':'w',
67 'label':'Authentication cookie name'},
68 {'id':'name_cookie', 'type': 'string', 'mode':'w',
69 'label':'User name form variable'},
70 {'id':'pw_cookie', 'type': 'string', 'mode':'w',
71 'label':'User password form variable'},
72 {'id':'persist_cookie', 'type': 'string', 'mode':'w',
73 'label':'User name persistence form variable'},
74 {'id':'auto_login_page', 'type': 'string', 'mode':'w',
75 'label':'Login page ID'},
76 {'id':'logout_page', 'type': 'string', 'mode':'w',
77 'label':'Logout page ID'},
78 {'id':'unauth_page', 'type': 'string', 'mode':'w',
79 'label':'Failed authorization page ID'},
80 {'id':'local_cookie_path', 'type': 'boolean', 'mode':'w',
81 'label':'Use cookie paths to limit scope'},
82 {'id':'cache_header_value', 'type': 'string', 'mode':'w',
83 'label':'Cache-Control header value'},
84 {'id':'log_username', 'type':'boolean', 'mode': 'w',
85 'label':'Log cookie auth username to access log'}
86 )
88 auth_cookie = '__ac'
89 name_cookie = '__ac_name'
90 pw_cookie = '__ac_password'
91 persist_cookie = '__ac_persistent'
92 auto_login_page = 'login_form'
93 unauth_page = ''
94 logout_page = 'logged_out'
95 local_cookie_path = 0
96 cache_header_value = 'private'
97 log_username = 1
99 security.declarePrivate('delRequestVar')
100 def delRequestVar(self, req, name):
101 # No errors of any sort may propagate, and we don't care *what*
102 # they are, even to log them.
103 try: del req.other[name]
104 except: pass
105 try: del req.form[name]
106 except: pass
107 try: del req.cookies[name]
108 except: pass
109 try: del req.environ[name]
110 except: pass
112 security.declarePublic('getCookiePath')
113 def getCookiePath(self):
114 if not self.local_cookie_path:
115 return '/'
116 parent = aq_parent(aq_inner(self))
117 if parent is not None:
118 return '/' + parent.absolute_url(1)
119 else:
120 return '/'
122 # Allow overridable cookie set/expiration methods.
123 security.declarePrivate('getCookieMethod')
124 def getCookieMethod(self, name, default=None):
125 return getattr(self, name, default)
127 security.declarePrivate('defaultSetAuthCookie')
128 def defaultSetAuthCookie(self, resp, cookie_name, cookie_value):
129 kw = {}
130 req = getattr(self, 'REQUEST', None)
131 if req is not None and req.get('SERVER_URL', '').startswith('https:'):
132 # Ask the client to send back the cookie only in SSL mode
133 kw['secure'] = 'y'
134 resp.setCookie(cookie_name, cookie_value,
135 path=self.getCookiePath(), **kw)
137 security.declarePrivate('defaultExpireAuthCookie')
138 def defaultExpireAuthCookie(self, resp, cookie_name):
139 resp.expireCookie(cookie_name, path=self.getCookiePath())
141 def _setAuthHeader(self, ac, request, response):
142 """Set the auth headers for both the Zope and Medusa http request
143 objects.
144 """
145 request._auth = 'Basic %s' % ac
146 response._auth = 1
147 if self.log_username:
148 # Set the authorization header in the medusa http request
149 # so that the username can be logged to the Z2.log
150 try:
151 # Put the full-arm latex glove on now...
152 medusa_headers = response.stdout._request._header_cache
153 except AttributeError:
154 pass
155 else:
156 medusa_headers['authorization'] = request._auth
158 security.declarePrivate('modifyRequest')
159 def modifyRequest(self, req, resp):
160 """Copies cookie-supplied credentials to the basic auth fields.
162 Returns a flag indicating what the user is trying to do with
163 cookies: ATTEMPT_NONE, ATTEMPT_LOGIN, or ATTEMPT_RESUME. If
164 cookie login is disabled for this request, raises
165 CookieCrumblerDisabled.
166 """
167 if (req.__class__ is not HTTPRequest
168 or not req['REQUEST_METHOD'] in ('HEAD', 'GET', 'PUT', 'POST')
169 or req.environ.has_key('WEBDAV_SOURCE_PORT')):
170 raise CookieCrumblerDisabled
172 # attempt may contain information about an earlier attempt to
173 # authenticate using a higher-up cookie crumbler within the
174 # same request.
175 attempt = getattr(req, '_cookie_auth', ATTEMPT_NONE)
177 if attempt == ATTEMPT_NONE:
178 if req._auth:
179 # An auth header was provided and no cookie crumbler
180 # created it. The user must be using basic auth.
181 raise CookieCrumblerDisabled
183 if req.has_key(self.pw_cookie) and req.has_key(self.name_cookie):
184 # Attempt to log in and set cookies.
185 attempt = ATTEMPT_LOGIN
186 name = req[self.name_cookie]
187 pw = req[self.pw_cookie]
188 ac = encodestring('%s:%s' % (name, pw)).rstrip()
189 self._setAuthHeader(ac, req, resp)
190 if req.get(self.persist_cookie, 0):
191 # Persist the user name (but not the pw or session)
192 expires = (DateTime() + 365).toZone('GMT').rfc822()
193 resp.setCookie(self.name_cookie, name,
194 path=self.getCookiePath(),
195 expires=expires)
196 else:
197 # Expire the user name
198 resp.expireCookie(self.name_cookie,
199 path=self.getCookiePath())
200 method = self.getCookieMethod( 'setAuthCookie'
201 , self.defaultSetAuthCookie )
202 method( resp, self.auth_cookie, quote( ac ) )
203 self.delRequestVar(req, self.name_cookie)
204 self.delRequestVar(req, self.pw_cookie)
206 elif req.has_key(self.auth_cookie):
207 # Attempt to resume a session if the cookie is valid.
208 # Copy __ac to the auth header.
209 ac = unquote(req[self.auth_cookie])
210 if ac and ac != 'deleted':
211 try:
212 decodestring(ac)
213 except:
214 # Not a valid auth header.
215 pass
216 else:
217 attempt = ATTEMPT_RESUME
218 self._setAuthHeader(ac, req, resp)
219 self.delRequestVar(req, self.auth_cookie)
220 method = self.getCookieMethod(
221 'twiddleAuthCookie', None)
222 if method is not None:
223 method(resp, self.auth_cookie, quote(ac))
225 req._cookie_auth = attempt
226 return attempt
229 def __call__(self, container, req):
230 '''The __before_publishing_traverse__ hook.'''
231 resp = self.REQUEST['RESPONSE']
232 try:
233 attempt = self.modifyRequest(req, resp)
234 except CookieCrumblerDisabled:
235 return
236 if req.get('disable_cookie_login__', 0):
237 return
239 if (self.unauth_page or
240 attempt == ATTEMPT_LOGIN or attempt == ATTEMPT_NONE):
241 # Modify the "unauthorized" response.
242 req._hold(ResponseCleanup(resp))
243 resp.unauthorized = self.unauthorized
244 resp._unauthorized = self._unauthorized
245 if attempt != ATTEMPT_NONE:
246 # Trying to log in or resume a session
247 if self.cache_header_value:
248 # we don't want caches to cache the resulting page
249 resp.setHeader('Cache-Control', self.cache_header_value)
250 # demystify this in the response.
251 resp.setHeader('X-Cache-Control-Hdr-Modified-By',
252 'CookieCrumbler')
253 phys_path = self.getPhysicalPath()
254 if self.logout_page:
255 # Cookies are in use.
256 page = getattr(container, self.logout_page, None)
257 if page is not None:
258 # Provide a logout page.
259 req._logout_path = phys_path + ('logout',)
260 req._credentials_changed_path = (
261 phys_path + ('credentialsChanged',))
263 security.declarePublic('credentialsChanged')
264 def credentialsChanged(self, user, name, pw):
265 ac = encodestring('%s:%s' % (name, pw)).rstrip()
266 method = self.getCookieMethod( 'setAuthCookie'
267 , self.defaultSetAuthCookie )
268 resp = self.REQUEST['RESPONSE']
269 method( resp, self.auth_cookie, quote( ac ) )
271 def _cleanupResponse(self):
272 resp = self.REQUEST['RESPONSE']
273 # No errors of any sort may propagate, and we don't care *what*
274 # they are, even to log them.
275 try: del resp.unauthorized
276 except: pass
277 try: del resp._unauthorized
278 except: pass
279 return resp
281 security.declarePrivate('unauthorized')
282 def unauthorized(self):
283 resp = self._cleanupResponse()
284 # If we set the auth cookie before, delete it now.
285 if resp.cookies.has_key(self.auth_cookie):
286 del resp.cookies[self.auth_cookie]
287 # Redirect if desired.
288 url = self.getUnauthorizedURL()
289 if url is not None:
290 raise Redirect, url
291 # Fall through to the standard unauthorized() call.
292 resp.unauthorized()
294 def _unauthorized(self):
295 resp = self._cleanupResponse()
296 # If we set the auth cookie before, delete it now.
297 if resp.cookies.has_key(self.auth_cookie):
298 del resp.cookies[self.auth_cookie]
299 # Redirect if desired.
300 url = self.getUnauthorizedURL()
301 if url is not None:
302 resp.redirect(url, lock=1)
303 # We don't need to raise an exception.
304 return
305 # Fall through to the standard _unauthorized() call.
306 resp._unauthorized()
308 security.declarePublic('getUnauthorizedURL')
309 def getUnauthorizedURL(self):
310 '''
311 Redirects to the login page.
312 '''
313 req = self.REQUEST
314 resp = req['RESPONSE']
315 attempt = getattr(req, '_cookie_auth', ATTEMPT_NONE)
316 if attempt == ATTEMPT_NONE:
317 # An anonymous user was denied access to something.
318 page_id = self.auto_login_page
319 retry = ''
320 elif attempt == ATTEMPT_LOGIN:
321 # The login attempt failed. Try again.
322 page_id = self.auto_login_page
323 retry = '1'
324 else:
325 # An authenticated user was denied access to something.
326 page_id = self.unauth_page
327 retry = ''
328 if page_id:
329 page = self.restrictedTraverse(page_id, None)
330 if page is not None:
331 came_from = req.get('came_from', None)
332 if came_from is None:
333 came_from = req.get('VIRTUAL_URL', None)
334 if came_from is None:
335 came_from = '%s%s%s' % ( req['SERVER_URL'].strip(),
336 req['SCRIPT_NAME'].strip(),
337 req['PATH_INFO'].strip() )
338 query = req.get('QUERY_STRING')
339 if query:
340 # Include the query string in came_from
341 if not query.startswith('?'):
342 query = '?' + query
343 came_from = came_from + query
344 url = '%s?came_from=%s&retry=%s&disable_cookie_login__=1' % (
345 page.absolute_url(), quote(came_from), retry)
346 return url
347 return None
349 # backward compatible alias
350 getLoginURL = getUnauthorizedURL
352 security.declarePublic('logout')
353 def logout(self):
354 '''
355 Logs out the user and redirects to the logout page.
356 '''
357 req = self.REQUEST
358 resp = req['RESPONSE']
359 method = self.getCookieMethod( 'expireAuthCookie'
360 , self.defaultExpireAuthCookie )
361 method( resp, cookie_name=self.auth_cookie )
362 if self.logout_page:
363 page = self.restrictedTraverse(self.logout_page, None)
364 if page is not None:
365 resp.redirect('%s?disable_cookie_login__=1'
366 % page.absolute_url())
367 return ''
368 # We should not normally get here.
369 return 'Logged out.'
371 # Installation and removal of traversal hooks.
373 def manage_beforeDelete(self, item, container):
374 if item is self:
375 handle = self.meta_type + '/' + self.getId()
376 BeforeTraverse.unregisterBeforeTraverse(container, handle)
378 def manage_afterAdd(self, item, container):
379 if item is self:
380 handle = self.meta_type + '/' + self.getId()
381 container = container.this()
382 nc = BeforeTraverse.NameCaller(self.getId())
383 BeforeTraverse.registerBeforeTraverse(container, nc, handle)
385 security.declarePublic('propertyLabel')
386 def propertyLabel(self, id):
387 """Return a label for the given property id
388 """
389 for p in self._properties:
390 if p['id'] == id:
391 return p.get('label', id)
392 return id
394 Globals.InitializeClass(CookieCrumbler)
397 class ResponseCleanup:
398 def __init__(self, resp):
399 self.resp = resp
401 def __del__(self):
402 # Free the references.
403 #
404 # No errors of any sort may propagate, and we don't care *what*
405 # they are, even to log them.
406 try: del self.resp.unauthorized
407 except: pass
408 try: del self.resp._unauthorized
409 except: pass
410 try: del self.resp
411 except: pass
414 manage_addCCForm = HTMLFile('dtml/addCC', globals())
415 manage_addCCForm.__name__ = 'addCC'
417 def _create_forms(ob):
418 ''' Create default forms inside ob '''
419 import os
420 from OFS.DTMLMethod import addDTMLMethod
421 dtmldir = os.path.join(os.path.dirname(__file__), 'dtml')
422 for fn in ('index_html', 'logged_in', 'logged_out', 'login_form',
423 'standard_login_footer', 'standard_login_header'):
424 filename = os.path.join(dtmldir, fn + '.dtml')
425 f = open(filename, 'rt')
426 try: data = f.read()
427 finally: f.close()
428 addDTMLMethod(ob, fn, file=data)
430 def manage_addCC(dispatcher, id, create_forms=0, REQUEST=None):
431 ' '
432 ob = CookieCrumbler()
433 ob.id = id
434 dispatcher._setObject(ob.getId(), ob)
435 ob = getattr(dispatcher.this(), ob.getId())
436 if create_forms:
437 _create_forms(ob)
438 if REQUEST is not None:
439 return dispatcher.manage_main(dispatcher, REQUEST)