[Checkins] SVN: z3c.password/branches/adamg-options/ adjusted lockOutPeriod behavour
Adam Groszer
agroszer at gmail.com
Sun Jun 14 10:15:47 EDT 2009
Log message for revision 100949:
adjusted lockOutPeriod behavour
Changed:
U z3c.password/branches/adamg-options/CHANGES.txt
U z3c.password/branches/adamg-options/src/z3c/password/principal.py
U z3c.password/branches/adamg-options/src/z3c/password/principal.txt
-=-
Modified: z3c.password/branches/adamg-options/CHANGES.txt
===================================================================
--- z3c.password/branches/adamg-options/CHANGES.txt 2009-06-14 13:19:30 UTC (rev 100948)
+++ z3c.password/branches/adamg-options/CHANGES.txt 2009-06-14 14:15:47 UTC (rev 100949)
@@ -21,7 +21,7 @@
2. raise TooManyLoginFailures if too many bad tries
3. raise PasswordExpired if expired AND password matches
4. return whether password matches
- More details in principal.txt
+ More details in ``principal.txt``
- Added Russian translation
Modified: z3c.password/branches/adamg-options/src/z3c/password/principal.py
===================================================================
--- z3c.password/branches/adamg-options/src/z3c/password/principal.py 2009-06-14 13:19:30 UTC (rev 100948)
+++ z3c.password/branches/adamg-options/src/z3c/password/principal.py 2009-06-14 14:15:47 UTC (rev 100949)
@@ -31,7 +31,7 @@
failedAttempts = 0
maxFailedAttempts = None
- lockedOutOn = None
+ lastFailedAttempt = None
lockOutPeriod = None
def getPassword(self):
@@ -50,51 +50,62 @@
return datetime.datetime.now()
def checkPassword(self, pwd, ignoreExpiration=False, ignoreFailures=False):
+ # keep this as fast as possible, because it will be called (usually)
+ # for EACH request
+
# Check the password
same = super(PrincipalMixIn, self).checkPassword(pwd)
- if not ignoreFailures and self.lockedOutOn is not None:
- lockPeriod = self._lockOutPeriod()
- if lockPeriod is not None:
- #check if the user locked himself previously
- if self.lockedOutOn + lockPeriod > self.now():
- if not same:
- self.lockedOutOn = self.now()
- raise interfaces.AccountLocked(self)
- else:
- self.failedAttempts = 0
- self.lockedOutOn = None
+ if not ignoreFailures and self.lastFailedAttempt is not None:
+ attempts = self._maxFailedAttempts()
+ if attempts is not None and self.failedAttempts > attempts:
+ lockPeriod = self._lockOutPeriod()
+ if lockPeriod is not None:
+ #check if the user locked himself
+ if self.lastFailedAttempt + lockPeriod > self.now():
+ if not same:
+ self.lastFailedAttempt = self.now()
+ raise interfaces.AccountLocked(self)
# If this was a failed attempt, record it, otherwise reset the failures
if same and self.failedAttempts != 0:
self.failedAttempts = 0
- self.lockedOutOn = None
- if not same:
- self.failedAttempts += 1
- # If the maximum amount of failures has been reached notify the system
- # by raising an error.
- if not ignoreFailures:
- attempts = self._maxFailedAttempts()
- if attempts is not None:
- if (attempts and self.failedAttempts > attempts):
- #record the time when TooManyLoginFailures happened
- self.lockedOutOn = self.now()
-
- raise interfaces.TooManyLoginFailures(self)
-
+ self.lastFailedAttempt = None
if same:
if not ignoreExpiration:
if self.passwordExpired:
raise interfaces.PasswordExpired(self)
# Make sure the password has not been expired
- expires = self._passwordExpiresAfter()
- if expires is not None:
- if self.passwordSetOn + expires < self.now():
+ expiresOn = self.passwordExpiresOn()
+ if expiresOn is not None:
+ if expiresOn < self.now():
raise interfaces.PasswordExpired(self)
+ else:
+ lockPeriod = self._lockOutPeriod()
+ if lockPeriod is not None and self.lastFailedAttempt is not None:
+ if self.lastFailedAttempt + lockPeriod < self.now():
+ #reset count if the tries were outside of the lockPeriod
+ self.failedAttempts = 0
+ self.failedAttempts += 1
+ self.lastFailedAttempt = self.now()
+
+ # If the maximum amount of failures has been reached notify the
+ # system by raising an error.
+ if not ignoreFailures:
+ attempts = self._maxFailedAttempts()
+ if attempts is not None and self.failedAttempts > attempts:
+ raise interfaces.TooManyLoginFailures(self)
+
return same
+ def passwordExpiresOn(self):
+ expires = self._passwordExpiresAfter()
+ if expires is None:
+ return None
+ return self.passwordSetOn + expires
+
def _optionsUtility(self):
return zope.component.queryUtility(
interfaces.IPasswordOptionsUtility, default=None)
@@ -107,7 +118,7 @@
if options is None:
return self.passwordExpiresAfter
else:
- if options.passwordExpiresAfter:
+ if options.passwordExpiresAfter is not None:
return datetime.timedelta(days=options.passwordExpiresAfter)
else:
return self.passwordExpiresAfter
@@ -120,7 +131,7 @@
if options is None:
return self.lockOutPeriod
else:
- if options.lockOutPeriod:
+ if options.lockOutPeriod is not None:
return datetime.timedelta(minutes=options.lockOutPeriod)
else:
return self.lockOutPeriod
@@ -133,7 +144,7 @@
if options is None:
return self.maxFailedAttempts
else:
- if options.maxFailedAttempts:
+ if options.maxFailedAttempts is not None:
return options.maxFailedAttempts
else:
return self.maxFailedAttempts
\ No newline at end of file
Modified: z3c.password/branches/adamg-options/src/z3c/password/principal.txt
===================================================================
--- z3c.password/branches/adamg-options/src/z3c/password/principal.txt 2009-06-14 13:19:30 UTC (rev 100948)
+++ z3c.password/branches/adamg-options/src/z3c/password/principal.txt 2009-06-14 14:15:47 UTC (rev 100949)
@@ -37,10 +37,11 @@
since the last successful one. This value is used to determine when to lock
the account after the maximum amount of failures has been reached.
-- ``lockedOutOn``
+- ``lastFailedAttempt``
- The date/time at which the failedAttempts went over maxFailedAttempts.
+ The date/time at which the failedAttempts last bad password was entered.
Used to implement automatic account lock after too many login failures.
+ Cleared when a good password is entered.
- ``lockOutPeriod``
@@ -416,7 +417,7 @@
The timestamp of the last bad try is recorded:
- >>> user.lockedOutOn
+ >>> user.lastFailedAttempt
datetime.datetime(2009, 6, 14, 13, 4)
The user cannot login within the next 60 minutes.
@@ -432,7 +433,7 @@
BTW, beating on the bad password starts the 60 minutes again:
- >>> user.lockedOutOn
+ >>> user.lastFailedAttempt
datetime.datetime(2009, 6, 14, 13, 15)
Where the good password just does not let the user in:
@@ -444,7 +445,7 @@
...
AccountLocked: The account is locked, because the password was entered incorrectly too often.
- >>> user.lockedOutOn
+ >>> user.lastFailedAttempt
datetime.datetime(2009, 6, 14, 13, 15)
The user has to wait, till the time has passed:
@@ -457,7 +458,7 @@
The good login resets all the properties:
- >>> user.lockedOutOn
+ >>> user.lastFailedAttempt
>>> user.failedAttempts
0
@@ -500,7 +501,7 @@
The timestamp of the last bad try is recorded:
- >>> user.lockedOutOn
+ >>> user.lastFailedAttempt
datetime.datetime(2009, 6, 14, 13, 4)
The user cannot login within the next 60 minutes.
@@ -516,7 +517,7 @@
BTW, beating on the bad password starts the 60 minutes again:
- >>> user.lockedOutOn
+ >>> user.lastFailedAttempt
datetime.datetime(2009, 6, 14, 13, 15)
Where the good password just does not let the user in:
@@ -528,7 +529,7 @@
...
AccountLocked: The account is locked, because the password was entered incorrectly too often.
- >>> user.lockedOutOn
+ >>> user.lastFailedAttempt
datetime.datetime(2009, 6, 14, 13, 15)
The user has to wait, till the time has passed:
@@ -541,11 +542,66 @@
The good login resets all the properties:
- >>> user.lockedOutOn
+ >>> user.lastFailedAttempt
>>> user.failedAttempts
0
+Edge case.
+The number of bad login tries has to exceed ``maxFailedAttempts`` within the
+``lockOutPeriod``.
+
+ >>> NOW = datetime.datetime(2009, 6, 14, 13, 0)
+
+ >>> user = MyPrincipal('srichter', '123123', u'Stephan Richter')
+ >>> user.lockOutPeriod = datetime.timedelta(minutes=60)
+
+ >>> NOW = datetime.datetime(2009, 6, 14, 13, 0)+datetime.timedelta(minutes=1)
+ >>> user.checkPassword('456456')
+ False
+
+ >>> NOW = datetime.datetime(2009, 6, 14, 13, 0)+datetime.timedelta(minutes=2)
+ >>> user.checkPassword('456456')
+ False
+
+ >>> NOW = datetime.datetime(2009, 6, 14, 13, 0)+datetime.timedelta(minutes=3)
+ >>> user.checkPassword('456456')
+ False
+
+ >>> user.lastFailedAttempt
+ datetime.datetime(2009, 6, 14, 13, 3)
+ >>> user.failedAttempts
+ 3
+
+If the user tries again after the lockOutPeriod passed, the ``failedAttempts``
+get reset:
+
+ >>> NOW = datetime.datetime(2009, 6, 14, 13, 0)+datetime.timedelta(minutes=65)
+ >>> user.checkPassword('456456')
+ False
+
+ >>> user.lastFailedAttempt
+ datetime.datetime(2009, 6, 14, 14, 5)
+ >>> user.failedAttempts
+ 1
+
+
+Edge case.
+When there is no ``maxFailedAttempts`` set, we can bang on with a bad password
+forever.
+
+ >>> poptions.maxFailedAttempts = None
+ >>> user = MyPrincipal('srichter', '123123', u'Stephan Richter')
+ >>> user.maxFailedAttempts = None
+
+ >>> NOW = datetime.datetime(2009, 6, 14, 13, 0)
+
+ >>> for x in xrange(256):
+ ... NOW = datetime.datetime(2009, 6, 14, 13)+datetime.timedelta(minutes=x)
+ ... if user.checkPassword('456456'):
+ ... print 'bug'
+
+
Coverage happiness
------------------
More information about the Checkins
mailing list