summaryrefslogtreecommitdiff
path: root/src/mailman/rules
diff options
context:
space:
mode:
Diffstat (limited to 'src/mailman/rules')
-rw-r--r--src/mailman/rules/approved.py12
-rw-r--r--src/mailman/rules/docs/approved.rst5
-rw-r--r--src/mailman/rules/docs/header-matching.rst10
-rw-r--r--src/mailman/rules/tests/test_approved.py92
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')