summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--setup.py1
-rw-r--r--src/mailman/app/membership.py8
-rw-r--r--src/mailman/app/subscriptions.py4
-rw-r--r--src/mailman/commands/cli_members.py4
-rw-r--r--src/mailman/config/schema.cfg12
-rw-r--r--src/mailman/model/docs/requests.rst4
-rw-r--r--src/mailman/rest/docs/users.rst2
-rw-r--r--src/mailman/rest/users.py9
-rw-r--r--src/mailman/utilities/passwords.py389
-rw-r--r--src/mailman/utilities/tests/test_passwords.py204
10 files changed, 18 insertions, 619 deletions
diff --git a/setup.py b/setup.py
index 76d57e23a..6dc4bbffd 100644
--- a/setup.py
+++ b/setup.py
@@ -98,6 +98,7 @@ case second `m'. Any other spelling is incorrect.""",
'flufl.enum',
'flufl.i18n',
'flufl.lock',
+ 'flufl.password',
'httplib2',
'lazr.config',
'lazr.smtptest',
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')