summaryrefslogtreecommitdiff
path: root/src/mailman/rest
diff options
context:
space:
mode:
Diffstat (limited to 'src/mailman/rest')
-rw-r--r--src/mailman/rest/docs/addresses.rst1
-rw-r--r--src/mailman/rest/docs/domains.rst123
-rw-r--r--src/mailman/rest/docs/users.rst99
-rw-r--r--src/mailman/rest/domains.py33
-rw-r--r--src/mailman/rest/listconf.py13
-rw-r--r--src/mailman/rest/tests/test_domains.py55
-rw-r--r--src/mailman/rest/users.py99
-rw-r--r--src/mailman/rest/validator.py9
8 files changed, 374 insertions, 58 deletions
diff --git a/src/mailman/rest/docs/addresses.rst b/src/mailman/rest/docs/addresses.rst
index fd3520be9..f70b64a39 100644
--- a/src/mailman/rest/docs/addresses.rst
+++ b/src/mailman/rest/docs/addresses.rst
@@ -190,6 +190,7 @@ representation:
created_on: 2005-08-01T07:49:23
display_name: Cris X. Person
http_etag: "..."
+ is_server_owner: False
password: ...
self_link: http://localhost:9001/3.0/users/1
user_id: 1
diff --git a/src/mailman/rest/docs/domains.rst b/src/mailman/rest/docs/domains.rst
index a78dacd85..34e3b9a18 100644
--- a/src/mailman/rest/docs/domains.rst
+++ b/src/mailman/rest/docs/domains.rst
@@ -28,15 +28,12 @@ Once a domain is added, it is accessible through the API.
>>> domain_manager.add(
... 'example.com', 'An example domain', 'http://lists.example.com')
- <Domain example.com, An example domain,
- base_url: http://lists.example.com,
- contact_address: postmaster@example.com>
+ <Domain example.com, An example domain, base_url: http://lists.example.com>
>>> transaction.commit()
>>> dump_json('http://localhost:9001/3.0/domains')
entry 0:
base_url: http://lists.example.com
- contact_address: postmaster@example.com
description: An example domain
http_etag: "..."
mail_host: example.com
@@ -51,24 +48,18 @@ At the top level, all domains are returned as separate entries.
>>> domain_manager.add(
... 'example.org',
- ... base_url='http://mail.example.org',
- ... contact_address='listmaster@example.org')
- <Domain example.org, base_url: http://mail.example.org,
- contact_address: listmaster@example.org>
+ ... base_url='http://mail.example.org')
+ <Domain example.org, base_url: http://mail.example.org>
>>> domain_manager.add(
... 'lists.example.net',
... 'Porkmasters',
- ... 'http://example.net',
- ... 'porkmaster@example.net')
- <Domain lists.example.net, Porkmasters,
- base_url: http://example.net,
- contact_address: porkmaster@example.net>
+ ... 'http://example.net')
+ <Domain lists.example.net, Porkmasters, base_url: http://example.net>
>>> transaction.commit()
>>> dump_json('http://localhost:9001/3.0/domains')
entry 0:
base_url: http://lists.example.com
- contact_address: postmaster@example.com
description: An example domain
http_etag: "..."
mail_host: example.com
@@ -76,7 +67,6 @@ At the top level, all domains are returned as separate entries.
url_host: lists.example.com
entry 1:
base_url: http://mail.example.org
- contact_address: listmaster@example.org
description: None
http_etag: "..."
mail_host: example.org
@@ -84,7 +74,6 @@ At the top level, all domains are returned as separate entries.
url_host: mail.example.org
entry 2:
base_url: http://example.net
- contact_address: porkmaster@example.net
description: Porkmasters
http_etag: "..."
mail_host: lists.example.net
@@ -103,7 +92,6 @@ The information for a single domain is available by following one of the
>>> dump_json('http://localhost:9001/3.0/domains/lists.example.net')
base_url: http://example.net
- contact_address: porkmaster@example.net
description: Porkmasters
http_etag: "..."
mail_host: lists.example.net
@@ -165,7 +153,6 @@ Now the web service knows about our new domain.
>>> dump_json('http://localhost:9001/3.0/domains/lists.example.com')
base_url: http://lists.example.com
- contact_address: postmaster@lists.example.com
description: None
http_etag: "..."
mail_host: lists.example.com
@@ -176,9 +163,7 @@ And the new domain is in our database.
::
>>> domain_manager['lists.example.com']
- <Domain lists.example.com,
- base_url: http://lists.example.com,
- contact_address: postmaster@lists.example.com>
+ <Domain lists.example.com, base_url: http://lists.example.com>
# Unlock the database.
>>> transaction.abort()
@@ -190,8 +175,7 @@ address.
>>> dump_json('http://localhost:9001/3.0/domains', {
... 'mail_host': 'my.example.com',
... 'description': 'My new domain',
- ... 'base_url': 'http://allmy.example.com',
- ... 'contact_address': 'helpme@example.com'
+ ... 'base_url': 'http://allmy.example.com'
... })
content-length: 0
date: ...
@@ -200,7 +184,6 @@ address.
>>> dump_json('http://localhost:9001/3.0/domains/my.example.com')
base_url: http://allmy.example.com
- contact_address: helpme@example.com
description: My new domain
http_etag: "..."
mail_host: my.example.com
@@ -208,9 +191,7 @@ address.
url_host: allmy.example.com
>>> domain_manager['my.example.com']
- <Domain my.example.com, My new domain,
- base_url: http://allmy.example.com,
- contact_address: helpme@example.com>
+ <Domain my.example.com, My new domain, base_url: http://allmy.example.com>
# Unlock the database.
>>> transaction.abort()
@@ -229,4 +210,92 @@ Domains can also be deleted via the API.
status: 204
+Domain owners
+=============
+
+Domains can have owners. By posting some addresses to the owners resource,
+you can add some domain owners. Currently our domain has no owners:
+
+ >>> dump_json('http://localhost:9001/3.0/domains/my.example.com/owners')
+ http_etag: ...
+ start: 0
+ total_size: 0
+
+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'), ('owner', 'bart@example.com')
+ ... ))
+ content-length: 0
+ date: ...
+ server: ...
+ status: 204
+
+ >>> dump_json('http://localhost:9001/3.0/domains/my.example.com/owners')
+ entry 0:
+ created_on: 2005-08-01T07:49:23
+ http_etag: ...
+ is_server_owner: False
+ self_link: http://localhost:9001/3.0/users/1
+ user_id: 1
+ entry 1:
+ created_on: 2005-08-01T07:49:23
+ http_etag: ...
+ is_server_owner: False
+ self_link: http://localhost:9001/3.0/users/2
+ user_id: 2
+ http_etag: ...
+ start: 0
+ total_size: 2
+
+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: ...
+ is_server_owner: False
+ self_link: http://localhost:9001/3.0/users/1
+ user_id: 1
+ entry 1:
+ created_on: 2005-08-01T07:49:23
+ http_etag: ...
+ is_server_owner: False
+ self_link: http://localhost:9001/3.0/users/2
+ user_id: 2
+ http_etag: ...
+ start: 0
+ total_size: 2
+
+
.. _Domains: ../../model/docs/domains.html
diff --git a/src/mailman/rest/docs/users.rst b/src/mailman/rest/docs/users.rst
index 824492333..13390a00f 100644
--- a/src/mailman/rest/docs/users.rst
+++ b/src/mailman/rest/docs/users.rst
@@ -34,6 +34,7 @@ Anne's user record is returned as an entry into the collection of all users.
created_on: 2005-08-01T07:49:23
display_name: Anne Person
http_etag: "..."
+ is_server_owner: False
self_link: http://localhost:9001/3.0/users/1
user_id: 1
http_etag: "..."
@@ -50,11 +51,13 @@ returned in the REST API.
created_on: 2005-08-01T07:49:23
display_name: Anne Person
http_etag: "..."
+ is_server_owner: False
self_link: http://localhost:9001/3.0/users/1
user_id: 1
entry 1:
created_on: 2005-08-01T07:49:23
http_etag: "..."
+ is_server_owner: False
self_link: http://localhost:9001/3.0/users/2
user_id: 2
http_etag: "..."
@@ -76,6 +79,7 @@ page.
created_on: 2005-08-01T07:49:23
display_name: Anne Person
http_etag: "..."
+ is_server_owner: False
self_link: http://localhost:9001/3.0/users/1
user_id: 1
http_etag: "..."
@@ -86,6 +90,7 @@ page.
entry 0:
created_on: 2005-08-01T07:49:23
http_etag: "..."
+ is_server_owner: False
self_link: http://localhost:9001/3.0/users/2
user_id: 2
http_etag: "..."
@@ -120,6 +125,7 @@ one was assigned to her.
>>> dump_json('http://localhost:9001/3.0/users/3')
created_on: 2005-08-01T07:49:23
http_etag: "..."
+ is_server_owner: False
password: {plaintext}...
self_link: http://localhost:9001/3.0/users/3
user_id: 3
@@ -131,6 +137,7 @@ address.
>>> dump_json('http://localhost:9001/3.0/users/cris@example.com')
created_on: 2005-08-01T07:49:23
http_etag: "..."
+ is_server_owner: False
password: {plaintext}...
self_link: http://localhost:9001/3.0/users/3
user_id: 3
@@ -158,6 +165,7 @@ Dave's user record includes his display name.
created_on: 2005-08-01T07:49:23
display_name: Dave Person
http_etag: "..."
+ is_server_owner: False
password: {plaintext}...
self_link: http://localhost:9001/3.0/users/4
user_id: 4
@@ -190,6 +198,7 @@ because it has the hash algorithm prefix (i.e. the *{plaintext}* marker).
created_on: 2005-08-01T07:49:23
display_name: Elly Person
http_etag: "..."
+ is_server_owner: False
password: {plaintext}supersekrit
self_link: http://localhost:9001/3.0/users/5
user_id: 5
@@ -214,6 +223,7 @@ Dave's display name has been updated.
created_on: 2005-08-01T07:49:23
display_name: David Person
http_etag: "..."
+ is_server_owner: False
password: {plaintext}...
self_link: http://localhost:9001/3.0/users/4
user_id: 4
@@ -238,6 +248,7 @@ addition of the algorithm prefix.
created_on: 2005-08-01T07:49:23
display_name: David Person
http_etag: "..."
+ is_server_owner: False
password: {plaintext}clockwork angels
self_link: http://localhost:9001/3.0/users/4
user_id: 4
@@ -246,8 +257,9 @@ You can change both the display name and the password by PUTing the full
resource.
>>> dump_json('http://localhost:9001/3.0/users/4', {
- ... 'display_name': 'David Personhood',
... 'cleartext_password': 'the garden',
+ ... 'display_name': 'David Personhood',
+ ... 'is_server_owner': False,
... }, method='PUT')
content-length: 0
date: ...
@@ -260,6 +272,7 @@ Dave's user record has been updated.
created_on: 2005-08-01T07:49:23
display_name: David Personhood
http_etag: "..."
+ is_server_owner: False
password: {plaintext}the garden
self_link: http://localhost:9001/3.0/users/4
user_id: 4
@@ -343,6 +356,7 @@ addresses can be used to look up Fred's user record.
created_on: 2005-08-01T07:49:23
display_name: Fred Person
http_etag: "..."
+ is_server_owner: False
self_link: http://localhost:9001/3.0/users/6
user_id: 6
@@ -350,6 +364,7 @@ addresses can be used to look up Fred's user record.
created_on: 2005-08-01T07:49:23
display_name: Fred Person
http_etag: "..."
+ is_server_owner: False
self_link: http://localhost:9001/3.0/users/6
user_id: 6
@@ -357,6 +372,7 @@ addresses can be used to look up Fred's user record.
created_on: 2005-08-01T07:49:23
display_name: Fred Person
http_etag: "..."
+ is_server_owner: False
self_link: http://localhost:9001/3.0/users/6
user_id: 6
@@ -364,6 +380,7 @@ addresses can be used to look up Fred's user record.
created_on: 2005-08-01T07:49:23
display_name: Fred Person
http_etag: "..."
+ is_server_owner: False
self_link: http://localhost:9001/3.0/users/6
user_id: 6
@@ -382,6 +399,7 @@ password is hashed and getting her user record returns the hashed password.
created_on: 2005-08-01T07:49:23
display_name: Elly Person
http_etag: "..."
+ is_server_owner: False
password: {plaintext}supersekrit
self_link: http://localhost:9001/3.0/users/5
user_id: 5
@@ -399,3 +417,82 @@ This time, Elly successfully logs into Mailman.
date: ...
server: ...
status: 204
+
+
+Server owners
+=============
+
+Users can be designated as server owners. Elly is not currently a server
+owner.
+
+ >>> dump_json('http://localhost:9001/3.0/users/5')
+ created_on: 2005-08-01T07:49:23
+ display_name: Elly Person
+ http_etag: "..."
+ is_server_owner: False
+ password: {plaintext}supersekrit
+ self_link: http://localhost:9001/3.0/users/5
+ user_id: 5
+
+Let's make her a server owner.
+::
+
+ >>> dump_json('http://localhost:9001/3.0/users/5', {
+ ... 'is_server_owner': True,
+ ... }, method='PATCH')
+ content-length: 0
+ date: ...
+ server: ...
+ status: 204
+
+ >>> dump_json('http://localhost:9001/3.0/users/5')
+ created_on: 2005-08-01T07:49:23
+ display_name: Elly Person
+ http_etag: "..."
+ is_server_owner: True
+ password: {plaintext}supersekrit
+ self_link: http://localhost:9001/3.0/users/5
+ user_id: 5
+
+Elly later retires as server owner.
+::
+
+ >>> dump_json('http://localhost:9001/3.0/users/5', {
+ ... 'is_server_owner': False,
+ ... }, method='PATCH')
+ content-length: 0
+ date: ...
+ server: ...
+ status: 204
+
+ >>> dump_json('http://localhost:9001/3.0/users/5')
+ created_on: 2005-08-01T07:49:23
+ display_name: Elly Person
+ http_etag: "..."
+ is_server_owner: False
+ password: {plaintext}...
+ self_link: http://localhost:9001/3.0/users/5
+ user_id: 5
+
+Gwen, a new users, takes over as a server owner.
+::
+
+ >>> dump_json('http://localhost:9001/3.0/users', {
+ ... 'display_name': 'Gwen Person',
+ ... 'email': 'gwen@example.com',
+ ... 'is_server_owner': True,
+ ... })
+ content-length: 0
+ date: ...
+ location: http://localhost:9001/3.0/users/7
+ server: ...
+ status: 201
+
+ >>> dump_json('http://localhost:9001/3.0/users/7')
+ created_on: 2005-08-01T07:49:23
+ display_name: Gwen Person
+ http_etag: "..."
+ is_server_owner: True
+ password: {plaintext}...
+ self_link: http://localhost:9001/3.0/users/7
+ user_id: 7
diff --git a/src/mailman/rest/domains.py b/src/mailman/rest/domains.py
index 345e8327d..bf6fc5ca5 100644
--- a/src/mailman/rest/domains.py
+++ b/src/mailman/rest/domains.py
@@ -29,7 +29,8 @@ from mailman.rest.helpers import (
BadRequest, CollectionMixin, NotFound, bad_request, child, created, etag,
no_content, not_found, okay, path_to)
from mailman.rest.lists import ListsForDomain
-from mailman.rest.validator import Validator
+from mailman.rest.users import OwnersForDomain
+from mailman.rest.validator import Validator, list_of_strings_validator
from zope.component import getUtility
@@ -41,7 +42,6 @@ class _DomainBase(CollectionMixin):
"""See `CollectionMixin`."""
return dict(
base_url=domain.base_url,
- contact_address=domain.contact_address,
description=domain.description,
mail_host=domain.mail_host,
self_link=path_to('domains/{0}'.format(domain.mail_host)),
@@ -88,6 +88,17 @@ class ADomain(_DomainBase):
else:
return BadRequest(), []
+ @child()
+ def owners(self, request, segments):
+ """/domains/<domain>/owners"""
+ if len(segments) == 0:
+ domain = getUtility(IDomainManager).get(self._domain)
+ if domain is None:
+ return NotFound()
+ return OwnersForDomain(domain)
+ else:
+ return BadRequest(), []
+
class AllDomains(_DomainBase):
"""The domains."""
@@ -99,12 +110,18 @@ class AllDomains(_DomainBase):
validator = Validator(mail_host=str,
description=str,
base_url=str,
- contact_address=str,
- _optional=('description', 'base_url',
- 'contact_address'))
- domain = domain_manager.add(**validator(request))
- except BadDomainSpecificationError:
- bad_request(response, b'Domain exists')
+ 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))
except ValueError as error:
bad_request(response, str(error))
else:
diff --git a/src/mailman/rest/listconf.py b/src/mailman/rest/listconf.py
index 92d1169d4..04dea996f 100644
--- a/src/mailman/rest/listconf.py
+++ b/src/mailman/rest/listconf.py
@@ -33,7 +33,8 @@ from mailman.interfaces.mailinglist import (
IAcceptableAliasSet, ReplyToMunging, SubscriptionPolicy)
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)
@@ -73,14 +74,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
@@ -97,7 +90,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 bf53c8e70..716ded580 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',
]
@@ -41,6 +42,18 @@ class TestDomains(unittest.TestCase):
with transaction():
self._mlist = create_list('test@example.com')
+ def test_create_domains(self):
+ # Create a domain with owners.
+ data = dict(
+ mail_host='example.org',
+ description='Example domain',
+ base_url='http://example.org',
+ owner=['someone@example.com', 'secondowner@example.com'],
+ )
+ content, response = call_api(
+ 'http://localhost:9001/3.0/domains', data, method="POST")
+ self.assertEqual(response.status, 201)
+
def test_bogus_endpoint_extension(self):
# /domains/<domain>/lists/<anything> is not a valid endpoint.
with self.assertRaises(HTTPError) as cm:
@@ -87,3 +100,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 a912b6129..7b1ec8040 100644
--- a/src/mailman/rest/users.py
+++ b/src/mailman/rest/users.py
@@ -22,6 +22,7 @@ __all__ = [
'AddressUser',
'AllUsers',
'Login',
+ 'OwnersForDomain',
]
@@ -37,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
@@ -47,27 +49,42 @@ 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(
- display_name=GetterSetter(str),
cleartext_password=PasswordEncrypterGetterSetter(),
+ display_name=GetterSetter(str),
+ is_server_owner=GetterSetter(as_boolean),
)
CREATION_FIELDS = dict(
- email=str,
display_name=str,
+ email=str,
+ is_server_owner=bool,
password=str,
- _optional=('display_name', 'password'),
+ _optional=('display_name', 'password', 'is_server_owner'),
)
@@ -78,6 +95,7 @@ def create_user(arguments, response):
# strip that out (if it exists), then create the user, adding the password
# after the fact if successful.
password = arguments.pop('password', None)
+ is_server_owner = arguments.pop('is_server_owner', False)
try:
user = getUtility(IUserManager).create_user(**arguments)
except ExistingAddressError as error:
@@ -88,6 +106,7 @@ def create_user(arguments, response):
# This will have to be reset since it cannot be retrieved.
password = generate(int(config.passwords.password_length))
user.password = config.password_context.encrypt(password)
+ user.is_server_owner = is_server_owner
location = path_to('users/{}'.format(user.user_id.int))
created(response, location)
return user
@@ -105,10 +124,11 @@ class _UserBase(CollectionMixin):
# but we serialize its integer equivalent.
user_id = user.user_id.int
resource = dict(
- user_id=user_id,
created_on=user.created_on,
+ is_server_owner=user.is_server_owner,
self_link=path_to('users/{}'.format(user_id)),
- )
+ user_id=user_id,
+ )
# Add the password attribute, only if the user has a password. Same
# with the real name. These could be None or the empty string.
if user.password:
@@ -293,7 +313,8 @@ class AddressUser(_UserBase):
del fields['email']
fields['user_id'] = int
fields['auto_create'] = as_boolean
- fields['_optional'] = fields['_optional'] + ('user_id', 'auto_create')
+ fields['_optional'] = fields['_optional'] + (
+ 'user_id', 'auto_create', 'is_server_owner')
try:
validator = Validator(**fields)
arguments = validator(request)
@@ -328,7 +349,8 @@ class AddressUser(_UserBase):
# Process post data and check for an existing user.
fields = CREATION_FIELDS.copy()
fields['user_id'] = int
- fields['_optional'] = fields['_optional'] + ('user_id', 'email')
+ fields['_optional'] = fields['_optional'] + (
+ 'user_id', 'email', 'is_server_owner')
try:
validator = Validator(**fields)
arguments = validator(request)
@@ -377,3 +399,56 @@ class Login:
no_content(response)
else:
forbidden(response)
+
+
+
+class OwnersForDomain(_UserBase):
+ """Owners for a particular domain."""
+
+ def __init__(self, domain):
+ self._domain = domain
+
+ 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 """
+ if self._domain is None:
+ not_found(response)
+ return
+ validator = Validator(
+ owner=ListOfDomainOwners(list_of_strings_validator))
+ try:
+ validator.update(self._domain, request)
+ except ValueError as error:
+ bad_request(response, str(error))
+ return
+ return no_content(response)
+
+ def on_delete(self, request, response):
+ """DELETE to /domains/<domain>/owners"""
+ if self._domain is None:
+ not_found(response)
+ try:
+ # No arguments.
+ Validator()(request)
+ except ValueError as error:
+ bad_request(response, str(error))
+ return
+ 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
+ def _get_collection(self, request):
+ """See `CollectionMixin`."""
+ return list(self._domain.owners)
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."""