[Checkins] SVN: z3c.password/trunk/ merge of branch adamg-evenhigher:
Adam Groszer
agroszer at gmail.com
Mon Jun 22 04:25:20 EDT 2009
Log message for revision 101210:
merge of branch adamg-evenhigher:
- Feature: Even harder password settings:
* ``minLowerLetter``
* ``minUpperLetter``
* ``minDigits``
* ``minSpecials``
* ``minOthers``
* ``minUniqueCharacters``
* ``minUniqueLetters``: count and do not allow less then specified number
- Feature:
* ``disallowPasswordReuse``: do not allow to set a previously used password
- 100% test coverage
Changed:
U z3c.password/trunk/CHANGES.txt
U z3c.password/trunk/src/z3c/password/README.txt
U z3c.password/trunk/src/z3c/password/field.py
U z3c.password/trunk/src/z3c/password/interfaces.py
U z3c.password/trunk/src/z3c/password/password.py
U z3c.password/trunk/src/z3c/password/principal.py
U z3c.password/trunk/src/z3c/password/principal.txt
-=-
Modified: z3c.password/trunk/CHANGES.txt
===================================================================
--- z3c.password/trunk/CHANGES.txt 2009-06-22 07:56:49 UTC (rev 101209)
+++ z3c.password/trunk/CHANGES.txt 2009-06-22 08:25:20 UTC (rev 101210)
@@ -5,8 +5,20 @@
0.7.0 (unreleased)
------------------
-- ...
+- Feature: Even harder password settings:
+ * ``minLowerLetter``
+ * ``minUpperLetter``
+ * ``minDigits``
+ * ``minSpecials``
+ * ``minOthers``
+ * ``minUniqueCharacters``
+ * ``minUniqueLetters``: count and do not allow less then specified number
+- Feature:
+ * ``disallowPasswordReuse``: do not allow to set a previously used password
+
+- 100% test coverage
+
0.6.0 (2009-06-17)
------------------
Modified: z3c.password/trunk/src/z3c/password/README.txt
===================================================================
--- z3c.password/trunk/src/z3c/password/README.txt 2009-06-22 07:56:49 UTC (rev 101209)
+++ z3c.password/trunk/src/z3c/password/README.txt 2009-06-22 08:25:20 UTC (rev 101210)
@@ -153,13 +153,187 @@
... _ =pwd.generate()
+Even higher security settings
+-----------------------------
+We can specify how many of a selected character group we want to have in the
+password.
+
+We want to have at least 5 lowercase letters in the password:
+
+ >>> pwd = password.HighSecurityPasswordUtility(seed=8)
+ >>> pwd.minLowerLetter = 5
+
+ >>> pwd.verify('FOOBAR123')
+ Traceback (most recent call last):
+ ...
+ TooFewGroupCharacters
+
+ >>> pwd.verify('foobAR123')
+ Traceback (most recent call last):
+ ...
+ TooFewGroupCharacters
+
+ >>> pwd.verify('foobaR123')
+
+ >>> pwd.generate()
+ 'Us;iwbzM[J'
+
+ >>> pwd.generate()
+ 'soXVg[V$uw'
+
+
+We want to have at least 5 uppercase letters in the password:
+
+ >>> pwd = password.HighSecurityPasswordUtility(seed=8)
+ >>> pwd.minUpperLetter = 5
+
+ >>> pwd.verify('foobar123')
+ Traceback (most recent call last):
+ ...
+ TooFewGroupCharacters
+
+ >>> pwd.verify('FOOBar123')
+ Traceback (most recent call last):
+ ...
+ TooFewGroupCharacters
+
+ >>> pwd.verify('fOOBAR123')
+
+ >>> pwd.generate()
+ 'OvMPN3Bi'
+
+ >>> pwd.generate()
+ 'l:zB.VA at MH'
+
+
+We want to have at least 5 digits in the password:
+
+ >>> pwd = password.HighSecurityPasswordUtility(seed=8)
+ >>> pwd.minDigits = 5
+
+ >>> pwd.verify('foobar123')
+ Traceback (most recent call last):
+ ...
+ TooFewGroupCharacters
+
+ >>> pwd.verify('FOOBa1234')
+ Traceback (most recent call last):
+ ...
+ TooFewGroupCharacters
+
+ >>> pwd.verify('fOBA12345')
+
+ >>> pwd.generate()
+ '(526vK(>Z42v'
+
+ >>> pwd.generate()
+ '3Z&Mtq35Y840'
+
+
+We want to have at least 5 specials in the password:
+
+ >>> pwd = password.HighSecurityPasswordUtility(seed=8)
+ >>> pwd.minSpecials = 5
+
+ >>> pwd.verify('foo(bar)')
+ Traceback (most recent call last):
+ ...
+ TooFewGroupCharacters
+
+ >>> pwd.verify('FO.#(Ba1)')
+ Traceback (most recent call last):
+ ...
+ TooFewGroupCharacters
+
+ >>> pwd.verify('fO.,;()5')
+
+ >>> pwd.generate()
+ '?d{*~2q|P'
+
+ >>> pwd.generate()
+ '(8a5\\(^}vB'
+
+We want to have at least 5 others in the password:
+
+ >>> pwd = password.HighSecurityPasswordUtility(seed=8)
+ >>> pwd.minOthers = 5
+
+ >>> pwd.verify('foobar'+unichr(0x0c3)+unichr(0x0c4))
+ Traceback (most recent call last):
+ ...
+ TooFewGroupCharacters
+
+ >>> pwd.verify('foobar'+unichr(0x0c3)+unichr(0x0c4)+unichr(0x0e1))
+ Traceback (most recent call last):
+ ...
+ TooFewGroupCharacters
+
+ >>> pwd.verify('fOO'+unichr(0x0e1)*5)
+
+
+Generating passwords with others not yet supported
+
+ #>>> pwd.generate()
+ #'?d{*~2q|P'
+ #
+ #>>> pwd.generate()
+ #'(8a5\\(^}vB'
+
+We want to have at least 5 different characters in the password:
+
+ >>> pwd = password.HighSecurityPasswordUtility(seed=8)
+ >>> pwd.minUniqueCharacters = 5
+
+ >>> pwd.verify('foofoo1212')
+ Traceback (most recent call last):
+ ...
+ TooFewUniqueCharacters
+
+ >>> pwd.verify('FOOfoo2323')
+ Traceback (most recent call last):
+ ...
+ TooFewUniqueCharacters
+
+ >>> pwd.verify('fOOBAR123')
+
+ >>> pwd.generate()
+ '{l%ix~t8R'
+
+ >>> pwd.generate()
+ 'Us;iwbzM[J'
+
+
+We want to have at least 5 different letters in the password:
+
+ >>> pwd = password.HighSecurityPasswordUtility(seed=8)
+ >>> pwd.minUniqueLetters = 5
+
+ >>> pwd.verify('foofoo1212')
+ Traceback (most recent call last):
+ ...
+ TooFewUniqueLetters
+
+ >>> pwd.verify('FOOBfoob2323')
+ Traceback (most recent call last):
+ ...
+ TooFewUniqueLetters
+
+ >>> pwd.verify('fOOBAR123')
+
+ >>> pwd.generate()
+ '{l%ix~t8R'
+
+ >>> pwd.generate()
+ 'Us;iwbzM[J'
+
+
The Password Field
------------------
The password field can be used to specify an advanced password. It extends the
standard ``zope.schema`` password field with the ``checker`` attribute. The
-checker is either a password utility (as specified above) or the name of sucha
+checker is either a password utility (as specified above) or the name of such a
a utility. The checker is used to verify whether a password is acceptable or
not.
@@ -169,6 +343,8 @@
>>> from zope.app.authentication.password import PlainTextPasswordManager
>>> from z3c.password import field
+ >>> pwd = password.HighSecurityPasswordUtility(seed=8)
+
>>> pwdField = field.Password(
... __name__='password',
... title=u'Password',
@@ -182,3 +358,102 @@
...
TooShortPassword
+Validation must work on bound fields too:
+
+Let's now create a principal:
+
+ >>> from zope.app.authentication import principalfolder
+ >>> from z3c.password import principal
+
+ >>> class MyPrincipal(principal.PrincipalMixIn,
+ ... principalfolder.InternalPrincipal):
+ ... pass
+
+ >>> user = MyPrincipal('srichter', '123123', u'Stephan Richter')
+
+Bind the field:
+
+ >>> bound = pwdField.bind(user)
+
+ >>> bound.validate(u'fooBar12')
+ >>> bound.validate(u'fooBar')
+ Traceback (most recent call last):
+ ...
+ TooShortPassword
+
+Let's create a principal without the PrincipalMixIn:
+
+ >>> user = principalfolder.InternalPrincipal('srichter', '123123',
+ ... u'Stephan Richter')
+
+Bind the field:
+
+ >>> bound = pwdField.bind(user)
+
+ >>> bound.validate(u'fooBar12')
+ >>> bound.validate(u'fooBar')
+ Traceback (most recent call last):
+ ...
+ TooShortPassword
+
+
+Other common usecase is to do a utility and specify it's name as checker.
+
+ >>> import zope.component
+ >>> zope.component.provideUtility(pwd, name='my password checker')
+
+Recreate the field:
+
+ >>> pwdField = field.Password(
+ ... __name__='password',
+ ... title=u'Password',
+ ... checker='my password checker')
+
+Let's validate a value:
+
+ >>> pwdField.validate(u'fooBar12')
+ >>> pwdField.validate(u'fooBar')
+ Traceback (most recent call last):
+ ...
+ TooShortPassword
+
+
+Edge cases.
+
+No checker specified.
+
+ >>> pwdField = field.Password(
+ ... __name__='password',
+ ... title=u'Password')
+
+Validation silently succeeds with a checker:
+
+ >>> pwdField.validate(u'fooBar12')
+ >>> pwdField.validate(u'fooBar')
+
+Bad utility name.
+
+ >>> pwdField = field.Password(
+ ... __name__='password',
+ ... title=u'Password',
+ ... checker='foobar password checker')
+
+Burps on the utility lookup as expected:
+
+ >>> pwdField.validate(u'fooBar12')
+ Traceback (most recent call last):
+ ...
+ ComponentLookupError:...
+
+Bound object does not have the property:
+
+ >>> pwdField = field.Password(
+ ... __name__='foobar',
+ ... title=u'Password',
+ ... checker=pwd)
+
+ >>> bound = pwdField.bind(user)
+
+Validation silently succeeds:
+
+ >>> bound.validate(u'fooBar12')
\ No newline at end of file
Modified: z3c.password/trunk/src/z3c/password/field.py
===================================================================
--- z3c.password/trunk/src/z3c/password/field.py 2009-06-22 07:56:49 UTC (rev 101209)
+++ z3c.password/trunk/src/z3c/password/field.py 2009-06-22 08:25:20 UTC (rev 101210)
@@ -30,7 +30,7 @@
def checker(self):
if self._checker is None:
return None
- if not isinstance(self._checker, (str, unicode)):
+ if not isinstance(self._checker, basestring):
return self._checker
return zope.component.getUtility(
interfaces.IPasswordUtility, self._checker)
@@ -43,4 +43,15 @@
old = self.get(self.context)
except AttributeError:
pass
- self.checker.verify(value, old)
+ checker = self.checker
+ if checker is not None:
+ self.checker.verify(value, old)
+
+ #try to check for disallowPasswordReuse here too, to raise
+ #problems ASAP
+ if self.context is not None:
+ try:
+ self.context._checkDisallowedPreviousPassword(value)
+ except AttributeError:
+ #if _checkDisallowedPreviousPassword is missing
+ pass
Modified: z3c.password/trunk/src/z3c/password/interfaces.py
===================================================================
--- z3c.password/trunk/src/z3c/password/interfaces.py 2009-06-22 07:56:49 UTC (rev 101209)
+++ z3c.password/trunk/src/z3c/password/interfaces.py 2009-06-22 08:25:20 UTC (rev 101210)
@@ -40,6 +40,15 @@
class TooManyGroupCharacters(InvalidPassword):
__doc__ = _('''Password contains too many characters of one group.''')
+class TooFewGroupCharacters(InvalidPassword):
+ __doc__ = _('''Password does not contain enough characters of one group.''')
+
+class TooFewUniqueCharacters(InvalidPassword):
+ __doc__ = _('''Password does not contain enough unique characters.''')
+
+class TooFewUniqueLetters(InvalidPassword):
+ __doc__ = _('''Password does not contain enough unique letters.''')
+
class PasswordExpired(Exception):
__doc__ = _('''The password has expired.''')
@@ -47,6 +56,13 @@
self.principal = principal
Exception.__init__(self, self.__doc__)
+class PreviousPasswordNotAllowed(InvalidPassword):
+ __doc__ = _('''The password set was already used before.''')
+
+ def __init__(self, principal):
+ self.principal = principal
+ Exception.__init__(self, self.__doc__)
+
class TooManyLoginFailures(Exception):
__doc__ = _('''The password was entered incorrectly too often.''')
@@ -117,9 +133,9 @@
@zope.interface.invariant
def minMaxLength(task):
if task.minLength is not None and task.maxLength is not None:
- if task.minLength > task.minLength:
+ if task.minLength > task.maxLength:
raise zope.interface.Invalid(
- u"Minimum length must be greater than the maximum length.")
+ u"Minimum length must not be greater than the maximum length.")
groupMax = zope.schema.Int(
title=_(u'Maximum Characters of Group'),
@@ -135,7 +151,123 @@
required=False,
default=None)
+ minLowerLetter = zope.schema.Int(
+ title=_(u'Minimum Number of Lowercase letters'),
+ description=_(u'The minimum amount of lowercase letters that a '
+ u'password must have.'),
+ required=False,
+ default=None)
+ minUpperLetter = zope.schema.Int(
+ title=_(u'Minimum Number of Uppercase letters'),
+ description=_(u'The minimum amount of uppercase letters that a '
+ u'password must have.'),
+ required=False,
+ default=None)
+
+ minDigits = zope.schema.Int(
+ title=_(u'Minimum Number of Numeric digits'),
+ description=_(u'The minimum amount of numeric digits that a '
+ u'password must have.'),
+ required=False,
+ default=None)
+
+ minSpecials = zope.schema.Int(
+ title=_(u'Minimum Number of Special characters'),
+ description=_(u'The minimum amount of special characters that a '
+ u'password must have.'),
+ required=False,
+ default=None)
+
+ #WARNING! generating a password with Others is not yet supported
+ minOthers = zope.schema.Int(
+ title=_(u'Minimum Number of Other characters'),
+ description=_(u'The minimum amount of other characters that a '
+ u'password must have.'),
+ required=False,
+ default=None)
+
+ @zope.interface.invariant
+ def saneMinimums(task):
+ minl = 0
+ if task.minLowerLetter:
+ if task.minLowerLetter > task.groupMax:
+ raise zope.interface.Invalid(
+ u"Any group minimum length must NOT be greater than "
+ u"the maximum group length.")
+
+ minl += task.minLowerLetter
+ if task.minUpperLetter:
+ if task.minUpperLetter > task.groupMax:
+ raise zope.interface.Invalid(
+ u"Any group minimum length must NOT be greater than "
+ u"the maximum group length.")
+
+ minl += task.minUpperLetter
+ if task.minDigits:
+ if task.minDigits > task.groupMax:
+ raise zope.interface.Invalid(
+ u"Any group minimum length must NOT be greater than "
+ u"the maximum group length.")
+
+ minl += task.minDigits
+ if task.minSpecials:
+ if task.minSpecials > task.groupMax:
+ raise zope.interface.Invalid(
+ u"Any group minimum length must NOT be greater than "
+ u"the maximum group length.")
+
+ minl += task.minSpecials
+ if task.minOthers:
+ if task.minOthers > task.groupMax:
+ raise zope.interface.Invalid(
+ u"Any group minimum length must NOT be greater than "
+ u"the maximum group length.")
+
+ minl += task.minOthers
+
+ if task.maxLength is not None:
+ if minl > task.maxLength:
+ raise zope.interface.Invalid(
+ u"Sum of group minimum lengths must NOT be greater than "
+ u"the maximum password length.")
+
+ minUniqueLetters = zope.schema.Int(
+ title=_(u'Minimum Number of Unique letters'),
+ description=_(u'The minimum amount of unique letters that a '
+ u'password must have. This is against passwords '
+ u'like `aAaA0000`. All characters taken lowercase.'),
+ required=False,
+ default=None)
+
+ @zope.interface.invariant
+ def minUniqueLettersLength(task):
+ if (task.minUniqueLetters is not None
+ and task.minUniqueLetters is not None):
+ if task.minUniqueLetters > task.maxLength:
+ raise zope.interface.Invalid(
+ u"Minimum unique letters number must not be greater than "
+ u"the maximum length.")
+
+ minUniqueCharacters = zope.schema.Int(
+ title=_(u'Minimum Number of Unique characters'),
+ description=_(u'The minimum amount of unique characters that a '
+ u'password must have. This is against passwords '
+ u'like `aAaA0000`. All characters taken lowercase.'),
+ required=False,
+ default=None)
+
+ @zope.interface.invariant
+ def minUniqueCharactersLength(task):
+ if (task.minUniqueCharacters is not None
+ and task.minUniqueCharacters is not None):
+ if task.minUniqueCharacters > task.maxLength:
+ raise zope.interface.Invalid(
+ u"Minimum unique characters length must not be greater than "
+ u"the maximum length.")
+
+
+
class IPasswordOptionsUtility(zope.interface.Interface):
"""Different general security options.
@@ -169,3 +301,9 @@
'password can be provided.'),
required=False,
default=None)
+
+ disallowPasswordReuse = zope.schema.Bool(
+ title=_(u'Disallow Password Reuse'),
+ description=_(u'Do not allow to set a previously set password again.'),
+ required=False,
+ default=False)
Modified: z3c.password/trunk/src/z3c/password/password.py
===================================================================
--- z3c.password/trunk/src/z3c/password/password.py 2009-06-22 07:56:49 UTC (rev 101209)
+++ z3c.password/trunk/src/z3c/password/password.py 2009-06-22 08:25:20 UTC (rev 101210)
@@ -54,6 +54,20 @@
interfaces.IHighSecurityPasswordUtility['groupMax'])
maxSimilarity = FieldProperty(
interfaces.IHighSecurityPasswordUtility['maxSimilarity'])
+ minLowerLetter = FieldProperty(
+ interfaces.IHighSecurityPasswordUtility['minLowerLetter'])
+ minUpperLetter = FieldProperty(
+ interfaces.IHighSecurityPasswordUtility['minUpperLetter'])
+ minDigits = FieldProperty(
+ interfaces.IHighSecurityPasswordUtility['minDigits'])
+ minSpecials = FieldProperty(
+ interfaces.IHighSecurityPasswordUtility['minSpecials'])
+ minOthers = FieldProperty(
+ interfaces.IHighSecurityPasswordUtility['minOthers'])
+ minUniqueCharacters = FieldProperty(
+ interfaces.IHighSecurityPasswordUtility['minUniqueCharacters'])
+ minUniqueLetters = FieldProperty(
+ interfaces.IHighSecurityPasswordUtility['minUniqueLetters'])
LOWERLETTERS = string.letters[:26]
UPPERLETTERS = string.letters[26:]
@@ -65,13 +79,28 @@
u'for more details.')
def __init__(self, minLength=8, maxLength=12, groupMax=6,
- maxSimilarity=0.6, seed=None):
+ maxSimilarity=0.6, seed=None,
+ minLowerLetter=None, minUpperLetter=None, minDigits=None,
+ minSpecials=None, minOthers=None,
+ minUniqueCharacters=None, minUniqueLetters=None):
self.minLength = minLength
self.maxLength = maxLength
self.groupMax = groupMax
self.maxSimilarity = maxSimilarity
self.random = random.Random(seed or time.time())
+ self.minLowerLetter = minLowerLetter
+ self.minUpperLetter = minUpperLetter
+ self.minDigits = minDigits
+ self.minSpecials = minSpecials
+ self.minOthers = minOthers
+ self.minUniqueCharacters = minUniqueCharacters
+ self.minUniqueLetters = minUniqueLetters
+ def _checkSimilarity(self, new, ref):
+ sm = difflib.SequenceMatcher(None, new, ref)
+ if sm.ratio() > self.maxSimilarity:
+ raise interfaces.TooSimilarPassword()
+
def verify(self, new, ref=None):
'''See interfaces.IHighSecurityPasswordUtility'''
# 0. Make sure we got a password.
@@ -85,20 +114,23 @@
# 2. Ensure that the password is sufficiently different to the old
# one.
if ref is not None:
- sm = difflib.SequenceMatcher(None, new, ref)
- if sm.ratio() > self.maxSimilarity:
- raise interfaces.TooSimilarPassword()
+ self._checkSimilarity(new, ref)
# 3. Ensure that the password's character set is complex enough.
num_lower_letters = 0
num_upper_letters = 0
num_digits = 0
num_specials = 0
num_others = 0
+ uniqueChars = set()
+ uniqueLetters = set()
for char in new:
+ uniqueChars.add(char.lower())
if char in self.LOWERLETTERS:
num_lower_letters += 1
+ uniqueLetters.add(char.lower())
elif char in self.UPPERLETTERS:
num_upper_letters += 1
+ uniqueLetters.add(char.lower())
elif char in self.DIGITS:
num_digits += 1
elif char in self.SPECIALS:
@@ -111,6 +143,35 @@
num_specials > self.groupMax or
num_others > self.groupMax):
raise interfaces.TooManyGroupCharacters()
+
+ if (self.minLowerLetter is not None
+ and num_lower_letters < self.minLowerLetter):
+ raise interfaces.TooFewGroupCharacters()
+
+ if (self.minUpperLetter is not None
+ and num_upper_letters < self.minUpperLetter):
+ raise interfaces.TooFewGroupCharacters()
+
+ if (self.minDigits is not None
+ and num_digits < self.minDigits):
+ raise interfaces.TooFewGroupCharacters()
+
+ if (self.minSpecials is not None
+ and num_specials < self.minSpecials):
+ raise interfaces.TooFewGroupCharacters()
+
+ if (self.minOthers is not None
+ and num_others < self.minOthers):
+ raise interfaces.TooFewGroupCharacters()
+
+ if (self.minUniqueCharacters is not None
+ and len(uniqueChars) < self.minUniqueCharacters):
+ raise interfaces.TooFewUniqueCharacters()
+
+ if (self.minUniqueLetters is not None
+ and len(uniqueLetters) < self.minUniqueLetters):
+ raise interfaces.TooFewUniqueLetters()
+
return
def generate(self, ref=None):
@@ -120,11 +181,14 @@
new = ''
# Determine the length of the password
length = self.random.randint(self.minLength, self.maxLength)
+
# Generate the password
chars = self.LOWERLETTERS + self.UPPERLETTERS + \
self.DIGITS + self.SPECIALS
+
for count in xrange(length):
new += self.random.choice(chars)
+
# Verify the new password
try:
self.verify(new, ref)
@@ -133,3 +197,28 @@
else:
verified = True
return new
+
+class PasswordOptionsUtility(object):
+ """An implementation of the security options."""
+ zope.interface.implements(interfaces.IPasswordOptionsUtility)
+
+ changePasswordOnNextLogin = FieldProperty(
+ interfaces.IPasswordOptionsUtility['changePasswordOnNextLogin'])
+ passwordExpiresAfter = FieldProperty(
+ interfaces.IPasswordOptionsUtility['passwordExpiresAfter'])
+ lockOutPeriod = FieldProperty(
+ interfaces.IPasswordOptionsUtility['lockOutPeriod'])
+ maxFailedAttempts = FieldProperty(
+ interfaces.IPasswordOptionsUtility['maxFailedAttempts'])
+ disallowPasswordReuse = FieldProperty(
+ interfaces.IPasswordOptionsUtility['disallowPasswordReuse'])
+
+ def __init__(self, changePasswordOnNextLogin=None,
+ passwordExpiresAfter=None,
+ lockOutPeriod=None, maxFailedAttempts=None,
+ disallowPasswordReuse=None):
+ self.changePasswordOnNextLogin = changePasswordOnNextLogin
+ self.passwordExpiresAfter = passwordExpiresAfter
+ self.lockOutPeriod = lockOutPeriod
+ self.maxFailedAttempts = maxFailedAttempts
+ self.disallowPasswordReuse = disallowPasswordReuse
\ No newline at end of file
Modified: z3c.password/trunk/src/z3c/password/principal.py
===================================================================
--- z3c.password/trunk/src/z3c/password/principal.py 2009-06-22 07:56:49 UTC (rev 101209)
+++ z3c.password/trunk/src/z3c/password/principal.py 2009-06-22 08:25:20 UTC (rev 101210)
@@ -17,6 +17,7 @@
"""
__docformat__ = "reStructuredText"
import datetime
+import persistent.list
import zope.component
from z3c.password import interfaces
@@ -34,16 +35,40 @@
lastFailedAttempt = None
lockOutPeriod = None
+ disallowPasswordReuse = None
+ previousPasswords = None
+
+ def _checkDisallowedPreviousPassword(self, password):
+ if self._disallowPasswordReuse():
+ if self.previousPasswords is not None:
+ #hack, but this should work with zope.app.authentication and
+ #z3c.authenticator
+ passwordManager = self._getPasswordManager()
+
+ for pwd in self.previousPasswords:
+ if passwordManager.checkPassword(pwd, password):
+ raise interfaces.PreviousPasswordNotAllowed(self)
+
def getPassword(self):
return super(PrincipalMixIn, self).getPassword()
def setPassword(self, password, passwordManagerName=None):
+ self._checkDisallowedPreviousPassword(password)
+
super(PrincipalMixIn, self).setPassword(password, passwordManagerName)
+
+ if self._disallowPasswordReuse():
+ if self.previousPasswords is None:
+ self.previousPasswords = persistent.list.PersistentList()
+
+ self.previousPasswords.append(self.password)
+
self.passwordSetOn = self.now()
self.failedAttempts = 0
self.lastFailedAttempt = None
self.passwordExpired = False
+
password = property(getPassword, setPassword)
def now(self):
@@ -175,4 +200,17 @@
if options.maxFailedAttempts is not None:
return options.maxFailedAttempts
else:
- return self.maxFailedAttempts
\ No newline at end of file
+ return self.maxFailedAttempts
+
+ def _disallowPasswordReuse(self):
+ if self.disallowPasswordReuse is not None:
+ return self.disallowPasswordReuse
+
+ options = self._optionsUtility()
+ if options is None:
+ return self.disallowPasswordReuse
+ else:
+ if options.disallowPasswordReuse is not None:
+ return options.disallowPasswordReuse
+ else:
+ return self.disallowPasswordReuse
\ No newline at end of file
Modified: z3c.password/trunk/src/z3c/password/principal.txt
===================================================================
--- z3c.password/trunk/src/z3c/password/principal.txt 2009-06-22 07:56:49 UTC (rev 101209)
+++ z3c.password/trunk/src/z3c/password/principal.txt 2009-06-22 08:25:20 UTC (rev 101210)
@@ -47,8 +47,17 @@
A time delta object after the user can try again after too many login
failures.
- If ``None`` login will enabled by a correct password.
+- ``disallowPasswordReuse``
+
+ Do not allow setting a password again that was used anytime before.
+ Set to True to enable.
+
+- ``previousPasswords``
+
+ Previous (encoded) password stored when required for
+ ``disallowPasswordReuse``
+
There is the IPasswordOptionsUtility utility, with which you can provide
options for some features.
Strategy is that if the same option/property exists on the principal
@@ -71,7 +80,6 @@
Number of minutes (integer!) after the user can try again after too many login
failures.
- If ``None`` login will enabled by a correct password.
- ``maxFailedAttempts``
@@ -79,7 +87,12 @@
password before the password is locked and no new password can be provided.
(or lockOutPeriod kicks in)
+- ``disallowPasswordReuse``
+ Do not allow setting a password again that was used anytime before.
+ Set to True to enable.
+
+
Let's now create a principal:
>>> from zope.app.authentication import principalfolder
@@ -200,14 +213,7 @@
>>> import zope.interface
>>> import zope.component
>>> from z3c.password import interfaces
- >>> class PasswordOptionsUtility(object):
- ... zope.interface.implements(interfaces.IPasswordOptionsUtility)
- ...
- ... changePasswordOnNextLogin = False
- ... passwordExpiresAfter = None
- ... lockOutPeriod = None
- ... maxFailedAttempts = None
-
+ >>> from z3c.password.password import PasswordOptionsUtility
>>> poptions = PasswordOptionsUtility()
>>> zope.component.provideUtility(poptions)
@@ -590,7 +596,67 @@
1
-Edge case.
+``disallowPasswordReuse``
+-------------------------
+
+Set this option to True to disallow setting a password that was used anytime
+before.
+
+ >>> user = MyPrincipal('srichter', '123123', u'Stephan Richter')
+
+Set ``disallowPasswordReuse``:
+(passwords will be stored from this point on, so the first '123123' NOT)
+
+ >>> poptions.disallowPasswordReuse = True
+
+ >>> user.setPassword('234234')
+ >>> user.setPassword('345345')
+ >>> user.setPassword('456456')
+ >>> user.setPassword('123123')
+
+Setting a used password again holds an exception:
+
+ >>> user.setPassword('234234')
+ Traceback (most recent call last):
+ ...
+ PreviousPasswordNotAllowed: The password set was already used before.
+
+Something else works:
+
+ >>> user.setPassword('789789')
+
+ >>> poptions.disallowPasswordReuse = False
+
+Same works when option is set on the user:
+
+ >>> user = MyPrincipal('srichter', '123123', u'Stephan Richter')
+
+Set ``disallowPasswordReuse``:
+(passwords will be stored from this point on, so the first '123123' NOT)
+
+ >>> user.disallowPasswordReuse = True
+
+ >>> user.setPassword('234234')
+ >>> user.setPassword('345345')
+ >>> user.setPassword('456456')
+ >>> user.setPassword('123123')
+
+Setting a used password again holds an exception:
+
+ >>> user.setPassword('234234')
+ Traceback (most recent call last):
+ ...
+ PreviousPasswordNotAllowed: The password set was already used before.
+
+Something else works:
+
+ >>> user.setPassword('789789')
+
+
+
+Edge cases
+----------
+
When there is no ``maxFailedAttempts`` set, we can bang on with a bad password
forever.
@@ -606,6 +672,32 @@
... print 'bug'
+
+``failedAttempts`` needs to be reset on a successful check:
+
+ >>> user = MyPrincipal('srichter', '123123', u'Stephan Richter')
+ >>> user.checkPassword('456456')
+ False
+
+``failedAttempts`` gets set on a bad check:
+
+ >>> user.failedAttempts
+ 1
+ >>> user.lastFailedAttempt
+ datetime.datetime(2009, 6, 14, 17, 15)
+
+ >>> user.checkPassword('123123')
+ True
+
+Gets reset on a successful check:
+
+ >>> user.failedAttempts
+ 0
+ >>> user.lastFailedAttempt is None
+ True
+
+
+
Coverage happiness
------------------
More information about the Checkins
mailing list