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

zopefoundation / z3c.password / 5397839207

pending completion
5397839207

push

github

web-flow
Config with pure python template (#6)

* Drop support for Python 2.7, 3.5, 3.6.
* Add support for Python 3.11.

Co-authored-by: Jens Vagelpohl <jens@netz.ooo>

169 of 224 branches covered (75.45%)

Branch coverage included in aggregate %.

24 of 24 new or added lines in 4 files covered. (100.0%)

492 of 531 relevant lines covered (92.66%)

0.93 hits per line

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

97.1
/src/z3c/password/principal.py
1
##############################################################################
2
#
3
# Copyright (c) 2006 Zope Foundation and Contributors.
4
# All Rights Reserved.
5
#
6
# This software is subject to the provisions of the Zope Public License,
7
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this 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
"""Principal MixIn for Advanced Password Management
1✔
15
"""
16
import datetime
1✔
17

18
import persistent.list
1✔
19
import zope.component
1✔
20
from zope.security.management import getInteraction
1✔
21

22
from z3c.password import interfaces
1✔
23

24

25
class PrincipalMixIn:
1✔
26
    """A Principal Mixin class for ``zope.app.principalfolder``'s internal
27
    principal."""
28

29
    passwordExpiresAfter = None
1✔
30
    passwordSetOn = None
1✔
31
    # force PasswordExpired, e.g. for changePasswordOnNextLogin
32
    passwordExpired = False
1✔
33

34
    failedAttempts = 0
1✔
35
    failedAttemptCheck = interfaces.TML_CHECK_ALL
1✔
36
    maxFailedAttempts = None
1✔
37
    lastFailedAttempt = None
1✔
38
    lockOutPeriod = None
1✔
39

40
    disallowPasswordReuse = None
1✔
41
    previousPasswords = None
1✔
42

43
    passwordOptionsUtilityName = None
1✔
44

45
    def _checkDisallowedPreviousPassword(self, password):
1✔
46
        if self._disallowPasswordReuse():
1✔
47
            if self.previousPasswords is not None and password is not None:
1✔
48
                # hack, but this should work with zope.app.authentication and
49
                # z3c.authenticator
50
                passwordManager = self._getPasswordManager()
1✔
51

52
                for pwd in self.previousPasswords:
1✔
53
                    if passwordManager.checkPassword(pwd, password):
1✔
54
                        raise interfaces.PreviousPasswordNotAllowed(self)
1✔
55

56
    def getPassword(self):
1✔
57
        return super().getPassword()
1✔
58

59
    def setPassword(self, password, passwordManagerName=None):
1✔
60
        self._checkDisallowedPreviousPassword(password)
1✔
61

62
        super().setPassword(password, passwordManagerName)
1✔
63

64
        if self._disallowPasswordReuse():
1✔
65
            if self.previousPasswords is None:
1✔
66
                self.previousPasswords = persistent.list.PersistentList()
1✔
67

68
            if self.password is not None:
1!
69
                # storm/custom property does not like a simple append
70
                ppwd = self.previousPasswords
1✔
71
                ppwd.append(self.password)
1✔
72
                self.previousPasswords = ppwd
1✔
73

74
        self.passwordSetOn = self.now()
1✔
75
        self.failedAttempts = 0
76
        self.lastFailedAttempt = None
1✔
77
        self.passwordExpired = False
1✔
78

79
    password = property(getPassword, setPassword)
1✔
80

81
    def now(self):
1✔
82
        # hook to facilitate testing and easier override
83
        return datetime.datetime.now()
1✔
84

85
    def _isRelevantRequest(self):
1✔
86
        fac = self._failedAttemptCheck()
1✔
87
        if fac is None:
1✔
88
            return True
1✔
89

90
        if fac == interfaces.TML_CHECK_ALL:
1!
91
            return True
×
92

93
        interaction = getInteraction()
1✔
94
        try:
1✔
95
            request = interaction.participations[0]
1✔
96
        except IndexError:
1✔
97
            return True  # no request, we regard that as relevant.
1✔
98

99
        if fac == interfaces.TML_CHECK_NONRESOURCE:
1✔
100
            if '/@@/' in request.getURL():
1✔
101
                return False
1✔
102
            return True
1✔
103

104
        if fac == interfaces.TML_CHECK_POSTONLY:
1!
105
            if request.method == 'POST':
1✔
106
                return True
1✔
107
            return False
1✔
108

109
    def checkPassword(self, pwd, ignoreExpiration=False, ignoreFailures=False):
1✔
110
        # keep this as fast as possible, because it will be called (usually)
111
        # for EACH request
112

113
        # Check the password
114
        same = super().checkPassword(pwd)
1✔
115

116
        # Do not try to record failed attempts or raise account locked
117
        # errors for requests that are irrelevant in this regard.
118
        if not self._isRelevantRequest():
1✔
119
            return same
1✔
120

121
        if not ignoreFailures and self.lastFailedAttempt is not None:
1✔
122
            if self.tooManyLoginFailures():
1✔
123
                locked = self.accountLocked()
1✔
124
                if locked is None:
1✔
125
                    # no lockPeriod
126
                    pass
1✔
127
                elif locked:
1✔
128
                    # account locked by tooManyLoginFailures and within
129
                    # lockPeriod
130
                    if not same:
1✔
131
                        self.lastFailedAttempt = self.now()
1✔
132
                    raise interfaces.AccountLocked(self)
1✔
133
                else:
134
                    # account locked by tooManyLoginFailures and out of
135
                    # lockPeriod
136
                    self.failedAttempts = 0
137
                    self.lastFailedAttempt = None
1✔
138

139
        if same:
1✔
140
            # successful attempt
141
            if not ignoreExpiration:
1!
142
                if self.passwordExpired:
1✔
143
                    raise interfaces.PasswordExpired(self)
1✔
144

145
                # Make sure the password has not been expired
146
                expiresOn = self.passwordExpiresOn()
1✔
147
                if expiresOn is not None:
1✔
148
                    if expiresOn < self.now():
1✔
149
                        raise interfaces.PasswordExpired(self)
1✔
150
            add = 0
1✔
151
        else:
152
            # failed attempt, record it, increase counter
153
            self.failedAttempts += 1
154
            self.lastFailedAttempt = self.now()
1✔
155
            add = 1
1✔
156

157
        # If the maximum amount of failures has been reached notify the
158
        # system by raising an error.
159
        if not ignoreFailures:
1✔
160
            if self.tooManyLoginFailures(add):
1✔
161
                raise interfaces.TooManyLoginFailures(self)
1✔
162

163
        if same and self.failedAttempts != 0:
164
            # if all nice and good clear failure counter
165
            self.failedAttempts = 0
166
            self.lastFailedAttempt = None
167

168
        return same
1✔
169

170
    def tooManyLoginFailures(self, add=0):
1✔
171
        attempts = self._maxFailedAttempts()
1✔
172
        # this one needs to be >=, because... data just does not
173
        # get saved on an exception when running under of a full Zope env.
174
        # the dance around ``add`` has the same roots
175
        # we need to be able to increase the failedAttempts count and not raise
176
        # at the same time
177
        if attempts is not None:
1✔
178
            attempts += add
1✔
179
            if self.failedAttempts >= attempts:
180
                return True
181
        return False
1✔
182

183
    def accountLocked(self):
1✔
184
        lockPeriod = self._lockOutPeriod()
1✔
185
        if lockPeriod is not None:
1✔
186
            # check if the user locked himself
187
            if (self.lastFailedAttempt is not None
1✔
188
                    and self.lastFailedAttempt + lockPeriod > self.now()):
189
                return True
1✔
190
            else:
191
                return False
1✔
192
        return None
1✔
193

194
    def passwordExpiresOn(self):
1✔
195
        expires = self._passwordExpiresAfter()
1✔
196
        if expires is None:
1✔
197
            return None
1✔
198
        if self.passwordSetOn is None:
1!
199
            return None
×
200
        return self.passwordSetOn + expires
1✔
201

202
    def _optionsUtility(self):
1✔
203
        if self.passwordOptionsUtilityName:
1✔
204
            # if we have a utility name, then it must be there
205
            return zope.component.getUtility(
1✔
206
                interfaces.IPasswordOptionsUtility,
207
                name=self.passwordOptionsUtilityName)
208
        return zope.component.queryUtility(
1✔
209
            interfaces.IPasswordOptionsUtility, default=None)
210

211
    def _passwordExpiresAfter(self):
1✔
212
        if self.passwordExpiresAfter is not None:
1✔
213
            return self.passwordExpiresAfter
1✔
214

215
        options = self._optionsUtility()
1✔
216
        if options is None:
1✔
217
            return self.passwordExpiresAfter
1✔
218
        else:
219
            days = options.passwordExpiresAfter
1✔
220
            if days is not None:
1✔
221
                return datetime.timedelta(days=days)
1✔
222
            else:
223
                return self.passwordExpiresAfter
1✔
224

225
    def _lockOutPeriod(self):
1✔
226
        if self.lockOutPeriod is not None:
1✔
227
            return self.lockOutPeriod
1✔
228

229
        options = self._optionsUtility()
1✔
230
        if options is None:
1✔
231
            return self.lockOutPeriod
1✔
232
        else:
233
            minutes = options.lockOutPeriod
1✔
234
            if minutes is not None:
1✔
235
                return datetime.timedelta(minutes=minutes)
1✔
236
            else:
237
                return self.lockOutPeriod
1✔
238

239
    def _failedAttemptCheck(self):
1✔
240
        if self.failedAttemptCheck is not None:
241
            return self.failedAttemptCheck
242

243
        options = self._optionsUtility()
1✔
244
        if options is None:
1✔
245
            return self.failedAttemptCheck
246
        else:
247
            fac = options.failedAttemptCheck
1✔
248
            if fac is not None:
1✔
249
                return fac
1✔
250
            else:
251
                return self.failedAttemptCheck
252

253
    def _maxFailedAttempts(self):
1✔
254
        if self.maxFailedAttempts is not None:
1✔
255
            return self.maxFailedAttempts
1✔
256

257
        options = self._optionsUtility()
1✔
258
        if options is None:
1✔
259
            return self.maxFailedAttempts
1✔
260
        else:
261
            count = options.maxFailedAttempts
1✔
262
            if count is not None:
1✔
263
                return count
1✔
264
            else:
265
                return self.maxFailedAttempts
1✔
266

267
    def _disallowPasswordReuse(self):
1✔
268
        if self.disallowPasswordReuse is not None:
1✔
269
            return self.disallowPasswordReuse
1✔
270

271
        options = self._optionsUtility()
1✔
272
        if options is None:
1✔
273
            return self.disallowPasswordReuse
1✔
274
        else:
275
            dpr = options.disallowPasswordReuse
1✔
276
            if dpr is not None:
1✔
277
                return dpr
1✔
278
            else:
279
                return self.disallowPasswordReuse
1✔
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