diff options
| -rw-r--r-- | src/mailman/config/schema.cfg | 4 | ||||
| -rw-r--r-- | src/mailman/model/docs/users.txt | 4 | ||||
| -rw-r--r-- | src/mailman/utilities/passwords.py | 38 | ||||
| -rw-r--r-- | src/mailman/utilities/tests/test_passwords.py | 35 |
4 files changed, 77 insertions, 4 deletions
diff --git a/src/mailman/config/schema.cfg b/src/mailman/config/schema.cfg index 09f575459..8175ac4a6 100644 --- a/src/mailman/config/schema.cfg +++ b/src/mailman/config/schema.cfg @@ -136,8 +136,8 @@ recipient: [passwords] -# When Mailman generates them, this is the default length of member passwords. -member_password_length: 8 +# When Mailman generates them, this is the default length of passwords. +password_length: 8 # Specify the type of passwords to use, when Mailman generates the passwords # itself, as would be the case for membership requests where the user did not diff --git a/src/mailman/model/docs/users.txt b/src/mailman/model/docs/users.txt index 29d8601cd..31cc58918 100644 --- a/src/mailman/model/docs/users.txt +++ b/src/mailman/model/docs/users.txt @@ -3,8 +3,8 @@ Users ===== Users are entities that represent people. A user has a real name and a -password. Optionally a user may have some preferences and a set of addresses -they control. A user also knows which mailing lists they are subscribed to. +optional encoded password. A user may also have an optional preferences and a +set of addresses they control. See `usermanager.txt`_ for examples of how to create, delete, and find users. diff --git a/src/mailman/utilities/passwords.py b/src/mailman/utilities/passwords.py index c14584748..0de4255da 100644 --- a/src/mailman/utilities/passwords.py +++ b/src/mailman/utilities/passwords.py @@ -27,23 +27,29 @@ __all__ = [ 'Schemes', 'check_response', 'make_secret', + 'make_user_friendly_password', ] import os import re import hmac +import random import hashlib from array import array from base64 import urlsafe_b64decode as decode from base64 import urlsafe_b64encode as encode from flufl.enum import Enum +from itertools import chain, product +from string import ascii_lowercase +from mailman.config import config from mailman.core import errors SALT_LENGTH = 20 # bytes ITERATIONS = 2000 +EMPTYSTRING = '' @@ -315,3 +321,35 @@ def lookup_scheme(scheme_name): :rtype: `PasswordScheme` """ return _SCHEMES_BY_TAG.get(scheme_name.lower()) + + + +# Password generation. + +_vowels = tuple('aeiou') +_consonants = tuple(c for c in ascii_lowercase if c not in _vowels) +_syllables = tuple(x + y for (x, y) in + chain(product(_vowels, _consonants), + product(_consonants, _vowels))) + + +def make_user_friendly_password(length=None): + """Make a random *user friendly* password. + + Such passwords are nominally easier to pronounce and thus remember. Their + security in relationship to purely random passwords has not been + determined. + + :param length: Minimum length in characters for the resulting password. + The password will always be an even number of characters. When + omitted, the system default length will be used. + :type length: int + :return: The user friendly password. + :rtype: unicode + """ + if length is None: + length = int(config.passwords.password_length) + syllables = [] + while len(syllables) * 2 < length: + syllables.append(random.choice(_syllables)) + return EMPTYSTRING.join(syllables)[:length] diff --git a/src/mailman/utilities/tests/test_passwords.py b/src/mailman/utilities/tests/test_passwords.py index 60c201a5a..7b6989779 100644 --- a/src/mailman/utilities/tests/test_passwords.py +++ b/src/mailman/utilities/tests/test_passwords.py @@ -27,7 +27,11 @@ __all__ = [ import unittest +from itertools import izip_longest + +from mailman.config import config from mailman.core import errors +from mailman.testing.layers import ConfigLayer from mailman.utilities import passwords @@ -138,6 +142,36 @@ class TestSchemeLookup(unittest.TestCase): +# See itertools doc page examples. +def _grouper(seq): + args = [iter(seq)] * 2 + return list(izip_longest(*args)) + + +class TestPasswordGeneration(unittest.TestCase): + layer = ConfigLayer + + def test_default_user_friendly_password_length(self): + self.assertEqual(len(passwords.make_user_friendly_password()), + int(config.passwords.password_length)) + + def test_provided_user_friendly_password_length(self): + self.assertEqual(len(passwords.make_user_friendly_password(12)), 12) + + def test_provided_odd_user_friendly_password_length(self): + self.assertEqual(len(passwords.make_user_friendly_password(15)), 15) + + def test_user_friendly_password(self): + password = passwords.make_user_friendly_password() + for pair in _grouper(password): + # There will always be one vowel and one non-vowel. + vowel = (pair[0] if pair[0] in 'aeiou' else pair[1]) + consonant = (pair[0] if pair[0] not in 'aeiou' else pair[1]) + self.assertTrue(vowel in 'aeiou', vowel) + self.assertTrue(consonant not in 'aeiou', consonant) + + + def test_suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(TestBogusPasswords)) @@ -147,4 +181,5 @@ def test_suite(): suite.addTest(unittest.makeSuite(TestSSHAPasswords)) suite.addTest(unittest.makeSuite(TestPBKDF2Passwords)) suite.addTest(unittest.makeSuite(TestSchemeLookup)) + suite.addTest(unittest.makeSuite(TestPasswordGeneration)) return suite |
