diff options
| author | Aurélien Bompard | 2014-12-08 16:43:08 +0100 |
|---|---|---|
| committer | Aurélien Bompard | 2014-12-08 16:43:08 +0100 |
| commit | cb4eb3cdb6fb938dd4347079cebf0f35ced1cb9d (patch) | |
| tree | 08239b9edc057c08d9f60c9e4d84b1834487989b | |
| parent | 8dfd0a2e2d37f282b71df8e7c115d4fefa106d7b (diff) | |
| download | mailman-cb4eb3cdb6fb938dd4347079cebf0f35ced1cb9d.tar.gz mailman-cb4eb3cdb6fb938dd4347079cebf0f35ced1cb9d.tar.zst mailman-cb4eb3cdb6fb938dd4347079cebf0f35ced1cb9d.zip | |
| -rw-r--r-- | src/mailman/rest/addresses.py | 10 | ||||
| -rw-r--r-- | src/mailman/rest/docs/addresses.rst | 5 | ||||
| -rw-r--r-- | src/mailman/rest/docs/users.rst | 4 | ||||
| -rw-r--r-- | src/mailman/rest/tests/test_addresses.py | 136 | ||||
| -rw-r--r-- | src/mailman/rest/users.py | 140 |
5 files changed, 274 insertions, 21 deletions
diff --git a/src/mailman/rest/addresses.py b/src/mailman/rest/addresses.py index fa3d099b6..4321b7664 100644 --- a/src/mailman/rest/addresses.py +++ b/src/mailman/rest/addresses.py @@ -62,6 +62,9 @@ class _AddressBase(CollectionMixin): representation['display_name'] = address.display_name if address.verified_on: representation['verified_on'] = address.verified_on + if address.user: + representation['user'] = path_to( + 'users/{0}'.format(address.user.user_id.int)) return representation def _get_collection(self, request): @@ -156,6 +159,13 @@ class AnAddress(_AddressBase): child = _VerifyResource(self._address, 'unverify') return child, [] + @child() + def user(self, request, segments): + """/addresses/<email>/user""" + if self._address is None: + return NotFound(), [] + from mailman.rest.users import AddressUser # avoid circular imports + return AddressUser(self._address) class UserAddresses(_AddressBase): diff --git a/src/mailman/rest/docs/addresses.rst b/src/mailman/rest/docs/addresses.rst index fec0c194b..8d7ca6835 100644 --- a/src/mailman/rest/docs/addresses.rst +++ b/src/mailman/rest/docs/addresses.rst @@ -161,6 +161,7 @@ addresses live in the /addresses namespace. original_email: dave@example.com registered_on: 2005-08-01T07:49:23 self_link: http://localhost:9001/3.0/addresses/dave@example.com + user: http://localhost:9001/3.0/users/1 http_etag: "..." start: 0 total_size: 1 @@ -172,6 +173,7 @@ addresses live in the /addresses namespace. original_email: dave@example.com registered_on: 2005-08-01T07:49:23 self_link: http://localhost:9001/3.0/addresses/dave@example.com + user: http://localhost:9001/3.0/users/1 A user can be associated with multiple email addresses. You can add new addresses to an existing user. @@ -208,6 +210,7 @@ The user controls these new addresses. original_email: dave.person@example.org registered_on: 2005-08-01T07:49:23 self_link: http://localhost:9001/3.0/addresses/dave.person@example.org + user: http://localhost:9001/3.0/users/1 entry 1: display_name: Dave Person email: dave@example.com @@ -215,6 +218,7 @@ The user controls these new addresses. original_email: dave@example.com registered_on: 2005-08-01T07:49:23 self_link: http://localhost:9001/3.0/addresses/dave@example.com + user: http://localhost:9001/3.0/users/1 entry 2: display_name: Davie P email: dp@example.org @@ -222,6 +226,7 @@ The user controls these new addresses. original_email: dp@example.org registered_on: 2005-08-01T07:49:23 self_link: http://localhost:9001/3.0/addresses/dp@example.org + user: http://localhost:9001/3.0/users/1 http_etag: "..." start: 0 total_size: 3 diff --git a/src/mailman/rest/docs/users.rst b/src/mailman/rest/docs/users.rst index 04533f578..b2adcaccb 100644 --- a/src/mailman/rest/docs/users.rst +++ b/src/mailman/rest/docs/users.rst @@ -329,18 +329,21 @@ order by original (i.e. case-preserved) email address. registered_on: 2005-08-01T07:49:23 self_link: http://localhost:9001/3.0/addresses/fred.q.person@example.com + user: http://localhost:9001/3.0/users/6 entry 1: email: fperson@example.com http_etag: "..." original_email: fperson@example.com registered_on: 2005-08-01T07:49:23 self_link: http://localhost:9001/3.0/addresses/fperson@example.com + user: http://localhost:9001/3.0/users/6 entry 2: email: fred.person@example.com http_etag: "..." original_email: fred.person@example.com registered_on: 2005-08-01T07:49:23 self_link: http://localhost:9001/3.0/addresses/fred.person@example.com + user: http://localhost:9001/3.0/users/6 entry 3: display_name: Fred Person email: fred@example.com @@ -348,6 +351,7 @@ order by original (i.e. case-preserved) email address. original_email: fred@example.com registered_on: 2005-08-01T07:49:23 self_link: http://localhost:9001/3.0/addresses/fred@example.com + user: http://localhost:9001/3.0/users/6 http_etag: "..." start: 0 total_size: 4 diff --git a/src/mailman/rest/tests/test_addresses.py b/src/mailman/rest/tests/test_addresses.py index f4aeb3013..9d2d2bd24 100644 --- a/src/mailman/rest/tests/test_addresses.py +++ b/src/mailman/rest/tests/test_addresses.py @@ -206,3 +206,139 @@ class TestAddresses(unittest.TestCase): 'email': 'anne.person@example.org', }) self.assertEqual(cm.exception.code, 404) + + def test_address_with_user(self): + with transaction(): + anne = getUtility(IUserManager).create_user('anne@example.com') + json, headers = call_api( + 'http://localhost:9001/3.0/addresses/anne@example.com') + self.assertEqual(headers['status'], '200') + self.assertIn("user", json) + self.assertEqual(json["user"], "http://localhost:9001/3.0/users/1") + + def test_address_without_user(self): + with transaction(): + anne = getUtility(IUserManager).create_address('anne@example.com') + json, headers = call_api( + 'http://localhost:9001/3.0/addresses/anne@example.com') + self.assertEqual(headers['status'], '200') + self.assertNotIn("user", json) + + def test_user_subresource_on_unlinked_address(self): + with transaction(): + anne = 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') + self.assertEqual(cm.exception.code, 404) + + def test_user_subresource(self): + user_manager = getUtility(IUserManager) + with transaction(): + anne = user_manager.create_user('anne@example.com', 'Anne') + json, headers = call_api( + 'http://localhost:9001/3.0/addresses/anne@example.com/user') + self.assertEqual(headers['status'], '200') + self.assertEqual(json["user_id"], 1) + self.assertEqual(json["display_name"], "Anne") + self.assertEqual(json["self_link"], "http://localhost:9001/3.0/users/1") + + def test_user_subresource_post(self): + user_manager = getUtility(IUserManager) + with transaction(): + anne = user_manager.create_user('anne.person@example.org', 'Anne') + anne_addr = user_manager.create_address('anne@example.com') + response, headers = call_api( + 'http://localhost:9001/3.0/addresses/anne@example.com/user', { + 'user_id': anne.user_id.int, + }) + self.assertEqual(headers['status'], '200') + self.assertEqual(anne_addr.user, anne) + self.assertEqual(sorted([a.email for a in anne.addresses]), + ['anne.person@example.org', 'anne@example.com']) + + def test_user_subresource_post_new_user(self): + user_manager = getUtility(IUserManager) + with transaction(): + anne_addr = user_manager.create_address('anne@example.com') + response, headers = call_api( + 'http://localhost:9001/3.0/addresses/anne@example.com/user', { + 'display_name': 'Anne', + }) + self.assertEqual(headers['status'], '201') + anne = user_manager.get_user('anne@example.com') + self.assertTrue(anne is not None) + self.assertEqual(anne.display_name, 'Anne') + self.assertEqual([a.email for a in anne.addresses], + ['anne@example.com']) + self.assertEqual(anne_addr.user, anne) + self.assertEqual(headers['location'], + 'http://localhost:9001/3.0/users/1') + + def test_user_subresource_post_conflict(self): + with transaction(): + anne = getUtility(IUserManager).create_user('anne@example.com') + with self.assertRaises(HTTPError) as cm: + call_api( + 'http://localhost:9001/3.0/addresses/anne@example.com/user', { + 'email': 'anne.person@example.org', + }) + self.assertEqual(cm.exception.code, 409) + + def test_user_subresource_post_new_user_no_autocreate(self): + with transaction(): + anne = getUtility(IUserManager).create_address('anne@example.com') + with self.assertRaises(HTTPError) as cm: + json, headers = call_api( + 'http://localhost:9001/3.0/addresses/anne@example.com/user', { + 'display_name': 'Anne', + 'autocreate': 0, + }) + print("test_user_subresource_post_new_user_no_autocreate", headers, json) + self.assertEqual(cm.exception.code, 404) + + def test_user_subresource_unlink(self): + user_manager = getUtility(IUserManager) + with transaction(): + anne = user_manager.create_user('anne@example.com') + response, headers = call_api( + 'http://localhost:9001/3.0/addresses/anne@example.com/user', + method="DELETE") + self.assertEqual(headers["status"], '204') + anne_addr = user_manager.get_address('anne@example.com') + self.assertTrue(anne_addr.user is None, "The address is still linked") + self.assertTrue(user_manager.get_user('anne@example.com') is None) + + def test_user_subresource_put(self): + user_manager = getUtility(IUserManager) + with transaction(): + anne_1 = user_manager.create_user('anne@example.com', 'Anne 1') + anne_2 = user_manager.create_user(display_name='Anne 2') + response, headers = call_api( + 'http://localhost:9001/3.0/addresses/anne@example.com/user', { + 'user_id': anne_2.user_id.int, + }, method="PUT") + self.assertEqual(headers['status'], '200') + self.assertEqual(anne_1.addresses, []) + self.assertEqual([a.email for a in anne_2.addresses], + ['anne@example.com']) + self.assertEqual(anne_2, + user_manager.get_address('anne@example.com').user) + + def test_user_subresource_put_create(self): + user_manager = getUtility(IUserManager) + with transaction(): + anne = user_manager.create_user('anne@example.com', 'Anne') + response, headers = call_api( + 'http://localhost:9001/3.0/addresses/anne@example.com/user', { + 'email': 'anne.person@example.org', + }, method="PUT") + self.assertEqual(headers['status'], '201') + self.assertEqual(anne.addresses, []) + anne_person = user_manager.get_user('anne.person@example.org') + self.assertTrue(anne_person is not None) + self.assertEqual(sorted([a.email for a in anne_person.addresses]), + ["anne.person@example.org", "anne@example.com"]) + anne_addr = user_manager.get_address('anne@example.com') + self.assertTrue(anne_addr is not None) + self.assertEqual(anne_addr.user, anne_person) diff --git a/src/mailman/rest/users.py b/src/mailman/rest/users.py index cfea36cfa..80c53b565 100644 --- a/src/mailman/rest/users.py +++ b/src/mailman/rest/users.py @@ -39,7 +39,8 @@ from mailman.interfaces.usermanager import IUserManager from mailman.rest.addresses import UserAddresses from mailman.rest.helpers import ( BadRequest, CollectionMixin, GetterSetter, NotFound, bad_request, child, - created, etag, forbidden, no_content, not_found, okay, paginate, path_to) + created, etag, forbidden, no_content, not_found, okay, paginate, path_to, + conflict) from mailman.rest.preferences import Preferences from mailman.rest.validator import PatchValidator, Validator @@ -62,6 +63,34 @@ ATTRIBUTES = dict( cleartext_password=PasswordEncrypterGetterSetter(), ) +CREATION_FIELDS = dict( + email=unicode, + display_name=unicode, + password=unicode, + _optional=('display_name', 'password'), + ) + + +def create_user(arguments, response): + """Create a new user.""" + # We can't pass the 'password' argument to the user creation method, + # so strip that out (if it exists), then create the user, adding the + # password after the fact if successful. + password = arguments.pop('password', None) + try: + user = getUtility(IUserManager).create_user(**arguments) + except ExistingAddressError as error: + bad_request( + response, b'Address already exists: {0}'.format(error.address)) + return + if password is None: + # 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) + location = path_to('users/{0}'.format(user.user_id.int)) + created(response, location) + return user + class _UserBase(CollectionMixin): @@ -105,30 +134,12 @@ class AllUsers(_UserBase): def on_post(self, request, response): """Create a new user.""" try: - validator = Validator(email=unicode, - display_name=unicode, - password=unicode, - _optional=('display_name', 'password')) + validator = Validator(**CREATION_FIELDS) arguments = validator(request) except ValueError as error: bad_request(response, str(error)) return - # We can't pass the 'password' argument to the user creation method, - # so strip that out (if it exists), then create the user, adding the - # password after the fact if successful. - password = arguments.pop('password', None) - try: - user = getUtility(IUserManager).create_user(**arguments) - except ExistingAddressError as error: - bad_request( - response, b'Address already exists: {0}'.format(error.address)) - return - if password is None: - # 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) - location = path_to('users/{0}'.format(user.user_id.int)) - created(response, location) + create_user(arguments, response) @@ -242,6 +253,93 @@ class AUser(_UserBase): +class AddressUser(_UserBase): + """The user linked to an address.""" + + def __init__(self, address): + self._address = address + self._user = address.user + + def on_get(self, request, response): + """Return a single user end-point.""" + if self._user is None: + not_found(response) + else: + okay(response, self._resource_as_json(self._user)) + + def on_delete(self, request, response): + """Delete the named user, all her memberships, and addresses.""" + if self._user is None: + not_found(response) + return + self._user.unlink(self._address) + no_content(response) + + def on_post(self, request, response): + """Link a user to the address, and create it if needed.""" + if self._user: + conflict(response) + return + # Check for an existing user + fields = CREATION_FIELDS.copy() + del fields["email"] + fields["user_id"] = int + fields["autocreate"] = int + fields["_optional"] = fields["_optional"] + ('user_id', 'autocreate') + try: + validator = Validator(**fields) + arguments = validator(request) + except ValueError as error: + bad_request(response, str(error)) + return + user_manager = getUtility(IUserManager) + if "user_id" in arguments: + user_id = UUID(int=arguments["user_id"]) + user = user_manager.get_user_by_id(user_id) + if user is None: + not_found(response, b"No user with ID {0}".format( + arguments["user_id"])) + return + okay(response) + else: + if arguments.get("autocreate", True): # autocreate by default + arguments.pop("autocreate", None) + user = create_user(arguments, response) # will set "201 Created" + else: + not_found(response, b"No such user, and automatic " + "creation is disabled.") + return + user.link(self._address) + + def on_put(self, request, response): + """Set or replace the addresse's user.""" + if self._user: + self._user.unlink(self._address) + # Process post data and check for an existing user + fields = CREATION_FIELDS.copy() + fields["user_id"] = int + fields["_optional"] = fields["_optional"] + ('user_id', 'email') + try: + validator = Validator(**fields) + arguments = validator(request) + except ValueError as error: + bad_request(response, str(error)) + return + user_manager = getUtility(IUserManager) + if 'user_id' in arguments: + user_id = UUID(int=arguments["user_id"]) + user = user_manager.get_user_by_id(user_id) + if user is None: + not_found(response, b"No user with ID {0}".format( + arguments["user_id"])) + return + okay(response) + else: + user = create_user(arguments, response) # will set "201 Created" + user.link(self._address) + + + class Login: """<api>/users/<uid>/login""" |
