summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/mailman/rest/docs/domains.rst40
-rw-r--r--src/mailman/rest/domains.py13
-rw-r--r--src/mailman/rest/listconf.py13
-rw-r--r--src/mailman/rest/tests/test_domains.py44
-rw-r--r--src/mailman/rest/users.py49
-rw-r--r--src/mailman/rest/validator.py9
6 files changed, 131 insertions, 37 deletions
diff --git a/src/mailman/rest/docs/domains.rst b/src/mailman/rest/docs/domains.rst
index f57dee572..34e3b9a18 100644
--- a/src/mailman/rest/docs/domains.rst
+++ b/src/mailman/rest/docs/domains.rst
@@ -224,17 +224,9 @@ you can add some domain owners. Currently our domain has no owners:
Anne and Bart volunteer to be a domain owners.
::
- >>> dump_json('http://localhost:9001/3.0/domains/my.example.com/owners', {
- ... 'owner': 'anne@example.com',
- ... })
- content-length: 0
- date: ...
- server: ...
- status: 204
-
- >>> dump_json('http://localhost:9001/3.0/domains/my.example.com/owners', {
- ... 'owner': 'bart@example.com',
- ... })
+ >>> dump_json('http://localhost:9001/3.0/domains/my.example.com/owners', (
+ ... ('owner', 'anne@example.com'), ('owner', 'bart@example.com')
+ ... ))
content-length: 0
date: ...
server: ...
@@ -261,8 +253,34 @@ We can delete all the domain owners.
>>> dump_json('http://localhost:9001/3.0/domains/my.example.com/owners',
... method='DELETE')
+ content-length: 0
+ date: ...
+ server: ...
+ status: 204
+
+Now there are no owners.
>>> dump_json('http://localhost:9001/3.0/domains/my.example.com/owners')
+ http_etag: ...
+ start: 0
+ total_size: 0
+
+New domains can be created with owners.
+
+ >>> dump_json('http://localhost:9001/3.0/domains', (
+ ... ('mail_host', 'your.example.com'),
+ ... ('owner', 'anne@example.com'),
+ ... ('owner', 'bart@example.com'),
+ ... ))
+ content-length: 0
+ date: ...
+ location: http://localhost:9001/3.0/domains/your.example.com
+ server: ...
+ status: 201
+
+The new domain has the expected owners.
+
+ >>> dump_json('http://localhost:9001/3.0/domains/your.example.com/owners')
entry 0:
created_on: 2005-08-01T07:49:23
http_etag: ...
diff --git a/src/mailman/rest/domains.py b/src/mailman/rest/domains.py
index dff2f2073..bf6fc5ca5 100644
--- a/src/mailman/rest/domains.py
+++ b/src/mailman/rest/domains.py
@@ -30,7 +30,7 @@ from mailman.rest.helpers import (
no_content, not_found, okay, path_to)
from mailman.rest.lists import ListsForDomain
from mailman.rest.users import OwnersForDomain
-from mailman.rest.validator import Validator
+from mailman.rest.validator import Validator, list_of_strings_validator
from zope.component import getUtility
@@ -110,10 +110,15 @@ class AllDomains(_DomainBase):
validator = Validator(mail_host=str,
description=str,
base_url=str,
- owners=list,
- _optional=('description', 'base_url',
- 'owners'))
+ owner=list_of_strings_validator,
+ _optional=(
+ 'description', 'base_url', 'owner'))
values = validator(request)
+ # For consistency, owners are passed in as multiple `owner` keys,
+ # but .add() requires an `owners` keyword. Match impedence.
+ owners = values.pop('owner', None)
+ if owners is not None:
+ values['owners'] = owners
domain = domain_manager.add(**values)
except BadDomainSpecificationError as error:
bad_request(response, str(error))
diff --git a/src/mailman/rest/listconf.py b/src/mailman/rest/listconf.py
index e83f52833..d618ce116 100644
--- a/src/mailman/rest/listconf.py
+++ b/src/mailman/rest/listconf.py
@@ -32,7 +32,8 @@ from mailman.interfaces.autorespond import ResponseAction
from mailman.interfaces.mailinglist import IAcceptableAliasSet, ReplyToMunging
from mailman.rest.helpers import (
GetterSetter, bad_request, etag, no_content, okay)
-from mailman.rest.validator import PatchValidator, Validator, enum_validator
+from mailman.rest.validator import (
+ PatchValidator, Validator, enum_validator, list_of_strings_validator)
@@ -72,14 +73,6 @@ def pipeline_validator(pipeline_name):
raise ValueError('Unknown pipeline: {}'.format(pipeline_name))
-def list_of_str(values):
- """Turn a list of things into a list of unicodes."""
- for value in values:
- if not isinstance(value, str):
- raise ValueError('Expected str, got {!r}'.format(value))
- return values
-
-
# This is the list of IMailingList attributes that are exposed through the
# REST API. The values of the keys are the GetterSetter instance holding the
@@ -96,7 +89,7 @@ def list_of_str(values):
# (e.g. datetimes, timedeltas, enums).
ATTRIBUTES = dict(
- acceptable_aliases=AcceptableAliases(list_of_str),
+ acceptable_aliases=AcceptableAliases(list_of_strings_validator),
admin_immed_notify=GetterSetter(as_boolean),
admin_notify_mchanges=GetterSetter(as_boolean),
administrivia=GetterSetter(as_boolean),
diff --git a/src/mailman/rest/tests/test_domains.py b/src/mailman/rest/tests/test_domains.py
index 13299516c..f87ceb3cc 100644
--- a/src/mailman/rest/tests/test_domains.py
+++ b/src/mailman/rest/tests/test_domains.py
@@ -18,6 +18,7 @@
"""REST domain tests."""
__all__ = [
+ 'TestDomainOwners',
'TestDomains',
]
@@ -27,7 +28,6 @@ import unittest
from mailman.app.lifecycle import create_list
from mailman.database.transaction import transaction
from mailman.interfaces.listmanager import IListManager
-from mailman.interfaces.domain import IDomainManager
from mailman.testing.helpers import call_api
from mailman.testing.layers import RESTLayer
from urllib.error import HTTPError
@@ -99,3 +99,45 @@ class TestDomains(unittest.TestCase):
call_api('http://localhost:9001/3.0/domains/example.com',
method='DELETE')
self.assertEqual(cm.exception.code, 404)
+
+
+
+class TestDomainOwners(unittest.TestCase):
+ layer = RESTLayer
+
+ def test_get_missing_domain_owners(self):
+ # Try to get the owners of a missing domain.
+ with self.assertRaises(HTTPError) as cm:
+ call_api('http://localhost:9001/3.0/domains/example.net/owners')
+ self.assertEqual(cm.exception.code, 404)
+
+ def test_post_to_missing_domain_owners(self):
+ # Try to add owners to a missing domain.
+ with self.assertRaises(HTTPError) as cm:
+ call_api('http://localhost:9001/3.0/domains/example.net/owners', (
+ ('owner', 'dave@example.com'), ('owner', 'elle@example.com'),
+ ))
+ self.assertEqual(cm.exception.code, 404)
+
+ def test_delete_missing_domain_owners(self):
+ # Try to delete the owners of a missing domain.
+ with self.assertRaises(HTTPError) as cm:
+ call_api('http://localhost:9001/3.0/domains/example.net/owners',
+ method='DELETE')
+ self.assertEqual(cm.exception.code, 404)
+
+ def test_bad_post(self):
+ # Send POST data with an invalid attribute.
+ with self.assertRaises(HTTPError) as cm:
+ call_api('http://localhost:9001/3.0/domains/example.com/owners', (
+ ('guy', 'dave@example.com'), ('gal', 'elle@example.com'),
+ ))
+ self.assertEqual(cm.exception.code, 400)
+
+ def test_bad_delete(self):
+ # Send DELETE with any data.
+ with self.assertRaises(HTTPError) as cm:
+ call_api('http://localhost:9001/3.0/domains/example.com/owners', {
+ 'owner': 'dave@example.com',
+ }, method='DELETE')
+ self.assertEqual(cm.exception.code, 400)
diff --git a/src/mailman/rest/users.py b/src/mailman/rest/users.py
index a38299d00..7b1ec8040 100644
--- a/src/mailman/rest/users.py
+++ b/src/mailman/rest/users.py
@@ -38,7 +38,8 @@ from mailman.rest.helpers import (
conflict, created, etag, forbidden, no_content, not_found, okay, paginate,
path_to)
from mailman.rest.preferences import Preferences
-from mailman.rest.validator import PatchValidator, Validator
+from mailman.rest.validator import (
+ PatchValidator, Validator, list_of_strings_validator)
from passlib.utils import generate_password as generate
from uuid import UUID
from zope.component import getUtility
@@ -48,14 +49,27 @@ from zope.component import getUtility
# Attributes of a user which can be changed via the REST API.
class PasswordEncrypterGetterSetter(GetterSetter):
def __init__(self):
- super(PasswordEncrypterGetterSetter, self).__init__(
- config.password_context.encrypt)
+ super().__init__(config.password_context.encrypt)
def get(self, obj, attribute):
assert attribute == 'cleartext_password'
- super(PasswordEncrypterGetterSetter, self).get(obj, 'password')
+ super().get(obj, 'password')
def put(self, obj, attribute, value):
assert attribute == 'cleartext_password'
- super(PasswordEncrypterGetterSetter, self).put(obj, 'password', value)
+ super().put(obj, 'password', value)
+
+
+class ListOfDomainOwners(GetterSetter):
+ def get(self, domain, attribute):
+ assert attribute == 'owner', (
+ 'Unexpected attribute: {}'.format(attribute))
+ def sort_key(owner):
+ return owner.addresses[0].email
+ return sorted(domain.owners, key=sort_key)
+
+ def put(self, domain, attribute, value):
+ assert attribute == 'owner', (
+ 'Unexpected attribute: {}'.format(attribute))
+ domain.add_owners(value)
ATTRIBUTES = dict(
@@ -396,29 +410,42 @@ class OwnersForDomain(_UserBase):
def on_get(self, request, response):
"""/domains/<domain>/owners"""
+ if self._domain is None:
+ not_found(response)
+ return
resource = self._make_collection(request)
okay(response, etag(resource))
def on_post(self, request, response):
"""POST to /domains/<domain>/owners """
- validator = Validator(owner=GetterSetter(str))
+ if self._domain is None:
+ not_found(response)
+ return
+ validator = Validator(
+ owner=ListOfDomainOwners(list_of_strings_validator))
try:
- values = validator(request)
+ validator.update(self._domain, request)
except ValueError as error:
bad_request(response, str(error))
return
- self._domain.add_owner(values['owner'])
return no_content(response)
def on_delete(self, request, response):
"""DELETE to /domains/<domain>/owners"""
- validator = Validator(owner=GetterSetter(str))
+ if self._domain is None:
+ not_found(response)
try:
- values = validator(request)
+ # No arguments.
+ Validator()(request)
except ValueError as error:
bad_request(response, str(error))
return
- self._domain.remove_owner(owner)
+ owner_email = [
+ owner.addresses[0].email
+ for owner in self._domain.owners
+ ]
+ for email in owner_email:
+ self._domain.remove_owner(email)
return no_content(response)
@paginate
diff --git a/src/mailman/rest/validator.py b/src/mailman/rest/validator.py
index 867991a36..d09886e36 100644
--- a/src/mailman/rest/validator.py
+++ b/src/mailman/rest/validator.py
@@ -22,6 +22,7 @@ __all__ = [
'Validator',
'enum_validator',
'language_validator',
+ 'list_of_strings_validator',
'subscriber_validator',
]
@@ -66,6 +67,14 @@ def language_validator(code):
return getUtility(ILanguageManager)[code]
+def list_of_strings_validator(values):
+ """Turn a list of things into a list of unicodes."""
+ for value in values:
+ if not isinstance(value, str):
+ raise ValueError('Expected str, got {!r}'.format(value))
+ return values
+
+
class Validator:
"""A validator of parameter input."""