• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zopefoundation / Products.CMFCore / 6246931310

20 Sep 2023 09:54AM UTC coverage: 86.008% (-0.3%) from 86.266%
6246931310

Pull #131

github

mauritsvanrees
gha: don't need setup-python on 27 as we use the 27 container.
Pull Request #131: Make decodeFolderFilter and encodeFolderFilter non-public.

2466 of 3689 branches covered (0.0%)

Branch coverage included in aggregate %.

6 of 6 new or added lines in 1 file covered. (100.0%)

17297 of 19289 relevant lines covered (89.67%)

0.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

72.8
/src/Products/CMFCore/CookieCrumbler.py
1
##############################################################################
2
#
3
# Copyright (c) 2001 Zope Foundation and Contributors.
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.
1✔
14
"""
15

16
import base64
1✔
17

18
import six
1✔
19
from six.moves.urllib.parse import quote
1✔
20
from six.moves.urllib.parse import unquote
1✔
21

22
from AccessControl.class_init import InitializeClass
1✔
23
from AccessControl.Permissions import view_management_screens
1✔
24
from AccessControl.SecurityInfo import ClassSecurityInfo
1✔
25
from Acquisition import aq_inner
1✔
26
from Acquisition import aq_parent
1✔
27
from App.special_dtml import HTMLFile
1✔
28
from DateTime.DateTime import DateTime
1✔
29
from OFS.interfaces import IObjectWillBeMovedEvent
1✔
30
from OFS.PropertyManager import PropertyManager
1✔
31
from OFS.SimpleItem import SimpleItem
1✔
32
from zope.component import getUtility
1✔
33
from zope.container.interfaces import IObjectMovedEvent
1✔
34
from zope.globalrequest import getRequest
1✔
35
from zope.interface import implementer
1✔
36
from ZPublisher import BeforeTraverse
1✔
37
from ZPublisher.HTTPRequest import HTTPRequest
1✔
38

39
from .interfaces import IActionsTool
1✔
40
from .interfaces import ICookieCrumbler
1✔
41
from .utils import UniqueObject
1✔
42
from .utils import registerToolInterface
1✔
43

44

45
# Constants.
46
ATTEMPT_NONE = 0       # No attempt at authentication
1✔
47
ATTEMPT_LOGIN = 1      # Attempt to log in
1✔
48
ATTEMPT_RESUME = 2     # Attempt to resume session
1✔
49

50
ModifyCookieCrumblers = 'Modify Cookie Crumblers'
1✔
51
ViewManagementScreens = view_management_screens
1✔
52

53

54
class CookieCrumblerDisabled(Exception):
1✔
55

56
    """Cookie crumbler should not be used for a certain request.
57
    """
58

59

60
@implementer(ICookieCrumbler)
1✔
61
class CookieCrumbler(UniqueObject, PropertyManager, SimpleItem):
1✔
62

63
    """Reads cookies during traversal and simulates the HTTP auth headers.
