[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