summaryrefslogtreecommitdiff
path: root/src/mailman/utilities/passwords.py
diff options
context:
space:
mode:
authorBarry Warsaw2011-04-12 18:09:36 -0400
committerBarry Warsaw2011-04-12 18:09:36 -0400
commit5bb93de8db9b251a53968f0e1cf0b22d472e1a57 (patch)
treeaac85174fe3cea5e09113b9c9293fc484c773a66 /src/mailman/utilities/passwords.py
parent980e9dff9811466dcb9b44539d694b6eac32a17b (diff)
parent7c6633d17617ac60f11ff7de44160a9d804d4777 (diff)
downloadmailman-5bb93de8db9b251a53968f0e1cf0b22d472e1a57.tar.gz
mailman-5bb93de8db9b251a53968f0e1cf0b22d472e1a57.tar.zst
mailman-5bb93de8db9b251a53968f0e1cf0b22d472e1a57.zip
Lots of work to update the model for users, passwords, membership, testing,
and the REST API. A highlight of the merged changes: * The REST API now has a /users top-level URL under which user information can be accessed, and new users can be created. * IUsers now have a unique `user_id` which is evident in the REST API. Under testing, these uids are predictable, but otherwise, they're entirely random but guaranteed to be unique. * IUsers now have a `created_on` attribute. Like `user_id` these are predictable under testing, but otherwise reflect the actual date. * User passwords are now 'encrypted' (hashed) as defined by the config file. - new mailman.cfg variables password_scheme and password_length * IMember gets a `user` attribute which is a convenience for getting the IUser associated with the member. * mmsitepass is gone: - creator_pw_file - site_pw_file * Improved test initialization and its hook into zc.buildout.
Diffstat (limited to 'src/mailman/utilities/passwords.py')
-rw-r--r--src/mailman/utilities/passwords.py79
1 files changed, 77 insertions, 2 deletions
diff --git a/src/mailman/utilities/passwords.py b/src/mailman/utilities/passwords.py
index c14584748..896872436 100644
--- a/src/mailman/utilities/passwords.py
+++ b/src/mailman/utilities/passwords.py
@@ -26,24 +26,32 @@ __metaclass__ = type
__all__ = [
'Schemes',
'check_response',
+ 'encrypt_password',
'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 = ''
+SCHEME_RE = r'{(?P<scheme>[^}]+?)}(?P<rest>.*)'
@@ -288,8 +296,7 @@ def check_response(challenge, response):
:return: True if the response matches the challenge.
:rtype: bool
"""
- mo = re.match(r'{(?P<scheme>[^}]+?)}(?P<rest>.*)',
- challenge, re.IGNORECASE)
+ mo = re.match(SCHEME_RE, challenge, re.IGNORECASE)
if not mo:
return False
# See above for why we convert here. However because we should have
@@ -315,3 +322,71 @@ def lookup_scheme(scheme_name):
:rtype: `PasswordScheme`
"""
return _SCHEMES_BY_TAG.get(scheme_name.lower())
+
+
+def encrypt_password(password, scheme=None):
+ """Return an encrypted password.
+
+ If the given password is already encrypted (i.e. it has a scheme prefix),
+ then the password is return unchanged. Otherwise, it is encrypted with
+ the given scheme or the default scheme.
+
+ :param password: The plain text or encrypted password.
+ :type password: string
+ :param scheme: The scheme enum to use for encryption. If not given, the
+ system default scheme is used. This can be a `Schemes` enum item, or
+ the scheme name as a string.
+ :type scheme: `Schemes` enum, or string.
+ :return: The encrypted password.
+ :rtype: bytes
+ """
+ if not isinstance(password, (bytes, unicode)):
+ raise ValueError('Got {0}, expected unicode or bytes'.format(
+ type(password)))
+ if re.match(SCHEME_RE, password, re.IGNORECASE):
+ # Just ensure we're getting bytes back.
+ if isinstance(password, unicode):
+ return password.encode('us-ascii')
+ assert isinstance(password, bytes), 'Expected bytes'
+ return password
+ if scheme is None:
+ password_scheme = lookup_scheme(config.passwords.password_scheme)
+ elif scheme in Schemes:
+ password_scheme = scheme
+ else:
+ password_scheme = lookup_scheme(scheme)
+ if password_scheme is None:
+ raise ValueError('Bad password scheme: {0}'.format(scheme))
+ return make_secret(password, password_scheme)
+
+
+
+# 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]