64
    """
65

66
    id = 'cookie_authentication'
1✔
67

68
    manage_options = (
1✔
69
        PropertyManager.manage_options +
70
        SimpleItem.manage_options)
71

72
    meta_type = 'Cookie Crumbler'
1✔
73
    zmi_icon = 'fa fa-cookie-bite'
1✔
74

75
    security = ClassSecurityInfo()
1✔
76
    security.declareProtected(ModifyCookieCrumblers,  # NOQA: flake8: D001
1✔
77
                              'manage_editProperties')
78
    security.declareProtected(ModifyCookieCrumblers,  # NOQA: flake8: D001
1✔
79
                              'manage_changeProperties')
80
    security.declareProtected(ViewManagementScreens,  # NOQA: flake8: D001
1✔
81
                              'manage_propertiesForm')
82

83
    # By default, anonymous users can view login/logout pages.
84
    _View_Permission = ('Anonymous',)
1✔
85

86
    _properties = ({'id': 'title', 'type': 'string', 'mode': 'w',
1✔
87
                    'label': 'Title'},
88
                   {'id': 'auth_cookie', 'type': 'string', 'mode': 'w',
89
                    'label': 'Authentication cookie name'},
90
                   {'id': 'name_cookie', 'type': 'string', 'mode': 'w',
91
                    'label': 'User name form variable'},
92
                   {'id': 'pw_cookie', 'type': 'string', 'mode': 'w',
93
                    'label': 'User password form variable'},
94
                   {'id': 'persist_cookie', 'type': 'string', 'mode': 'w',
95
                    'label': 'User name persistence form variable'},
96
                   {'id': 'local_cookie_path', 'type': 'boolean', 'mode': 'w',
97
                    'label': 'Use cookie paths to limit scope'},
98
                   {'id': 'cache_header_value', 'type': 'string', 'mode': 'w',
99
                    'label': 'Cache-Control header value'},
100
                   {'id': 'log_username', 'type': 'boolean', 'mode': 'w',
101
                    'label': 'Log cookie auth username to access log'},
102
                   )
103

104
    auth_cookie = '__ac'
1✔
105
    name_cookie = '__ac_name'
1✔
106
    pw_cookie = '__ac_password'  # not used as cookie, just as request key
1✔
107
    persist_cookie = '__ac_persistent'
1✔
108
    local_cookie_path = False
1✔
109
    cache_header_value = 'private'
1✔
110
    log_username = True
1✔
111

112
    @security.private
1✔
113
    def delRequestVar(self, req, name):
1✔
114
        # No errors of any sort may propagate, and we don't care *what*
115
        # they are, even to log them.
116
        try:
1✔
117
            del req.other[name]
1✔
118
        except Exception:
1✔
119
            pass
1✔
120
        try:
1✔
121
            del req.form[name]
1✔
122
        except Exception:
1✔
123
            pass
1✔
124
        try:
1✔
125
            del req.cookies[name]
1✔
126
        except Exception:
×
127
            pass
×
128
        try:
1✔
129
            del req.environ[name]
1✔
130
        except Exception:
1✔
131
            pass
1✔
132

133
    @security.public
1✔
134
    def getCookiePath(self):
1✔
135
        if not self.local_cookie_path:
1!
136
            return '/'
1✔
137
        parent = aq_parent(aq_inner(self))
×
138
        if parent is not None:
×
139
            return '/' + parent.absolute_url(1)
×
140
        else:
141
            return '/'
×
142

143
    # Allow overridable cookie set/expiration methods.
144
    @security.private
1✔
145
    def getCookieMethod(self, name, default=None):
1✔
146
        return getattr(self, name, default)
1✔
147

148
    @security.private
1✔
149
    def defaultSetAuthCookie(self, resp, cookie_name, cookie_value):
1✔
150
        kw = {}
1✔
151
        req = getRequest()
1✔
152
        if req is not None and req.get('SERVER_URL', '').startswith('https:'):
1!
153
            # Ask the client to send back the cookie only in SSL mode
154
            kw['secure'] = 'y'
×
155
        resp.setCookie(cookie_name, cookie_value,
1✔
156
                       path=self.getCookiePath(), **kw)
157

158
    @security.private
1✔
159
    def defaultExpireAuthCookie(self, resp, cookie_name):
1✔
160
        resp.expireCookie(cookie_name, path=self.getCookiePath())
×
161

162
    def _setAuthHeader(self, ac, request, response):
1✔
163
        """Set the auth headers for both the Zope and Medusa http request
164
        objects.
165
        """
166
        request._auth = 'Basic %s' % ac
1✔
167
        response._auth = 1
1✔
168
        if self.log_username:
1!
169
            # Set the authorization header in the medusa http request
170
            # so that the username can be logged to the Z2.log
171
            try:
1✔
172
                # Put the full-arm latex glove on now...
173
                medusa_headers = response.stdout._request._header_cache
1✔
174
            except AttributeError:
1✔
175
                pass
1✔
176
            else:
177
                medusa_headers['authorization'] = request._auth
×
178

179
    @security.private
1✔
180
    def modifyRequest(self, req, resp):
1✔
181
        """Copies cookie-supplied credentials to the basic auth fields.
