summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/mailman/config/schema.cfg4
-rw-r--r--src/mailman/model/docs/users.txt4
-rw-r--r--src/mailman/utilities/passwords.py38
-rw-r--r--src/mailman/utilities/tests/test_passwords.py35
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