summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/mailman/rest/addresses.py10
-rw-r--r--src/mailman/rest/docs/addresses.rst5
-rw-r--r--src/mailman/rest/docs/users.rst4
-rw-r--r--src/mailman/rest/tests/test_addresses.py136
-rw-r--r--src/mailman/rest/users.py140
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"""