summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBarry Warsaw2010-10-11 15:01:22 -0400
committerBarry Warsaw2010-10-11 15:01:22 -0400
commit9dd13dc84e39702c8abb0a4bf8d513bf3a35ebbd (patch)
tree6a43de65a5d27740fc40c5bfc83b729b3af05934
parent90814a40b82e559ebfa999df2121ba2e8e32500f (diff)
parent14caf656788903a553c4a374b3f9a934a4014033 (diff)
downloadmailman-9dd13dc84e39702c8abb0a4bf8d513bf3a35ebbd.tar.gz
mailman-9dd13dc84e39702c8abb0a4bf8d513bf3a35ebbd.tar.zst
mailman-9dd13dc84e39702c8abb0a4bf8d513bf3a35ebbd.zip
-rw-r--r--src/mailman/config/schema.cfg8
-rw-r--r--src/mailman/docs/ACKNOWLEDGMENTS.txt1
-rw-r--r--src/mailman/docs/NEWS.txt4
-rw-r--r--src/mailman/rest/docs/basic.txt28
-rw-r--r--src/mailman/rest/root.py18
-rw-r--r--src/mailman/testing/layers.py10
-rw-r--r--src/mailman/tests/test_documentation.py23
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):