summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJ08nY2017-06-01 20:53:53 +0200
committerJ08nY2017-08-07 17:39:07 +0200
commita665dccf9404d6f95d8a4587f05d748b504e1f9d (patch)
treea2bc4f3e4cbb443224d66a5cae4b78131b4e4213
parent1f1a35e7ccde1cfe239286a9a6333ebe8882d8f3 (diff)
downloadmailman-a665dccf9404d6f95d8a4587f05d748b504e1f9d.tar.gz
mailman-a665dccf9404d6f95d8a4587f05d748b504e1f9d.tar.zst
mailman-a665dccf9404d6f95d8a4587f05d748b504e1f9d.zip
-rw-r--r--src/mailman/app/docs/plugins.rst3
-rw-r--r--src/mailman/interfaces/plugin.py6
-rw-r--r--src/mailman/rest/docs/plugins.rst59
-rw-r--r--src/mailman/rest/plugins.py68
-rw-r--r--src/mailman/rest/root.py15
-rw-r--r--src/mailman/rest/tests/test_plugins.py104
-rw-r--r--src/mailman/testing/plugin.py69
7 files changed, 322 insertions, 2 deletions
diff --git a/src/mailman/app/docs/plugins.rst b/src/mailman/app/docs/plugins.rst
index 42038b654..a66e4413d 100644
--- a/src/mailman/app/docs/plugins.rst
+++ b/src/mailman/app/docs/plugins.rst
@@ -113,6 +113,9 @@ initialization process.
... global counter
... print('post-hook:', counter)
... counter += 1
+ ...
+ ... def rest_object(self):
+ ... pass
... """, file=fp)
>>> fp.close()
diff --git a/src/mailman/interfaces/plugin.py b/src/mailman/interfaces/plugin.py
index 9e226824f..f1b7212f0 100644
--- a/src/mailman/interfaces/plugin.py
+++ b/src/mailman/interfaces/plugin.py
@@ -30,3 +30,9 @@ class IPlugin(Interface):
def post_hook():
"""A plugin hook called in the second initialization step. After DB."""
+
+ def rest_object():
+ """Return the REST object which will be hooked up to the REST api.
+
+ This object will be served at the /<api>/plugins/<plugin.name>/ root.
+ """
diff --git a/src/mailman/rest/docs/plugins.rst b/src/mailman/rest/docs/plugins.rst
new file mode 100644
index 000000000..787726e55
--- /dev/null
+++ b/src/mailman/rest/docs/plugins.rst
@@ -0,0 +1,59 @@
+=========
+ Plugins
+=========
+
+Plugins can supply REST routes.
+::
+
+ >>> dump_json('http://localhost:9001/3.1/plugins')
+ entry 0:
+ class: mailman.testing.plugin.ExamplePlugin
+ enable: True
+ http_etag: "..."
+ name: example
+ path:
+ http_etag: "..."
+ start: 0
+ total_size: 1
+
+ >>> dump_json('http://localhost:9001/3.1/plugins/example')
+ example-plugin-reply: yes
+ http_etag: "..."
+
+ >>> dump_json('http://localhost:9001/3.1/plugins/example/good')
+ good: True
+ http_etag: "..."
+
+ >>> dump_json('http://localhost:9001/3.1/plugins/example/bad')
+ Traceback (most recent call last):
+ ...
+ urllib.error.HTTPError: HTTP Error 400: ...
+
+For example this route that presents a counter.
+::
+
+ >>> dump_json('http://localhost:9001/3.1/plugins/example/count')
+ count: 0
+ http_etag: "..."
+
+ >>> call_http('http://localhost:9001/3.1/plugins/example/count',
+ ... {'count':3}, method='POST')
+ content-length: 0
+ date: ...
+ server: ...
+ status: 204
+
+ >>> dump_json('http://localhost:9001/3.1/plugins/example/count')
+ count: 3
+ http_etag: "..."
+
+ >>> call_http('http://localhost:9001/3.1/plugins/example/count',
+ ... method='DELETE')
+ content-length: 0
+ date: ...
+ server: ...
+ status: 204
+
+ >>> dump_json('http://localhost:9001/3.1/plugins/example/count')
+ count: 0
+ http_etag: "..."
diff --git a/src/mailman/rest/plugins.py b/src/mailman/rest/plugins.py
new file mode 100644
index 000000000..ac8fc0ace
--- /dev/null
+++ b/src/mailman/rest/plugins.py
@@ -0,0 +1,68 @@
+# Copyright (C) 2010-2017 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/>.
+
+"""REST for plugins, dynamically proxies requests to plugin's rest_object."""
+
+from lazr.config import as_boolean
+from mailman.config import config
+from mailman.rest.helpers import CollectionMixin, NotFound, etag, okay
+from public import public
+
+
+@public
+class AllPlugins(CollectionMixin):
+ """Read-only list of all plugin configs."""
+
+ def _resource_as_dict(self, plugin_config):
+ """See `CollectionMixin`."""
+ resource = {
+ 'name': plugin_config.name.split('.')[-1],
+ 'class': plugin_config['class'],
+ 'enable': as_boolean(plugin_config['enable']),
+ 'path': plugin_config['path']
+ }
+ return resource
+
+ def _get_collection(self, request):
+ """See `CollectionMixin`."""
+ return config.plugin_configs
+
+ def on_get(self, request, response):
+ """/plugins"""
+ resource = self._make_collection(request)
+ okay(response, etag(resource))
+
+
+@public
+class APlugin:
+ """REST proxy to the plugins rest_object."""
+
+ def __init__(self, plugin_name):
+ self.__rest_object = None
+ if plugin_name in config.plugins.keys():
+ plugin = config.plugins[plugin_name]
+ self.__rest_object = plugin.rest_object()
+ # If the plugin doesn't exist or doesn't provide a rest_object,
+ # just proxy to NotFound
+ if self.__rest_object is None:
+ self.__rest_object = NotFound()
+
+ def __getattr__(self, attrib):
+ return getattr(self.__rest_object, attrib)
+
+ def __dir__(self):
+ return dir(self.__rest_object)
diff --git a/src/mailman/rest/root.py b/src/mailman/rest/root.py
index 76289078b..46116aba6 100644
--- a/src/mailman/rest/root.py
+++ b/src/mailman/rest/root.py
@@ -30,6 +30,7 @@ from mailman.rest.helpers import (
BadRequest, NotFound, child, etag, no_content, not_found, okay)
from mailman.rest.lists import AList, AllLists, Styles
from mailman.rest.members import AMember, AllMembers, FindMembers
+from mailman.rest.plugins import APlugin, AllPlugins
from mailman.rest.preferences import ReadOnlyPreferences
from mailman.rest.queues import AQueue, AQueueFile, AllQueues
from mailman.rest.templates import TemplateFinder
@@ -308,6 +309,20 @@ class TopLevel:
return BadRequest(), []
@child()
+ def plugins(self, context, segments):
+ """/<api>/plugins
+ /<api>/plugins/<plugin_name>
+ /<api>/plugins/<plugin_name>/...
+ """
+ if self.api.version_info < (3, 1):
+ return NotFound(), []
+ if len(segments) == 0:
+ return AllPlugins(), []
+ else:
+ plugin_name = segments.pop(0)
+ return APlugin(plugin_name), segments
+
+ @child()
def bans(self, context, segments):
"""/<api>/bans
/<api>/bans/<email>
diff --git a/src/mailman/rest/tests/test_plugins.py b/src/mailman/rest/tests/test_plugins.py
new file mode 100644
index 000000000..f6d7afb76
--- /dev/null
+++ b/src/mailman/rest/tests/test_plugins.py
@@ -0,0 +1,104 @@
+# Copyright (C) 2010-2017 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 RESTability of plugins."""
+
+import unittest
+
+from mailman.testing.helpers import call_api
+from mailman.testing.layers import RESTLayer
+from urllib.error import HTTPError
+
+
+class TestAPI31Plugins(unittest.TestCase):
+ layer = RESTLayer
+
+ def test_list_plugins(self):
+ json, response = call_api('http://localhost:9001/3.1/plugins')
+ self.assertEqual(response.status_code, 200)
+ json.pop('http_etag')
+ self.assertEqual(len(json.keys()), 3)
+ self.assertIn('entries', json.keys())
+ entries = json['entries']
+ self.assertEqual(len(entries), 1)
+ plugin_conf = entries[0]
+ self.assertEqual(plugin_conf['name'], 'example')
+ self.assertEqual(plugin_conf['enable'], True)
+ self.assertEqual(plugin_conf['class'],
+ 'mailman.testing.plugin.ExamplePlugin')
+ self.assertEqual(plugin_conf['path'], '')
+
+ def test_non_existent_plugin(self):
+ with self.assertRaises(HTTPError) as cm:
+ call_api('http://localhost:9001/3.1/plugins/does-not-exist')
+ self.assertEqual(cm.exception.code, 404)
+
+ def test_example_plugin(self):
+ json, response = call_api('http://localhost:9001/3.1/plugins/example')
+ self.assertEqual(response.status_code, 200)
+ json.pop('http_etag')
+ self.assertEqual(len(json.keys()), 1)
+ self.assertEqual(json['example-plugin-reply'], 'yes')
+
+ with self.assertRaises(HTTPError) as cm:
+ call_api('http://localhost:9001/3.1/plugins/example',
+ method='POST')
+ self.assertEqual(cm.exception.code, 400)
+
+ def test_example_plugin_child(self):
+ json, response = \
+ call_api('http://localhost:9001/3.1/plugins/example/good')
+ self.assertEqual(response.status_code, 200)
+ json.pop('http_etag')
+ self.assertEqual(len(json.keys()), 1)
+ self.assertEqual(json['good'], True)
+
+ with self.assertRaises(HTTPError) as cm:
+ call_api('http://localhost:9001/3.1/plugins/example/bad')
+ self.assertEqual(cm.exception.code, 400)
+
+ def test_example_plugin_counter(self):
+ json, response = \
+ call_api('http://localhost:9001/3.1/plugins/example/count')
+ self.assertEqual(response.status_code, 200)
+ json.pop('http_etag')
+ self.assertEqual(len(json.keys()), 1)
+ self.assertEqual(json['count'], 0)
+
+ json, response = \
+ call_api('http://localhost:9001/3.1/plugins/example/count',
+ method='POST', data={'count': 5})
+ self.assertEqual(response.status_code, 204)
+
+ json, response = \
+ call_api('http://localhost:9001/3.1/plugins/example/count')
+ self.assertEqual(response.status_code, 200)
+ json.pop('http_etag')
+ self.assertEqual(len(json.keys()), 1)
+ self.assertEqual(json['count'], 5)
+
+ json, response = \
+ call_api('http://localhost:9001/3.1/plugins/example/count',
+ method='DELETE')
+ self.assertEqual(response.status_code, 204)
+
+ json, response = \
+ call_api('http://localhost:9001/3.1/plugins/example/count')
+ self.assertEqual(response.status_code, 200)
+ json.pop('http_etag')
+ self.assertEqual(len(json.keys()), 1)
+ self.assertEqual(json['count'], 0)
diff --git a/src/mailman/testing/plugin.py b/src/mailman/testing/plugin.py
index 75667844a..a8b001406 100644
--- a/src/mailman/testing/plugin.py
+++ b/src/mailman/testing/plugin.py
@@ -15,22 +15,87 @@
# You should have received a copy of the GNU General Public License along with
# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-"""Example plugin with hooks for testing purposes."""
+"""Example plugin with hooks and REST for testing purposes."""
+from mailman.config import config
from mailman.interfaces.plugin import IPlugin
+from mailman.rest.helpers import bad_request, child, etag, no_content, okay
+from mailman.rest.validator import Validator
from public import public
from zope.interface import implementer
@public
+class GoodREST:
+
+ def on_get(self, request, response):
+ okay(response, etag({'good': True}))
+
+
+@public
+class BadREST:
+
+ def on_get(self, request, response):
+ bad_request(response, etag({'good': False}))
+
+
+@public
+class CountingREST:
+
+ def __init__(self):
+ self._plugin = config.plugins['example']
+
+ def on_get(self, request, response):
+ okay(response, etag({'count': self._plugin.counter}))
+
+ def on_post(self, request, response):
+ try:
+ counter = Validator(count=int)(request)
+ self._plugin.counter = counter['count']
+ except ValueError as error:
+ bad_request(response, str(error))
+ else:
+ no_content(response)
+
+ def on_delete(self, request, response):
+ self._plugin.counter = 0
+ no_content(response)
+
+
+@public
+class RESTExample:
+
+ def on_get(self, request, response):
+ okay(response, etag({'example-plugin-reply': 'yes'}))
+
+ def on_post(self, request, response):
+ bad_request(response)
+
+ @child()
+ def good(self, context, segments):
+ return GoodREST(), []
+
+ @child()
+ def bad(self, context, segments):
+ return BadREST(), []
+
+ @child()
+ def count(self, context, segments):
+ return CountingREST(), []
+
+
+@public
@implementer(IPlugin)
class ExamplePlugin:
def __init__(self):
- pass
+ self.counter = 0
def pre_hook(self):
pass
def post_hook(self):
pass
+
+ def rest_object(self):
+ return RESTExample()