diff options
Diffstat (limited to 'src/mailman/rest')
| -rw-r--r-- | src/mailman/rest/docs/addresses.rst | 1 | ||||
| -rw-r--r-- | src/mailman/rest/docs/domains.rst | 123 | ||||
| -rw-r--r-- | src/mailman/rest/docs/users.rst | 99 | ||||
| -rw-r--r-- | src/mailman/rest/domains.py | 33 | ||||
| -rw-r--r-- | src/mailman/rest/listconf.py | 13 | ||||
| -rw-r--r-- | src/mailman/rest/tests/test_domains.py | 55 | ||||
| -rw-r--r-- | src/mailman/rest/users.py | 99 | ||||
| -rw-r--r-- | src/mailman/rest/validator.py | 9 |
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.""" |
