diff options
| author | Barry Warsaw | 2011-04-12 18:09:36 -0400 |
|---|---|---|
| committer | Barry Warsaw | 2011-04-12 18:09:36 -0400 |
| commit | 5bb93de8db9b251a53968f0e1cf0b22d472e1a57 (patch) | |
| tree | aac85174fe3cea5e09113b9c9293fc484c773a66 /src/mailman/utilities/passwords.py | |
| parent | 980e9dff9811466dcb9b44539d694b6eac32a17b (diff) | |
| parent | 7c6633d17617ac60f11ff7de44160a9d804d4777 (diff) | |
| download | mailman-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.py | 79 |
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] |
