root/vendor/CMF/tags/1.6.0/CMFCore/CookieCrumbler.py

Revision 33342, 17.0 kB (checked in by sfermigier, 3 years ago)

Import CMF 1.6.0 beta

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