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

zopefoundation / Products.PluggableAuthService / 5303493172

pending completion
5303493172

push

github

web-flow
Drop support for Python 2.7, 3.5, 3.6. (#116)

* Drop zserver extra in setup.py. Thus dropping FTP support.
* Drop support for Zope < 5.
Co-authored-by: Jens Vagelpohl <jens@plyp.com>

1288 of 1745 branches covered (73.81%)

Branch coverage included in aggregate %.

127 of 127 new or added lines in 30 files covered. (100.0%)

9619 of 10349 relevant lines covered (92.95%)

0.93 hits per line

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

86.07
/src/Products/PluggableAuthService/plugins/CookieAuthHelper.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
7
# distribution.
8
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
9
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
10
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
11
# FOR A PARTICULAR PURPOSE.
12
#
13
##############################################################################
14
""" Class: CookieAuthHelper
1✔
15
"""
16

17
import codecs
1✔
18
from base64 import decodebytes
1✔
19
from base64 import encodebytes
1✔
20
from binascii import Error
1✔
21
from binascii import hexlify
1✔
22
from urllib.parse import quote
1✔
23
from urllib.parse import unquote
1✔
24

25
from AccessControl.class_init import InitializeClass
1✔
26
from AccessControl.Permissions import view
1✔
27
from AccessControl.SecurityInfo import ClassSecurityInfo
1✔
28
from Acquisition import aq_inner
1✔
29
from Acquisition import aq_parent
1✔
30
from OFS.Folder import Folder
1✔
31
from Products.PageTemplates.PageTemplateFile import PageTemplateFile
1✔
32
from Products.PageTemplates.ZopePageTemplate import ZopePageTemplate
1✔
33
from zope.interface import Interface
1✔
34

35
from ..interfaces.plugins import IChallengePlugin
1✔
36
from ..interfaces.plugins import ICredentialsResetPlugin
1✔
37
from ..interfaces.plugins import ICredentialsUpdatePlugin
1✔
38
from ..interfaces.plugins import ILoginPasswordHostExtractionPlugin
1✔
39
from ..plugins.BasePlugin import BasePlugin
1✔
40
from ..utils import classImplements
1✔
41
from ..utils import url_local
1✔
42

43

44
class ICookieAuthHelper(Interface):
1✔
45
    """ Marker interface.
46
    """
47

48

49
manage_addCookieAuthHelperForm = PageTemplateFile(
1✔
50
    'www/caAdd', globals(), __name__='manage_addCookieAuthHelperForm')
51

52

53
def addCookieAuthHelper(dispatcher, id, title=None, cookie_name='',
1✔
54
                        REQUEST=None):
55
    """ Add a Cookie Auth Helper to a Pluggable Auth Service. """
56
    sp = CookieAuthHelper(id, title, cookie_name)
1✔
57
    dispatcher._setObject(sp.getId(), sp)
1✔
58

59
    if REQUEST is not None:
1!
60
        REQUEST['RESPONSE'].redirect('%s/manage_workspace'
×
61
                                     '?manage_tabs_message='
62
                                     'CookieAuthHelper+added.' %
63
                                     dispatcher.absolute_url())
64

65

66
def decode_cookie(raw):
1✔
67
    value = unquote(raw)
1✔
68
    value = value.encode('utf8')
1✔
69
    value = decodebytes(value)
1✔
70
    value = value.decode('utf8')
1✔
71
    return value
1✔
72

73

74
def decode_hex(raw):
1✔
75
    if isinstance(raw, str):
1!
76
        raw = raw.encode('utf8')
1✔
77
    value = codecs.decode(raw, 'hex_codec')
1✔
78
    value = value.decode('utf-8')
1✔
79
    return value
1✔
80

81

82
class CookieAuthHelper(Folder, BasePlugin):
1✔
83
    """ Multi-plugin for managing details of Cookie Authentication. """
84

85
    meta_type = 'Cookie Auth Helper'
1✔
86
    zmi_icon = 'fas fa-cookie-bite'
1✔
87
    cookie_name = '__ginger_snap'
1✔
88
    login_path = 'login_form'
1✔
89
    cookie_same_site = 'Lax'
1✔
90
    cookie_same_site_choices = ('None', 'Lax', 'Strict')
1✔
91
    cookie_secure = False
1✔
92
    security = ClassSecurityInfo()
1✔
93

94
    _properties = ({'id': 'title', 'label': 'Title',
1✔
95
                    'type': 'string', 'mode': 'w'},
96
                   {'id': 'cookie_name', 'label': 'Cookie Name',
97
                    'type': 'string', 'mode': 'w'},
98
                   {'id': 'cookie_secure', 'type': 'boolean', 'mode': 'w',
99
                    'label': 'Send cookie over HTTPS only'},
100
                   {'id': 'cookie_same_site', 'type': 'selection',
101
                    'label': 'Cookie SameSite restriction', 'mode': 'w',
102
                    'select_variable': 'cookie_same_site_choices'},
103
                   {'id': 'login_path', 'label': 'Login Form',
104
                    'type': 'string', 'mode': 'w'})
105

106
    manage_options = (BasePlugin.manage_options[:1]
1✔
107
                      + Folder.manage_options[:1]
108
                      + Folder.manage_options[2:])
109

110
    def __init__(self, id, title=None, cookie_name=''):
1✔
111
        self._setId(id)
1✔
112
        self.title = title
1✔
113

114
        if cookie_name:
1✔
115
            self.cookie_name = cookie_name
1✔
116

117
    @security.private
1✔
118
    def extractCredentials(self, request):
1✔
119
        """ Extract credentials from cookie or 'request'. """
120
        creds = {}
1✔
121
        cookie = request.get(self.cookie_name, '')
1✔
122
        # Look in the request.form for the names coming from the login form
123
        login = request.form.get('__ac_name', '')
1✔
124

125
        if login and '__ac_password' in request.form:
1✔
126
            creds['login'] = login
1✔
127
            creds['password'] = request.form.get('__ac_password', '')
1✔
128

129
        elif cookie and cookie != 'deleted':
1✔
130
            try:
1✔
131
                cookie_val = decode_cookie(cookie)
1✔
132
            except Error:
1✔
133
                # Cookie is in a different format, so it is not ours
134
                return creds
1✔
135

136
            try:
1✔
137
                login, password = cookie_val.split(':')
1✔
138
            except ValueError:
×
139
                # Cookie is in a different format, so it is not ours
140
                return creds
×
141

142
            try:
1✔
143
                creds['login'] = decode_hex(login)
1✔
144
                creds['password'] = decode_hex(password)
1✔
145
            except (Error, TypeError):
1✔
146
                # Cookie is in a different format, so it is not ours
147
                return {}
1✔
148

149
        if creds:
1✔
150
            creds['remote_host'] = request.get('REMOTE_HOST', '')
1✔
151

152
            try:
1✔
153
                creds['remote_address'] = request.getClientAddr()
1✔
154
            except AttributeError:
1✔
155
                creds['remote_address'] = request.get('REMOTE_ADDR', '')
1✔
156

157
        return creds
1✔
158

159
    @security.private
1✔
160
    def challenge(self, request, response, **kw):
1✔
161
        """ Challenge the user for credentials. """
162
        return self.unauthorized()
1✔
163

164
    @security.private
1✔
165
    def get_cookie_value(self, login, new_password):
1✔
166
        cookie_str = b':'.join([
1✔
167
            hexlify(login.encode('utf-8')),
168
            hexlify(new_password.encode('utf-8'))])
169
        cookie_val = encodebytes(cookie_str)
1✔
170
        cookie_val = cookie_val.rstrip()
1✔
171
        return cookie_val
1✔
172

173
    @security.private
1✔
174
    def updateCredentials(self, request, response, login, new_password):
1✔
175
        """ Respond to change of credentials (NOOP for basic auth). """
176
        cookie_val = self.get_cookie_value(login, new_password)
1✔
177
        cookie_secure = self.cookie_same_site == 'None' or self.cookie_secure
1✔
178
        response.setCookie(self.cookie_name, quote(cookie_val),
1✔
179
                           path='/', same_site=self.cookie_same_site,
180
                           secure=cookie_secure)
181

182
    @security.private
1✔
183
    def resetCredentials(self, request, response):
1✔
184
        """ Raise unauthorized to tell browser to clear credentials. """
185
        response.expireCookie(self.cookie_name, path='/')
1✔
186

187
    @security.private
1✔
188
    def manage_afterAdd(self, item, container):
1✔
189
        """ Setup tasks upon instantiation """
190
        if 'login_form' not in self.objectIds():
1!
191
            login_form = ZopePageTemplate(id='login_form',
1✔
192
                                          text=BASIC_LOGIN_FORM)
193
            login_form.title = 'Login Form'
1✔
194
            login_form.manage_permission(view, roles=['Anonymous'], acquire=1)
1✔
195
            self._setObject('login_form', login_form, set_owner=0)
1✔
196

197
    @security.private
1✔
198
    def unauthorized(self):
1✔
199
        req = self.REQUEST
1✔
200
        resp = req['RESPONSE']
1✔
201

202
        # If we set the auth cookie before, delete it now.
203
        if self.cookie_name in resp.cookies:
1!
204
            del resp.cookies[self.cookie_name]
×
205

206
        # Redirect if desired.
207
        url = self.getLoginURL()
1✔
208
        if url is not None:
1!
209
            came_from = req.get('came_from', None)
1✔
210

211
            if came_from is None:
1!
212
                came_from = req.get('ACTUAL_URL', '')
1✔
213
                query = req.get('QUERY_STRING')
1✔
214
                if query:
1!
215
                    if not query.startswith('?'):
×
216
                        query = '?' + query
×
217
                    came_from = came_from + query
×
218
            else:
219
                # If came_from contains a value it means the user
220
                # must be coming through here a second time
221
                # Reasons could be typos when providing credentials
222
                # or a redirect loop (see below)
223
                req_url = req.get('ACTUAL_URL', '')
×
224

225
                if req_url and req_url == url:
×
226
                    # Oops... The login_form cannot be reached by the user -
227
                    # it might be protected itself due to misconfiguration -
228
                    # the only sane thing to do is to give up because we are
229
                    # in an endless redirect loop.
230
                    return 0
×
231

232
            # Sanitize the return URL ``came_from`` and only allow local URLs
233
            # to prevent an open exploitable redirect issue
234
            came_from = url_local(came_from)
1✔
235

236
            if '?' in url:
1!
237
                sep = '&'
×
238
            else:
239
                sep = '?'
1✔
240
            url = '{}{}came_from={}'.format(url, sep, quote(came_from))
1✔
241
            resp.redirect(url, lock=1)
1✔
242
            resp.setHeader('Expires', 'Sat, 01 Jan 2000 00:00:00 GMT')
1✔
243
            resp.setHeader('Cache-Control', 'no-cache')
1✔
244
            return 1
1✔
245

246
        # Could not challenge.
247
        return 0
×
248

249
    @security.private
1✔
250
    def getLoginURL(self):
1✔
251
        """ Where to send people for logging in """
252
        if self.login_path.startswith('/') or '://' in self.login_path:
1!
253
            return self.login_path
×
254
        elif self.login_path != '':
1!
255
            return '{}/{}'.format(self.absolute_url(), self.login_path)
1✔
256
        else:
257
            return None
×
258

259
    @security.public
1✔
260
    def login(self):
1✔
261
        """ Set a cookie and redirect to the url that we tried to
262
        authenticate against originally.
263
        """
264
        request = self.REQUEST
1✔
265
        response = request['RESPONSE']
1✔
266

267
        login = request.get('__ac_name', '')
1✔
268
        password = request.get('__ac_password', '')
1✔
269

270
        # In order to use the CookieAuthHelper for its nice login page
271
        # facility but store and manage credentials somewhere else we need
272
        # to make sure that upon login only plugins activated as
273
        # IUpdateCredentialPlugins get their updateCredentials method
274
        # called. If the method is called on the CookieAuthHelper it will
275
        # simply set its own auth cookie, to the exclusion of any other
276
        # plugins that might want to store the credentials.
277
        pas_instance = self._getPAS()
1✔
278

279
        if pas_instance is not None:
1✔
280
            pas_instance.updateCredentials(request, response, login, password)
1✔
281
        came_from = request.form.get('came_from')
1✔
282
        if came_from is not None:
1✔
283
            return response.redirect(url_local(came_from))
1✔
284
        # When this happens, this either means
285
        # - the administrator did not setup the login form properly
286
        # - the user manipulated the login form and removed `came_from`
287
        # Still, the user provided correct credentials and is logged in.
288
        pas_root = aq_parent(aq_inner(self._getPAS()))
1✔
289
        return response.redirect(pas_root.absolute_url())
1✔
290

291

292
classImplements(CookieAuthHelper, ICookieAuthHelper,
1✔
293
                ILoginPasswordHostExtractionPlugin, IChallengePlugin,
294
                ICredentialsUpdatePlugin, ICredentialsResetPlugin)
295

296
InitializeClass(CookieAuthHelper)
1✔
297

298

299
BASIC_LOGIN_FORM = """<html>
1✔
300
  <head>
301
    <title> Login Form </title>
302
  </head>
303

304
  <body>
305

306
    <h3> Please log in </h3>
307

308
    <form method="post" action=""
309
          tal:attributes="action string:${here/absolute_url}/login">
310

311
      <input type="hidden" name="came_from" value=""
312
             tal:attributes="value request/came_from | string:"/>
313
      <table cellpadding="2">
314
        <tr>
315
          <td><b>Login:</b> </td>
316
          <td><input type="text" name="__ac_name" size="30" /></td>
317
        </tr>
318
        <tr>
319
          <td><b>Password:</b></td>
320
          <td><input type="password" name="__ac_password" size="30" /></td>
321
        </tr>
322
        <tr>
323
          <td colspan="2">
324
            <br />
325
            <input type="submit" value=" Log In " />
326
          </td>
327
        </tr>
328
      </table>
329

330
    </form>
331

332
  </body>
333

334
</html>
335
"""
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