182

183
        Returns a flag indicating what the user is trying to do with
184
        cookies: ATTEMPT_NONE, ATTEMPT_LOGIN, or ATTEMPT_RESUME.  If
185
        cookie login is disabled for this request, raises
186
        CookieCrumblerDisabled.
187
        """
188
        if not isinstance(req, HTTPRequest) or  \
1!
189
           req['REQUEST_METHOD'] not in ('HEAD', 'GET', 'PUT', 'POST') or \
190
           'WEBDAV_SOURCE_PORT' in req.environ:
191
            raise CookieCrumblerDisabled
×
192

193
        # attempt may contain information about an earlier attempt to
194
        # authenticate using a higher-up cookie crumbler within the
195
        # same request.
196
        attempt = getattr(req, '_cookie_auth', ATTEMPT_NONE)
1✔
197

198
        if attempt == ATTEMPT_NONE:
1!
199
            if req._auth:
1!
200
                # An auth header was provided and no cookie crumbler
201
                # created it.  The user must be using basic auth.
202
                raise CookieCrumblerDisabled
×
203

204
            if self.pw_cookie in req and self.name_cookie in req:
1✔
205
                # Attempt to log in and set cookies.
206
                attempt = ATTEMPT_LOGIN
1✔
207
                name = req[self.name_cookie]
1✔
208
                pw = req[self.pw_cookie]
1✔
209
                if six.PY2:
1!
210
                    ac = base64.encodestring('%s:%s' % (name, pw)).rstrip()
×
211
                else:
212
                    ac = base64.encodebytes(
1✔
213
                        ('%s:%s' % (name, pw)).encode()).rstrip().decode()
214
                self._setAuthHeader(ac, req, resp)
1✔
215
                if req.get(self.persist_cookie, 0):
1!
216
                    # Persist the user name (but not the pw or session)
217
                    expires = (DateTime() + 365).toZone('GMT').rfc822()
×
218
                    resp.setCookie(self.name_cookie, name,
×
219
                                   path=self.getCookiePath(),
220
                                   expires=expires)
221
                else:
222
                    # Expire the user name
223
                    resp.expireCookie(self.name_cookie,
1✔
224
                                      path=self.getCookiePath())
225
                method = self.getCookieMethod('setAuthCookie',
1✔
226
                                              self.defaultSetAuthCookie)
227
                method(resp, self.auth_cookie, quote(ac))
1✔
228
                self.delRequestVar(req, self.name_cookie)
1✔
229
                self.delRequestVar(req, self.pw_cookie)
1✔
230

231
            elif self.auth_cookie in req:
1✔
232
                # Attempt to resume a session if the cookie is valid.
233
                # Copy __ac to the auth header.
234
                ac = unquote(req[self.auth_cookie])
1✔
235
                if ac and ac != 'deleted':
1!
236
                    try:
1✔
237
                        if six.PY2:
1!
238
                            base64.decodestring(ac)
×
239
                        else:
240
                            base64.decodebytes(ac.encode())
1✔
241
                    except Exception:
×
242
                        # Not a valid auth header.
243
                        pass
×
244
                    else:
245
                        attempt = ATTEMPT_RESUME
1✔
246
                        self._setAuthHeader(ac, req, resp)
1✔
247
                        self.delRequestVar(req, self.auth_cookie)
1✔
248
                        method = self.getCookieMethod(
1✔
249
                            'twiddleAuthCookie', None)
250
                        if method is not None:
1!
251
                            method(resp, self.auth_cookie, quote(ac))
×
252

253
        req._cookie_auth = attempt
1✔
254
        return attempt
1✔
255

256
    def __call__(self, container, req):
1✔
257
        """The __before_publishing_traverse__ hook."""
258
        resp = req['RESPONSE']
1✔
259
        try:
1✔
260
            attempt = self.modifyRequest(req, resp)
1✔
261
        except CookieCrumblerDisabled:
×
262
            return
×
263
        if attempt != ATTEMPT_NONE:
1✔
264
            # Trying to log in or resume a session
265
            if self.cache_header_value:
1✔
266
                # we don't want caches to cache the resulting page
267
                resp.setHeader('Cache-Control', self.cache_header_value)
1✔
268
                # demystify this in the response.
269
                resp.setHeader('X-Cache-Control-Hdr-Modified-By',
1✔
270
                               'CookieCrumbler')
271
            phys_path = self.getPhysicalPath()
1✔
272
            # Cookies are in use.
273
            # Provide a logout page.
274
            req._logout_path = phys_path + ('logout',)
1✔
275

276
    @security.public
1✔
277
    def credentialsChanged(self, user, name, pw, request=None):
1✔
278
        """
