[Checkins] SVN: z3c.password/ Import new password utility that can be used to implement high-security

Stephan Richter srichter at cosmos.phy.tufts.edu
Tue Oct 17 14:08:13 EDT 2006


Log message for revision 70759:
  Import new password utility that can be used to implement high-security 
  passwords. We use this for a bank we are working with.
  

Changed:
  A   z3c.password/
  A   z3c.password/branches/
  A   z3c.password/tags/
  A   z3c.password/trunk/
  A   z3c.password/trunk/src/
  A   z3c.password/trunk/src/__init__.py
  A   z3c.password/trunk/src/z3c/
  A   z3c.password/trunk/src/z3c/__init__.py
  A   z3c.password/trunk/src/z3c/password/
  A   z3c.password/trunk/src/z3c/password/DEPENDENCIES.txt
  A   z3c.password/trunk/src/z3c/password/README.txt
  A   z3c.password/trunk/src/z3c/password/__init__.py
  A   z3c.password/trunk/src/z3c/password/field.py
  A   z3c.password/trunk/src/z3c/password/interfaces.py
  A   z3c.password/trunk/src/z3c/password/password.py
  A   z3c.password/trunk/src/z3c/password/principal.py
  A   z3c.password/trunk/src/z3c/password/tests.py

-=-
Added: z3c.password/trunk/src/__init__.py
===================================================================
--- z3c.password/trunk/src/__init__.py	2006-10-17 17:04:10 UTC (rev 70758)
+++ z3c.password/trunk/src/__init__.py	2006-10-17 18:08:12 UTC (rev 70759)
@@ -0,0 +1 @@
+# Make a package.


Property changes on: z3c.password/trunk/src/__init__.py
___________________________________________________________________
Name: svn:keywords
   + Id

Added: z3c.password/trunk/src/z3c/__init__.py
===================================================================
--- z3c.password/trunk/src/z3c/__init__.py	2006-10-17 17:04:10 UTC (rev 70758)
+++ z3c.password/trunk/src/z3c/__init__.py	2006-10-17 18:08:12 UTC (rev 70759)
@@ -0,0 +1 @@
+# Make a package.


Property changes on: z3c.password/trunk/src/z3c/__init__.py
___________________________________________________________________
Name: svn:keywords
   + Id

Added: z3c.password/trunk/src/z3c/password/DEPENDENCIES.txt
===================================================================
--- z3c.password/trunk/src/z3c/password/DEPENDENCIES.txt	2006-10-17 17:04:10 UTC (rev 70758)
+++ z3c.password/trunk/src/z3c/password/DEPENDENCIES.txt	2006-10-17 18:08:12 UTC (rev 70759)
@@ -0,0 +1 @@
+zope.app.authentication


Property changes on: z3c.password/trunk/src/z3c/password/DEPENDENCIES.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: z3c.password/trunk/src/z3c/password/README.txt
===================================================================
--- z3c.password/trunk/src/z3c/password/README.txt	2006-10-17 17:04:10 UTC (rev 70758)
+++ z3c.password/trunk/src/z3c/password/README.txt	2006-10-17 18:08:12 UTC (rev 70759)
@@ -0,0 +1,283 @@
+============================
+Advnaced Password Management
+============================
+
+This package provides an API and implementation of a password generation and
+verification utility. A high-security implementation is provided that is
+suitable for banks and other high-security institutions. The package also
+offers a field and a property for those fields.
+
+The Password Utility
+--------------------
+
+The password utilities are located in the ``password`` module.
+
+  >>> from z3c.password import password
+
+The module provides a trivial sample and a high-security implementation of the
+utility. The password utility provides two methods.
+
+  >>> pwd = password.TrivialPasswordUtility()
+
+The first is to verify the password. The first argument is the new password
+and the second, optional argument a reference password, usually the old
+one. When the verification fails an ``InvalidPassword`` exception is raised,
+otherwise the method simply returns. In the case of the trivial password
+utility, this method always returns:
+
+  >>> pwd.verify('foo')
+  >>> pwd.verify('foobar', 'foo')
+
+The second method generates a password conform to the security constraints of
+the password utility. The trivial password utility always returns the password
+"trivial".
+
+  >>> pwd.generate()
+  'trivial'
+  >>> pwd.generate('foo')
+  'trivial'
+
+The ``generate()`` method also accepts the optional reference
+password. Finally, each password utility must provide a description explaining
+its security constraints:
+
+  >>> print pwd.description
+  All passwords are accepted and always the "trivial" password is generated.
+
+Let's now look at the high-security password utility. In its constructor you
+can specify several constraints; the minimum and maximum length of the
+password, the maximum of characters of one group (lower letters, upper
+letters, digits, punctuation, other), and the maximum similarity score.
+
+  >>> pwd = password.HighSecurityPasswordUtility()
+  >>> pwd.minLength
+  8
+  >>> pwd.maxLength
+  12
+  >>> pwd.groupMax
+  6
+  >>> pwd.maxSimilarity
+  0.59999999999999998
+
+- When the password is empty, then the password is invalid:
+
+  >>> pwd.verify(None)
+  Traceback (most recent call last):
+  ...
+  InvalidPassword: No new password specified.
+
+  >>> pwd.verify('')
+  Traceback (most recent call last):
+  ...
+  InvalidPassword: No new password specified.
+
+  >>> pwd.verify('', 'other')
+  Traceback (most recent call last):
+  ...
+  InvalidPassword: No new password specified.
+
+- Next, it is verified that the password has the correct length:
+
+  >>> pwd.verify('foo')
+  Traceback (most recent call last):
+  ...
+  InvalidPassword: New password is too long or too short.
+
+  >>> pwd.verify('foobar-foobar')
+  Traceback (most recent call last):
+  ...
+  InvalidPassword: New password is too long or too short.
+
+  >>> pwd.verify('fooBar12')
+
+- Once the length is verified, the password is checked for similarity. If no
+  reference password is provided, then this check always passes:
+
+  >>> pwd.verify('fooBar12')
+
+  >>> pwd.verify('fooBar12', 'fooBAR--')
+
+  >>> pwd.verify('fooBar12', 'foobar12')
+  Traceback (most recent call last):
+  ...
+  InvalidPassword: New password too similar to old one.
+
+- The final check ensures that the password does not have too many characters
+  of one group. The groups are: lower letters, upper letters, digits,
+  punctuation, and others.
+
+  >>> pwd.verify('fooBarBlah')
+  Traceback (most recent call last):
+  ...
+  InvalidPassword: New password contains too many characters of one group.
+
+  >>> pwd.verify('FOOBARBlah')
+  Traceback (most recent call last):
+  ...
+  InvalidPassword: New password contains too many characters of one group.
+
+  >>> pwd.verify('12345678')
+  Traceback (most recent call last):
+  ...
+  InvalidPassword: New password contains too many characters of one group.
+
+  >>> pwd.verify('........')
+  Traceback (most recent call last):
+  ...
+  InvalidPassword: New password contains too many characters of one group.
+
+Let's now verify a list of password that were provided by a bank:
+
+  >>> for new in ('K7PzX2JZ', 'DznMLIww', 'ks59Ursq', 'YUcsuIrQ', 'bPEUFGSa',
+  ...             'lUmtG0TP', 'ISfUKoTe', 'NKGY0aIJ', 'XyUuSHX4', 'CaFE1R5p'):
+  ...     pwd.verify(new)
+
+Let's now generate some passwords. To make them predictable, we specify a seed
+when initializing the utility:
+
+  >>> pwd = password.HighSecurityPasswordUtility(seed=8)
+
+  >>> 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
+a utility. The checker is used to verify whether a password is acceptable or
+not.
+
+Let's now create the field:
+
+  >>> import datetime
+  >>> from zope.app.authentication.password import PlainTextPasswordManager
+  >>> from z3c.password import field
+
+  >>> pwdField = field.Password(
+  ...     __name__='password',
+  ...     title=u'Password',
+  ...     checker=pwd)
+
+Let's validate a value:
+
+  >>> pwdField.validate(u'fooBar12')
+  >>> pwdField.validate(u'fooBar')
+  Traceback (most recent call last):
+  ...
+  InvalidPassword: New password is too long or too short.
+
+
+The Principal Mix-in
+--------------------
+
+The principal mixin is a quick and functional example on how to use the
+password utility and field. The mix-in class defines the following additional
+attributes:
+
+
+- ``passwordExpiresAfter``
+
+  A time delta object that describes for how long the password is valid before
+  a new one has to be specified. If ``None``, the password will never expire.
+
+- ``maxFailedAttempts``
+
+  An integer specifying the amount of failed attempts allowed to check the
+  password before the password is locked and no new password can be provided.
+
+- ``passwordSetOn``
+
+  The date/time at which the password was last set. This value is used to
+  determine the expiration of a password.
+
+- ``failedAttempts``
+
+  This is a counter that keeps track of the amount of failed login attempts
+  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.
+
+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')
+
+Since the password has been immediately set, the ```passwordSetOn`` attribute
+should have a value:
+
+  >>> user.passwordSetOn
+  datetime.datetime(...)
+
+Initially, the amount of failed attempts is zero, ...
+
+  >>> user.failedAttempts
+  0
+
+but after checking the password incorrectly, the value is updated:
+
+  >>> user.checkPassword('456456')
+  False
+  >>> user.failedAttempts
+  1
+
+Initially there is no constraint on user, but let's add some:
+
+  >>> user.passwordExpiresAfter
+  >>> user.passwordExpiresAfter = datetime.timedelta(180)
+
+  >>> user.maxFailedAttempts
+  >>> user.maxFailedAttempts = 3
+
+Let's now provide the incorrect password a couple more times:
+
+  >>> user.checkPassword('456456')
+  False
+  >>> user.checkPassword('456456')
+  False
+  >>> user.checkPassword('456456')
+  Traceback (most recent call last):
+  ...
+  TooManyLoginFailures
+
+As you can see, once the maximum mount of attempts is reached, the system does
+not allow you to log in at all anymore. At this point the password has to be
+reset otherwise. However, you can tell the ``check()`` method explicitly to
+ignore the failure count:
+
+  >>> user.checkPassword('456456', ignoreFailures=True)
+  False
+
+Let's now reset the failure count.
+
+  >>> user.failedAttempts = 0
+
+Next we expire password:
+
+  >>> user.passwordSetOn = datetime.datetime.now() + datetime.timedelta(-181)
+
+A corresponding exception should be raised:
+
+  >>> user.checkPassword('456456')
+  Traceback (most recent call last):
+  ...
+  PasswordExpired
+
+Like for the too-many-failures exception above, you can explicitely turn off
+the expiration check:
+
+  >>> user.checkPassword('456456', ignoreExpiration=True)
+  False
+
+It is the responsibility of the presentation code to provide views for those
+two exceptions. For the latter, it is common to allow the user to enter a new
+password after providing the old one as verification.


Property changes on: z3c.password/trunk/src/z3c/password/README.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: z3c.password/trunk/src/z3c/password/__init__.py
===================================================================
--- z3c.password/trunk/src/z3c/password/__init__.py	2006-10-17 17:04:10 UTC (rev 70758)
+++ z3c.password/trunk/src/z3c/password/__init__.py	2006-10-17 18:08:12 UTC (rev 70759)
@@ -0,0 +1 @@
+# Make a package.


Property changes on: z3c.password/trunk/src/z3c/password/__init__.py
___________________________________________________________________
Name: svn:keywords
   + Id

Added: z3c.password/trunk/src/z3c/password/field.py
===================================================================
--- z3c.password/trunk/src/z3c/password/field.py	2006-10-17 17:04:10 UTC (rev 70758)
+++ z3c.password/trunk/src/z3c/password/field.py	2006-10-17 18:08:12 UTC (rev 70759)
@@ -0,0 +1,43 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Password Field Implementation
+
+$Id$
+"""
+__docformat__ = "reStructuredText"
+import zope.component
+import zope.schema
+from z3c.password import interfaces
+
+class Password(zope.schema.Password):
+
+    def __init__(self, checker=None, **kw):
+        self._checker = checker
+        super(Password, self).__init__(**kw)
+
+    @property
+    def checker(self):
+        if self._checker is None:
+            return None
+        if not isinstance(self._checker, (str, unicode)):
+            return self._checker
+        return zope.component.getUtility(
+            interfaces.IPasswordUtility, self._checker)
+
+    def validate(self, value):
+        super(Password, self).validate(value)
+        old = None
+        if self.context is not None:
+            old = self.get(self.context)
+        self.checker.verify(value, old)


Property changes on: z3c.password/trunk/src/z3c/password/field.py
___________________________________________________________________
Name: svn:keywords
   + Id

Added: z3c.password/trunk/src/z3c/password/interfaces.py
===================================================================
--- z3c.password/trunk/src/z3c/password/interfaces.py	2006-10-17 17:04:10 UTC (rev 70758)
+++ z3c.password/trunk/src/z3c/password/interfaces.py	2006-10-17 18:08:12 UTC (rev 70759)
@@ -0,0 +1,120 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Password Utility Interfaces
+
+$Id$
+"""
+__docformat__ = "reStructuredText"
+import zope.interface
+import zope.schema
+from zope.exceptions.interfaces import UserError
+
+class InvalidPassword(zope.schema.ValidationError):
+    """Invalid Password"""
+
+    def __init__(self, reason):
+        self.reason = reason
+        Exception.__init__(self)
+
+    def __str__(self):
+        return self.reason.encode('utf-8')
+
+
+class PasswordExpired(UserError):
+    """The password has expired."""
+
+    def __init__(self, principal):
+        self.principal = principal
+        UserError.__init__(self)
+
+
+class TooManyLoginFailures(UserError):
+    """The password was entered incorrectly too often."""
+
+    def __init__(self, principal):
+        self.principal = principal
+        UserError.__init__(self)
+
+
+class IPasswordUtility(zope.interface.Interface):
+    """Component to verify and generate passwords.
+
+    The purpose of this utility is to make common password-related tasks, such
+    as verification and creation simple. However, only the collection of those
+    utilites provide an overall net worth.
+    """
+
+    description = zope.schema.Text(
+        title=u'Description',
+        description=u'A description of the password utility.',
+        required=False)
+
+    def verify(new, ref=None):
+        """Check whether the new password is valid.
+
+        When a passward is good, the method simply returns, otherwise an
+        ``InvalidPassword`` exception is raised.
+
+        It is up to the implementation to define the semantics of a valid
+        password. The sematics should ideally be described in the description.
+
+        The ``ref`` argument is a reference password. In many scenarios it
+        will be the old password, so that the method can ensure sufficient
+        dissimilarity between the new and old password.
+        """
+
+    def generate(ref=None):
+        """Generate a valid password.
+
+        The ``ref`` argument is a reference password. In many scenarios it
+        will be the old password, so that the method can ensure sufficient
+        dissimilarity between the new and old password.
+        """
+
+
+class IHighSecurityPasswordUtility(IPasswordUtility):
+    """A password utility for very secure passwords."""
+
+    minLength = zope.schema.Int(
+        title=u'Minimum Length',
+        description=u'The minimum length of the password.',
+        required=False,
+        default=None)
+
+    maxLength = zope.schema.Int(
+        title=u'Maximum Length',
+        description=u'The maximum length of the password.',
+        required=False,
+        default=None)
+
+    @zope.interface.invariant
+    def minMaxLength(task):
+        if task.minLength is not None and task.maxLength is not None:
+            if task.minLength > task.minLength:
+                raise zope.interface.Invalid(
+                    u"Minimum lnegth must be greater than the maximum length.")
+
+    groupMax = zope.schema.Int(
+        title=u'Maximum Characters of Group',
+        description=u'The maximum amount of characters that a password can '
+                    u'have from one group. The groups are: digits, letters, '
+                    u'punctuation.',
+        required=False,
+        default=None)
+
+    maxSimilarity = zope.schema.Float(
+        title=u'Old/New Similarity',
+        description=u'',
+        required=False,
+        default=None)


Property changes on: z3c.password/trunk/src/z3c/password/interfaces.py
___________________________________________________________________
Name: svn:keywords
   + Id

Added: z3c.password/trunk/src/z3c/password/password.py
===================================================================
--- z3c.password/trunk/src/z3c/password/password.py	2006-10-17 17:04:10 UTC (rev 70758)
+++ z3c.password/trunk/src/z3c/password/password.py	2006-10-17 18:08:12 UTC (rev 70759)
@@ -0,0 +1,136 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Password Utility Implementation
+
+$Id$
+"""
+__docformat__ = "reStructuredText"
+import difflib
+import random
+import string
+import time
+import zope.interface
+from zope.schema.fieldproperty import FieldProperty
+
+from z3c.password import interfaces
+
+
+class TrivialPasswordUtility(object):
+    """A trivial password utility."""
+    zope.interface.implements(interfaces.IPasswordUtility)
+
+    description = (u'All passwords are accepted and always the "trivial" '
+                   u'password is generated.')
+
+    def verify(self, new, ref=None):
+        '''See interfaces.IPasswordUtility'''
+        return
+
+    def generate(self, ref=None):
+        '''See interfaces.IPasswordUtility'''
+        return 'trivial'
+
+
+class HighSecurityPasswordUtility(object):
+    """An implementation of the high-security password API."""
+    zope.interface.implements(interfaces.IHighSecurityPasswordUtility)
+
+    minLength = FieldProperty(
+        interfaces.IHighSecurityPasswordUtility['minLength'])
+    maxLength = FieldProperty(
+        interfaces.IHighSecurityPasswordUtility['maxLength'])
+    groupMax = FieldProperty(
+        interfaces.IHighSecurityPasswordUtility['groupMax'])
+    maxSimilarity = FieldProperty(
+        interfaces.IHighSecurityPasswordUtility['maxSimilarity'])
+
+    LOWERLETTERS = string.letters[:26]
+    UPPERLETTERS = string.letters[26:]
+    DIGITS = string.digits
+    SPECIALS = string.punctuation
+
+    description = (u'Passwords generated and verified by this utility conform '
+                   u'strictly to the specified parameters. See the interface '
+                   u'for more details.')
+
+    def __init__(self, minLength=8, maxLength=12, groupMax=6,
+                 maxSimilarity=0.6, seed=None):
+        self.minLength = minLength
+        self.maxLength = maxLength
+        self.groupMax = groupMax
+        self.maxSimilarity = maxSimilarity
+        self.random = random.Random(seed or time.time())
+
+    def verify(self, new, ref=None):
+        '''See interfaces.IHighSecurityPasswordUtility'''
+        # 0. Make sure we got a password.
+        if not new:
+            raise interfaces.InvalidPassword(u'No new password specified.')
+        # 1. Make sure the password has the right length.
+        if len(new) < self.minLength or len(new) > self.maxLength:
+            raise interfaces.InvalidPassword(
+                u'New password is too long or too short.')
+        # 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.InvalidPassword(
+                    u'New password too similar to old one.')
+        # 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
+        for char in new:
+            if char in self.LOWERLETTERS:
+                num_lower_letters += 1
+            elif char in self.UPPERLETTERS:
+                num_upper_letters += 1
+            elif char in self.DIGITS:
+                num_digits += 1
+            elif char in self.SPECIALS:
+                num_specials += 1
+            else:
+                num_others += 1
+        if (num_lower_letters > self.groupMax or
+            num_upper_letters > self.groupMax or
+            num_digits > self.groupMax or
+            num_specials > self.groupMax or
+            num_others > self.groupMax):
+            raise interfaces.InvalidPassword(
+                u'New password contains too many characters of one group.')
+        return
+
+    def generate(self, ref=None):
+        '''See interfaces.IHighSecurityPasswordUtility'''
+        verified = False
+        while not verified:
+            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)
+            except interfaces.InvalidPassword:
+                verified = False
+            else:
+                verified = True
+        return new


Property changes on: z3c.password/trunk/src/z3c/password/password.py
___________________________________________________________________
Name: svn:keywords
   + Id

Added: z3c.password/trunk/src/z3c/password/principal.py
===================================================================
--- z3c.password/trunk/src/z3c/password/principal.py	2006-10-17 17:04:10 UTC (rev 70758)
+++ z3c.password/trunk/src/z3c/password/principal.py	2006-10-17 18:08:12 UTC (rev 70759)
@@ -0,0 +1,60 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Principal MixIn for Advnaced Password Management
+
+$Id$
+"""
+__docformat__ = "reStructuredText"
+import datetime
+from z3c.password import interfaces
+
+class PrincipalMixIn(object):
+    """A Principal Mixin class for ``zope.app.principalfolder``'s internal
+    principal."""
+
+    passwordExpiresAfter = None
+    maxFailedAttempts = None
+
+    passwordSetOn = None
+    failedAttempts = 0
+
+    def getPassword(self):
+        return super(PrincipalMixIn, self).getPassword()
+
+    def setPassword(self, password, passwordManagerName=None):
+        super(PrincipalMixIn, self).setPassword(password, passwordManagerName)
+        self.passwordSetOn = datetime.datetime.now()
+
+    password = property(getPassword, setPassword)
+
+    def checkPassword(self, pwd, ignoreExpiration=False, ignoreFailures=False):
+        # Make sure the password has not been expired
+        if not ignoreExpiration and self.passwordExpiresAfter is not None:
+            expirationDate = self.passwordSetOn + self.passwordExpiresAfter
+            if expirationDate < datetime.datetime.now():
+                raise interfaces.PasswordExpired(self)
+        # Check the password
+        same = super(PrincipalMixIn, self).checkPassword(pwd)
+        # If this was a failed attempt, record it, otherwise reset the failures
+        if same and self.failedAttempts != 0:
+            self.failedAttempts = 0
+        if not same:
+            self.failedAttempts += 1
+        # If the maximum amount of failures has been reached notify the system
+        # by sending an event and then raising an error.
+        if not ignoreFailures and self.maxFailedAttempts is not None:
+            if (self.maxFailedAttempts and
+                self.failedAttempts > self.maxFailedAttempts):
+                raise interfaces.TooManyLoginFailures(self)
+        return same


Property changes on: z3c.password/trunk/src/z3c/password/principal.py
___________________________________________________________________
Name: svn:keywords
   + Id

Added: z3c.password/trunk/src/z3c/password/tests.py
===================================================================
--- z3c.password/trunk/src/z3c/password/tests.py	2006-10-17 17:04:10 UTC (rev 70758)
+++ z3c.password/trunk/src/z3c/password/tests.py	2006-10-17 18:08:12 UTC (rev 70759)
@@ -0,0 +1,44 @@
+##############################################################################
+#
+# Copyright (c) 2006 Lovely Systems and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Test Setup
+
+$Id$
+"""
+__docformat__ = "reStructuredText"
+
+import doctest
+import unittest
+import zope.component
+from zope.testing.doctestunit import DocFileSuite
+from zope.app.authentication import password
+from zope.app.testing import placelesssetup
+
+def setUp(test):
+    placelesssetup.setUp(test)
+    zope.component.provideUtility(
+        password.PlainTextPasswordManager(), name='Plain Text')
+
+def tearDown(test):
+    placelesssetup.tearDown(test)
+
+def test_suite():
+    return unittest.TestSuite((
+        DocFileSuite('README.txt',
+                     setUp=setUp, tearDown=tearDown,
+                     optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,
+                     ),
+        ))
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')


Property changes on: z3c.password/trunk/src/z3c/password/tests.py
___________________________________________________________________
Name: svn:keywords
   + Id



More information about the Checkins mailing list