summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/mailman/docs/NEWS.rst2
-rw-r--r--src/mailman/interfaces/user.py3
-rw-r--r--src/mailman/interfaces/usermanager.py3
-rw-r--r--src/mailman/model/docs/usermanager.rst44
-rw-r--r--src/mailman/model/usermanager.py9
-rw-r--r--src/mailman/rest/docs/owners.rst89
-rw-r--r--src/mailman/rest/root.py10
-rw-r--r--src/mailman/rest/tests/test_owners.py40
-rw-r--r--src/mailman/rest/users.py15
9 files changed, 213 insertions, 2 deletions
diff --git a/src/mailman/docs/NEWS.rst b/src/mailman/docs/NEWS.rst
index db86363e4..2f45fd31b 100644
--- a/src/mailman/docs/NEWS.rst
+++ b/src/mailman/docs/NEWS.rst
@@ -78,6 +78,8 @@ REST
Bompard.
* The REST API incorrectly parsed `is_server_owner` values when given
explicitly in the POST that creates a user. (Closes #136)
+ * A new top-level resource `<api>/owners` can be used to get the list of
+ server owners as `IUser`s. (Closes #135)
* By POSTing to a user resource with an existing unlinked address, you can
link the address to the user. Given by Abhilash Raj.
diff --git a/src/mailman/interfaces/user.py b/src/mailman/interfaces/user.py
index 1068890c1..76ceb3f37 100644
--- a/src/mailman/interfaces/user.py
+++ b/src/mailman/interfaces/user.py
@@ -70,6 +70,9 @@ class IUser(Interface):
memberships = Attribute(
"""A roster of this user's memberships.""")
+ is_server_owner = Attribute(
+ """Boolean flag indicating whether the user is a server owner.""")
+
def register(email, display_name=None):
"""Register the given email address and link it to this user.
diff --git a/src/mailman/interfaces/usermanager.py b/src/mailman/interfaces/usermanager.py
index 5f3a324cc..da2f460df 100644
--- a/src/mailman/interfaces/usermanager.py
+++ b/src/mailman/interfaces/usermanager.py
@@ -126,3 +126,6 @@ class IUserManager(Interface):
members = Attribute(
"""An iterator of all the `IMembers` in the database.""")
+
+ server_owners = Attribute(
+ """An iterator over all the `IUsers` who are server owners.""")
diff --git a/src/mailman/model/docs/usermanager.rst b/src/mailman/model/docs/usermanager.rst
index 8e40b621e..a98f88670 100644
--- a/src/mailman/model/docs/usermanager.rst
+++ b/src/mailman/model/docs/usermanager.rst
@@ -201,3 +201,47 @@ The user has a single unverified address object.
>>> for address in cris.addresses:
... print(repr(address))
<Address: Cris Person <cris@example.com> [not verified] at ...>
+
+
+Server owners
+=============
+
+Some users are designated as *server owners*. At first there are no server
+owners.
+
+ >>> len(list(user_manager.server_owners))
+ 0
+
+Dan is made a server owner.
+
+ >>> user_4.is_server_owner = True
+ >>> owners = list(user_manager.server_owners)
+ >>> len(owners)
+ 1
+ >>> owners[0]
+ <User "Dan Person" (...) at ...>
+
+Now Ben and Claire are also made server owners.
+
+ >>> user_2.is_server_owner = True
+ >>> user_3.is_server_owner = True
+ >>> owners = list(user_manager.server_owners)
+ >>> len(owners)
+ 3
+ >>> from operator import attrgetter
+ >>> for user in sorted(owners, key=attrgetter('display_name')):
+ ... print(user)
+ <User "Ben Person" (...) at ...>
+ <User "Claire Person" (...) at ...>
+ <User "Dan Person" (...) at ...>
+
+Clair retires as a server owner.
+
+ >>> user_3.is_server_owner = False
+ >>> owners = list(user_manager.server_owners)
+ >>> len(owners)
+ 2
+ >>> for user in sorted(owners, key=attrgetter('display_name')):
+ ... print(user)
+ <User "Ben Person" (...) at ...>
+ <User "Dan Person" (...) at ...>
diff --git a/src/mailman/model/usermanager.py b/src/mailman/model/usermanager.py
index 3d7777099..5d63a2146 100644
--- a/src/mailman/model/usermanager.py
+++ b/src/mailman/model/usermanager.py
@@ -141,4 +141,11 @@ class UserManager:
def members(self, store):
"""See `IUserManager."""
for member in store.query(Member).all():
- yield member
+ yield member
+
+ @property
+ @dbconnection
+ def server_owners(self, store):
+ """ See `IUserManager."""
+ users = store.query(User).filter_by(is_server_owner=True)
+ yield from users
diff --git a/src/mailman/rest/docs/owners.rst b/src/mailman/rest/docs/owners.rst
new file mode 100644
index 000000000..42be49c02
--- /dev/null
+++ b/src/mailman/rest/docs/owners.rst
@@ -0,0 +1,89 @@
+===============
+ Server owners
+===============
+
+Certain users can be designated as *server owners*. This role has no direct
+function in the core, but it can be used by clients of the REST API to
+determine additional permissions. For example, Postorius might allow server
+owners to create new domains.
+
+Initially, there are no server owners.
+
+ >>> dump_json('http://localhost:9001/3.0/owners')
+ http_etag: "..."
+ start: 0
+ total_size: 0
+
+When new users are created in the core, they do not become server owners by
+default.
+
+ >>> from zope.component import getUtility
+ >>> from mailman.interfaces.usermanager import IUserManager
+ >>> user_manager = getUtility(IUserManager)
+ >>> anne = user_manager.create_user('anne@example.com', 'Anne Person')
+ >>> transaction.commit()
+ >>> dump_json('http://localhost:9001/3.0/owners')
+ http_etag: "..."
+ start: 0
+ total_size: 0
+
+Anne's server owner flag is set.
+
+ >>> anne.is_server_owner = True
+ >>> transaction.commit()
+
+And now we can find her user record.
+
+ >>> dump_json('http://localhost:9001/3.0/owners')
+ entry 0:
+ created_on: 2005-08-01T07:49:23
+ display_name: Anne Person
+ http_etag: "..."
+ is_server_owner: True
+ self_link: http://localhost:9001/3.0/users/1
+ user_id: 1
+ http_etag: "..."
+ start: 0
+ total_size: 1
+
+Bart and Cate are also users, but not server owners.
+
+ >>> bart = user_manager.create_user('bart@example.com', 'Bart Person')
+ >>> cate = user_manager.create_user('cate@example.com', 'Cate Person')
+ >>> transaction.commit()
+ >>> dump_json('http://localhost:9001/3.0/owners')
+ entry 0:
+ created_on: 2005-08-01T07:49:23
+ display_name: Anne Person
+ http_etag: "..."
+ is_server_owner: True
+ self_link: http://localhost:9001/3.0/users/1
+ user_id: 1
+ http_etag: "..."
+ start: 0
+ total_size: 1
+
+Anne retires as a server owner, with Bart and Cate taking over.
+
+ >>> anne.is_server_owner = False
+ >>> bart.is_server_owner = True
+ >>> cate.is_server_owner = True
+ >>> transaction.commit()
+ >>> dump_json('http://localhost:9001/3.0/owners')
+ entry 0:
+ created_on: 2005-08-01T07:49:23
+ display_name: Bart Person
+ http_etag: "..."
+ is_server_owner: True
+ self_link: http://localhost:9001/3.0/users/2
+ user_id: 2
+ entry 1:
+ created_on: 2005-08-01T07:49:23
+ display_name: Cate Person
+ http_etag: "..."
+ is_server_owner: True
+ self_link: http://localhost:9001/3.0/users/3
+ user_id: 3
+ http_etag: "..."
+ start: 0
+ total_size: 2
diff --git a/src/mailman/rest/root.py b/src/mailman/rest/root.py
index ca8bbc4a9..0682619b1 100644
--- a/src/mailman/rest/root.py
+++ b/src/mailman/rest/root.py
@@ -39,7 +39,7 @@ from mailman.rest.members import AMember, AllMembers, FindMembers
from mailman.rest.preferences import ReadOnlyPreferences
from mailman.rest.queues import AQueue, AQueueFile, AllQueues
from mailman.rest.templates import TemplateFinder
-from mailman.rest.users import AUser, AllUsers
+from mailman.rest.users import AUser, AllUsers, ServerOwners
from zope.component import getUtility
@@ -234,6 +234,14 @@ class TopLevel:
return AUser(user_id), segments
@child()
+ def owners(self, request, segments):
+ """/<api>/owners"""
+ if len(segments) != 0:
+ return BadRequest(), []
+ else:
+ return ServerOwners(), segments
+
+ @child()
def templates(self, request, segments):
"""/<api>/templates/<fqdn_listname>/<template>/[<language>]
diff --git a/src/mailman/rest/tests/test_owners.py b/src/mailman/rest/tests/test_owners.py
new file mode 100644
index 000000000..2cbebc5c4
--- /dev/null
+++ b/src/mailman/rest/tests/test_owners.py
@@ -0,0 +1,40 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+"""Additional tests for the top-level owners resource."""
+
+__all__ = [
+ 'TestOwners',
+ ]
+
+
+import unittest
+
+from mailman.testing.helpers import call_api
+from mailman.testing.layers import RESTLayer
+from urllib.error import HTTPError
+
+
+
+class TestOwners(unittest.TestCase):
+ layer = RESTLayer
+
+ def test_bogus_trailing_path(self):
+ # Nothing is allowed after the top-level /owners resource.
+ with self.assertRaises(HTTPError) as cm:
+ call_api('http://localhost:9001/3.0/owners/anne')
+ self.assertEqual(cm.exception.code, 400)
diff --git a/src/mailman/rest/users.py b/src/mailman/rest/users.py
index 252e7757b..6c352fd8c 100644
--- a/src/mailman/rest/users.py
+++ b/src/mailman/rest/users.py
@@ -464,3 +464,18 @@ class OwnersForDomain(_UserBase):
def _get_collection(self, request):
"""See `CollectionMixin`."""
return list(self._domain.owners)
+
+
+
+class ServerOwners(_UserBase):
+ """All server owners."""
+
+ def on_get(self, request, response):
+ """/owners"""
+ resource = self._make_collection(request)
+ okay(response, etag(resource))
+
+ @paginate
+ def _get_collection(self, request):
+ """See `CollectionMixin`."""
+ return list(getUtility(IUserManager).server_owners)