279
        Updates cookie credentials if user details are changed.
280
        """
281
        if request is None:
×
282
            request = getRequest()  # BBB for Membershiptool
×
283
        reponse = request['RESPONSE']
×
284
        if six.PY2:
×
285
            ac = base64.encodestring('%s:%s' % (name, pw)).rstrip()
×
286
        else:
287
            ac = base64.encodebytes(
×
288
                ('%s:%s' % (name, pw)).encode()).rstrip().decode()
289
        method = self.getCookieMethod('setAuthCookie',
×
290
                                      self.defaultSetAuthCookie)
291
        method(reponse, self.auth_cookie, quote(ac))
×
292

293
    @security.public
1✔
294
    def logout(self, response=None):
1✔
295
        """
296
        Logs out the user
297
        """
298
        target = None
×
299
        if response is None:
×
300
            response = getRequest()['RESPONSE']  # BBB for App.Management
×
301
            atool = getUtility(IActionsTool)
×
302
            target = atool.getActionInfo('user/logout')['url']
×
303
        method = self.getCookieMethod('expireAuthCookie',
×
304
                                      self.defaultExpireAuthCookie)
305
        method(response, cookie_name=self.auth_cookie)
×
306
        # BBB for App.Management
307
        if target is not None:
×
308
            response.redirect(target)
×
309

310
    @security.public
1✔
311
    def propertyLabel(self, id):
1✔
312
        """Return a label for the given property id
313
        """
314
        for p in self._properties:
×
315
            if p['id'] == id:
×
316
                return p.get('label', id)
×
317
        return id
×
318

319

320
InitializeClass(CookieCrumbler)
1✔
321
registerToolInterface('cookie_authentication', ICookieCrumbler)
1✔
322

323

324
def handleCookieCrumblerEvent(ob, event):
1✔
325
    """ Event subscriber for (un)registering a CC as a before traverse hook.
326
    """
327
    if not ICookieCrumbler.providedBy(ob):
1!
328
        return
×
329

330
    if IObjectMovedEvent.providedBy(event):
1✔
331
        if event.newParent is not None:
1✔
332
            # register before traverse hook
333
            handle = ob.meta_type + '/' + ob.getId()
1✔
334
            nc = BeforeTraverse.NameCaller(ob.getId())
1✔
335
            BeforeTraverse.registerBeforeTraverse(event.newParent, nc, handle)
1✔
336
    elif IObjectWillBeMovedEvent.providedBy(event):
1!
337
        if event.oldParent is not None:
1✔
338
            # unregister before traverse hook
339
            handle = ob.meta_type + '/' + ob.getId()
1✔
340
            BeforeTraverse.unregisterBeforeTraverse(event.oldParent, handle)
1✔
341

342

343
manage_addCCForm = HTMLFile('dtml/addCC', globals())
1✔
344
manage_addCCForm.__name__ = 'addCC'
1✔
345

346

347
def manage_addCC(dispatcher, id, title='', REQUEST=None):
1✔
348
    """ """
349
    ob = CookieCrumbler()
1✔
350
    ob.id = id
1✔
351
    ob.title = title
1✔
352
    dispatcher._setObject(ob.getId(), ob)
1✔
353
    ob = getattr(dispatcher.this(), ob.getId())
1✔
354
    if REQUEST is not None:
1!
355
        return dispatcher.manage_main(dispatcher, REQUEST)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc