summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/mailman/config/config.py3
-rw-r--r--src/mailman/docs/NEWS.rst7
-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.rst34
-rw-r--r--src/mailman/rest/listconf.py (renamed from src/mailman/rest/configuration.py)0
-rw-r--r--src/mailman/rest/lists.py2
-rw-r--r--src/mailman/rest/root.py33
-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.py12
-rw-r--r--src/mailman/rest/tests/test_systemconf.py181
-rw-r--r--src/mailman/runners/docs/rest.rst2
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