From 0a942a671a06d65b6355b383a5d140dc93ad9587 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Sun, 22 Apr 2012 12:17:20 -0400 Subject: - Modernize the helpers.py module by using print() functions. - Rewrite soem of the code to use the new transaction context manager, instead of explicitly accessing the config.db global. --- src/mailman/testing/helpers.py | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) (limited to 'src/mailman/testing/helpers.py') diff --git a/src/mailman/testing/helpers.py b/src/mailman/testing/helpers.py index 3648a6710..62e3b9d2a 100644 --- a/src/mailman/testing/helpers.py +++ b/src/mailman/testing/helpers.py @@ -17,7 +17,7 @@ """Various test helpers.""" -from __future__ import absolute_import, unicode_literals +from __future__ import absolute_import, print_function, unicode_literals __metaclass__ = type __all__ = [ @@ -64,6 +64,7 @@ from zope.component import getUtility from mailman.bin.master import Loop as Master from mailman.config import config +from mailman.database.transaction import transaction from mailman.email.message import Message from mailman.interfaces.member import MemberRole from mailman.interfaces.messages import IMessageStore @@ -237,13 +238,14 @@ def get_lmtp_client(quiet=False): # It's possible the process has started but is not yet accepting # connections. Wait a little while. lmtp = LMTP() + #lmtp.debuglevel = 1 until = datetime.datetime.now() + as_timedelta(config.devmode.wait) while datetime.datetime.now() < until: try: response = lmtp.connect( config.mta.lmtp_host, int(config.mta.lmtp_port)) if not quiet: - print response + print(response) return lmtp except socket.error as error: if error[0] == errno.ECONNREFUSED: @@ -397,19 +399,19 @@ def subscribe(mlist, first_name, role=MemberRole.member): user_manager = getUtility(IUserManager) email = '{0}person@example.com'.format(first_name[0].lower()) full_name = '{0} Person'.format(first_name) - person = user_manager.get_user(email) - if person is None: - address = user_manager.get_address(email) - if address is None: - person = user_manager.create_user(email, full_name) + with transaction(): + person = user_manager.get_user(email) + if person is None: + address = user_manager.get_address(email) + if address is None: + person = user_manager.create_user(email, full_name) + preferred_address = list(person.addresses)[0] + mlist.subscribe(preferred_address, role) + else: + mlist.subscribe(address, role) + else: preferred_address = list(person.addresses)[0] mlist.subscribe(preferred_address, role) - else: - mlist.subscribe(address, role) - else: - preferred_address = list(person.addresses)[0] - mlist.subscribe(preferred_address, role) - config.db.commit() @@ -437,9 +439,9 @@ def reset_the_world(): os.remove(os.path.join(dirpath, filename)) # Clear out messages in the message store. message_store = getUtility(IMessageStore) - for message in message_store.messages: - message_store.delete_message(message['message-id']) - config.db.commit() + with transaction(): + for message in message_store.messages: + message_store.delete_message(message['message-id']) # Reset the global style manager. getUtility(IStyleManager).populate() # Remove all dynamic header-match rules. -- cgit v1.2.3-70-g09d2 From 3e2231b8835820d240112e7e07cff2a369f178f2 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Mon, 2 Jul 2012 16:08:58 -0400 Subject: * `passlib`_ is now used for all password hashing instead of flufl.password. The default hash is `sha512_crypt`. * 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. * 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. --- src/mailman/app/membership.py | 4 +- src/mailman/app/tests/test_membership.py | 19 +----- src/mailman/chains/accept.py | 11 +--- src/mailman/chains/base.py | 12 ---- src/mailman/chains/discard.py | 11 +--- src/mailman/chains/docs/moderation.rst | 14 ++-- src/mailman/chains/hold.py | 10 +-- src/mailman/chains/owner.py | 10 +-- src/mailman/chains/reject.py | 12 ++-- src/mailman/chains/tests/test_hold.py | 12 ++-- src/mailman/chains/tests/test_owner.py | 8 ++- src/mailman/config/config.py | 12 ++-- src/mailman/config/passlib.cfg | 10 +++ src/mailman/config/schema.cfg | 12 ++-- src/mailman/config/tests/__init__.py | 0 src/mailman/config/tests/test_configuration.py | 53 +++++++++++++++ src/mailman/core/initialize.py | 4 ++ src/mailman/docs/NEWS.rst | 29 ++++++++- src/mailman/interfaces/chain.py | 37 +++++++++++ src/mailman/interfaces/configuration.py | 41 ++++++++++++ src/mailman/rest/users.py | 3 +- src/mailman/rules/approved.py | 12 +++- src/mailman/rules/docs/approved.rst | 4 +- src/mailman/rules/docs/header-matching.rst | 10 +-- src/mailman/rules/tests/test_approved.py | 89 ++++++++++++++++++++++++-- src/mailman/runners/docs/incoming.rst | 10 +-- src/mailman/testing/helpers.py | 8 ++- src/mailman/testing/layers.py | 2 - src/mailman/testing/passlib.cfg | 4 ++ src/mailman/testing/testing.cfg | 3 + src/mailman/utilities/passwords.py | 62 ++++++++++-------- src/mailman/utilities/tests/test_passwords.py | 60 +++++++++++++++++ 32 files changed, 438 insertions(+), 150 deletions(-) create mode 100644 src/mailman/config/passlib.cfg create mode 100644 src/mailman/config/tests/__init__.py create mode 100644 src/mailman/config/tests/test_configuration.py create mode 100644 src/mailman/interfaces/configuration.py create mode 100644 src/mailman/testing/passlib.cfg create mode 100644 src/mailman/utilities/tests/test_passwords.py (limited to 'src/mailman/testing/helpers.py') diff --git a/src/mailman/app/membership.py b/src/mailman/app/membership.py index e11cef44a..c73735b35 100644 --- a/src/mailman/app/membership.py +++ b/src/mailman/app/membership.py @@ -30,6 +30,7 @@ from email.utils import formataddr 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 +39,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 @@ -96,7 +96,7 @@ def add_member(mlist, email, display_name, password, delivery_mode, language, display_name if display_name else address.display_name) user.link(address) # Encrypt the password using the currently selected hash scheme. - user.password = encrypt(password) + 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/tests/test_membership.py b/src/mailman/app/tests/test_membership.py index c3c310b6c..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. @@ -178,22 +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: passlib.hash.sha1_crypt - """) - - 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, - '{sha1_crypt}$sha1$40000$$nY5NBnPWWAD5KI4X8Jjzp7.1YhV6') + 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,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 AcceptEvent log = logging.getLogger('mailman.vette') 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,25 +22,20 @@ 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') - + Subject: aardvark Hits: @@ -109,7 +109,7 @@ moderator approval. >>> with event_subscribers(on_chain): ... process(mlist, msg, {}, 'default-posting-chain') - + 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') - + 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') - + Subject: dingo Hits: @@ -197,7 +197,7 @@ moderator approval. >>> with event_subscribers(on_chain): ... process(mlist, msg, {}, 'default-posting-chain') - + 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,19 +29,15 @@ 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,27 +22,23 @@ 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') 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: # 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'], '') self.assertTrue(isinstance(event.chain, BuiltInOwnerChain)) diff --git a/src/mailman/config/config.py b/src/mailman/config/config.py index fc82a2be7..0cd5e0e32 100644 --- a/src/mailman/config/config.py +++ b/src/mailman/config/config.py @@ -32,11 +32,14 @@ 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 @@ -46,11 +49,6 @@ from mailman.utilities.modules import call_name SPACE = ' ' - -class IConfiguration(Interface): - """Marker interface; used for adaptation in the REST API.""" - - @implementer(IConfiguration) class Configuration: @@ -70,6 +68,7 @@ class Configuration: self.handlers = {} self.pipelines = {} self.commands = {} + self.password_context = None def _clear(self): """Clear the cached configuration variables.""" @@ -138,6 +137,7 @@ class Configuration: # 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.""" 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 8d17d806c..e3c6e607c 100644 --- a/src/mailman/config/schema.cfg +++ b/src/mailman/config/schema.cfg @@ -154,11 +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. This is the path to a passlib hash -# algorithm. See http://packages.python.org/passlib/lib/passlib.hash.html -password_scheme: passlib.hash.pbkdf2_sha512 +# 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 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 . + +"""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/initialize.py b/src/mailman/core/initialize.py index 389a45f3b..255e22ed9 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.utilities.passwords import initialize as initialize_passwords + initialize_passwords() # 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. diff --git a/src/mailman/docs/NEWS.rst b/src/mailman/docs/NEWS.rst index fb819ccc4..c3df9382d 100644 --- a/src/mailman/docs/NEWS.rst +++ b/src/mailman/docs/NEWS.rst @@ -14,8 +14,10 @@ Here is a history of user visible changes to Mailman. Architecture ------------ + * `passlib`_ is now used for all password hashing instead of flufl.password. * Internally, all datetimes are kept in the UTC timezone, however because of - LP: #280708, they are stored in the database in naive format. + LP: #280708, they are stored in the database in naive format. The default + hash is `sha512_crypt`. * `received_time` is now added to the message metadata by the LMTP runner instead of by `Switchboard.enqueue()`. This latter no longer depends on `received_time` in the metadata. @@ -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.]` 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', ] @@ -33,6 +39,37 @@ from flufl.enum import Enum 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. 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 . + +"""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/rest/users.py b/src/mailman/rest/users.py index 6bab7c789..bce541ae5 100644 --- a/src/mailman/rest/users.py +++ b/src/mailman/rest/users.py @@ -38,7 +38,6 @@ 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 @@ -103,7 +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)) - user.password = encrypt(password) + 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 dadc25322..3ff7d21ec 100644 --- a/src/mailman/rules/approved.py +++ b/src/mailman/rules/approved.py @@ -30,9 +30,9 @@ import re from email.iterators import typed_subpart_iterator from zope.interface import implementer +from mailman.config import config from mailman.core.i18n import _ from mailman.interfaces.rules import IRule -from mailman.utilities.passwords import verify EMPTYSTRING = '' @@ -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 7f0714e17..3f3d54455 100644 --- a/src/mailman/rules/docs/approved.rst +++ b/src/mailman/rules/docs/approved.rst @@ -20,8 +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 mailman.utilities.passwords import encrypt - >>> mlist.moderator_password = encrypt('super secret') + >>> 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 + HoldEvent hold >>> 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 + DiscardEvent discard 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 de409f654..a1b8f99ac 100644 --- a/src/mailman/rules/tests/test_approved.py +++ b/src/mailman/rules/tests/test_approved.py @@ -25,17 +25,20 @@ __all__ = [ 'TestApprovedNonASCII', 'TestApprovedPseudoHeader', 'TestApprovedPseudoHeaderMIME', + 'TestPasswordHashMigration', ] +import os import unittest 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 -from mailman.utilities.passwords import encrypt @@ -46,7 +49,8 @@ class TestApproved(unittest.TestCase): def setUp(self): self._mlist = create_list('test@example.com') - self._mlist.moderator_password = encrypt('super secret') + self._mlist.moderator_password = config.password_context.encrypt( + 'super secret') self._rule = approved.Approved() self._msg = mfs("""\ From: anne@example.com @@ -147,7 +151,8 @@ class TestApprovedPseudoHeader(unittest.TestCase): def setUp(self): self._mlist = create_list('test@example.com') - self._mlist.moderator_password = encrypt('super secret') + self._mlist.moderator_password = config.password_context.encrypt( + 'super secret') self._rule = approved.Approved() self._msg = mfs("""\ From: anne@example.com @@ -279,7 +284,8 @@ class TestApprovedPseudoHeaderMIME(unittest.TestCase): def setUp(self): self._mlist = create_list('test@example.com') - self._mlist.moderator_password = encrypt('super secret') + self._mlist.moderator_password = config.password_context.encrypt( + 'super secret') self._rule = approved.Approved() self._msg_text_template = """\ From: anne@example.com @@ -410,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: +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() - + 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() - + 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() - + From: aperson@example.com To: test@example.com diff --git a/src/mailman/testing/helpers.py b/src/mailman/testing/helpers.py index 62e3b9d2a..5252c5334 100644 --- a/src/mailman/testing/helpers.py +++ b/src/mailman/testing/helpers.py @@ -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 7a019f53a..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: passlib.hash.plaintext [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 index b9981f057..65c4ff291 100644 --- a/src/mailman/utilities/passwords.py +++ b/src/mailman/utilities/passwords.py @@ -21,37 +21,49 @@ from __future__ import absolute_import, print_function, unicode_literals __metaclass__ = type __all__ = [ - 'encrypt', - 'verify', + 'initialize', ] -import re -from passlib.registry import get_crypt_handler +from passlib.context import CryptContext +from pkg_resources import resource_string +from zope import event -from mailman.config import config -from mailman.testing import layers -from mailman.utilities.modules import find_name +from mailman.interfaces.configuration import ConfigurationUpdatedEvent -SCHEME_RE = r'{(?P[^}]+?)}(?P.*)'.encode() + + +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 encrypt(secret): - hasher = find_name(config.passwords.password_scheme) - # For reproducibility, don't use any salt in the test suite. - kws = {} - if layers.is_testing and 'salt' in hasher.setting_kwds: - kws['salt'] = b'' - hashed = hasher.encrypt(secret, **kws) - return b'{{{0}}}{1}'.format(hasher.name, hashed) - - -def verify(hashed, password): - mo = re.match(SCHEME_RE, hashed, re.IGNORECASE) - if not mo: - return False - scheme, secret = mo.groups(('scheme', 'rest')) - hasher = get_crypt_handler(scheme) - return hasher.verify(password, secret) +# Create and register a post-processing handler for the configuration file. + +def _update_context(event): + if isinstance(event, ConfigurationUpdatedEvent): + # Just reset the password context. + event.config.password_context = PasswordContext(event.config) + + +def initialize(): + event.subscribers.append(_update_context) 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 . + +"""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') -- cgit v1.2.3-70-g09d2 From bbbf437a64e2bcf2ac72c329f0faf53ed91f3432 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Mon, 2 Jul 2012 16:56:35 -0400 Subject: More consistency in the way post-configuration changes are processed. Less magic in the _post_process() method, more ConfigurationUpdatedEvents. More centralization of event initialization. Added property Configuration.language_configs. Instead of initializing events in initialize_2(), initialize them in initialize_1() and do it before the configuration is loaded. --- src/mailman/app/events.py | 11 ++++++++++- src/mailman/config/config.py | 26 ++++++-------------------- src/mailman/core/i18n.py | 8 +++++++- src/mailman/core/initialize.py | 6 ++---- src/mailman/core/switchboard.py | 30 ++++++++++++++++++------------ src/mailman/languages/manager.py | 18 ++++++++++++++++++ src/mailman/styles/manager.py | 9 +++++++++ src/mailman/testing/helpers.py | 4 ++-- src/mailman/utilities/passwords.py | 11 ++--------- 9 files changed, 74 insertions(+), 49 deletions(-) (limited to 'src/mailman/testing/helpers.py') 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/config/config.py b/src/mailman/config/config.py index 0cd5e0e32..f6c39fcec 100644 --- a/src/mailman/config/config.py +++ b/src/mailman/config/config.py @@ -41,7 +41,6 @@ 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 @@ -117,26 +116,7 @@ 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): @@ -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/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 255e22ed9..f4659e638 100644 --- a/src/mailman/core/initialize.py +++ b/src/mailman/core/initialize.py @@ -112,8 +112,8 @@ def initialize_1(config_path=None): os.umask(007) # Initialize configuration event subscribers. This must be done before # setting up the configuration system. - from mailman.utilities.passwords import initialize as initialize_passwords - initialize_passwords() + 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. @@ -156,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 @@ -165,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/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/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 5252c5334..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: diff --git a/src/mailman/utilities/passwords.py b/src/mailman/utilities/passwords.py index 65c4ff291..95c85c47a 100644 --- a/src/mailman/utilities/passwords.py +++ b/src/mailman/utilities/passwords.py @@ -21,14 +21,13 @@ from __future__ import absolute_import, print_function, unicode_literals __metaclass__ = type __all__ = [ - 'initialize', + 'handle_ConfigurationUpdatedEvent', ] from passlib.context import CryptContext from pkg_resources import resource_string -from zope import event from mailman.interfaces.configuration import ConfigurationUpdatedEvent @@ -57,13 +56,7 @@ class PasswordContext: -# Create and register a post-processing handler for the configuration file. - -def _update_context(event): +def handle_ConfigurationUpdatedEvent(event): if isinstance(event, ConfigurationUpdatedEvent): # Just reset the password context. event.config.password_context = PasswordContext(event.config) - - -def initialize(): - event.subscribers.append(_update_context) -- cgit v1.2.3-70-g09d2