From ae2a7c9a22f5b6eeed1a6884c6dcd87ed9ba673d Mon Sep 17 00:00:00 2001 From: Abhilash Raj Date: Sat, 21 Mar 2015 00:32:10 +0530 Subject: add domainowner and serverowner options * Add is_serverowner flag in User model and api * Add owner table for user-domain's many to many relationship * add owners subresource in domain's rest api --- src/mailman/rest/domains.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) (limited to 'src/mailman/rest/domains.py') diff --git a/src/mailman/rest/domains.py b/src/mailman/rest/domains.py index 345e8327d..d312737df 100644 --- a/src/mailman/rest/domains.py +++ b/src/mailman/rest/domains.py @@ -29,6 +29,7 @@ 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.users import OwnersForDomain from mailman.rest.validator import 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//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,12 @@ class AllDomains(_DomainBase): validator = Validator(mail_host=str, description=str, base_url=str, - contact_address=str, + owner_id=int, _optional=('description', 'base_url', - 'contact_address')) + 'owner_id')) domain = domain_manager.add(**validator(request)) - except BadDomainSpecificationError: - bad_request(response, b'Domain exists') + except BadDomainSpecificationError as error: + bad_request(response, str(error)) except ValueError as error: bad_request(response, str(error)) else: -- cgit v1.2.3-70-g09d2 From fe12351e6f0e11f48bd714357f05aa7a34ec7e90 Mon Sep 17 00:00:00 2001 From: Abhilash Raj Date: Tue, 31 Mar 2015 06:26:08 +0530 Subject: * Add `drop_column` inside sqlite check, fix indentation * Change `Owner` to `DomainOwner` * Fix indentation errors in docs * add multiple owners using `add_owners` * all dummy addresses should be using example.com, example.org to avoid conflict ever * add dummy tests --- .../46e92facee7_add_serverowner_domainowner.py | 25 +++++++--------------- src/mailman/interfaces/domain.py | 4 ++-- src/mailman/model/docs/domains.rst | 8 +++---- src/mailman/model/domain.py | 25 ++++++++++------------ src/mailman/model/tests/test_domain.py | 15 +++++-------- src/mailman/model/user.py | 6 +++--- src/mailman/rest/domains.py | 4 ++-- src/mailman/rest/tests/test_domains.py | 5 +++++ 8 files changed, 40 insertions(+), 52 deletions(-) (limited to 'src/mailman/rest/domains.py') diff --git a/src/mailman/database/alembic/versions/46e92facee7_add_serverowner_domainowner.py b/src/mailman/database/alembic/versions/46e92facee7_add_serverowner_domainowner.py index 613e4d22d..e4dc25ab8 100644 --- a/src/mailman/database/alembic/versions/46e92facee7_add_serverowner_domainowner.py +++ b/src/mailman/database/alembic/versions/46e92facee7_add_serverowner_domainowner.py @@ -15,31 +15,22 @@ import sqlalchemy as sa def upgrade(): - ### commands auto generated by Alembic - please adjust! ### - op.create_table('owner', - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('domain_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['domain_id'], ['domain.id'], ), - sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), - sa.PrimaryKeyConstraint('user_id', 'domain_id') + op.create_table('domain_owner', + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('domain_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['domain_id'], ['domain.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('user_id', 'domain_id') ) op.add_column('user', sa.Column('is_server_owner', sa.Boolean(), nullable=True)) if op.get_bind().dialect.name != 'sqlite': op.drop_column('domain', 'contact_address') - # The migration below may be because the first migration was created - # before we changed this attribute to a primary_key - op.create_foreign_key('_preferred_address', 'user', 'address', - ['_preferred_address_id'], ['id']) - ### end Alembic commands ### def downgrade(): - ### commands auto generated by Alembic - please adjust! ### - op.drop_column('user', 'is_server_owner') if op.get_bind().dialect.name != 'sqlite': + op.drop_column('user', 'is_server_owner') op.add_column('domain', sa.Column('contact_address', sa.VARCHAR(), nullable=True)) - op.drop_constraint('_preferred_address', 'user', type_='foreignkey') - op.drop_table('owner') - ### end Alembic commands ### + op.drop_table('domain_owner') diff --git a/src/mailman/interfaces/domain.py b/src/mailman/interfaces/domain.py index fd889a720..94a9eefd3 100644 --- a/src/mailman/interfaces/domain.py +++ b/src/mailman/interfaces/domain.py @@ -122,8 +122,8 @@ class IDomainManager(Interface): interface of the domain. If not given, it defaults to http://`mail_host`/ :type base_url: string - :param owner_id: Owner of the domain, defaults to None - :type owner_id: integer + :param owner: Owner of the domain, defaults to None + :type owner: `Iuser` :return: The new domain object :rtype: `IDomain` :raises `BadDomainSpecificationError`: when the `mail_host` is diff --git a/src/mailman/model/docs/domains.rst b/src/mailman/model/docs/domains.rst index 491c0e612..dd0904f2b 100644 --- a/src/mailman/model/docs/domains.rst +++ b/src/mailman/model/docs/domains.rst @@ -9,10 +9,10 @@ Domains >>> manager = getUtility(IDomainManager) >>> manager.remove('example.com') - >>> from mailman.interfaces.usermanager import IUserManager - >>> user_manager = getUtility(IUserManager) + >>> from mailman.interfaces.usermanager import IUserManager + >>> user_manager = getUtility(IUserManager) >>> user = user_manager.create_user('test@example.org') - >>> config.db.commit() + >>> config.db.commit() Domains are how Mailman interacts with email host names and web host names. :: @@ -58,7 +58,7 @@ Domains can have explicit descriptions. ... 'example.net', ... base_url='http://lists.example.net', ... description='The example domain', - ... owner_id=1) + ... owner=user) diff --git a/src/mailman/model/domain.py b/src/mailman/model/domain.py index a3a059cb9..6809688d6 100644 --- a/src/mailman/model/domain.py +++ b/src/mailman/model/domain.py @@ -29,7 +29,7 @@ from mailman.interfaces.domain import ( BadDomainSpecificationError, DomainCreatedEvent, DomainCreatingEvent, DomainDeletedEvent, DomainDeletingEvent, IDomain, IDomainManager) from mailman.model.mailinglist import MailingList -from mailman.model.user import User, Owner +from mailman.model.user import User, DomainOwner from urllib.parse import urljoin, urlparse from sqlalchemy import Column, Integer, Unicode from sqlalchemy.orm import relationship, backref @@ -50,7 +50,7 @@ class Domain(Model): base_url = Column(Unicode) description = Column(Unicode) owners = relationship("User", - secondary="owner", + secondary="domain_owner", backref="domains") def __init__(self, mail_host, @@ -68,7 +68,7 @@ class Domain(Model): `mail_host` using the http protocol. :type base_url: string :param owner: The `User` who is the owner of this domain - :type owner: mailman.models.user.User + :type owner: `IUser` """ self.mail_host = mail_host self.base_url = (base_url @@ -111,11 +111,15 @@ class Domain(Model): 'base_url: {0.base_url}>').format(self) def add_owner(self, owner): - """ Add a domain owner""" + """Add a domain owner""" self.owners.append(owner) - @dbconnection - def remove_owner(self, store, owner): + def add_owners(self, owners): + """Add multiple owners""" + for each in owners: + self.owners.append(each) + + def remove_owner(self, owner): """ Remove a domain owner""" self.owners.remove(owner) @@ -129,20 +133,13 @@ class DomainManager: mail_host, description=None, base_url=None, - owner_id=None): + owner=None): """See `IDomainManager`.""" # Be sure the mail_host is not already registered. This is probably # a constraint that should (also) be maintained in the database. if self.get(mail_host) is not None: raise BadDomainSpecificationError( 'Duplicate email host: %s' % mail_host) - # Be sure that the owner exists - owner = None - if owner_id is not None: - owner = store.query(User).get(owner_id) - if owner is None: - raise BadDomainSpecificationError( - 'Owner of this domain does not exist') notify(DomainCreatingEvent(mail_host)) domain = Domain(mail_host, description, base_url, owner) diff --git a/src/mailman/model/tests/test_domain.py b/src/mailman/model/tests/test_domain.py index ffa79e32e..950c34969 100644 --- a/src/mailman/model/tests/test_domain.py +++ b/src/mailman/model/tests/test_domain.py @@ -81,18 +81,14 @@ class TestDomainManager(unittest.TestCase): self.assertRaises(KeyError, self._manager.remove, 'doesnotexist.com') def test_domain_create_with_owner(self): - user = getUtility(IUserManager).create_user('someuser@somedomain.org') + user = getUtility(IUserManager).create_user('someuser@example.org') config.db.commit() - domain = self._manager.add('example.org', owner_id=user.id) + domain = self._manager.add('example.org', owner=user) self.assertEqual(len(domain.owners), 1) self.assertEqual(domain.owners[0].id, user.id) - def test_domain_create_with_non_existent_owner(self): - with self.assertRaises(BadDomainSpecificationError): - self._manager.add('testdomain.org', owner_id=100) - def test_add_domain_owner(self): - user = getUtility(IUserManager).create_user('someuser@somedomain.org') + user = getUtility(IUserManager).create_user('someuser@example.org') config.db.commit() domain = self._manager.add('example.org') domain.add_owner(user) @@ -102,7 +98,7 @@ class TestDomainManager(unittest.TestCase): def test_remove_domain_owner(self): user = getUtility(IUserManager).create_user('someuser@somedomain.org') config.db.commit() - domain = self._manager.add('example.org', owner_id=user.id) + domain = self._manager.add('example.org', owner=user) domain.remove_owner(user) self.assertEqual(len(domain.owners), 0) @@ -140,5 +136,4 @@ class TestDomainLifecycleEvents(unittest.TestCase): self.assertEqual(listmanager.get('fly@example.com'), fly) def test_owners_are_deleted_when_domain_is(self): - #TODO: Complete this - pass + self._domainmanager.remove('example.net') diff --git a/src/mailman/model/user.py b/src/mailman/model/user.py index b75a3f077..a8bc556cd 100644 --- a/src/mailman/model/user.py +++ b/src/mailman/model/user.py @@ -179,9 +179,9 @@ class User(Model): return Memberships(self) -class Owner(Model): - """Doomain to owners(user) association class""" +class DomainOwner(Model): + """Domain to owners(user) association class""" - __tablename__ = 'owner' + __tablename__ = 'domain_owner' user_id = Column(Integer, ForeignKey('user.id'), primary_key=True) domain_id = Column(Integer, ForeignKey('domain.id'), primary_key=True) diff --git a/src/mailman/rest/domains.py b/src/mailman/rest/domains.py index d312737df..2f41ecfd9 100644 --- a/src/mailman/rest/domains.py +++ b/src/mailman/rest/domains.py @@ -110,9 +110,9 @@ class AllDomains(_DomainBase): validator = Validator(mail_host=str, description=str, base_url=str, - owner_id=int, + owner=int, _optional=('description', 'base_url', - 'owner_id')) + 'owner')) domain = domain_manager.add(**validator(request)) except BadDomainSpecificationError as error: bad_request(response, str(error)) diff --git a/src/mailman/rest/tests/test_domains.py b/src/mailman/rest/tests/test_domains.py index bf53c8e70..9ebc0c0d8 100644 --- a/src/mailman/rest/tests/test_domains.py +++ b/src/mailman/rest/tests/test_domains.py @@ -41,6 +41,11 @@ class TestDomains(unittest.TestCase): with transaction(): self._mlist = create_list('test@example.com') + def test_create_domain(self): + """Create domain via REST""" + # TODO: Complete this + # Tests should be failing with improper REST API. + def test_bogus_endpoint_extension(self): # /domains//lists/ is not a valid endpoint. with self.assertRaises(HTTPError) as cm: -- cgit v1.2.3-70-g09d2 From 17fa7ac10ddd6ca0916cdcdd3a5e8c1414e9bcbc Mon Sep 17 00:00:00 2001 From: Abhilash Raj Date: Mon, 6 Apr 2015 03:58:22 +0530 Subject: * implement left over methods * add and remove owners using the address --- src/mailman/interfaces/domain.py | 4 ++-- src/mailman/model/docs/domains.rst | 10 +++++----- src/mailman/model/domain.py | 32 ++++++++++++++++++++------------ src/mailman/model/tests/test_domain.py | 25 ++++++++++--------------- src/mailman/model/user.py | 1 + src/mailman/rest/domains.py | 8 +++++--- src/mailman/rest/lists.py | 2 +- src/mailman/rest/tests/test_domains.py | 15 +++++++++++---- src/mailman/rest/users.py | 25 ++++++++++++------------- 9 files changed, 67 insertions(+), 55 deletions(-) (limited to 'src/mailman/rest/domains.py') diff --git a/src/mailman/interfaces/domain.py b/src/mailman/interfaces/domain.py index 94a9eefd3..ee5eafc78 100644 --- a/src/mailman/interfaces/domain.py +++ b/src/mailman/interfaces/domain.py @@ -122,8 +122,8 @@ class IDomainManager(Interface): interface of the domain. If not given, it defaults to http://`mail_host`/ :type base_url: string - :param owner: Owner of the domain, defaults to None - :type owner: `Iuser` + :param owners: List of owners of the domain, defaults to None + :type owners: list :return: The new domain object :rtype: `IDomain` :raises `BadDomainSpecificationError`: when the `mail_host` is diff --git a/src/mailman/model/docs/domains.rst b/src/mailman/model/docs/domains.rst index dd0904f2b..ded52f817 100644 --- a/src/mailman/model/docs/domains.rst +++ b/src/mailman/model/docs/domains.rst @@ -58,7 +58,7 @@ Domains can have explicit descriptions. ... 'example.net', ... base_url='http://lists.example.net', ... description='The example domain', - ... owner=user) + ... owners=['user@domain.com']) @@ -67,13 +67,13 @@ Domains can have explicit descriptions. -Domains can have multiple number of owners, ideally one of the owners -should have a verified preferred address. However this is not checked -right now and contact_address from config is used as a fallback. +Domains can have multiple owners, ideally one of the owners should have a +verified preferred address. However this is not checked right now and +contact_address from config can be used as a fallback. :: >>> net_domain = manager['example.net'] - >>> net_domain.add_owner(user_manager.get_user('test@example.org')) + >>> net_domain.add_owner('test@example.org') Domains can list all associated mailing lists with the mailing_lists property. diff --git a/src/mailman/model/domain.py b/src/mailman/model/domain.py index 6809688d6..32ea7db9b 100644 --- a/src/mailman/model/domain.py +++ b/src/mailman/model/domain.py @@ -28,6 +28,7 @@ from mailman.database.transaction import dbconnection from mailman.interfaces.domain import ( BadDomainSpecificationError, DomainCreatedEvent, DomainCreatingEvent, DomainDeletedEvent, DomainDeletingEvent, IDomain, IDomainManager) +from mailman.interfaces.usermanager import IUserManager from mailman.model.mailinglist import MailingList from mailman.model.user import User, DomainOwner from urllib.parse import urljoin, urlparse @@ -35,6 +36,7 @@ from sqlalchemy import Column, Integer, Unicode from sqlalchemy.orm import relationship, backref from zope.event import notify from zope.interface import implementer +from zope.component import getUtility @@ -46,7 +48,7 @@ class Domain(Model): id = Column(Integer, primary_key=True) - mail_host = Column(Unicode) # TODO: add index? + mail_host = Column(Unicode) base_url = Column(Unicode) description = Column(Unicode) owners = relationship("User", @@ -56,7 +58,7 @@ class Domain(Model): def __init__(self, mail_host, description=None, base_url=None, - owner=None): + owners=[]): """Create and register a domain. :param mail_host: The host name for the email interface. @@ -67,16 +69,16 @@ class Domain(Model): scheme. If not given, it will be constructed from the `mail_host` using the http protocol. :type base_url: string - :param owner: The `User` who is the owner of this domain - :type owner: `IUser` + :param owners: List of `User` who are the owners of this domain + :type owners: list """ self.mail_host = mail_host self.base_url = (base_url if base_url is not None else 'http://' + mail_host) self.description = description - if owner is not None: - self.owners.append(owner) + if len(owners): + self.add_owners(owners) @property def url_host(self): @@ -112,16 +114,22 @@ class Domain(Model): def add_owner(self, owner): """Add a domain owner""" - self.owners.append(owner) + user_manager = getUtility(IUserManager) + user = user_manager.get_user(owner) + if user is None: + user = user_manager.create_user(owner) + self.owners.append(user) def add_owners(self, owners): """Add multiple owners""" - for each in owners: - self.owners.append(each) + assert(isinstance(owners, list)) + for owner in owners: + self.add_owner(owner) def remove_owner(self, owner): """ Remove a domain owner""" - self.owners.remove(owner) + user_manager = getUtility(IUserManager) + self.owners.remove(user_manager.get_user(owner)) @implementer(IDomainManager) @@ -133,7 +141,7 @@ class DomainManager: mail_host, description=None, base_url=None, - owner=None): + owners=[]): """See `IDomainManager`.""" # Be sure the mail_host is not already registered. This is probably # a constraint that should (also) be maintained in the database. @@ -142,7 +150,7 @@ class DomainManager: 'Duplicate email host: %s' % mail_host) notify(DomainCreatingEvent(mail_host)) - domain = Domain(mail_host, description, base_url, owner) + domain = Domain(mail_host, description, base_url, owners) store.add(domain) notify(DomainCreatedEvent(domain)) return domain diff --git a/src/mailman/model/tests/test_domain.py b/src/mailman/model/tests/test_domain.py index 950c34969..8223aa00b 100644 --- a/src/mailman/model/tests/test_domain.py +++ b/src/mailman/model/tests/test_domain.py @@ -81,25 +81,23 @@ class TestDomainManager(unittest.TestCase): self.assertRaises(KeyError, self._manager.remove, 'doesnotexist.com') def test_domain_create_with_owner(self): - user = getUtility(IUserManager).create_user('someuser@example.org') - config.db.commit() - domain = self._manager.add('example.org', owner=user) + domain = self._manager.add('example.org', + owners=['someuser@example.org']) self.assertEqual(len(domain.owners), 1) - self.assertEqual(domain.owners[0].id, user.id) + self.assertEqual(domain.owners[0].addresses[0].email, + 'someuser@example.org') def test_add_domain_owner(self): - user = getUtility(IUserManager).create_user('someuser@example.org') - config.db.commit() domain = self._manager.add('example.org') - domain.add_owner(user) + domain.add_owner('someuser@example.org') self.assertEqual(len(domain.owners), 1) - self.assertEqual(domain.owners[0].id, user.id) + self.assertEqual(domain.owners[0].addresses[0].email, + 'someuser@example.org') def test_remove_domain_owner(self): - user = getUtility(IUserManager).create_user('someuser@somedomain.org') - config.db.commit() - domain = self._manager.add('example.org', owner=user) - domain.remove_owner(user) + domain = self._manager.add('example.org', + owners=['someuser@example.org']) + domain.remove_owner('someuser@example.org') self.assertEqual(len(domain.owners), 0) @@ -134,6 +132,3 @@ class TestDomainLifecycleEvents(unittest.TestCase): self.assertEqual(listmanager.get('dog@example.org'), None) self.assertEqual(listmanager.get('ewe@example.com'), ewe) self.assertEqual(listmanager.get('fly@example.com'), fly) - - def test_owners_are_deleted_when_domain_is(self): - self._domainmanager.remove('example.net') diff --git a/src/mailman/model/user.py b/src/mailman/model/user.py index a8bc556cd..5fecc1836 100644 --- a/src/mailman/model/user.py +++ b/src/mailman/model/user.py @@ -19,6 +19,7 @@ __all__ = [ 'User', + 'DomainOwner' ] diff --git a/src/mailman/rest/domains.py b/src/mailman/rest/domains.py index 2f41ecfd9..41a1c50bd 100644 --- a/src/mailman/rest/domains.py +++ b/src/mailman/rest/domains.py @@ -25,6 +25,7 @@ __all__ = [ from mailman.interfaces.domain import ( BadDomainSpecificationError, IDomainManager) +from mailman.interfaces.usermanager import IUserManager from mailman.rest.helpers import ( BadRequest, CollectionMixin, NotFound, bad_request, child, created, etag, no_content, not_found, okay, path_to) @@ -110,10 +111,11 @@ class AllDomains(_DomainBase): validator = Validator(mail_host=str, description=str, base_url=str, - owner=int, + owners=list, _optional=('description', 'base_url', - 'owner')) - domain = domain_manager.add(**validator(request)) + 'owners')) + values = validator(request) + domain = domain_manager.add(**values) except BadDomainSpecificationError as error: bad_request(response, str(error)) except ValueError as error: diff --git a/src/mailman/rest/lists.py b/src/mailman/rest/lists.py index f6bc27917..641ddec8e 100644 --- a/src/mailman/rest/lists.py +++ b/src/mailman/rest/lists.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2015 by the Free Software Foundation, Inc. + # Copyright (C) 2010-2015 by the Free Software Foundation, Inc. # # This file is part of GNU Mailman. # diff --git a/src/mailman/rest/tests/test_domains.py b/src/mailman/rest/tests/test_domains.py index 9ebc0c0d8..13299516c 100644 --- a/src/mailman/rest/tests/test_domains.py +++ b/src/mailman/rest/tests/test_domains.py @@ -27,6 +27,7 @@ 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 @@ -41,10 +42,16 @@ class TestDomains(unittest.TestCase): with transaction(): self._mlist = create_list('test@example.com') - def test_create_domain(self): - """Create domain via REST""" - # TODO: Complete this - # Tests should be failing with improper REST API. + def test_create_domains(self): + """Test Create domain via REST""" + data = {'mail_host': 'example.org', + 'description': 'Example domain', + 'base_url': 'http://example.org', + 'owners': ['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//lists/ is not a valid endpoint. diff --git a/src/mailman/rest/users.py b/src/mailman/rest/users.py index 5a3f0118d..b8eaee448 100644 --- a/src/mailman/rest/users.py +++ b/src/mailman/rest/users.py @@ -22,6 +22,7 @@ __all__ = [ 'AddressUser', 'AllUsers', 'Login', + 'OwnersForDomain', ] @@ -395,27 +396,25 @@ class OwnersForDomain(_UserBase): def on_post(self, request, response): """POST to /domains//owners """ - validator = Validator(owner_id=GetterSetter(int)) + validator = Validator(owner=GetterSetter(str)) try: values = validator(request) except ValueError as error: bad_request(response, str(error)) return - owner = getUtility(IUserManager).get_user_by_id(values['owner_id']) - self._domain.add_owner(owner) + self._domain.add_owner(values['owner']) return no_content(response) - def on_patch(self, request, response): - # TODO: complete this - pass - - def on_put(self, request, response): - # TODO: complete this - pass - def on_delete(self, request, response): - # TODO: complete this - pass + """DELETE to /domains//owners""" + validator = Validator(owner=GetterSetter(str)) + try: + values = validator(request) + except ValueError as error: + bad_request(response, str(error)) + return + self._domain.remove_owner(owner) + return no_content(response) @paginate def _get_collection(self, request): -- cgit v1.2.3-70-g09d2 From 20edc89f208b201e34628cc021c5bfe36ef5a035 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Mon, 6 Apr 2015 19:07:42 -0400 Subject: Checkpointing: * Cleanups. * Updates to domains and users. * Allow is_server_owner to be PUT. --- lp1423756.txt | 309 +++++++++++++++++++++ .../46e92facee7_add_serverowner_domainowner.py | 34 ++- src/mailman/model/docs/users.rst | 15 + src/mailman/model/user.py | 8 +- src/mailman/rest/docs/domains.rst | 76 ++++- src/mailman/rest/docs/users.rst | 82 +++++- src/mailman/rest/domains.py | 1 - src/mailman/rest/lists.py | 2 +- src/mailman/rest/users.py | 25 +- 9 files changed, 525 insertions(+), 27 deletions(-) create mode 100644 lp1423756.txt (limited to 'src/mailman/rest/domains.py') diff --git a/lp1423756.txt b/lp1423756.txt new file mode 100644 index 000000000..50227c1df --- /dev/null +++ b/lp1423756.txt @@ -0,0 +1,309 @@ +=== modified file 'src/mailman/rest/docs/domains.rst' +--- src/mailman/rest/docs/domains.rst 2014-12-16 01:01:53 +0000 ++++ src/mailman/rest/docs/domains.rst 2015-04-06 15:37:49 +0000 +@@ -29,14 +29,12 @@ + >>> domain_manager.add( + ... 'example.com', 'An example domain', 'http://lists.example.com') + ++ 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 +49,19 @@ + + >>> domain_manager.add( + ... 'example.org', +- ... base_url='http://mail.example.org', +- ... contact_address='listmaster@example.org') +- ++ ... base_url='http://mail.example.org') ++ + >>> domain_manager.add( + ... 'lists.example.net', + ... 'Porkmasters', +- ... 'http://example.net', +- ... 'porkmaster@example.net') ++ ... 'http://example.net') + ++ 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 +69,6 @@ + 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 +76,6 @@ + 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 +94,6 @@ + + >>> 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 +155,6 @@ + + >>> 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 +165,7 @@ + :: + + >>> domain_manager['lists.example.com'] +- ++ + + # Unlock the database. + >>> transaction.abort() +@@ -190,8 +177,7 @@ + >>> 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 +186,6 @@ + + >>> 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 +193,7 @@ + url_host: allmy.example.com + + >>> domain_manager['my.example.com'] +- ++ + + # Unlock the database. + >>> transaction.abort() + +=== modified file 'src/mailman/rest/domains.py' +--- src/mailman/rest/domains.py 2015-01-05 01:40:47 +0000 ++++ src/mailman/rest/domains.py 2015-04-06 15:37:49 +0000 +@@ -25,10 +25,12 @@ + + from mailman.interfaces.domain import ( + BadDomainSpecificationError, IDomainManager) ++from mailman.interfaces.usermanager import IUserManager + 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.users import OwnersForDomain + from mailman.rest.validator import Validator + from zope.component import getUtility + +@@ -41,7 +43,6 @@ + """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 +89,17 @@ + else: + return BadRequest(), [] + ++ @child() ++ def owners(self, request, segments): ++ """/domains//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 +111,13 @@ + validator = Validator(mail_host=str, + description=str, + base_url=str, +- contact_address=str, ++ owners=list, + _optional=('description', 'base_url', +- 'contact_address')) +- domain = domain_manager.add(**validator(request)) +- except BadDomainSpecificationError: +- bad_request(response, b'Domain exists') ++ 'owners')) ++ values = validator(request) ++ domain = domain_manager.add(**values) ++ except BadDomainSpecificationError as error: ++ bad_request(response, str(error)) + except ValueError as error: + bad_request(response, str(error)) + else: + +=== modified file 'src/mailman/rest/tests/test_domains.py' +--- src/mailman/rest/tests/test_domains.py 2015-01-05 01:40:47 +0000 ++++ src/mailman/rest/tests/test_domains.py 2015-04-06 15:37:49 +0000 +@@ -27,6 +27,7 @@ + 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 +@@ -41,6 +42,17 @@ + with transaction(): + self._mlist = create_list('test@example.com') + ++ def test_create_domains(self): ++ """Test Create domain via REST""" ++ data = {'mail_host': 'example.org', ++ 'description': 'Example domain', ++ 'base_url': 'http://example.org', ++ 'owners': ['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//lists/ is not a valid endpoint. + with self.assertRaises(HTTPError) as cm: + +=== modified file 'src/mailman/rest/users.py' +--- src/mailman/rest/users.py 2015-03-20 16:38:00 +0000 ++++ src/mailman/rest/users.py 2015-04-06 15:37:49 +0000 +@@ -22,6 +22,7 @@ + 'AddressUser', + 'AllUsers', + 'Login', ++ 'OwnersForDomain', + ] + + +@@ -67,8 +68,9 @@ + email=str, + display_name=str, + password=str, +- _optional=('display_name', 'password'), +- ) ++ is_server_owner=bool, ++ _optional=('display_name', 'password', 'is_server_owner'), ++) + + + +@@ -108,7 +110,8 @@ + user_id=user_id, + created_on=user.created_on, + self_link=path_to('users/{}'.format(user_id)), +- ) ++ is_server_owner=user.is_server_owner, ++ ) + # 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 +296,8 @@ + 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 +332,8 @@ + # 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 +382,41 @@ + 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//owners""" ++ resource = self._make_collection(request) ++ okay(response, etag(resource)) ++ ++ def on_post(self, request, response): ++ """POST to /domains//owners """ ++ validator = Validator(owner=GetterSetter(str)) ++ try: ++ values = validator(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//owners""" ++ validator = Validator(owner=GetterSetter(str)) ++ try: ++ values = validator(request) ++ except ValueError as error: ++ bad_request(response, str(error)) ++ return ++ self._domain.remove_owner(owner) ++ return no_content(response) ++ ++ @paginate ++ def _get_collection(self, request): ++ """See `CollectionMixin`.""" ++ return list(self._domain.owners) + diff --git a/src/mailman/database/alembic/versions/46e92facee7_add_serverowner_domainowner.py b/src/mailman/database/alembic/versions/46e92facee7_add_serverowner_domainowner.py index e4dc25ab8..fc489cae5 100644 --- a/src/mailman/database/alembic/versions/46e92facee7_add_serverowner_domainowner.py +++ b/src/mailman/database/alembic/versions/46e92facee7_add_serverowner_domainowner.py @@ -1,3 +1,20 @@ +# Copyright (C) 2015 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# GNU Mailman. If not, see . + """add_serverowner_domainowner Revision ID: 46e92facee7 @@ -6,7 +23,7 @@ Create Date: 2015-03-20 16:01:25.007242 """ -# revision identifiers, used by Alembic. +# Revision identifiers, used by Alembic. revision = '46e92facee7' down_revision = '33e1f5f6fa8' @@ -15,15 +32,17 @@ import sqlalchemy as sa def upgrade(): - op.create_table('domain_owner', + op.create_table( + 'domain_owner', sa.Column('user_id', sa.Integer(), nullable=False), sa.Column('domain_id', sa.Integer(), nullable=False), sa.ForeignKeyConstraint(['domain_id'], ['domain.id'], ), sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), sa.PrimaryKeyConstraint('user_id', 'domain_id') - ) - op.add_column('user', sa.Column('is_server_owner', sa.Boolean(), - nullable=True)) + ) + op.add_column( + 'user', + sa.Column('is_server_owner', sa.Boolean(), nullable=True)) if op.get_bind().dialect.name != 'sqlite': op.drop_column('domain', 'contact_address') @@ -31,6 +50,7 @@ def upgrade(): def downgrade(): if op.get_bind().dialect.name != 'sqlite': op.drop_column('user', 'is_server_owner') - op.add_column('domain', sa.Column('contact_address', sa.VARCHAR(), - nullable=True)) + op.add_column( + 'domain', + sa.Column('contact_address', sa.VARCHAR(), nullable=True)) op.drop_table('domain_owner') diff --git a/src/mailman/model/docs/users.rst b/src/mailman/model/docs/users.rst index 0b926d6a7..0d6a0f368 100644 --- a/src/mailman/model/docs/users.rst +++ b/src/mailman/model/docs/users.rst @@ -295,4 +295,19 @@ membership role. zperson@example.org xtest_2.example.com MemberRole.owner +Server owners +============= + +Some users are server owners. Zoe is not yet a server owner. + + >>> user_1.is_server_owner + False + +So, let's make her one. + + >>> user_1.is_server_owner = True + >>> user_1.is_server_owner + True + + .. _`usermanager.txt`: usermanager.html diff --git a/src/mailman/model/user.py b/src/mailman/model/user.py index 5fecc1836..f6aedd132 100644 --- a/src/mailman/model/user.py +++ b/src/mailman/model/user.py @@ -18,8 +18,8 @@ """Model for users.""" __all__ = [ + 'DomainOwner', 'User', - 'DomainOwner' ] @@ -35,7 +35,7 @@ from mailman.model.preferences import Preferences from mailman.model.roster import Memberships from mailman.utilities.datetime import factory as date_factory from mailman.utilities.uid import UniqueIDFactory -from sqlalchemy import Column, DateTime, ForeignKey, Integer, Unicode, Boolean +from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, Unicode from sqlalchemy.orm import relationship, backref from zope.event import notify from zope.interface import implementer @@ -180,9 +180,11 @@ class User(Model): return Memberships(self) + class DomainOwner(Model): - """Domain to owners(user) association class""" + """Internal table for associating domains to their owners.""" __tablename__ = 'domain_owner' + user_id = Column(Integer, ForeignKey('user.id'), primary_key=True) domain_id = Column(Integer, ForeignKey('domain.id'), primary_key=True) diff --git a/src/mailman/rest/docs/domains.rst b/src/mailman/rest/docs/domains.rst index 16204548c..f57dee572 100644 --- a/src/mailman/rest/docs/domains.rst +++ b/src/mailman/rest/docs/domains.rst @@ -28,8 +28,7 @@ Once a domain is added, it is accessible through the API. >>> domain_manager.add( ... 'example.com', 'An example domain', 'http://lists.example.com') - + >>> transaction.commit() >>> dump_json('http://localhost:9001/3.0/domains') @@ -55,8 +54,7 @@ At the top level, all domains are returned as separate entries. ... 'lists.example.net', ... 'Porkmasters', ... 'http://example.net') - + >>> transaction.commit() >>> dump_json('http://localhost:9001/3.0/domains') @@ -212,4 +210,74 @@ 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', + ... }) + content-length: 0 + date: ... + server: ... + status: 204 + + >>> dump_json('http://localhost:9001/3.0/domains/my.example.com/owners', { + ... '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') + + >>> 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 + + .. _Domains: ../../model/docs/domains.html diff --git a/src/mailman/rest/docs/users.rst b/src/mailman/rest/docs/users.rst index 343598518..13390a00f 100644 --- a/src/mailman/rest/docs/users.rst +++ b/src/mailman/rest/docs/users.rst @@ -257,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: ... @@ -416,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 41a1c50bd..dff2f2073 100644 --- a/src/mailman/rest/domains.py +++ b/src/mailman/rest/domains.py @@ -25,7 +25,6 @@ __all__ = [ from mailman.interfaces.domain import ( BadDomainSpecificationError, IDomainManager) -from mailman.interfaces.usermanager import IUserManager from mailman.rest.helpers import ( BadRequest, CollectionMixin, NotFound, bad_request, child, created, etag, no_content, not_found, okay, path_to) diff --git a/src/mailman/rest/lists.py b/src/mailman/rest/lists.py index 641ddec8e..f6bc27917 100644 --- a/src/mailman/rest/lists.py +++ b/src/mailman/rest/lists.py @@ -1,4 +1,4 @@ - # Copyright (C) 2010-2015 by the Free Software Foundation, Inc. +# Copyright (C) 2010-2015 by the Free Software Foundation, Inc. # # This file is part of GNU Mailman. # diff --git a/src/mailman/rest/users.py b/src/mailman/rest/users.py index b8eaee448..a38299d00 100644 --- a/src/mailman/rest/users.py +++ b/src/mailman/rest/users.py @@ -59,18 +59,19 @@ class PasswordEncrypterGetterSetter(GetterSetter): 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, - password=str, + email=str, is_server_owner=bool, + password=str, _optional=('display_name', 'password', 'is_server_owner'), -) + ) @@ -80,6 +81,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: @@ -90,6 +92,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 @@ -107,10 +110,10 @@ 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, - self_link=path_to('users/{}'.format(user_id)), 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. @@ -296,8 +299,8 @@ class AddressUser(_UserBase): del fields['email'] fields['user_id'] = int fields['auto_create'] = as_boolean - fields['_optional'] = fields['_optional'] + ('user_id', 'auto_create', - 'is_server_owner') + fields['_optional'] = fields['_optional'] + ( + 'user_id', 'auto_create', 'is_server_owner') try: validator = Validator(**fields) arguments = validator(request) @@ -332,8 +335,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', - 'is_server_owner') + fields['_optional'] = fields['_optional'] + ( + 'user_id', 'email', 'is_server_owner') try: validator = Validator(**fields) arguments = validator(request) @@ -383,6 +386,8 @@ class Login: else: forbidden(response) + + class OwnersForDomain(_UserBase): """Owners for a particular domain.""" -- cgit v1.2.3-70-g09d2 From a499404b568b303697b1969d8766f7f8433b76a7 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Mon, 6 Apr 2015 21:39:34 -0400 Subject: Add lots of test for various bits of domain owners. A little refactoring of list_of_str() into list_of_strings_validator() which is now put in the validators.py module and used in several places. Python 3 super() style. Add some code and tests to catch /owners references when doesn't exist. --- src/mailman/rest/docs/domains.rst | 40 +++++++++++++++++++-------- src/mailman/rest/domains.py | 13 ++++++--- src/mailman/rest/listconf.py | 13 +++------ src/mailman/rest/tests/test_domains.py | 44 +++++++++++++++++++++++++++++- src/mailman/rest/users.py | 49 ++++++++++++++++++++++++++-------- src/mailman/rest/validator.py | 9 +++++++ 6 files changed, 131 insertions(+), 37 deletions(-) (limited to 'src/mailman/rest/domains.py') 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//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//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//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.""" -- cgit v1.2.3-70-g09d2