diff options
| -rw-r--r-- | src/mailman/config/config.py | 3 | ||||
| -rw-r--r-- | src/mailman/docs/NEWS.rst | 7 | ||||
| -rw-r--r-- | src/mailman/rest/docs/listconf.rst (renamed from src/mailman/rest/docs/configuration.rst) | 0 | ||||
| -rw-r--r-- | src/mailman/rest/docs/systemconf.rst | 34 | ||||
| -rw-r--r-- | src/mailman/rest/listconf.py (renamed from src/mailman/rest/configuration.py) | 0 | ||||
| -rw-r--r-- | src/mailman/rest/lists.py | 2 | ||||
| -rw-r--r-- | src/mailman/rest/root.py | 33 | ||||
| -rw-r--r-- | src/mailman/rest/tests/test_listconf.py (renamed from src/mailman/rest/tests/test_configuration.py) | 0 | ||||
| -rw-r--r-- | src/mailman/rest/tests/test_root.py | 12 | ||||
| -rw-r--r-- | src/mailman/rest/tests/test_systemconf.py | 181 | ||||
| -rw-r--r-- | src/mailman/runners/docs/rest.rst | 2 |
11 files changed, 257 insertions, 17 deletions
diff --git a/src/mailman/config/config.py b/src/mailman/config/config.py index b2f400fc3..acddd1089 100644 --- a/src/mailman/config/config.py +++ b/src/mailman/config/config.py @@ -98,6 +98,9 @@ class Configuration: """Delegate to the configuration object.""" return getattr(self._config, name) + def __iter__(self): + return iter(self._config) + def load(self, filename=None): """Load the configuration from the schema and config files.""" schema_file = resource_filename('mailman.config', 'schema.cfg') diff --git a/src/mailman/docs/NEWS.rst b/src/mailman/docs/NEWS.rst index a219038eb..63c517f44 100644 --- a/src/mailman/docs/NEWS.rst +++ b/src/mailman/docs/NEWS.rst @@ -76,6 +76,13 @@ REST Given by Aurélien Bompard based on work by Nicolas Karageuzian. * The ``/3.0/system`` path is deprecated; use ``/3.0/system/versions`` to get the system version information. + * You can access the system configuration via the resource path + ``/3.0/system/configuration/<section>``. This returns a dictionary with + the keys being the section's variables and the values being their value + from ``mailman.cfg`` as verbatim strings. You can get a list of all + section names via ``/3.0/system/configuration`` which returns a dictionary + containing the ``http_etag`` and the section names as a sorted list under + the ``sections`` key. The system configuration resource is read-only. 3.0 beta 4 -- "Time and Motion" diff --git a/src/mailman/rest/docs/configuration.rst b/src/mailman/rest/docs/listconf.rst index 841ab3c27..841ab3c27 100644 --- a/src/mailman/rest/docs/configuration.rst +++ b/src/mailman/rest/docs/listconf.rst diff --git a/src/mailman/rest/docs/systemconf.rst b/src/mailman/rest/docs/systemconf.rst new file mode 100644 index 000000000..66953f4ba --- /dev/null +++ b/src/mailman/rest/docs/systemconf.rst @@ -0,0 +1,34 @@ +==================== +System configuration +==================== + +The entire system configuration is available through the REST API. You can +get a list of all defined sections. + + >>> dump_json('http://localhost:9001/3.0/system/configuration') + http_etag: ... + sections: ['antispam', 'archiver.mail_archive', 'archiver.master', ... + +You can also get all the values for a particular section. + + >>> dump_json('http://localhost:9001/3.0/system/configuration/mailman') + default_language: en + email_commands_max_lines: 10 + filtered_messages_are_preservable: no + http_etag: ... + layout: testing + noreply_address: noreply + pending_request_life: 3d + post_hook: + pre_hook: + sender_headers: from from_ reply-to sender + site_owner: noreply@example.com + +Dotted section names work too, for example, to get the French language +settings section. + + >>> dump_json('http://localhost:9001/3.0/system/configuration/language.fr') + charset: iso-8859-1 + description: French + enabled: yes + http_etag: ... diff --git a/src/mailman/rest/configuration.py b/src/mailman/rest/listconf.py index 6cf54a00e..6cf54a00e 100644 --- a/src/mailman/rest/configuration.py +++ b/src/mailman/rest/listconf.py diff --git a/src/mailman/rest/lists.py b/src/mailman/rest/lists.py index 866c6211f..a8546b95e 100644 --- a/src/mailman/rest/lists.py +++ b/src/mailman/rest/lists.py @@ -39,7 +39,7 @@ from mailman.interfaces.mailinglist import IListArchiverSet from mailman.interfaces.member import MemberRole from mailman.interfaces.styles import IStyleManager from mailman.interfaces.subscriptions import ISubscriptionService -from mailman.rest.configuration import ListConfiguration +from mailman.rest.listconf import ListConfiguration from mailman.rest.helpers import ( CollectionMixin, GetterSetter, NotFound, bad_request, child, created, etag, no_content, not_found, okay, paginate, path_to) diff --git a/src/mailman/rest/root.py b/src/mailman/rest/root.py index 8cbe4061a..34f058bf2 100644 --- a/src/mailman/rest/root.py +++ b/src/mailman/rest/root.py @@ -32,7 +32,7 @@ from mailman.interfaces.listmanager import IListManager from mailman.rest.addresses import AllAddresses, AnAddress from mailman.rest.domains import ADomain, AllDomains from mailman.rest.helpers import ( - BadRequest, NotFound, child, etag, okay, path_to) + BadRequest, NotFound, child, etag, not_found, okay, path_to) from mailman.rest.lists import AList, AllLists, Styles from mailman.rest.members import AMember, AllMembers, FindMembers from mailman.rest.preferences import ReadOnlyPreferences @@ -88,6 +88,27 @@ class Versions: okay(response, etag(resource)) +class SystemConfiguration: + def __init__(self, section=None): + self._section = section + + def on_get(self, request, response): + if self._section is None: + resource = dict( + sections=sorted(section.name for section in config)) + okay(response, etag(resource)) + return + missing = object() + section = getattr(config, self._section, missing) + if section is missing: + not_found(response) + return + # Sections don't have .keys(), .values(), or .items() but we can + # iterate over them. + resource = {key: section[key] for key in section} + okay(response, etag(resource)) + + class TopLevel: """Top level collections and entries.""" @@ -97,12 +118,18 @@ class TopLevel: if len(segments) == 0: # This provides backward compatibility; see /system/versions. return Versions() - elif len(segments) > 1: - return BadRequest(), [] elif segments[0] == 'preferences': + if len(segments) > 1: + return BadRequest(), [] return ReadOnlyPreferences(system_preferences, 'system'), [] elif segments[0] == 'versions': + if len(segments) > 1: + return BadRequest(), [] return Versions(), [] + elif segments[0] == 'configuration': + if len(segments) <= 2: + return SystemConfiguration(*segments[1:]), [] + return BadRequest(), [] else: return NotFound(), [] diff --git a/src/mailman/rest/tests/test_configuration.py b/src/mailman/rest/tests/test_listconf.py index d013cdce9..d013cdce9 100644 --- a/src/mailman/rest/tests/test_configuration.py +++ b/src/mailman/rest/tests/test_listconf.py diff --git a/src/mailman/rest/tests/test_root.py b/src/mailman/rest/tests/test_root.py index 7f3535496..59cd93637 100644 --- a/src/mailman/rest/tests/test_root.py +++ b/src/mailman/rest/tests/test_root.py @@ -19,7 +19,6 @@ __all__ = [ 'TestRoot', - 'TestSystemConfiguration', ] @@ -64,12 +63,6 @@ class TestRoot(unittest.TestCase): call_api('http://localhost:9001/3.0/does-not-exist') self.assertEqual(cm.exception.code, 404) - def test_system_url_too_long(self): - # /system/foo/bar is not allowed. - with self.assertRaises(HTTPError) as cm: - call_api('http://localhost:9001/3.0/system/foo/bar') - self.assertEqual(cm.exception.code, 400) - def test_system_url_not_preferences(self): # /system/foo where `foo` is not `preferences`. with self.assertRaises(HTTPError) as cm: @@ -130,8 +123,3 @@ class TestRoot(unittest.TestCase): self.assertEqual(content['title'], '401 Unauthorized') self.assertEqual(content['description'], 'User is not authorized for the REST API') - - - -class TestSystemConfiguration(unittest.TestCase): - layer = RESTLayer diff --git a/src/mailman/rest/tests/test_systemconf.py b/src/mailman/rest/tests/test_systemconf.py new file mode 100644 index 000000000..2eb4fa251 --- /dev/null +++ b/src/mailman/rest/tests/test_systemconf.py @@ -0,0 +1,181 @@ +# Copyright (C) 2014 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 system configuration read-only access.""" + +__all__ = [ + 'TestSystemConfiguration', + ] + + +import unittest + +from mailman.testing.helpers import call_api +from mailman.testing.layers import RESTLayer +from six.moves.urllib_error import HTTPError + + + +class TestSystemConfiguration(unittest.TestCase): + layer = RESTLayer + maxDiff = None + + def test_basic_system_configuration(self): + # Read some basic system configuration value, just to prove that the + # infrastructure works. + url = 'http://localhost:9001/3.0/system/configuration/mailman' + json, response = call_api(url) + # There must be an `http_etag` key, but we don't care about its value. + self.assertIn('http_etag', json) + del json['http_etag'] + self.assertEqual(json, dict( + site_owner='noreply@example.com', + noreply_address='noreply', + default_language='en', + sender_headers='from from_ reply-to sender', + email_commands_max_lines='10', + pending_request_life='3d', + pre_hook='', + post_hook='', + layout='testing', + filtered_messages_are_preservable='no', + )) + + def test_dotted_section(self): + # A dotted section works too. + url = 'http://localhost:9001/3.0/system/configuration/language.fr' + json, response = call_api(url) + # There must be an `http_etag` key, but we don't care about its value. + self.assertIn('http_etag', json) + del json['http_etag'] + self.assertEqual(json, dict( + description='French', + charset='iso-8859-1', + enabled='yes', + )) + + def test_multiline(self): + # Some values contain multiple lines. It is up to the client to split + # on whitespace. + url = 'http://localhost:9001/3.0/system/configuration/nntp' + json, response = call_api(url) + value = json['remove_headers'] + self.assertEqual(sorted(value.split()), [ + 'date-received', + 'nntp-posting-date', + 'nntp-posting-host', + 'posted', + 'posting-version', + 'received', + 'relay-version', + 'x-complaints-to', + 'x-trace', + 'xref', + ]) + + + def test_all_sections(self): + # Getting the top level configuration object returns a list of all + # existing sections. + url = 'http://localhost:9001/3.0/system/configuration' + json, response = call_api(url) + self.assertIn('http_etag', json) + self.assertEqual(sorted(json['sections']), [ + 'antispam', + 'archiver.mail_archive', + 'archiver.master', + 'archiver.mhonarc', + 'archiver.prototype', + 'bounces', + 'database', + 'devmode', + 'digests', + 'language.en', + 'language.fr', + 'language.ja', + 'logging.archiver', + 'logging.bounce', + 'logging.config', + 'logging.database', + 'logging.debug', + 'logging.error', + 'logging.fromusenet', + 'logging.http', + 'logging.locks', + 'logging.mischief', + 'logging.root', + 'logging.runner', + 'logging.smtp', + 'logging.subscribe', + 'logging.vette', + 'mailman', + 'mta', + 'nntp', + 'passwords', + 'paths.dev', + 'paths.fhs', + 'paths.local', + 'paths.testing', + 'runner.archive', + 'runner.bad', + 'runner.bounces', + 'runner.command', + 'runner.digest', + 'runner.in', + 'runner.lmtp', + 'runner.nntp', + 'runner.out', + 'runner.pipeline', + 'runner.rest', + 'runner.retry', + 'runner.shunt', + 'runner.virgin', + 'shell', + 'styles', + 'webservice', + ]) + + def test_no_such_section(self): + # A bogus section returns a 404. + url = 'http://localhost:9001/3.0/system/configuration/nosuchsection' + with self.assertRaises(HTTPError) as cm: + call_api(url) + self.assertEqual(cm.exception.code, 404) + + def test_too_many_path_components(self): + # More than two path components is an error, even if they name a valid + # configuration variable. + url = 'http://localhost:9001/3.0/system/configuration/mailman/layout' + with self.assertRaises(HTTPError) as cm: + call_api(url) + self.assertEqual(cm.exception.code, 400) + + def test_read_only(self): + # The entire configuration is read-only. + url = 'http://localhost:9001/3.0/system/configuration' + with self.assertRaises(HTTPError) as cm: + call_api(url, {'foo': 'bar'}) + # 405 is Method Not Allowed. + self.assertEqual(cm.exception.code, 405) + + def test_section_read_only(self): + # Sections are also read-only. + url = 'http://localhost:9001/3.0/system/configuration/mailman' + with self.assertRaises(HTTPError) as cm: + call_api(url, {'foo': 'bar'}) + # 405 is Method Not Allowed. + self.assertEqual(cm.exception.code, 405) diff --git a/src/mailman/runners/docs/rest.rst b/src/mailman/runners/docs/rest.rst index 9e8851eca..71a059ae1 100644 --- a/src/mailman/runners/docs/rest.rst +++ b/src/mailman/runners/docs/rest.rst @@ -14,7 +14,7 @@ The RESTful server can be used to access basic version information. http_etag: "..." mailman_version: GNU Mailman 3.0... (...) python_version: ... - self_link: http://localhost:9001/3.0/system + self_link: http://localhost:9001/3.0/system/versions Clean up |
