diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/mailman/commands/cli_info.py | 7 | ||||
| -rw-r--r-- | src/mailman/core/api.py (renamed from src/mailman/rest/api.py) | 28 | ||||
| -rw-r--r-- | src/mailman/interfaces/api.py | 34 | ||||
| -rw-r--r-- | src/mailman/rest/addresses.py | 9 | ||||
| -rw-r--r-- | src/mailman/rest/docs/helpers.rst | 28 | ||||
| -rw-r--r-- | src/mailman/rest/domains.py | 4 | ||||
| -rw-r--r-- | src/mailman/rest/helpers.py | 25 | ||||
| -rw-r--r-- | src/mailman/rest/lists.py | 4 | ||||
| -rw-r--r-- | src/mailman/rest/members.py | 11 | ||||
| -rw-r--r-- | src/mailman/rest/post_moderation.py | 8 | ||||
| -rw-r--r-- | src/mailman/rest/preferences.py | 7 | ||||
| -rw-r--r-- | src/mailman/rest/queues.py | 6 | ||||
| -rw-r--r-- | src/mailman/rest/root.py | 6 | ||||
| -rw-r--r-- | src/mailman/rest/tests/test_addresses.py | 12 | ||||
| -rw-r--r-- | src/mailman/rest/tests/test_validator.py | 2 | ||||
| -rw-r--r-- | src/mailman/rest/users.py | 9 |
16 files changed, 107 insertions, 93 deletions
diff --git a/src/mailman/commands/cli_info.py b/src/mailman/commands/cli_info.py index 757c02a3f..76e8e55da 100644 --- a/src/mailman/commands/cli_info.py +++ b/src/mailman/commands/cli_info.py @@ -26,9 +26,9 @@ import sys from lazr.config import as_boolean from mailman.config import config +from mailman.core.api import API30, API31 from mailman.core.i18n import _ from mailman.interfaces.command import ICLISubCommand -from mailman.rest.helpers import path_to from mailman.version import MAILMAN_VERSION_FULL from zope.interface import implementer @@ -68,9 +68,8 @@ class Info: print('devmode:', 'ENABLED' if as_boolean(config.devmode.enabled) else 'DISABLED', file=output) - print('REST root url:', - path_to('/', config.webservice.api_version), - file=output) + api = (API30 if config.webservice.api_version == '3.0' else API31) + print('REST root url:', api.path_to('/'), file=output) print('REST credentials: {0}:{1}'.format( config.webservice.admin_user, config.webservice.admin_pass), file=output) diff --git a/src/mailman/rest/api.py b/src/mailman/core/api.py index 998fac268..d23a9ec67 100644 --- a/src/mailman/rest/api.py +++ b/src/mailman/core/api.py @@ -23,6 +23,8 @@ __all__ = [ ] +from lazr.config import as_boolean +from mailman.config import config from mailman.interfaces.api import IAPI from uuid import UUID from zope.interface import implementer @@ -32,12 +34,25 @@ from zope.interface import implementer class API30: version = '3.0' + @classmethod + def path_to(cls, resource): + """See `IAPI`.""" + return '{}://{}:{}/{}/{}'.format( + ('https' if as_boolean(config.webservice.use_https) else 'http'), + config.webservice.hostname, + config.webservice.port, + cls.version, + (resource[1:] if resource.startswith('/') else resource), + ) + @staticmethod def from_uuid(uuid): + """See `IAPI`.""" return uuid.int @staticmethod def to_uuid(uuid_repr): + """See `IAPI`.""" return UUID(int=int(uuid_repr)) @@ -45,10 +60,23 @@ class API30: class API31: version = '3.1' + @classmethod + def path_to(cls, resource): + """See `IAPI`.""" + return '{}://{}:{}/{}/{}'.format( + ('https' if as_boolean(config.webservice.use_https) else 'http'), + config.webservice.hostname, + config.webservice.port, + cls.version, + (resource[1:] if resource.startswith('/') else resource), + ) + @staticmethod def from_uuid(uuid): + """See `IAPI`.""" return uuid.hex @staticmethod def to_uuid(uuid_repr): + """See `IAPI`.""" return UUID(hex=uuid_repr) diff --git a/src/mailman/interfaces/api.py b/src/mailman/interfaces/api.py index f2b33ea09..c64f37346 100644 --- a/src/mailman/interfaces/api.py +++ b/src/mailman/interfaces/api.py @@ -30,8 +30,36 @@ class IAPI(Interface): version = Attribute("""The REST API version.""") + def path_to(resource): + """Return the full REST URL to the given resource. + + :param resource: Resource path string without the leading scheme, + host, port, or API version information. + :type resource: str + :return: Full URL path to the resource, with the scheme, host, port + and API version prepended. + :rtype: str + """ + def from_uuid(uuid): - """Return the string representation of a UUID.""" + """Return the string representation of a UUID. + + :param uuid: The UUID to convert. + :type uuid: UUID + :return: The string representation of the UUID, as appropriate for the + API version. In 3.0 this is the representation of an integer, + while in 3.1 it is the hex representation. + :rtype: str + """ + + def to_uuid(uuid): + """Return the UUID from the string representation. - def to_uuid(uuid_repr): - """Return the UUID from the string representation.""" + :param uuid: The string representation of the UUID. + :type uuid: str + :return: The UUID converted from the string representation, as + appropriate for the API version. In 3.0, uuid is interpreted as + the integer representation of a UUID, while in 3.1 it is the hex + representation of the UUID. + :rtype: UUID + """ diff --git a/src/mailman/rest/addresses.py b/src/mailman/rest/addresses.py index 83ff87454..421e17bf1 100644 --- a/src/mailman/rest/addresses.py +++ b/src/mailman/rest/addresses.py @@ -51,7 +51,7 @@ class _AddressBase(CollectionMixin): email=address.email, original_email=address.original_email, registered_on=address.registered_on, - self_link=self.path_to('addresses/{}'.format(address.email)), + self_link=self.api.path_to('addresses/{}'.format(address.email)), ) # Add optional attributes. These can be None or the empty string. if address.display_name: @@ -60,7 +60,7 @@ class _AddressBase(CollectionMixin): representation['verified_on'] = address.verified_on if address.user: uid = self.api.from_uuid(address.user.user_id) - representation['user'] = self.path_to('users/{}'.format(uid)) + representation['user'] = self.api.path_to('users/{}'.format(uid)) return representation def _get_collection(self, request): @@ -213,14 +213,15 @@ class UserAddresses(_AddressBase): address = user_manager.get_address(validator(request)['email']) if address.user is None: address.user = self._user - location = self.path_to('addresses/{}'.format(address.email)) + location = self.api.path_to( + 'addresses/{}'.format(address.email)) created(response, location) else: bad_request(response, 'Address belongs to other user.') else: # Link the address to the current user and return it. address.user = self._user - location = self.path_to('addresses/{}'.format(address.email)) + location = self.api.path_to('addresses/{}'.format(address.email)) created(response, location) diff --git a/src/mailman/rest/docs/helpers.rst b/src/mailman/rest/docs/helpers.rst index ccd09a061..36870c079 100644 --- a/src/mailman/rest/docs/helpers.rst +++ b/src/mailman/rest/docs/helpers.rst @@ -5,34 +5,6 @@ REST API helpers There are a number of helpers that make building out the REST API easier. -Resource paths -============== - -For example, most resources don't have to worry about where they are rooted. -They only need to know where they are relative to the root URI, and this -function can return them the full path to the resource. We have to pass in -the REST API version because there is no request in flight. - - >>> from mailman.rest.helpers import path_to - >>> print(path_to('system', '3.0')) - http://localhost:9001/3.0/system - -Parameters like the ``scheme``, ``host``, and ``port`` can be set in the -configuration file. -:: - - >>> config.push('helpers', """ - ... [webservice] - ... hostname: geddy - ... port: 2112 - ... use_https: yes - ... """) - >>> cleanups.append((config.pop, 'helpers')) - - >>> print(path_to('system', '4.2')) - https://geddy:2112/4.2/system - - Etags ===== diff --git a/src/mailman/rest/domains.py b/src/mailman/rest/domains.py index b115f63bd..0128c9da0 100644 --- a/src/mailman/rest/domains.py +++ b/src/mailman/rest/domains.py @@ -44,7 +44,7 @@ class _DomainBase(CollectionMixin): base_url=domain.base_url, description=domain.description, mail_host=domain.mail_host, - self_link=self.path_to('domains/{}'.format(domain.mail_host)), + self_link=self.api.path_to('domains/{}'.format(domain.mail_host)), url_host=domain.url_host, ) @@ -123,7 +123,7 @@ class AllDomains(_DomainBase): except BadDomainSpecificationError as error: bad_request(response, str(error)) else: - location = self.path_to('domains/{}'.format(domain.mail_host)) + location = self.api.path_to('domains/{}'.format(domain.mail_host)) created(response, location) def on_get(self, request, response): diff --git a/src/mailman/rest/helpers.py b/src/mailman/rest/helpers.py index 425164ef7..135d950ac 100644 --- a/src/mailman/rest/helpers.py +++ b/src/mailman/rest/helpers.py @@ -32,7 +32,6 @@ __all__ = [ 'no_content', 'not_found', 'okay', - 'path_to', ] @@ -48,27 +47,6 @@ from pprint import pformat -def path_to(resource, api_version): - """Return the url path to a resource. - - :param resource: The canonical path to the resource, relative to the - system base URI. - :type resource: string - :param api_version: API version to report. - :type api_version: string - :return: The full path to the resource. - :rtype: bytes - """ - return '{0}://{1}:{2}/{3}/{4}'.format( - ('https' if as_boolean(config.webservice.use_https) else 'http'), - config.webservice.hostname, - config.webservice.port, - api_version, - (resource[1:] if resource.startswith('/') else resource), - ) - - - class ExtendedEncoder(json.JSONEncoder): """An extended JSON encoder which knows about other data types.""" @@ -185,9 +163,6 @@ class CollectionMixin: result['entries'] = entries return result - def path_to(self, resource): - return path_to(resource, self.api.version) - class GetterSetter: diff --git a/src/mailman/rest/lists.py b/src/mailman/rest/lists.py index 426c9c633..3e0c0bbca 100644 --- a/src/mailman/rest/lists.py +++ b/src/mailman/rest/lists.py @@ -110,7 +110,7 @@ class _ListBase(CollectionMixin): mail_host=mlist.mail_host, member_count=mlist.members.member_count, volume=mlist.volume, - self_link=self.path_to('lists/{}'.format(mlist.list_id)), + self_link=self.api.path_to('lists/{}'.format(mlist.list_id)), ) def _get_collection(self, request): @@ -216,7 +216,7 @@ class AllLists(_ListBase): reason = 'Domain does not exist: {}'.format(error.domain) bad_request(response, reason.encode('utf-8')) else: - location = self.path_to('lists/{0}'.format(mlist.list_id)) + location = self.api.path_to('lists/{0}'.format(mlist.list_id)) created(response, location) def on_get(self, request, response): diff --git a/src/mailman/rest/members.py b/src/mailman/rest/members.py index b666bc2f4..91db982c7 100644 --- a/src/mailman/rest/members.py +++ b/src/mailman/rest/members.py @@ -63,20 +63,21 @@ class _MemberBase(CollectionMixin): # issue #121 for details. member_id = self.api.from_uuid(member.member_id) response = dict( - address=self.path_to('addresses/{}'.format(member.address.email)), + address=self.api.path_to( + 'addresses/{}'.format(member.address.email)), delivery_mode=member.delivery_mode, email=member.address.email, list_id=member.list_id, member_id=member_id, moderation_action=member.moderation_action, role=role, - self_link=self.path_to('members/{}'.format(member_id)), + self_link=self.api.path_to('members/{}'.format(member_id)), ) # Add the user link if there is one. user = member.user if user is not None: user_id = self.api.from_uuid(user.user_id) - response['user'] = self.path_to('users/{}'.format(user_id)) + response['user'] = self.api.path_to('users/{}'.format(user_id)) return response def _get_collection(self, request): @@ -285,7 +286,7 @@ class AllMembers(_MemberBase): # UUIDs and need to be converted to URLs because JSON doesn't # directly support UUIDs. member_id = self.api.from_uuid(member.member_id) - location = self.path_to('members/{}'.format(member_id)) + location = self.api.path_to('members/{}'.format(member_id)) created(response, location) return # The member could not be directly subscribed because there are @@ -332,7 +333,7 @@ class AllMembers(_MemberBase): # UUIDs and need to be converted to URLs because JSON doesn't # directly support UUIDs. member_id = self.api.from_uuid(member.member_id) - location = self.path_to('members/{}'.format(member_id)) + location = self.api.path_to('members/{}'.format(member_id)) created(response, location) def on_get(self, request, response): diff --git a/src/mailman/rest/post_moderation.py b/src/mailman/rest/post_moderation.py index a9b3ba1b7..6283add75 100644 --- a/src/mailman/rest/post_moderation.py +++ b/src/mailman/rest/post_moderation.py @@ -28,8 +28,7 @@ from mailman.interfaces.action import Action from mailman.interfaces.messages import IMessageStore from mailman.interfaces.requests import IListRequests, RequestType from mailman.rest.helpers import ( - CollectionMixin, bad_request, child, etag, no_content, not_found, okay, - path_to) + CollectionMixin, bad_request, child, etag, no_content, not_found, okay) from mailman.rest.validator import Validator, enum_validator from zope.component import getUtility @@ -62,9 +61,8 @@ class _ModerationBase: # that's fine too. resource.pop('id', None) # Add a self_link. - resource['self_link'] = path_to( - 'lists/{}/held/{}'.format(self._mlist.list_id, request_id), - self.api.version) + resource['self_link'] = self.api.path_to( + 'lists/{}/held/{}'.format(self._mlist.list_id, request_id)) return resource diff --git a/src/mailman/rest/preferences.py b/src/mailman/rest/preferences.py index 4299dc0cd..694aa47e9 100644 --- a/src/mailman/rest/preferences.py +++ b/src/mailman/rest/preferences.py @@ -26,7 +26,7 @@ __all__ = [ from lazr.config import as_boolean from mailman.interfaces.member import DeliveryMode, DeliveryStatus from mailman.rest.helpers import ( - GetterSetter, bad_request, etag, no_content, not_found, okay, path_to) + GetterSetter, bad_request, etag, no_content, not_found, okay) from mailman.rest.validator import ( Validator, enum_validator, language_validator) @@ -64,9 +64,8 @@ class ReadOnlyPreferences: if preferred_language is not None: resource['preferred_language'] = preferred_language.code # Add the self link. - resource['self_link'] = path_to( - '{}/preferences'.format(self._base_url), - self.api.version) + resource['self_link'] = self.api.path_to( + '{}/preferences'.format(self._base_url)) okay(response, etag(resource)) diff --git a/src/mailman/rest/queues.py b/src/mailman/rest/queues.py index 983b7481e..4d3c9f58b 100644 --- a/src/mailman/rest/queues.py +++ b/src/mailman/rest/queues.py @@ -46,7 +46,7 @@ class _QueuesBase(CollectionMixin): directory=switchboard.queue_directory, count=len(files), files=files, - self_link=self.path_to('queues/{}'.format(name)), + self_link=self.api.path_to('queues/{}'.format(name)), ) def _get_collection(self, request): @@ -89,7 +89,7 @@ class AQueue(_QueuesBase): bad_request(response, str(error)) return else: - location = self.path_to( + location = self.api.path_to( 'queues/{}/{}'.format(self._name, filebase)) created(response, location) @@ -122,5 +122,5 @@ class AllQueues(_QueuesBase): def on_get(self, request, response): """<api>/queues""" resource = self._make_collection(request) - resource['self_link'] = self.path_to('queues') + resource['self_link'] = self.api.path_to('queues') okay(response, etag(resource)) diff --git a/src/mailman/rest/root.py b/src/mailman/rest/root.py index 8e6e9f716..ca6e031e8 100644 --- a/src/mailman/rest/root.py +++ b/src/mailman/rest/root.py @@ -26,15 +26,15 @@ import falcon from base64 import b64decode from mailman.config import config +from mailman.core.api import API30, API31 from mailman.core.constants import system_preferences from mailman.core.system import system from mailman.interfaces.listmanager import IListManager from mailman.model.uid import UID from mailman.rest.addresses import AllAddresses, AnAddress -from mailman.rest.api import API30, API31 from mailman.rest.domains import ADomain, AllDomains from mailman.rest.helpers import ( - BadRequest, NotFound, child, etag, no_content, not_found, okay, path_to) + BadRequest, NotFound, child, etag, no_content, not_found, okay) from mailman.rest.lists import AList, AllLists, Styles from mailman.rest.members import AMember, AllMembers, FindMembers from mailman.rest.preferences import ReadOnlyPreferences @@ -104,7 +104,7 @@ class Versions: mailman_version=system.mailman_version, python_version=system.python_version, api_version=self.api.version, - self_link=path_to('system/versions', self.api.version), + self_link=self.api.path_to('system/versions'), ) okay(response, etag(resource)) diff --git a/src/mailman/rest/tests/test_addresses.py b/src/mailman/rest/tests/test_addresses.py index bbb6ee2d1..0af52f607 100644 --- a/src/mailman/rest/tests/test_addresses.py +++ b/src/mailman/rest/tests/test_addresses.py @@ -369,6 +369,18 @@ class TestAddresses(unittest.TestCase): }) self.assertEqual(cm.exception.code, 403) + def test_user_subresource_post_no_such_user(self): + # Try to link an address to a nonexistent user. + with transaction(): + getUtility(IUserManager).create_address('anne@example.com') + with self.assertRaises(HTTPError) as cm: + call_api( + 'http://localhost:9001/3.0/addresses/anne@example.com/user', { + 'user_id': 2, + }) + self.assertEqual(cm.exception.code, 400) + self.assertEqual(cm.exception.reason, b'No user with ID 2') + def test_user_subresource_unlink(self): # By DELETEing the usr subresource, you can unlink a user from an # address. diff --git a/src/mailman/rest/tests/test_validator.py b/src/mailman/rest/tests/test_validator.py index 7d7900aaf..287928d3c 100644 --- a/src/mailman/rest/tests/test_validator.py +++ b/src/mailman/rest/tests/test_validator.py @@ -25,7 +25,7 @@ __all__ = [ import unittest from mailman.interfaces.usermanager import IUserManager -from mailman.rest.api import API30, API31 +from mailman.core.api import API30, API31 from mailman.rest.validator import ( list_of_strings_validator, subscriber_validator) from mailman.testing.layers import RESTLayer diff --git a/src/mailman/rest/users.py b/src/mailman/rest/users.py index 017a09525..238ec4562 100644 --- a/src/mailman/rest/users.py +++ b/src/mailman/rest/users.py @@ -35,7 +35,7 @@ from mailman.interfaces.usermanager import IUserManager from mailman.rest.addresses import UserAddresses from mailman.rest.helpers import ( BadRequest, CollectionMixin, GetterSetter, NotFound, bad_request, child, - conflict, created, etag, forbidden, no_content, not_found, okay, path_to) + conflict, created, etag, forbidden, no_content, not_found, okay) from mailman.rest.preferences import Preferences from mailman.rest.validator import ( PatchValidator, Validator, list_of_strings_validator) @@ -118,7 +118,7 @@ def create_user(arguments, request, response): user.is_server_owner = is_server_owner api = request.context['api'] user_id = api.from_uuid(user.user_id) - location = path_to('users/{}'.format(user_id), api.version) + location = request.context.get('api').path_to('users/{}'.format(user_id)) created(response, location) return user @@ -137,7 +137,7 @@ class _UserBase(CollectionMixin): resource = dict( created_on=user.created_on, is_server_owner=user.is_server_owner, - self_link=self.path_to('users/{}'.format(user_id)), + self_link=self.api.path_to('users/{}'.format(user_id)), user_id=user_id, ) # Add the password attribute, only if the user has a password. Same @@ -335,7 +335,8 @@ class AddressUser(_UserBase): user_id = arguments['user_id'] user = user_manager.get_user_by_id(user_id) if user is None: - not_found(response, b'No user with ID {}'.format(user_id)) + bad_request(response, 'No user with ID {}'.format( + self.api.from_uuid(user_id))) return okay(response) else: |
