diff options
| author | Barry Warsaw | 2010-10-11 15:01:22 -0400 |
|---|---|---|
| committer | Barry Warsaw | 2010-10-11 15:01:22 -0400 |
| commit | 9dd13dc84e39702c8abb0a4bf8d513bf3a35ebbd (patch) | |
| tree | 6a43de65a5d27740fc40c5bfc83b729b3af05934 /src | |
| parent | 90814a40b82e559ebfa999df2121ba2e8e32500f (diff) | |
| parent | 14caf656788903a553c4a374b3f9a934a4014033 (diff) | |
| download | mailman-9dd13dc84e39702c8abb0a4bf8d513bf3a35ebbd.tar.gz mailman-9dd13dc84e39702c8abb0a4bf8d513bf3a35ebbd.tar.zst mailman-9dd13dc84e39702c8abb0a4bf8d513bf3a35ebbd.zip | |
Diffstat (limited to 'src')
| -rw-r--r-- | src/mailman/config/schema.cfg | 8 | ||||
| -rw-r--r-- | src/mailman/docs/ACKNOWLEDGMENTS.txt | 1 | ||||
| -rw-r--r-- | src/mailman/docs/NEWS.txt | 4 | ||||
| -rw-r--r-- | src/mailman/rest/docs/basic.txt | 28 | ||||
| -rw-r--r-- | src/mailman/rest/root.py | 18 | ||||
| -rw-r--r-- | src/mailman/testing/layers.py | 10 | ||||
| -rw-r--r-- | src/mailman/tests/test_documentation.py | 23 |
7 files changed, 82 insertions, 10 deletions
diff --git a/src/mailman/config/schema.cfg b/src/mailman/config/schema.cfg index 2c2aade12..e15abb363 100644 --- a/src/mailman/config/schema.cfg +++ b/src/mailman/config/schema.cfg @@ -126,7 +126,7 @@ enabled: no # Set this to an address to force the SMTP RCPT TO recipents when devmode is # enabled. This way messages can't be accidentally sent to real addresses. -recipient: +recipient: [passwords] @@ -298,6 +298,12 @@ show_tracebacks: yes # The API version number for the current API. api_version: 3.0 +# The administrative username. +admin_user: restadmin + +# The administrative password. +admin_pass: restpass + [language.master] # Template for language definitions. The section name must be [language.xx] diff --git a/src/mailman/docs/ACKNOWLEDGMENTS.txt b/src/mailman/docs/ACKNOWLEDGMENTS.txt index b17d97f27..86a6fe9ad 100644 --- a/src/mailman/docs/ACKNOWLEDGMENTS.txt +++ b/src/mailman/docs/ACKNOWLEDGMENTS.txt @@ -81,6 +81,7 @@ left off the list! * Mike Avery * Stonewall Ballard * Moreno Baricevic +* Jimmy Bergman * Jeff Berliner * Stuart Bishop * David Blomquist diff --git a/src/mailman/docs/NEWS.txt b/src/mailman/docs/NEWS.txt index fdc81b53e..6ded4f2a9 100644 --- a/src/mailman/docs/NEWS.txt +++ b/src/mailman/docs/NEWS.txt @@ -16,6 +16,10 @@ Bugs fixed ---------- * Typo in scan_message() (LP: #645897) +REST +---- + * Add Basic Auth support for REST API security. (given by Jimmy Bergman) + 3.0 alpha 6 -- "Cut to the Chase" ================================= diff --git a/src/mailman/rest/docs/basic.txt b/src/mailman/rest/docs/basic.txt index e5dab9ea8..177082c4a 100644 --- a/src/mailman/rest/docs/basic.txt +++ b/src/mailman/rest/docs/basic.txt @@ -2,12 +2,20 @@ REST server =========== -Mailman exposes a REST_ HTTP server for administrative control. +Mailman exposes a REST HTTP server for administrative control. The server listens for connections on a configurable host name and port. + +It is always protected by HTTP basic authentication using a single global +username and password. The credentials are set in the webservice section +of the config using the admin_user and admin_pass properties. + Because the REST server has full administrative access, it should always be -run only on localhost, unless you really know what you're doing. The Mailman -major and minor version numbers are in the URL. +run only on localhost, unless you really know what you're doing. In addition +you should set the username and password to secure values and distribute them +to any REST clients with reasonable precautions. + +The Mailman major and minor version numbers are in the URL. System information can be retrieved from the server. By default JSON is returned. @@ -31,4 +39,18 @@ When you try to access a link that doesn't exist, you get the appropriate HTTP HTTPError: HTTP Error 404: 404 Not Found +Invalid credentials +=================== + +When you try to access the REST server using invalid credentials you will get +an appropriate HTTP 401 Unauthorized error. + + >>> dump_json('http://localhost:8001/3.0/system', + ... username='baduser', password='badpass') + Traceback (most recent call last): + ... + HTTPError: HTTP Error 401: 401 Unauthorized + ... + + .. _REST: http://en.wikipedia.org/wiki/REST diff --git a/src/mailman/rest/root.py b/src/mailman/rest/root.py index 6835586b8..f34e0eb77 100644 --- a/src/mailman/rest/root.py +++ b/src/mailman/rest/root.py @@ -25,7 +25,8 @@ __all__ = [ ] -from restish import http, resource +from base64 import b64decode +from restish import guard, http, resource from mailman.config import config from mailman.core.system import system @@ -36,6 +37,19 @@ from mailman.rest.members import AllMembers +def webservice_auth_checker(request, obj): + auth = request.environ.get('HTTP_AUTHORIZATION', '') + if auth.startswith('Basic '): + credentials = b64decode(auth[6:]) + username, password = credentials.split(':', 1) + if (username != config.webservice.admin_user or + password != config.webservice.admin_pass): + # Not authorized. + raise guard.GuardError(b'User is not authorized for the REST API') + else: + raise guard.GuardError(b'The REST API requires authentication') + + class Root(resource.Resource): """The RESTful root resource. @@ -44,7 +58,9 @@ class Root(resource.Resource): and we start at 3.0 to match the Mailman version number. That may not always be the case though. """ + @resource.child(config.webservice.api_version) + @guard.guard(webservice_auth_checker) def api_version(self, request, segments): return TopLevel() diff --git a/src/mailman/testing/layers.py b/src/mailman/testing/layers.py index 97b715131..fbb2dfa8a 100644 --- a/src/mailman/testing/layers.py +++ b/src/mailman/testing/layers.py @@ -35,9 +35,10 @@ import logging import datetime import tempfile +from base64 import b64encode from pkg_resources import resource_string from textwrap import dedent -from urllib2 import urlopen, URLError +from urllib2 import Request, URLError, urlopen from zope.component import getUtility from mailman.config import config @@ -272,7 +273,12 @@ class RESTLayer(SMTPLayer): until = datetime.datetime.now() + TEST_TIMEOUT while datetime.datetime.now() < until: try: - fp = urlopen('http://localhost:8001/3.0/system') + request = Request('http://localhost:8001/3.0/system') + basic_auth = '{0}:{1}'.format(config.webservice.admin_user, + config.webservice.admin_pass) + request.add_header('Authorization', + 'Basic ' + b64encode(basic_auth)) + fp = urlopen(request) except URLError: pass else: diff --git a/src/mailman/tests/test_documentation.py b/src/mailman/tests/test_documentation.py index c0f8dca20..f4ecd924c 100644 --- a/src/mailman/tests/test_documentation.py +++ b/src/mailman/tests/test_documentation.py @@ -35,6 +35,7 @@ import json import doctest import unittest +from base64 import b64encode from email import message_from_string from httplib2 import Http from urllib import urlencode @@ -109,7 +110,7 @@ def dump_msgdata(msgdata, *additional_skips): print '{0:{2}}: {1}'.format(key, msgdata[key], longest) -def call_http(url, data=None, method=None): +def call_http(url, data=None, method=None, username=None, password=None): """'Call' a URL with a given HTTP method and return the resulting object. The object will have been JSON decoded. @@ -120,6 +121,12 @@ def call_http(url, data=None, method=None): :type data: dict :param method: Alternative HTTP method to use. :type method: str + :param username: The HTTP Basic Auth user name. None means use the value + from the configuration. + :type username: str + :param password: The HTTP Basic Auth password. None means use the value + from the configuration. + :type username: str """ headers = {} if data is not None: @@ -131,6 +138,10 @@ def call_http(url, data=None, method=None): else: method = 'POST' method = method.upper() + basic_auth = '{0}:{1}'.format( + (config.webservice.admin_user if username is None else username), + (config.webservice.admin_pass if password is None else password)) + headers['Authorization'] = 'Basic ' + b64encode(basic_auth) response, content = Http().request(url, method, data, headers) # If we did not get a 2xx status code, make this look like a urllib2 # exception, for backward compatibility with existing doctests. @@ -143,7 +154,7 @@ def call_http(url, data=None, method=None): return json.loads(content) -def dump_json(url, data=None, method=None): +def dump_json(url, data=None, method=None, username=None, password=None): """Print the JSON dictionary read from a URL. :param url: The url to open, read, and print. @@ -152,8 +163,14 @@ def dump_json(url, data=None, method=None): :type data: dict :param method: Alternative HTTP method to use. :type method: str + :param username: The HTTP Basic Auth user name. None means use the value + from the configuration. + :type username: str + :param password: The HTTP Basic Auth password. None means use the value + from the configuration. + :type username: str """ - data = call_http(url, data, method) + data = call_http(url, data, method, username, password) if data is None: return for key in sorted(data): |
