summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorBarry Warsaw2012-07-03 10:36:39 -0400
committerBarry Warsaw2012-07-03 10:36:39 -0400
commit01415190ab44e69a8f09a6411564a7cb288404e8 (patch)
tree869503cb83afa74a17446363a6dbda5ba62d2b8d /src
parent3c8a07fc76176a8ea89ee6b73aef571d0b2c81ed (diff)
parent011677d7457f1d4c9cea3b030c8a50e941657812 (diff)
downloadmailman-01415190ab44e69a8f09a6411564a7cb288404e8.tar.gz
mailman-01415190ab44e69a8f09a6411564a7cb288404e8.tar.zst
mailman-01415190ab44e69a8f09a6411564a7cb288404e8.zip
Diffstat (limited to 'src')
-rw-r--r--src/mailman/app/events.py11
-rw-r--r--src/mailman/app/membership.py7
-rw-r--r--src/mailman/app/subscriptions.py2
-rw-r--r--src/mailman/app/tests/test_membership.py20
-rw-r--r--src/mailman/chains/accept.py11
-rw-r--r--src/mailman/chains/base.py12
-rw-r--r--src/mailman/chains/discard.py11
-rw-r--r--src/mailman/chains/docs/moderation.rst14
-rw-r--r--src/mailman/chains/hold.py10
-rw-r--r--src/mailman/chains/owner.py10
-rw-r--r--src/mailman/chains/reject.py12
-rw-r--r--src/mailman/chains/tests/test_hold.py12
-rw-r--r--src/mailman/chains/tests/test_owner.py8
-rw-r--r--src/mailman/commands/cli_members.py2
-rw-r--r--src/mailman/config/config.py38
-rw-r--r--src/mailman/config/passlib.cfg10
-rw-r--r--src/mailman/config/schema.cfg11
-rw-r--r--src/mailman/config/tests/__init__.py0
-rw-r--r--src/mailman/config/tests/test_configuration.py53
-rw-r--r--src/mailman/core/i18n.py8
-rw-r--r--src/mailman/core/initialize.py6
-rw-r--r--src/mailman/core/switchboard.py30
-rw-r--r--src/mailman/docs/NEWS.rst27
-rw-r--r--src/mailman/interfaces/chain.py37
-rw-r--r--src/mailman/interfaces/configuration.py41
-rw-r--r--src/mailman/languages/manager.py18
-rw-r--r--src/mailman/model/docs/requests.rst2
-rw-r--r--src/mailman/rest/docs/users.rst14
-rw-r--r--src/mailman/rest/users.py5
-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
-rw-r--r--src/mailman/runners/docs/incoming.rst10
-rw-r--r--src/mailman/styles/manager.py9
-rw-r--r--src/mailman/testing/helpers.py12
-rw-r--r--src/mailman/testing/layers.py2
-rw-r--r--src/mailman/testing/passlib.cfg4
-rw-r--r--src/mailman/testing/testing.cfg3
-rw-r--r--src/mailman/utilities/passwords.py62
-rw-r--r--src/mailman/utilities/tests/test_passwords.py60
41 files changed, 543 insertions, 180 deletions
diff --git a/src/mailman/app/events.py b/src/mailman/app/events.py
index 79376cca1..a4f385239 100644
--- a/src/mailman/app/events.py
+++ b/src/mailman/app/events.py
@@ -28,13 +28,22 @@ __all__ = [
from zope import event
from mailman.app import domain, moderator, subscriptions
+from mailman.core import i18n, switchboard
+from mailman.languages import manager as language_manager
+from mailman.styles import manager as style_manager
+from mailman.utilities import passwords
def initialize():
"""Initialize global event subscribers."""
event.subscribers.extend([
+ domain.handle_DomainDeletingEvent,
moderator.handle_ListDeletingEvent,
+ passwords.handle_ConfigurationUpdatedEvent,
subscriptions.handle_ListDeletedEvent,
- domain.handle_DomainDeletingEvent,
+ switchboard.handle_ConfigurationUpdatedEvent,
+ i18n.handle_ConfigurationUpdatedEvent,
+ style_manager.handle_ConfigurationUpdatedEvent,
+ language_manager.handle_ConfigurationUpdatedEvent,
])
diff --git a/src/mailman/app/membership.py b/src/mailman/app/membership.py
index e31a1695c..c73735b35 100644
--- a/src/mailman/app/membership.py
+++ b/src/mailman/app/membership.py
@@ -27,7 +27,6 @@ __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
@@ -96,10 +95,8 @@ def add_member(mlist, email, display_name, password, delivery_mode, language,
user.display_name = (
display_name if display_name else address.display_name)
user.link(address)
- # Encrypt the password using the currently selected scheme. The
- # scheme is recorded in the hashed password string.
- scheme = lookup(config.passwords.password_scheme.upper())
- user.password = make_secret(password, scheme)
+ # Encrypt the password using the currently selected hash scheme.
+ user.password = config.password_context.encrypt(password)
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 ebbe14492..7949f83ee 100644
--- a/src/mailman/app/subscriptions.py
+++ b/src/mailman/app/subscriptions.py
@@ -26,8 +26,8 @@ __all__ = [
]
-from flufl.password import generate
from operator import attrgetter
+from passlib.utils import generate_password as generate
from storm.expr import And, Or
from uuid import UUID
from zope.component import getUtility
diff --git a/src/mailman/app/tests/test_membership.py b/src/mailman/app/tests/test_membership.py
index 74e15d0e2..626be8b08 100644
--- a/src/mailman/app/tests/test_membership.py
+++ b/src/mailman/app/tests/test_membership.py
@@ -30,13 +30,11 @@ from zope.component import getUtility
from mailman.app.lifecycle import create_list
from mailman.app.membership import add_member
-from mailman.config import config
from mailman.core.constants import system_preferences
from mailman.interfaces.bans import IBanManager
from mailman.interfaces.member import (
AlreadySubscribedError, DeliveryMode, MemberRole, MembershipIsBannedError)
from mailman.interfaces.usermanager import IUserManager
-from mailman.testing.helpers import reset_the_world
from mailman.testing.layers import ConfigLayer
@@ -47,9 +45,6 @@ class AddMemberTest(unittest.TestCase):
def setUp(self):
self._mlist = create_list('test@example.com')
- def tearDown(self):
- reset_the_world()
-
def test_add_member_new_user(self):
# Test subscribing a user to a mailing list when the email address has
# not yet been associated with a user.
@@ -134,7 +129,7 @@ class AddMemberTest(unittest.TestCase):
self.assertEqual(member.address.email, 'aperson@example.com')
self.assertEqual(member.mailing_list, 'test@example.com')
self.assertEqual(member.role, MemberRole.moderator)
-
+
def test_add_member_twice(self):
# Adding a member with the same role twice causes an
# AlreadySubscribedError to be raised.
@@ -178,21 +173,10 @@ class AddMemberPasswordTest(unittest.TestCase):
def setUp(self):
self._mlist = create_list('test@example.com')
- # The default ssha scheme introduces a random salt, which is
- # inappropriate for unit tests.
- config.push('password scheme', """
- [passwords]
- password_scheme: sha
- """)
-
- def tearDown(self):
- config.pop('password scheme')
- reset_the_world()
def test_add_member_password(self):
# Test that the password stored with the new user is encrypted.
member = add_member(self._mlist, 'anne@example.com',
'Anne Person', 'abc', DeliveryMode.regular,
system_preferences.preferred_language)
- self.assertEqual(
- member.user.password, '{SHA}qZk-NkcGgWq6PiVxeFDCbJzQ2J0=')
+ self.assertEqual(member.user.password, '{plaintext}abc')
diff --git a/src/mailman/chains/accept.py b/src/mailman/chains/accept.py
index 4b326142b..852ca8c06 100644
--- a/src/mailman/chains/accept.py
+++ b/src/mailman/chains/accept.py
@@ -22,7 +22,6 @@ from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'AcceptChain',
- 'AcceptNotification',
]
@@ -30,9 +29,10 @@ import logging
from zope.event import notify
-from mailman.chains.base import ChainNotification, TerminalChainBase
+from mailman.chains.base import TerminalChainBase
from mailman.config import config
from mailman.core.i18n import _
+from mailman.interfaces.chain import AcceptEvent
log = logging.getLogger('mailman.vette')
@@ -40,11 +40,6 @@ SEMISPACE = '; '
-class AcceptNotification(ChainNotification):
- """A notification event signaling that a message is being accepted."""
-
-
-
class AcceptChain(TerminalChainBase):
"""Accept the message for posting."""
@@ -64,4 +59,4 @@ class AcceptChain(TerminalChainBase):
msg['X-Mailman-Rule-Misses'] = SEMISPACE.join(rule_misses)
config.switchboards['pipeline'].enqueue(msg, msgdata)
log.info('ACCEPT: %s', msg.get('message-id', 'n/a'))
- notify(AcceptNotification(mlist, msg, msgdata, self))
+ notify(AcceptEvent(mlist, msg, msgdata, self))
diff --git a/src/mailman/chains/base.py b/src/mailman/chains/base.py
index b75d66989..d317bf803 100644
--- a/src/mailman/chains/base.py
+++ b/src/mailman/chains/base.py
@@ -22,7 +22,6 @@ from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
'Chain',
- 'ChainNotification',
'Link',
'TerminalChainBase',
]
@@ -134,14 +133,3 @@ class ChainIterator:
def __iter__(self):
"""See `IChainIterator`."""
return self._chain.get_iterator()
-
-
-
-class ChainNotification:
- """Base class for chain notification events."""
-
- def __init__(self, mlist, msg, msgdata, chain):
- self.mlist = mlist
- self.msg = msg
- self.msgdata = msgdata
- self.chain = chain
diff --git a/src/mailman/chains/discard.py b/src/mailman/chains/discard.py
index 0a4bfe9ac..a52d25d7a 100644
--- a/src/mailman/chains/discard.py
+++ b/src/mailman/chains/discard.py
@@ -22,26 +22,21 @@ from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'DiscardChain',
- 'DiscardNotification',
]
import logging
from zope.event import notify
-from mailman.chains.base import ChainNotification, TerminalChainBase
+from mailman.chains.base import TerminalChainBase
from mailman.core.i18n import _
+from mailman.interfaces.chain import DiscardEvent
log = logging.getLogger('mailman.vette')
-class DiscardNotification(ChainNotification):
- """A notification event signaling that a message is being discarded."""
-
-
-
class DiscardChain(TerminalChainBase):
"""Discard a message."""
@@ -55,5 +50,5 @@ class DiscardChain(TerminalChainBase):
message away.
"""
log.info('DISCARD: %s', msg.get('message-id', 'n/a'))
- notify(DiscardNotification(mlist, msg, msgdata, self))
+ notify(DiscardEvent(mlist, msg, msgdata, self))
# Nothing more needs to happen.
diff --git a/src/mailman/chains/docs/moderation.rst b/src/mailman/chains/docs/moderation.rst
index fed120147..1e968ee68 100644
--- a/src/mailman/chains/docs/moderation.rst
+++ b/src/mailman/chains/docs/moderation.rst
@@ -45,9 +45,9 @@ In order to find out whether the message is held or accepted, we can subscribe
to Zope events that are triggered on each case.
::
- >>> from mailman.chains.base import ChainNotification
+ >>> from mailman.interfaces.chain import ChainEvent
>>> def on_chain(event):
- ... if isinstance(event, ChainNotification):
+ ... if isinstance(event, ChainEvent):
... print event
... print event.chain
... print 'Subject:', event.msg['subject']
@@ -74,7 +74,7 @@ built-in chain. No rules hit and so the message is accepted.
>>> from mailman.testing.helpers import event_subscribers
>>> with event_subscribers(on_chain):
... process(mlist, msg, {}, 'default-posting-chain')
- <mailman.chains.accept.AcceptNotification ...>
+ <mailman.interfaces.chain.AcceptEvent ...>
<mailman.chains.accept.AcceptChain ...>
Subject: aardvark
Hits:
@@ -109,7 +109,7 @@ moderator approval.
>>> with event_subscribers(on_chain):
... process(mlist, msg, {}, 'default-posting-chain')
- <mailman.chains.hold.HoldNotification ...>
+ <mailman.interfaces.chain.HoldEvent ...>
<mailman.chains.hold.HoldChain ...>
Subject: badger
Hits:
@@ -134,7 +134,7 @@ The list's member moderation action can also be set to `discard`...
>>> with event_subscribers(on_chain):
... process(mlist, msg, {}, 'default-posting-chain')
- <mailman.chains.discard.DiscardNotification ...>
+ <mailman.interfaces.chain.DiscardEvent ...>
<mailman.chains.discard.DiscardChain ...>
Subject: cougar
Hits:
@@ -158,7 +158,7 @@ The list's member moderation action can also be set to `discard`...
>>> with event_subscribers(on_chain):
... process(mlist, msg, {}, 'default-posting-chain')
- <mailman.chains.reject.RejectNotification ...>
+ <mailman.interfaces.chain.RejectEvent ...>
<mailman.chains.reject.RejectChain ...>
Subject: dingo
Hits:
@@ -197,7 +197,7 @@ moderator approval.
>>> with event_subscribers(on_chain):
... process(mlist, msg, {}, 'default-posting-chain')
- <mailman.chains.hold.HoldNotification ...>
+ <mailman.interfaces.chain.HoldEvent ...>
<mailman.chains.hold.HoldChain ...>
Subject: elephant
Hits:
diff --git a/src/mailman/chains/hold.py b/src/mailman/chains/hold.py
index a8b7ec57b..34389f82e 100644
--- a/src/mailman/chains/hold.py
+++ b/src/mailman/chains/hold.py
@@ -22,7 +22,6 @@ from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
'HoldChain',
- 'HoldNotification',
]
@@ -37,11 +36,12 @@ from zope.interface import implementer
from mailman.app.moderator import hold_message
from mailman.app.replybot import can_acknowledge
-from mailman.chains.base import ChainNotification, TerminalChainBase
+from mailman.chains.base import TerminalChainBase
from mailman.config import config
from mailman.core.i18n import _
from mailman.email.message import UserNotification
from mailman.interfaces.autorespond import IAutoResponseSet, Response
+from mailman.interfaces.chain import HoldEvent
from mailman.interfaces.languages import ILanguageManager
from mailman.interfaces.pending import IPendable, IPendings
from mailman.interfaces.usermanager import IUserManager
@@ -59,10 +59,6 @@ class HeldMessagePendable(dict):
PEND_KEY = 'held message'
-class HoldNotification(ChainNotification):
- """A notification event signaling that a message is being held."""
-
-
def autorespond_to_sender(mlist, sender, language=None):
"""Should Mailman automatically respond to this sender?
@@ -249,4 +245,4 @@ also appear in the first line of the body of the reply.""")),
log.info('HOLD: %s post from %s held, message-id=%s: %s',
mlist.fqdn_listname, msg.sender,
msg.get('message-id', 'n/a'), reason)
- notify(HoldNotification(mlist, msg, msgdata, self))
+ notify(HoldEvent(mlist, msg, msgdata, self))
diff --git a/src/mailman/chains/owner.py b/src/mailman/chains/owner.py
index ad0a04cea..22ed4b996 100644
--- a/src/mailman/chains/owner.py
+++ b/src/mailman/chains/owner.py
@@ -29,20 +29,16 @@ import logging
from zope.event import notify
-from mailman.chains.base import ChainNotification, TerminalChainBase
+from mailman.chains.base import TerminalChainBase
from mailman.config import config
from mailman.core.i18n import _
+from mailman.interfaces.chain import AcceptOwnerEvent
log = logging.getLogger('mailman.vette')
-class OwnerNotification(ChainNotification):
- """An event signaling that a message is accepted to the -owner address."""
-
-
-
class BuiltInOwnerChain(TerminalChainBase):
"""Default built-in -owner address chain."""
@@ -53,4 +49,4 @@ class BuiltInOwnerChain(TerminalChainBase):
# At least for now, everything posted to -owners goes through.
config.switchboards['pipeline'].enqueue(msg, msgdata)
log.info('OWNER: %s', msg.get('message-id', 'n/a'))
- notify(OwnerNotification(mlist, msg, msgdata, self))
+ notify(AcceptOwnerEvent(mlist, msg, msgdata, self))
diff --git a/src/mailman/chains/reject.py b/src/mailman/chains/reject.py
index cf1b86fb2..3adc69e3a 100644
--- a/src/mailman/chains/reject.py
+++ b/src/mailman/chains/reject.py
@@ -22,16 +22,17 @@ from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'RejectChain',
- 'RejectNotification',
]
import logging
+
from zope.event import notify
from mailman.app.bounces import bounce_message
-from mailman.chains.base import ChainNotification, TerminalChainBase
+from mailman.chains.base import TerminalChainBase
from mailman.core.i18n import _
+from mailman.interfaces.chain import RejectEvent
log = logging.getLogger('mailman.vette')
@@ -39,11 +40,6 @@ SEMISPACE = '; '
-class RejectNotification(ChainNotification):
- """A notification event signaling that a message is being rejected."""
-
-
-
class RejectChain(TerminalChainBase):
"""Reject/bounce a message."""
@@ -64,4 +60,4 @@ class RejectChain(TerminalChainBase):
# XXX Exception/reason
bounce_message(mlist, msg)
log.info('REJECT: %s', msg.get('message-id', 'n/a'))
- notify(RejectNotification(mlist, msg, msgdata, self))
+ notify(RejectEvent(mlist, msg, msgdata, self))
diff --git a/src/mailman/chains/tests/test_hold.py b/src/mailman/chains/tests/test_hold.py
index f2cd1dabf..515894505 100644
--- a/src/mailman/chains/tests/test_hold.py
+++ b/src/mailman/chains/tests/test_hold.py
@@ -30,10 +30,9 @@ from zope.component import getUtility
from mailman.app.lifecycle import create_list
from mailman.chains.hold import autorespond_to_sender
-from mailman.config import config
from mailman.interfaces.autorespond import IAutoResponseSet, Response
from mailman.interfaces.usermanager import IUserManager
-from mailman.testing.helpers import get_queue_messages
+from mailman.testing.helpers import configuration, get_queue_messages
from mailman.testing.layers import ConfigLayer
@@ -49,15 +48,12 @@ class TestAutorespond(unittest.TestCase):
self.maxDiff = None
self.eq = getattr(self, 'assertMultiLineEqual', self.assertEqual)
+ @configuration('mta', max_autoresponses_per_day=1)
def test_max_autoresponses_per_day(self):
# The last one we sent was the last one we should send today. Instead
# of sending an automatic response, send them the "no more today"
- # message.
- config.push('max-1', """
- [mta]
- max_autoresponses_per_day: 1
- """)
- # Simulate a response having been sent to an address already.
+ # message. Start by simulating a response having been sent to an
+ # address already.
anne = getUtility(IUserManager).create_address('anne@example.com')
response_set = IAutoResponseSet(self._mlist)
response_set.response_sent(anne, Response.hold)
diff --git a/src/mailman/chains/tests/test_owner.py b/src/mailman/chains/tests/test_owner.py
index db85d4967..b50c08e31 100644
--- a/src/mailman/chains/tests/test_owner.py
+++ b/src/mailman/chains/tests/test_owner.py
@@ -28,8 +28,9 @@ __all__ = [
import unittest
from mailman.app.lifecycle import create_list
-from mailman.chains.owner import BuiltInOwnerChain, OwnerNotification
+from mailman.chains.owner import BuiltInOwnerChain
from mailman.core.chains import process
+from mailman.interfaces.chain import AcceptOwnerEvent
from mailman.testing.helpers import (
event_subscribers,
get_queue_messages,
@@ -60,12 +61,13 @@ Message-ID: <ant>
# is processed by the owner chain.
events = []
def catch_event(event):
- events.append(event)
+ if isinstance(event, AcceptOwnerEvent):
+ events.append(event)
with event_subscribers(catch_event):
process(self._mlist, self._msg, {}, 'default-owner-chain')
self.assertEqual(len(events), 1)
event = events[0]
- self.assertTrue(isinstance(event, OwnerNotification))
+ self.assertTrue(isinstance(event, AcceptOwnerEvent))
self.assertEqual(event.mlist, self._mlist)
self.assertEqual(event.msg['message-id'], '<ant>')
self.assertTrue(isinstance(event.chain, BuiltInOwnerChain))
diff --git a/src/mailman/commands/cli_members.py b/src/mailman/commands/cli_members.py
index aef3991d8..7c5d3b8f3 100644
--- a/src/mailman/commands/cli_members.py
+++ b/src/mailman/commands/cli_members.py
@@ -29,8 +29,8 @@ import sys
import codecs
from email.utils import formataddr, parseaddr
-from flufl.password import generate
from operator import attrgetter
+from passlib.utils import generate_password as generate
from zope.component import getUtility
from zope.interface import implementer
diff --git a/src/mailman/config/config.py b/src/mailman/config/config.py
index fc82a2be7..f6c39fcec 100644
--- a/src/mailman/config/config.py
+++ b/src/mailman/config/config.py
@@ -32,13 +32,15 @@ from lazr.config import ConfigSchema, as_boolean
from pkg_resources import resource_stream
from string import Template
from zope.component import getUtility
-from zope.interface import Interface, implementer
+from zope.event import notify
+from zope.interface import implementer
import mailman.templates
from mailman import version
+from mailman.interfaces.configuration import (
+ ConfigurationUpdatedEvent, IConfiguration)
from mailman.interfaces.languages import ILanguageManager
-from mailman.interfaces.styles import IStyleManager
from mailman.utilities.filesystem import makedirs
from mailman.utilities.modules import call_name
@@ -47,11 +49,6 @@ SPACE = ' '
-class IConfiguration(Interface):
- """Marker interface; used for adaptation in the REST API."""
-
-
-
@implementer(IConfiguration)
class Configuration:
"""The core global configuration object."""
@@ -70,6 +67,7 @@ class Configuration:
self.handlers = {}
self.pipelines = {}
self.commands = {}
+ self.password_context = None
def _clear(self):
"""Clear the cached configuration variables."""
@@ -118,26 +116,8 @@ class Configuration:
"""Perform post-processing after loading the configuration files."""
# Expand and set up all directories.
self._expand_paths()
- # Set up the switchboards. Import this here to avoid circular imports.
- from mailman.core.switchboard import Switchboard
- Switchboard.initialize()
- # Set up all the languages.
- languages = self._config.getByCategory('language', [])
- language_manager = getUtility(ILanguageManager)
- for language in languages:
- if language.enabled:
- code = language.name.split('.')[1]
- language_manager.add(
- code, language.charset, language.description)
- # The default language must always be available.
- assert self._config.mailman.default_language in language_manager, (
- 'System default language code not defined: %s' %
- self._config.mailman.default_language)
self.ensure_directories_exist()
- getUtility(IStyleManager).populate()
- # Set the default system language.
- from mailman.core.i18n import _
- _.default = self.mailman.default_language
+ notify(ConfigurationUpdatedEvent(self))
def _expand_paths(self):
"""Expand all configuration paths."""
@@ -249,3 +229,9 @@ class Configuration:
"""Iterate over all the style configuration sections."""
for section in self._config.getByCategory('style', []):
yield section
+
+ @property
+ def language_configs(self):
+ """Iterate over all the language configuration sections."""
+ for section in self._config.getByCategory('language', []):
+ yield section
diff --git a/src/mailman/config/passlib.cfg b/src/mailman/config/passlib.cfg
new file mode 100644
index 000000000..805f0fb11
--- /dev/null
+++ b/src/mailman/config/passlib.cfg
@@ -0,0 +1,10 @@
+[passlib]
+# This is the output of passlibs.apps.custom_app_context.to_string().
+# See http://packages.python.org/passlib/index.html for details.
+schemes = sha512_crypt, sha256_crypt
+default = sha512_crypt
+all__vary_rounds = 0.1
+sha256_crypt__min_rounds = 80000
+sha512_crypt__min_rounds = 60000
+admin__sha256_crypt__min_rounds = 160000
+admin__sha512_crypt__min_rounds = 120000
diff --git a/src/mailman/config/schema.cfg b/src/mailman/config/schema.cfg
index 00b8d9325..e3c6e607c 100644
--- a/src/mailman/config/schema.cfg
+++ b/src/mailman/config/schema.cfg
@@ -154,10 +154,13 @@ wait: 10s
[passwords]
-# The default scheme to use to encrypt new passwords. Existing passwords
-# include the scheme that was used to encrypt them, so it's okay to change
-# this after users have been added.
-password_scheme: ssha
+# Where can we find the passlib configuration file? The path can be either a
+# file system path or a Python import path. If the value starts with python:
+# then it is a Python import path, otherwise it is a file system path. File
+# system paths must be absolute since no guarantees are made about the current
+# working directory. Python paths should not include the trailing .cfg, which
+# the file must end with.
+path: python:mailman.config.passlib
# When Mailman generates them, this is the default length of passwords.
password_length: 8
diff --git a/src/mailman/config/tests/__init__.py b/src/mailman/config/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/mailman/config/tests/__init__.py
diff --git a/src/mailman/config/tests/test_configuration.py b/src/mailman/config/tests/test_configuration.py
new file mode 100644
index 000000000..88e00cbb9
--- /dev/null
+++ b/src/mailman/config/tests/test_configuration.py
@@ -0,0 +1,53 @@
+# Copyright (C) 2012 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/>.
+
+"""Test the system-wide global configuration."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'TestConfiguration',
+ ]
+
+
+import unittest
+
+from mailman.interfaces.configuration import ConfigurationUpdatedEvent
+from mailman.testing.helpers import configuration, event_subscribers
+from mailman.testing.layers import ConfigLayer
+
+
+
+class TestConfiguration(unittest.TestCase):
+ layer = ConfigLayer
+
+ def test_push_and_pop_trigger_events(self):
+ # Pushing a new configuration onto the stack triggers a
+ # post-processing event.
+ events = []
+ def on_event(event):
+ if isinstance(event, ConfigurationUpdatedEvent):
+ # Record both the event and the top overlay.
+ events.append(event.config.overlays[0].name)
+ with event_subscribers(on_event):
+ with configuration('test', _configname='my test'):
+ pass
+ # There should be two pushed configuration names on the list now, one
+ # for the push leaving 'my test' on the top of the stack, and one for
+ # the pop, leaving the ConfigLayer's 'test config' on top.
+ self.assertEqual(events, ['my test', 'test config'])
diff --git a/src/mailman/core/i18n.py b/src/mailman/core/i18n.py
index 2d7c382c7..73453ae65 100644
--- a/src/mailman/core/i18n.py
+++ b/src/mailman/core/i18n.py
@@ -31,7 +31,7 @@ import time
from flufl.i18n import PackageStrategy, registry
import mailman.messages
-
+from mailman.interfaces.configuration import ConfigurationUpdatedEvent
_ = None
@@ -113,3 +113,9 @@ def ctime(date):
wday = daysofweek[wday]
mon = months[mon]
return _('$wday $mon $day $hh:$mm:$ss $tzname $year')
+
+
+
+def handle_ConfigurationUpdatedEvent(event):
+ if isinstance(event, ConfigurationUpdatedEvent):
+ _.default = event.config.mailman.default_language
diff --git a/src/mailman/core/initialize.py b/src/mailman/core/initialize.py
index 389a45f3b..f4659e638 100644
--- a/src/mailman/core/initialize.py
+++ b/src/mailman/core/initialize.py
@@ -110,6 +110,10 @@ def initialize_1(config_path=None):
# o-rwx although I think in most cases it doesn't hurt if other can read
# or write the files.
os.umask(007)
+ # Initialize configuration event subscribers. This must be done before
+ # setting up the configuration system.
+ from mailman.app.events import initialize as initialize_events
+ initialize_events()
# config_path will be set if the command line argument -C is given. That
# case overrides all others. When not given on the command line, the
# configuration file is searched for in the file system.
@@ -152,7 +156,6 @@ def initialize_2(debug=False, propagate_logs=None):
# Initialize the rules and chains. Do the imports here so as to avoid
# circular imports.
from mailman.app.commands import initialize as initialize_commands
- from mailman.app.events import initialize as initialize_events
from mailman.core.chains import initialize as initialize_chains
from mailman.core.pipelines import initialize as initialize_pipelines
from mailman.core.rules import initialize as initialize_rules
@@ -161,7 +164,6 @@ def initialize_2(debug=False, propagate_logs=None):
initialize_chains()
initialize_pipelines()
initialize_commands()
- initialize_events()
def initialize_3():
diff --git a/src/mailman/core/switchboard.py b/src/mailman/core/switchboard.py
index c65b92fac..1f16cb5fb 100644
--- a/src/mailman/core/switchboard.py
+++ b/src/mailman/core/switchboard.py
@@ -29,6 +29,7 @@ from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
'Switchboard',
+ 'handle_ConfigurationUpdatedEvent',
]
@@ -44,6 +45,7 @@ from zope.interface import implementer
from mailman.config import config
from mailman.email.message import Message
+from mailman.interfaces.configuration import ConfigurationUpdatedEvent
from mailman.interfaces.switchboard import ISwitchboard
from mailman.utilities.filesystem import makedirs
from mailman.utilities.string import expand
@@ -67,18 +69,6 @@ elog = logging.getLogger('mailman.error')
class Switchboard:
"""See `ISwitchboard`."""
- @staticmethod
- def initialize():
- """Initialize the global switchboards for input/output."""
- for conf in config.runner_configs:
- name = conf.name.split('.')[-1]
- assert name not in config.switchboards, (
- 'Duplicate runner name: {0}'.format(name))
- substitutions = config.paths
- substitutions['name'] = name
- path = expand(conf.path, substitutions)
- config.switchboards[name] = Switchboard(name, path)
-
def __init__(self, name, queue_directory,
slice=None, numslices=1, recover=False):
"""Create a switchboard object.
@@ -264,3 +254,19 @@ class Switchboard:
self.finish(filebase, preserve=True)
else:
os.rename(src, dst)
+
+
+
+def handle_ConfigurationUpdatedEvent(event):
+ """Initialize the global switchboards for input/output."""
+ if not isinstance(event, ConfigurationUpdatedEvent):
+ return
+ config = event.config
+ for conf in config.runner_configs:
+ name = conf.name.split('.')[-1]
+ assert name not in config.switchboards, (
+ 'Duplicate runner name: {0}'.format(name))
+ substitutions = config.paths
+ substitutions['name'] = name
+ path = expand(conf.path, substitutions)
+ config.switchboards[name] = Switchboard(name, path)
diff --git a/src/mailman/docs/NEWS.rst b/src/mailman/docs/NEWS.rst
index fb819ccc4..4a348a691 100644
--- a/src/mailman/docs/NEWS.rst
+++ b/src/mailman/docs/NEWS.rst
@@ -14,6 +14,8 @@ Here is a history of user visible changes to Mailman.
Architecture
------------
+ * `passlib`_ is now used for all password hashing instead of flufl.password.
+ The default hash is `sha512_crypt`. (LP: #1015758)
* Internally, all datetimes are kept in the UTC timezone, however because of
LP: #280708, they are stored in the database in naive format.
* `received_time` is now added to the message metadata by the LMTP runner
@@ -35,6 +37,22 @@ Architecture
`RunnerCrashEvent` is triggered, which contains references to the queue
runner, mailing list, message, metadata, and exception. Interested parties
can subscribe to that `zope.event` for notification.
+ * Events renamed and moved:
+ * `mailman.chains.accept.AcceptNotification`
+ * `mailman.chains.base.ChainNotification`
+ * `mailman.chains.discard.DiscardNotification`
+ * `mailman.chains.hold.HoldNotification`
+ * `mailman.chains.owner.OwnerNotification`
+ * `mailman.chains.reject.RejectNotification`
+ changed to (respectively):
+ * `mailman.interfaces.chains.AcceptEvent`
+ * `mailman.interfaces.chains.ChainEvent`
+ * `mailman.interfaces.chains.DiscardEvent`
+ * `mailman.interfaces.chains.HoldEvent`
+ * `mailman.interfaces.chains.AcceptOwnerEvent`
+ * `mailman.interfaces.chains.RejectEvent`
+ * A `ConfigurationUpdatedEvent` is triggered when the system-wide global
+ configuration stack is pushed or popped.
Configuration
-------------
@@ -42,9 +60,12 @@ Configuration
every `[archiver.<name>]` section. These are used to determine under what
circumstances a message destined for a specific archiver should have its
`Date:` header clobbered. (LP: #963612)
+ * With the switch to `passlib`_, `[passwords]password_scheme` has been
+ removed. Instead use `[passwords]path` to specify where to find the
+ `passlib.cfg` file. See the comments in `schema.cfg` for details.
* Configuration schema variable changes:
- [nntp]username -> [nntp]user
- [nntp]port (added)
+ * [nntp]username -> [nntp]user
+ * [nntp]port (added)
* Header check specifications in the `mailman.cfg` file have changed quite
bit. The previous `[spam.header.foo]` sections have been removed.
Instead, there's a new `[antispam]` section that contains a `header_checks`
@@ -66,6 +87,8 @@ Bug fixes
(LP: #953497)
* List-Post should be NO when posting is not allowed. (LP: #987563)
+.. _`passlib`: http://packages.python.org/passlib/index.html
+
3.0 beta 1 -- "The Twilight Zone"
=================================
diff --git a/src/mailman/interfaces/chain.py b/src/mailman/interfaces/chain.py
index 8858f874c..8862026f8 100644
--- a/src/mailman/interfaces/chain.py
+++ b/src/mailman/interfaces/chain.py
@@ -21,11 +21,17 @@ from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
+ 'AcceptEvent',
+ 'AcceptOwnerEvent',
+ 'ChainEvent',
+ 'DiscardEvent',
+ 'HoldEvent',
'IChain',
'IChainIterator',
'IChainLink',
'IMutableChain',
'LinkAction',
+ 'RejectEvent',
]
@@ -34,6 +40,37 @@ from zope.interface import Interface, Attribute
+class ChainEvent:
+ """Base class for chain notification events."""
+
+ def __init__(self, mlist, msg, msgdata, chain):
+ self.mlist = mlist
+ self.msg = msg
+ self.msgdata = msgdata
+ self.chain = chain
+
+
+class AcceptEvent(ChainEvent):
+ """A notification event signaling that a message is being accepted."""
+
+
+class AcceptOwnerEvent(ChainEvent):
+ """An event signaling that a message is accepted to the -owner address."""
+
+
+class DiscardEvent(ChainEvent):
+ """A notification event signaling that a message is being discarded."""
+
+
+class HoldEvent(ChainEvent):
+ """A notification event signaling that a message is being held."""
+
+
+class RejectEvent(ChainEvent):
+ """A notification event signaling that a message is being rejected."""
+
+
+
class LinkAction(Enum):
# Jump to another chain.
jump = 0
diff --git a/src/mailman/interfaces/configuration.py b/src/mailman/interfaces/configuration.py
new file mode 100644
index 000000000..8c4fb52a6
--- /dev/null
+++ b/src/mailman/interfaces/configuration.py
@@ -0,0 +1,41 @@
+# Copyright (C) 2012 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/>.
+
+"""Configuration system interface."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'ConfigurationUpdatedEvent',
+ 'IConfiguration',
+ ]
+
+
+from zope.interface import Interface
+
+
+
+class IConfiguration(Interface):
+ """Marker interface; used for adaptation in the REST API."""
+
+
+
+class ConfigurationUpdatedEvent:
+ """The system-wide global configuration was updated."""
+ def __init__(self, config):
+ self.config = config
diff --git a/src/mailman/languages/manager.py b/src/mailman/languages/manager.py
index 7844bd87c..87b56dbda 100644
--- a/src/mailman/languages/manager.py
+++ b/src/mailman/languages/manager.py
@@ -24,8 +24,11 @@ __all__ = [
'LanguageManager',
]
+
+from zope.component import getUtility
from zope.interface import implementer
+from mailman.interfaces.configuration import ConfigurationUpdatedEvent
from mailman.interfaces.languages import ILanguageManager
from mailman.languages.language import Language
@@ -72,3 +75,18 @@ class LanguageManager:
def clear(self):
"""See `ILanguageManager`."""
self._languages.clear()
+
+
+
+def handle_ConfigurationUpdatedEvent(event):
+ if not isinstance(event, ConfigurationUpdatedEvent):
+ return
+ manager = getUtility(ILanguageManager)
+ for language in event.config.language_configs:
+ if language.enabled:
+ code = language.name.split('.')[1]
+ manager.add(code, language.charset, language.description)
+ # the default language must always be available.
+ assert event.config.mailman.default_language in manager, (
+ 'system default language code not defined: {0}'.format(
+ event.config.mailman.default_language))
diff --git a/src/mailman/model/docs/requests.rst b/src/mailman/model/docs/requests.rst
index a20823a91..a51cbc099 100644
--- a/src/mailman/model/docs/requests.rst
+++ b/src/mailman/model/docs/requests.rst
@@ -696,7 +696,7 @@ Frank Person is now a member of the mailing list.
>>> print member.user.display_name
Frank Person
>>> print member.user.password
- {CLEARTEXT}abcxyz
+ {plaintext}abcxyz
Holding unsubscription requests
diff --git a/src/mailman/rest/docs/users.rst b/src/mailman/rest/docs/users.rst
index a20306e17..cdede10ee 100644
--- a/src/mailman/rest/docs/users.rst
+++ b/src/mailman/rest/docs/users.rst
@@ -94,7 +94,7 @@ It is also available via the location given in the response.
created_on: 2005-08-01T07:49:23
display_name: Bart Person
http_etag: "..."
- password: {CLEARTEXT}bbb
+ password: {plaintext}bbb
self_link: http://localhost:9001/3.0/users/3
user_id: 3
@@ -105,7 +105,7 @@ them with user ids. Thus, a user can be retrieved via its email address.
created_on: 2005-08-01T07:49:23
display_name: Bart Person
http_etag: "..."
- password: {CLEARTEXT}bbb
+ password: {plaintext}bbb
self_link: http://localhost:9001/3.0/users/3
user_id: 3
@@ -129,7 +129,7 @@ therefore cannot be retrieved. It can be reset though.
created_on: 2005-08-01T07:49:23
display_name: Cris Person
http_etag: "..."
- password: {CLEARTEXT}...
+ password: {plaintext}...
self_link: http://localhost:9001/3.0/users/4
user_id: 4
@@ -227,7 +227,7 @@ In fact, any of these addresses can be used to look up Bart's user record.
created_on: 2005-08-01T07:49:23
display_name: Bart Person
http_etag: "..."
- password: {CLEARTEXT}bbb
+ password: {plaintext}bbb
self_link: http://localhost:9001/3.0/users/3
user_id: 3
@@ -235,7 +235,7 @@ In fact, any of these addresses can be used to look up Bart's user record.
created_on: 2005-08-01T07:49:23
display_name: Bart Person
http_etag: "..."
- password: {CLEARTEXT}bbb
+ password: {plaintext}bbb
self_link: http://localhost:9001/3.0/users/3
user_id: 3
@@ -243,7 +243,7 @@ In fact, any of these addresses can be used to look up Bart's user record.
created_on: 2005-08-01T07:49:23
display_name: Bart Person
http_etag: "..."
- password: {CLEARTEXT}bbb
+ password: {plaintext}bbb
self_link: http://localhost:9001/3.0/users/3
user_id: 3
@@ -251,6 +251,6 @@ In fact, any of these addresses can be used to look up Bart's user record.
created_on: 2005-08-01T07:49:23
display_name: Bart Person
http_etag: "..."
- password: {CLEARTEXT}bbb
+ password: {plaintext}bbb
self_link: http://localhost:9001/3.0/users/3
user_id: 3
diff --git a/src/mailman/rest/users.py b/src/mailman/rest/users.py
index 4e1362120..bce541ae5 100644
--- a/src/mailman/rest/users.py
+++ b/src/mailman/rest/users.py
@@ -26,7 +26,7 @@ __all__ = [
]
-from flufl.password import lookup, make_secret, generate
+from passlib.utils import generate_password as generate
from restish import http, resource
from uuid import UUID
from zope.component import getUtility
@@ -102,8 +102,7 @@ class AllUsers(_UserBase):
if password is None:
# This will have to be reset since it cannot be retrieved.
password = generate(int(config.passwords.password_length))
- scheme = lookup(config.passwords.password_scheme.upper())
- user.password = make_secret(password, scheme)
+ user.password = config.password_context.encrypt(password)
location = path_to('users/{0}'.format(user.user_id.int))
return http.created(location, [], None)
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')
diff --git a/src/mailman/runners/docs/incoming.rst b/src/mailman/runners/docs/incoming.rst
index 4a9db778e..8830031e6 100644
--- a/src/mailman/runners/docs/incoming.rst
+++ b/src/mailman/runners/docs/incoming.rst
@@ -143,9 +143,9 @@ chain will now hold all posted messages, so nothing will show up in the
pipeline queue.
::
- >>> from mailman.chains.base import ChainNotification
+ >>> from mailman.interfaces.chain import ChainEvent
>>> def on_chain(event):
- ... if isinstance(event, ChainNotification):
+ ... if isinstance(event, ChainEvent):
... print event
... print event.chain
... print 'From: {0}\nTo: {1}\nMessage-ID: {2}'.format(
@@ -158,7 +158,7 @@ pipeline queue.
>>> with event_subscribers(on_chain):
... inject_message(mlist, msg)
... incoming.run()
- <mailman.chains.hold.HoldNotification ...>
+ <mailman.interfaces.chain.HoldEvent ...>
<mailman.chains.hold.HoldChain ...>
From: aperson@example.com
To: test@example.com
@@ -193,7 +193,7 @@ new chain and set it as the mailing list's start chain.
>>> with event_subscribers(on_chain):
... inject_message(mlist, msg)
... incoming.run()
- <mailman.chains.discard.DiscardNotification ...>
+ <mailman.interfaces.chain.DiscardEvent ...>
<mailman.chains.discard.DiscardChain ...>
From: aperson@example.com
To: test@example.com
@@ -222,7 +222,7 @@ just create a new chain that does.
>>> with event_subscribers(on_chain):
... inject_message(mlist, msg)
... incoming.run()
- <mailman.chains.reject.RejectNotification ...>
+ <mailman.interfaces.chain.RejectEvent ...>
<mailman.chains.reject.RejectChain ...>
From: aperson@example.com
To: test@example.com
diff --git a/src/mailman/styles/manager.py b/src/mailman/styles/manager.py
index 8ec832f18..c2729abfb 100644
--- a/src/mailman/styles/manager.py
+++ b/src/mailman/styles/manager.py
@@ -22,13 +22,16 @@ from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
'StyleManager',
+ 'handle_ConfigurationUpdatedEvent',
]
from operator import attrgetter
+from zope.component import getUtility
from zope.interface import implementer
from zope.interface.verify import verifyObject
+from mailman.interfaces.configuration import ConfigurationUpdatedEvent
from mailman.interfaces.styles import (
DuplicateStyleError, IStyle, IStyleManager)
from mailman.utilities.modules import call_name
@@ -88,3 +91,9 @@ class StyleManager:
"""See `IStyleManager`."""
# Let KeyErrors percolate up.
del self._styles[style.name]
+
+
+
+def handle_ConfigurationUpdatedEvent(event):
+ if isinstance(event, ConfigurationUpdatedEvent):
+ getUtility(IStyleManager).populate()
diff --git a/src/mailman/testing/helpers.py b/src/mailman/testing/helpers.py
index 62e3b9d2a..84f215574 100644
--- a/src/mailman/testing/helpers.py
+++ b/src/mailman/testing/helpers.py
@@ -343,14 +343,14 @@ def call_api(url, data=None, method=None, username=None, password=None):
@contextmanager
def event_subscribers(*subscribers):
- """Temporarily set the Zope event subscribers list.
+ """Temporarily extend the Zope event subscribers list.
:param subscribers: A sequence of event subscribers.
:type subscribers: sequence of callables, each receiving one argument, the
event.
"""
old_subscribers = event.subscribers[:]
- event.subscribers = list(subscribers)
+ event.subscribers.extend(subscribers)
try:
yield
finally:
@@ -363,8 +363,14 @@ class configuration:
def __init__(self, section, **kws):
self._section = section
+ # Most tests don't care about the name given to the temporary
+ # configuration. Usually we'll just craft a random one, but some
+ # tests do care, so give them a hook to set it.
+ if '_configname' in kws:
+ self._uuid = kws.pop('_configname')
+ else:
+ self._uuid = uuid.uuid4().hex
self._values = kws.copy()
- self._uuid = uuid.uuid4().hex
def _apply(self):
lines = ['[{0}]'.format(self._section)]
diff --git a/src/mailman/testing/layers.py b/src/mailman/testing/layers.py
index 0faa1c8e4..bbef6d5f4 100644
--- a/src/mailman/testing/layers.py
+++ b/src/mailman/testing/layers.py
@@ -117,8 +117,6 @@ class ConfigLayer(MockAndMonkeyLayer):
test_config = dedent("""
[mailman]
layout: testing
- [passwords]
- password_scheme: cleartext
[paths.testing]
var_dir: %s
[devmode]
diff --git a/src/mailman/testing/passlib.cfg b/src/mailman/testing/passlib.cfg
new file mode 100644
index 000000000..225ecd49b
--- /dev/null
+++ b/src/mailman/testing/passlib.cfg
@@ -0,0 +1,4 @@
+[passlib]
+# Use a predictable hashing algorithm with plain text and no salt. This is
+# *only* useful for debugging and unit testing.
+schemes = roundup_plaintext
diff --git a/src/mailman/testing/testing.cfg b/src/mailman/testing/testing.cfg
index 91613cc8d..5f19dca14 100644
--- a/src/mailman/testing/testing.cfg
+++ b/src/mailman/testing/testing.cfg
@@ -30,6 +30,9 @@ smtp_port: 9025
lmtp_port: 9024
incoming: mailman.testing.mta.FakeMTA
+[passwords]
+path: python:mailman.testing.passlib
+
[webservice]
port: 9001
diff --git a/src/mailman/utilities/passwords.py b/src/mailman/utilities/passwords.py
new file mode 100644
index 000000000..95c85c47a
--- /dev/null
+++ b/src/mailman/utilities/passwords.py
@@ -0,0 +1,62 @@
+# Copyright (C) 2012 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/>.
+
+"""A wrapper around passlib."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'handle_ConfigurationUpdatedEvent',
+ ]
+
+
+
+from passlib.context import CryptContext
+from pkg_resources import resource_string
+
+from mailman.interfaces.configuration import ConfigurationUpdatedEvent
+
+
+
+class PasswordContext:
+ def __init__(self, config):
+ # Is the context coming from a file system or Python path?
+ if config.passwords.path.startswith('python:'):
+ resource_path = config.passwords.path[7:]
+ package, dot, resource = resource_path.rpartition('.')
+ config_string = resource_string(package, resource + '.cfg')
+ else:
+ with open(config.passwords.path, 'rb') as fp:
+ config_string = fp.read()
+ self._context = CryptContext.from_string(config_string)
+
+ def encrypt(self, secret):
+ return self._context.encrypt(secret)
+
+ def verify(self, hashed, password):
+ # Support hash algorithm migration. Yes, the order of arguments is
+ # reversed, for backward compatibility with flufl.password. XXX fix
+ # this eventually.
+ return self._context.verify_and_update(password, hashed)
+
+
+
+def handle_ConfigurationUpdatedEvent(event):
+ if isinstance(event, ConfigurationUpdatedEvent):
+ # Just reset the password context.
+ event.config.password_context = PasswordContext(event.config)
diff --git a/src/mailman/utilities/tests/test_passwords.py b/src/mailman/utilities/tests/test_passwords.py
new file mode 100644
index 000000000..7b2931855
--- /dev/null
+++ b/src/mailman/utilities/tests/test_passwords.py
@@ -0,0 +1,60 @@
+# Copyright (C) 2012 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/>.
+
+"""Testing the password utility."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'TestPasswords',
+ ]
+
+
+import os
+import unittest
+
+from mailman.config import config
+from mailman.testing.helpers import configuration
+from mailman.testing.layers import ConfigLayer
+
+
+
+class TestPasswords(unittest.TestCase):
+ layer = ConfigLayer
+
+ def test_default_passlib(self):
+ # By default, testing uses the roundup_plaintext hash algorithm, which
+ # is just plaintext with a prefix.
+ self.assertEqual(config.password_context.encrypt('my password'),
+ '{plaintext}my password')
+
+ def test_passlib_from_file_path(self):
+ # Set up this test to use a passlib configuration file specified with
+ # a file system path. We prove we're using the new configuration
+ # because a non-prefixed, i.e. non-roundup, plaintext hash algorithm
+ # will be used. When a file system path is used, the file can end in
+ # any suffix.
+ config_file = os.path.join(config.VAR_DIR, 'passlib.config')
+ with open(config_file, 'w') as fp:
+ print("""\
+[passlib]
+schemes = plaintext
+""", file=fp)
+ with configuration('passwords', path=config_file):
+ self.assertEqual(config.password_context.encrypt('my password'),
+ 'my password')