diff options
| author | Barry Warsaw | 2012-01-01 14:13:46 -0500 |
|---|---|---|
| committer | Barry Warsaw | 2012-01-01 14:13:46 -0500 |
| commit | ea48a9360f0c0e0d1974fdcdce6b31821818ee76 (patch) | |
| tree | bc1b230b020d8954ccbafa57508f441b50c4f961 /src | |
| parent | d1352a018ccc979303fee1d67dca8ee6a588ec69 (diff) | |
| download | mailman-ea48a9360f0c0e0d1974fdcdce6b31821818ee76.tar.gz mailman-ea48a9360f0c0e0d1974fdcdce6b31821818ee76.tar.zst mailman-ea48a9360f0c0e0d1974fdcdce6b31821818ee76.zip | |
Diffstat (limited to 'src')
| -rw-r--r-- | src/mailman/app/membership.py | 8 | ||||
| -rw-r--r-- | src/mailman/app/subscriptions.py | 4 | ||||
| -rw-r--r-- | src/mailman/commands/cli_members.py | 4 | ||||
| -rw-r--r-- | src/mailman/config/schema.cfg | 12 | ||||
| -rw-r--r-- | src/mailman/model/docs/requests.rst | 4 | ||||
| -rw-r--r-- | src/mailman/rest/docs/users.rst | 2 | ||||
| -rw-r--r-- | src/mailman/rest/users.py | 9 | ||||
| -rw-r--r-- | src/mailman/utilities/passwords.py | 389 | ||||
| -rw-r--r-- | src/mailman/utilities/tests/test_passwords.py | 204 |
9 files changed, 17 insertions, 619 deletions
diff --git a/src/mailman/app/membership.py b/src/mailman/app/membership.py index 88c0751f0..c7a7550a5 100644 --- a/src/mailman/app/membership.py +++ b/src/mailman/app/membership.py @@ -27,9 +27,11 @@ __all__ = [ from email.utils import formataddr +from flufl.password import lookup, make_secret from zope.component import getUtility from mailman.app.notifications import send_goodbye_message +from mailman.config import config from mailman.core.i18n import _ from mailman.email.message import OwnerNotification from mailman.interfaces.address import IEmailValidator @@ -38,7 +40,6 @@ from mailman.interfaces.member import ( MemberRole, MembershipIsBannedError, NotAMemberError) from mailman.interfaces.usermanager import IUserManager from mailman.utilities.i18n import make -from mailman.utilities.passwords import encrypt_password @@ -55,7 +56,7 @@ def add_member(mlist, email, realname, password, delivery_mode, language, :type email: str :param realname: The subscriber's full name. :type realname: str - :param password: The subscriber's password. + :param password: The subscriber's plain text password. :type password: str :param delivery_mode: The delivery mode the subscriber has chosen. :type delivery_mode: DeliveryMode @@ -96,7 +97,8 @@ def add_member(mlist, email, realname, password, delivery_mode, language, user.link(address) # Encrypt the password using the currently selected scheme. The # scheme is recorded in the hashed password string. - user.password = encrypt_password(password) + scheme = lookup(config.passwords.password_scheme.upper()) + user.password = make_secret(password, scheme) user.preferences.preferred_language = language member = mlist.subscribe(address, role) member.preferences.delivery_mode = delivery_mode diff --git a/src/mailman/app/subscriptions.py b/src/mailman/app/subscriptions.py index 9eba89d8b..bb8642874 100644 --- a/src/mailman/app/subscriptions.py +++ b/src/mailman/app/subscriptions.py @@ -26,6 +26,7 @@ __all__ = [ ] +from flufl.password import generate from operator import attrgetter from storm.expr import And, Or from uuid import UUID @@ -43,7 +44,6 @@ from mailman.interfaces.subscriptions import ( ISubscriptionService, MissingUserError) from mailman.interfaces.usermanager import IUserManager from mailman.model.member import Member -from mailman.utilities.passwords import make_user_friendly_password @@ -161,7 +161,7 @@ class SubscriptionService: # password to a system default. This will have to get reset since # it can't be retrieved. Note that none of these are used unless # the address is completely new to us. - password = make_user_friendly_password() + password = generate(int(config.passwords.password_length)) return add_member(mlist, subscriber, real_name, password, delivery_mode, system_preferences.preferred_language, role) diff --git a/src/mailman/commands/cli_members.py b/src/mailman/commands/cli_members.py index 96469c0f1..268d65bdc 100644 --- a/src/mailman/commands/cli_members.py +++ b/src/mailman/commands/cli_members.py @@ -29,6 +29,7 @@ import sys import codecs from email.utils import formataddr, parseaddr +from flufl.password import generate from operator import attrgetter from zope.component import getUtility from zope.interface import implements @@ -40,7 +41,6 @@ from mailman.interfaces.command import ICLISubCommand from mailman.interfaces.listmanager import IListManager from mailman.interfaces.member import ( AlreadySubscribedError, DeliveryMode, DeliveryStatus) -from mailman.utilities.passwords import make_user_friendly_password @@ -198,7 +198,7 @@ class Members: real_name = real_name.decode(fp.encoding) email = email.decode(fp.encoding) # Give the user a default, user-friendly password. - password = make_user_friendly_password() + password = generate(int(config.passwords.password_length)) try: add_member(mlist, email, real_name, password, DeliveryMode.regular, diff --git a/src/mailman/config/schema.cfg b/src/mailman/config/schema.cfg index 8f0c863fc..f660117e9 100644 --- a/src/mailman/config/schema.cfg +++ b/src/mailman/config/schema.cfg @@ -149,18 +149,6 @@ password_scheme: ssha # 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 -# fill in a password, or during list creation, when auto-generation of admin -# passwords was selected. -# -# Set this value to 'yes' for classic Mailman user-friendly(er) passwords. -# These generate semi-pronounceable passwords which are easier to remember. -# Set this value to 'no' to use more cryptographically secure, but harder to -# remember, passwords -- if your operating system and Python version support -# the necessary feature (specifically that /dev/urandom be available). -user_friendly_passwords: yes - [runner.master] # Define which runners, and how many of them, to start. diff --git a/src/mailman/model/docs/requests.rst b/src/mailman/model/docs/requests.rst index e01544490..1bc66c40a 100644 --- a/src/mailman/model/docs/requests.rst +++ b/src/mailman/model/docs/requests.rst @@ -544,7 +544,7 @@ mailing list. >>> mlist.send_welcome_msg = True >>> id_4 = moderator.hold_subscription(mlist, ... 'fperson@example.org', 'Frank Person', - ... '{NONE}abcxyz', DeliveryMode.regular, 'en') + ... 'abcxyz', DeliveryMode.regular, 'en') A message will be sent to the moderators telling them about the held subscription and the fact that they may need to approve it. @@ -695,7 +695,7 @@ Frank Person is now a member of the mailing list. >>> print member.user.real_name Frank Person >>> print member.user.password - {NONE}abcxyz + {CLEARTEXT}abcxyz Holding unsubscription requests diff --git a/src/mailman/rest/docs/users.rst b/src/mailman/rest/docs/users.rst index 43df35b94..145b069d9 100644 --- a/src/mailman/rest/docs/users.rst +++ b/src/mailman/rest/docs/users.rst @@ -66,7 +66,7 @@ Creating users via the API ========================== New users can be created through the REST API. To do so requires the initial -email address for the user, and optionally the user's full name and password. +email address for the user, a password, and optionally the user's full name. :: >>> transaction.abort() diff --git a/src/mailman/rest/users.py b/src/mailman/rest/users.py index 857d29471..6423836f2 100644 --- a/src/mailman/rest/users.py +++ b/src/mailman/rest/users.py @@ -26,18 +26,18 @@ __all__ = [ ] +from flufl.password import lookup, make_secret, generate from restish import http, resource from uuid import UUID from zope.component import getUtility +from mailman.config import config from mailman.interfaces.address import ExistingAddressError from mailman.interfaces.usermanager import IUserManager from mailman.rest.addresses import UserAddresses from mailman.rest.helpers import CollectionMixin, etag, no_content, path_to from mailman.rest.preferences import Preferences from mailman.rest.validator import Validator -from mailman.utilities.passwords import ( - encrypt_password, make_user_friendly_password) @@ -101,8 +101,9 @@ class AllUsers(_UserBase): error.email)) if password is None: # This will have to be reset since it cannot be retrieved. - password = make_user_friendly_password() - user.password = encrypt_password(password) + password = generate(int(config.passwords.password_length)) + scheme = lookup(config.passwords.password_scheme.upper()) + user.password = make_secret(password, scheme) location = path_to('users/{0}'.format(user.user_id.int)) return http.created(location, [], None) diff --git a/src/mailman/utilities/passwords.py b/src/mailman/utilities/passwords.py deleted file mode 100644 index f595a3c55..000000000 --- a/src/mailman/utilities/passwords.py +++ /dev/null @@ -1,389 +0,0 @@ -# Copyright (C) 2007-2011 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman is free software: you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free -# Software Foundation, either version 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along with -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""Password hashing and verification schemes. - -Represents passwords using RFC 2307 syntax (as best we can tell). -""" - -from __future__ import absolute_import, unicode_literals - -__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>.*)' - - - -class PasswordScheme: - """Password scheme base class.""" - TAG = b'' - - @staticmethod - def make_secret(password): - """Return the hashed password. - - :param password: The clear text password. - :type password: string - :return: The encrypted password. - :rtype: string - """ - raise NotImplementedError - - @staticmethod - def check_response(challenge, response): - """Check a response against a challenge. - - It is expected that the scheme specifier prefix is already stripped - from the response string. - - :param challenge: The challenge. - :type challenge: string - :param response: The response. - :type response: string - :return: True if the response matches the challenge. - :rtype: bool - """ - raise NotImplementedError - - - -class NoPasswordScheme(PasswordScheme): - """A password scheme without passwords.""" - - TAG = b'NONE' - - @staticmethod - def make_secret(password): - """See `PasswordScheme`.""" - return b'' - - @staticmethod - def check_response(challenge, response): - """See `PasswordScheme`.""" - return False - - - -class ClearTextPasswordScheme(PasswordScheme): - """A password scheme that stores clear text passwords.""" - - TAG = b'CLEARTEXT' - - @staticmethod - def make_secret(password): - """See `PasswordScheme`.""" - return password - - @staticmethod - def check_response(challenge, response): - """See `PasswordScheme`.""" - return challenge == response - - - -class SHAPasswordScheme(PasswordScheme): - """A password scheme that encodes the password using SHA1.""" - - TAG = b'SHA' - - @staticmethod - def make_secret(password): - """See `PasswordScheme`.""" - h = hashlib.sha1(password) - return encode(h.digest()) - - @staticmethod - def check_response(challenge, response): - """See `PasswordScheme`.""" - h = hashlib.sha1(response) - return challenge == encode(h.digest()) - - - -class SSHAPasswordScheme(PasswordScheme): - """A password scheme that encodes the password using salted SHA1.""" - - TAG = b'SSHA' - - @staticmethod - def make_secret(password): - """See `PasswordScheme`.""" - salt = os.urandom(SALT_LENGTH) - h = hashlib.sha1(password) - h.update(salt) - return encode(h.digest() + salt) - - @staticmethod - def check_response(challenge, response): - """See `PasswordScheme`.""" - # Get the salt from the challenge - challenge_bytes = decode(challenge) - digest = challenge_bytes[:20] - salt = challenge_bytes[20:] - h = hashlib.sha1(response) - h.update(salt) - return digest == h.digest() - - - -# Basic algorithm given by Bob Fleck -class PBKDF2PasswordScheme(PasswordScheme): - """RFC 2989 password encoding scheme.""" - - # This is a bit nasty if we wanted a different prf or iterations. OTOH, - # we really have no clue what the standard LDAP-ish specification for - # those options is. - TAG = b'PBKDF2 SHA {0}'.format(ITERATIONS) - - @staticmethod - def _pbkdf2(password, salt, iterations): - """From RFC2898 sec. 5.2. Simplified to handle only 20 byte output - case. Output of 20 bytes means always exactly one block to handle, - and a constant block counter appended to the salt in the initial hmac - update. - """ - h = hmac.new(password, None, hashlib.sha1) - prf = h.copy() - prf.update(salt + b'\x00\x00\x00\x01') - T = U = array(b'i', prf.digest()) - while iterations: - prf = h.copy() - prf.update(U.tostring()) - U = array(b'i', prf.digest()) - T = array(b'i', (t ^ u for t, u in zip(T, U))) - iterations -= 1 - return T.tostring() - - @staticmethod - def make_secret(password): - """See `PasswordScheme`. - - From RFC2898 sec. 5.2. Simplified to handle only 20 byte output - case. Output of 20 bytes means always exactly one block to handle, - and a constant block counter appended to the salt in the initial hmac - update. - """ - salt = os.urandom(SALT_LENGTH) - digest = PBKDF2PasswordScheme._pbkdf2(password, salt, ITERATIONS) - derived_key = encode(digest + salt) - return derived_key - - @staticmethod - def check_response(challenge, response, prf, iterations): - """See `PasswordScheme`.""" - # Decode the challenge to get the number of iterations and salt. We - # don't support anything but SHA PRF. - if prf.lower() != b'sha': - return False - try: - iterations = int(iterations) - except (ValueError, TypeError): - return False - challenge_bytes = decode(challenge) - digest = challenge_bytes[:20] - salt = challenge_bytes[20:] - key = PBKDF2PasswordScheme._pbkdf2(response, salt, iterations) - return digest == key - - - -class Schemes(Enum): - """List of password schemes.""" - # no_scheme is deliberately ugly because no one should be using it. Yes, - # this makes cleartext inconsistent, but that's a common enough - # terminology to justify the missing underscore. - no_scheme = 1 - cleartext = 2 - sha = 3 - ssha = 4 - pbkdf2 = 5 - - -_SCHEMES_BY_ENUM = { - Schemes.no_scheme : NoPasswordScheme, - Schemes.cleartext : ClearTextPasswordScheme, - Schemes.sha : SHAPasswordScheme, - Schemes.ssha : SSHAPasswordScheme, - Schemes.pbkdf2 : PBKDF2PasswordScheme, - } - - -# Some scheme tags have arguments, but the key for this dictionary should just -# be the lowercased scheme name. -_SCHEMES_BY_TAG = dict((_SCHEMES_BY_ENUM[e].TAG.split(' ')[0].lower(), e) - for e in _SCHEMES_BY_ENUM) - -_DEFAULT_SCHEME = NoPasswordScheme - - - -def make_secret(password, scheme=None): - """Encrypt a password. - - :param password: The clear text password. - :type password: string - :param scheme: The password scheme name. - :type scheme: string - :return: The encrypted password. - :rtype: string - """ - # The hash algorithms operate on bytes not strings. The password argument - # as provided here by the client will be a string (in Python 2 either - # unicode or 8-bit, in Python 3 always unicode). We need to encode this - # string into a byte array, and the way to spell that in Python 2 is to - # encode the string to utf-8. The returned secret is a string, so it must - # be a unicode. - if isinstance(password, unicode): - password = password.encode('utf-8') - scheme_class = _SCHEMES_BY_ENUM.get(scheme) - if not scheme_class: - raise errors.BadPasswordSchemeError(scheme) - secret = scheme_class.make_secret(password) - return b'{{{0}}}{1}'.format(scheme_class.TAG, secret) - - -def check_response(challenge, response): - """Check a response against a challenge. - - :param challenge: The challenge. - :type challenge: string - :param response: The response. - :type response: string - :return: True if the response matches the challenge. - :rtype: bool - """ - 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 - # generated the challenge, we assume that it is already a byte string. - if isinstance(response, unicode): - response = response.encode('utf-8') - scheme_group, rest_group = mo.group('scheme', 'rest') - scheme_parts = scheme_group.split() - scheme = scheme_parts[0].lower() - scheme_enum = _SCHEMES_BY_TAG.get(scheme, _DEFAULT_SCHEME) - scheme_class = _SCHEMES_BY_ENUM[scheme_enum] - if isinstance(rest_group, unicode): - rest_group = rest_group.encode('utf-8') - return scheme_class.check_response(rest_group, response, *scheme_parts[1:]) - - -def lookup_scheme(scheme_name): - """Look up a password scheme. - - :param scheme_name: The password scheme name. - :type scheme_name: string - :return: Password scheme class. - :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] diff --git a/src/mailman/utilities/tests/test_passwords.py b/src/mailman/utilities/tests/test_passwords.py deleted file mode 100644 index 058cf97de..000000000 --- a/src/mailman/utilities/tests/test_passwords.py +++ /dev/null @@ -1,204 +0,0 @@ -# Copyright (C) 2007-2011 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman is free software: you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free -# Software Foundation, either version 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along with -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""Unit tests for the passwords module.""" - -from __future__ import absolute_import, unicode_literals - -__metaclass__ = type -__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 - - - -class PasswordsTestBase: - scheme = None - - def setUp(self): - # passwords; 8-bit or unicode strings; ascii or binary - self.pw8a = b'abc' - self.pw8b = b'abc\xc3\xbf' # 'abc\xff' - self.pwub = b'abc\xff' - # bad password; 8-bit or unicode; ascii or binary - self.bad8a = b'xyz' - self.bad8b = b'xyz\xc3\xbf' # 'xyz\xff' - self.badub = b'xyz\xff' - - def test_passwords(self): - unless = self.failUnless - failif = self.failIf - secret = passwords.make_secret(self.pw8a, self.scheme) - unless(passwords.check_response(secret, self.pw8a)) - failif(passwords.check_response(secret, self.bad8a)) - - def test_passwords_with_funky_chars(self): - unless = self.failUnless - failif = self.failIf - secret = passwords.make_secret(self.pw8b, self.scheme) - unless(passwords.check_response(secret, self.pw8b)) - failif(passwords.check_response(secret, self.bad8b)) - - def test_unicode_passwords_with_funky_chars(self): - unless = self.failUnless - failif = self.failIf - secret = passwords.make_secret(self.pwub, self.scheme) - unless(passwords.check_response(secret, self.pwub)) - failif(passwords.check_response(secret, self.badub)) - - - -class TestBogusPasswords(PasswordsTestBase, unittest.TestCase): - scheme = -1 - - def test_passwords(self): - self.assertRaises(errors.BadPasswordSchemeError, - passwords.make_secret, self.pw8a, self.scheme) - - def test_passwords_with_funky_chars(self): - self.assertRaises(errors.BadPasswordSchemeError, - passwords.make_secret, self.pw8b, self.scheme) - - def test_unicode_passwords_with_funky_chars(self): - self.assertRaises(errors.BadPasswordSchemeError, - passwords.make_secret, self.pwub, self.scheme) - - - -class TestNonePasswords(PasswordsTestBase, unittest.TestCase): - scheme = passwords.Schemes.no_scheme - - def test_passwords(self): - failif = self.failIf - secret = passwords.make_secret(self.pw8a, self.scheme) - failif(passwords.check_response(secret, self.pw8a)) - failif(passwords.check_response(secret, self.bad8a)) - - def test_passwords_with_funky_chars(self): - failif = self.failIf - secret = passwords.make_secret(self.pw8b, self.scheme) - failif(passwords.check_response(secret, self.pw8b)) - failif(passwords.check_response(secret, self.bad8b)) - - def test_unicode_passwords_with_funky_chars(self): - failif = self.failIf - secret = passwords.make_secret(self.pwub, self.scheme) - failif(passwords.check_response(secret, self.pwub)) - failif(passwords.check_response(secret, self.badub)) - - - -class TestCleartextPasswords(PasswordsTestBase, unittest.TestCase): - scheme = passwords.Schemes.cleartext - - -class TestSHAPasswords(PasswordsTestBase, unittest.TestCase): - scheme = passwords.Schemes.sha - - -class TestSSHAPasswords(PasswordsTestBase, unittest.TestCase): - scheme = passwords.Schemes.ssha - - -class TestPBKDF2Passwords(PasswordsTestBase, unittest.TestCase): - scheme = passwords.Schemes.pbkdf2 - - - -class TestSchemeLookup(unittest.TestCase): - def test_scheme_name_lookup(self): - unless = self.failUnless - unless(passwords.lookup_scheme('NONE') is passwords.Schemes.no_scheme) - unless(passwords.lookup_scheme('CLEARTEXT') is - passwords.Schemes.cleartext) - unless(passwords.lookup_scheme('SHA') is passwords.Schemes.sha) - unless(passwords.lookup_scheme('SSHA') is passwords.Schemes.ssha) - unless(passwords.lookup_scheme('PBKDF2') is passwords.Schemes.pbkdf2) - unless(passwords.lookup_scheme(' -bogus- ') is None) - - - -# 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_encrypt_password_plaintext_default_scheme(self): - # Test that a plain text password gets encrypted. - self.assertEqual(passwords.encrypt_password('abc'), - '{CLEARTEXT}abc') - - def test_encrypt_password_plaintext(self): - # Test that a plain text password gets encrypted with the given scheme. - scheme = passwords.Schemes.sha - self.assertEqual(passwords.encrypt_password('abc', scheme), - '{SHA}qZk-NkcGgWq6PiVxeFDCbJzQ2J0=') - - def test_encrypt_password_plaintext_by_scheme_name(self): - # Test that a plain text password gets encrypted with the given - # scheme, which is given by name. - self.assertEqual(passwords.encrypt_password('abc', 'cleartext'), - '{CLEARTEXT}abc') - - def test_encrypt_password_already_encrypted_default_scheme(self): - # Test that a password which is already encrypted is return unchanged. - self.assertEqual(passwords.encrypt_password('{SHA}abc'), '{SHA}abc') - - def test_encrypt_password_already_encrypted(self): - # Test that a password which is already encrypted is return unchanged, - # ignoring any requested scheme. - scheme = passwords.Schemes.cleartext - self.assertEqual(passwords.encrypt_password('{SHA}abc', scheme), - '{SHA}abc') - - def test_encrypt_password_password_value_error(self): - self.assertRaises(ValueError, passwords.encrypt_password, 7) - - def test_encrypt_password_scheme_value_error(self): - self.assertRaises(ValueError, passwords.encrypt_password, 'abc', 'foo') |
