diff options
Diffstat (limited to 'src/mailman/rules')
| -rw-r--r-- | src/mailman/rules/approved.py | 12 | ||||
| -rw-r--r-- | src/mailman/rules/docs/approved.rst | 5 | ||||
| -rw-r--r-- | src/mailman/rules/docs/header-matching.rst | 10 | ||||
| -rw-r--r-- | src/mailman/rules/tests/test_approved.py | 92 |
4 files changed, 101 insertions, 18 deletions
diff --git a/src/mailman/rules/approved.py b/src/mailman/rules/approved.py index 1f4fc1369..3ff7d21ec 100644 --- a/src/mailman/rules/approved.py +++ b/src/mailman/rules/approved.py @@ -28,9 +28,9 @@ __all__ = [ import re from email.iterators import typed_subpart_iterator -from flufl.password import verify from zope.interface import implementer +from mailman.config import config from mailman.core.i18n import _ from mailman.interfaces.rules import IRule @@ -119,8 +119,14 @@ class Approved: else: for header in HEADERS: del msg[header] - return (password is not missing and - verify(mlist.moderator_password, password)) + if password is missing: + return False + is_valid, new_hash = config.password_context.verify( + mlist.moderator_password, password) + if is_valid and new_hash: + # Hash algorithm migration. + mlist.moderator_password = new_hash + return is_valid diff --git a/src/mailman/rules/docs/approved.rst b/src/mailman/rules/docs/approved.rst index 9c61a7419..3f3d54455 100644 --- a/src/mailman/rules/docs/approved.rst +++ b/src/mailman/rules/docs/approved.rst @@ -20,9 +20,8 @@ which is shared among all the administrators. This password will not be stored in clear text, so it must be hashed using the configured hash protocol. - >>> from flufl.password import lookup, make_secret - >>> scheme = lookup(config.passwords.password_scheme.upper()) - >>> mlist.moderator_password = make_secret('super secret', scheme) + >>> mlist.moderator_password = config.password_context.encrypt( + ... 'super secret') The ``approved`` rule determines whether the message contains the proper approval or not. diff --git a/src/mailman/rules/docs/header-matching.rst b/src/mailman/rules/docs/header-matching.rst index 021974e69..20e55fadd 100644 --- a/src/mailman/rules/docs/header-matching.rst +++ b/src/mailman/rules/docs/header-matching.rst @@ -74,17 +74,19 @@ The header may exist and match the pattern. By default, when the header matches, it gets held for moderator approval. :: + >>> from mailman.interfaces.chain import ChainEvent >>> from mailman.testing.helpers import event_subscribers >>> def handler(event): - ... print event.__class__.__name__, \ - ... event.chain.name, event.msg['message-id'] + ... if isinstance(event, ChainEvent): + ... print event.__class__.__name__, \ + ... event.chain.name, event.msg['message-id'] >>> del msg['x-spam-score'] >>> msg['X-Spam-Score'] = '*****' >>> msgdata = {} >>> with event_subscribers(handler): ... process(mlist, msg, msgdata, 'header-match') - HoldNotification hold <ant> + HoldEvent hold <ant> >>> hits_and_misses(msgdata) Rule hits: @@ -100,7 +102,7 @@ discard such messages. >>> with event_subscribers(handler): ... with configuration('antispam', jump_chain='discard'): ... process(mlist, msg, msgdata, 'header-match') - DiscardNotification discard <ant> + DiscardEvent discard <ant> These programmatically added headers can be removed by flushing the chain. Now, nothing with match this message. diff --git a/src/mailman/rules/tests/test_approved.py b/src/mailman/rules/tests/test_approved.py index d078556ba..a1b8f99ac 100644 --- a/src/mailman/rules/tests/test_approved.py +++ b/src/mailman/rules/tests/test_approved.py @@ -25,17 +25,18 @@ __all__ = [ 'TestApprovedNonASCII', 'TestApprovedPseudoHeader', 'TestApprovedPseudoHeaderMIME', + 'TestPasswordHashMigration', ] +import os import unittest -from flufl.password import lookup, make_secret - from mailman.app.lifecycle import create_list from mailman.config import config from mailman.rules import approved from mailman.testing.helpers import ( + configuration, specialized_message_from_string as mfs) from mailman.testing.layers import ConfigLayer @@ -48,8 +49,8 @@ class TestApproved(unittest.TestCase): def setUp(self): self._mlist = create_list('test@example.com') - scheme = lookup(config.passwords.password_scheme.upper()) - self._mlist.moderator_password = make_secret('super secret', scheme) + self._mlist.moderator_password = config.password_context.encrypt( + 'super secret') self._rule = approved.Approved() self._msg = mfs("""\ From: anne@example.com @@ -150,8 +151,8 @@ class TestApprovedPseudoHeader(unittest.TestCase): def setUp(self): self._mlist = create_list('test@example.com') - scheme = lookup(config.passwords.password_scheme.upper()) - self._mlist.moderator_password = make_secret('super secret', scheme) + self._mlist.moderator_password = config.password_context.encrypt( + 'super secret') self._rule = approved.Approved() self._msg = mfs("""\ From: anne@example.com @@ -283,8 +284,8 @@ class TestApprovedPseudoHeaderMIME(unittest.TestCase): def setUp(self): self._mlist = create_list('test@example.com') - scheme = lookup(config.passwords.password_scheme.upper()) - self._mlist.moderator_password = make_secret('super secret', scheme) + self._mlist.moderator_password = config.password_context.encrypt( + 'super secret') self._rule = approved.Approved() self._msg_text_template = """\ From: anne@example.com @@ -415,3 +416,78 @@ This is a message body with a non-ascii character =E4 # unicode errors. LP: #949924. result = self._rule.check(self._mlist, self._msg, {}) self.assertFalse(result) + + +class TestPasswordHashMigration(unittest.TestCase): + """Test that password hashing migrations work.""" + # http://packages.python.org/passlib/lib/passlib.context-tutorial.html#integrating-hash-migration + + layer = ConfigLayer + + def setUp(self): + self._mlist = create_list('test@example.com') + # The default testing hash algorithm is "roundup_plaintext" which + # yields hashed passwords of the form: {plaintext}abc + # + # Migration is automatically supported when a more modern password + # hash is chosen after the original password is set. As long as the + # old password still validates, the migration happens automatically. + self._mlist.moderator_password = config.password_context.encrypt( + b'super secret') + self._rule = approved.Approved() + self._msg = mfs("""\ +From: anne@example.com +To: test@example.com +Subject: A Message with non-ascii body +Message-ID: <ant> +MIME-Version: 1.0 + +A message body. +""") + + def test_valid_password_migrates(self): + # Now that the moderator password is set, change the default password + # hashing algorithm. When the old password is validated, it will be + # automatically migrated to the new hash. + self.assertEqual(self._mlist.moderator_password, + b'{plaintext}super secret') + config_file = os.path.join(config.VAR_DIR, 'passlib.config') + # XXX passlib seems to choose the default hashing scheme even if it is + # deprecated. The default scheme is either specified explicitly, or + # is the first in this list. This seems like a bug. + with open(config_file, 'w') as fp: + print("""\ +[passlib] +schemes = roundup_plaintext, plaintext +default = plaintext +deprecated = roundup_plaintext +""", file=fp) + with configuration('passwords', path=config_file): + self._msg['Approved'] = 'super secret' + result = self._rule.check(self._mlist, self._msg, {}) + self.assertTrue(result) + self.assertEqual(self._mlist.moderator_password, b'super secret') + + def test_invalid_password_does_not_migrate(self): + # Now that the moderator password is set, change the default password + # hashing algorithm. When the old password is invalid, it will not be + # automatically migrated to the new hash. + self.assertEqual(self._mlist.moderator_password, + b'{plaintext}super secret') + config_file = os.path.join(config.VAR_DIR, 'passlib.config') + # XXX passlib seems to choose the default hashing scheme even if it is + # deprecated. The default scheme is either specified explicitly, or + # is the first in this list. This seems like a bug. + with open(config_file, 'w') as fp: + print("""\ +[passlib] +schemes = roundup_plaintext, plaintext +default = plaintext +deprecated = roundup_plaintext +""", file=fp) + with configuration('passwords', path=config_file): + self._msg['Approved'] = 'not the password' + result = self._rule.check(self._mlist, self._msg, {}) + self.assertFalse(result) + self.assertEqual(self._mlist.moderator_password, + b'{plaintext}super secret') |
