Merge lp:~jimmy-sigint/mailman/rest_api_ban_management into lp:mailman

Proposed by Jimmy Bergman
Status: Work in progress
Proposed branch: lp:~jimmy-sigint/mailman/rest_api_ban_management
Merge into: lp:mailman
Diff against target: 225 lines (+167/-0)
4 files modified
src/mailman/interfaces/bans.py (+10/-0)
src/mailman/model/bans.py (+5/-0)
src/mailman/rest/docs/membership.rst (+45/-0)
src/mailman/rest/lists.py (+107/-0)
To merge this branch: bzr merge lp:~jimmy-sigint/mailman/rest_api_ban_management
Reviewer Review Type Date Requested Status
Barry Warsaw Needs Information
Review via email: mp+125648@code.launchpad.net

Description of the change

This branch adds a REST API resource for managing per list bans, which is useful
in control panels targetting mailman 3.

See the src/mailman/rest/docs/membership.rst patch for usage.

To post a comment you must log in.
Revision history for this message
Barry Warsaw (barry) wrote :

Nice branch, thanks Jimmy. A few comments.

Note that there is an IBanManager utility which you can use to add and remove list-specific or global bans. Probably better to use this utility in the REST API rather than adding a new interface to IMailingList.

Also (perhaps because you didn't know about the IBanManager), your branch doesn't provide a way to set or delete global bans.

What do you think about a resource tree that looks something like this:

GET http://.../bans -> a list of all global bans
DELETE http://.../bans -> deletes all global bans
POST <email address hidden> http://.../bans -> adds <email address hidden> to the global bans
DELETE http://.../<email address hidden> -> deletes <email address hidden> from the global bans

Then for list-specific bans, we would use

GET http://.../lists/list.example.com/bans -> a list of all list-specific bans
DELETE http://.../lists/list.example.com/bans -> delete all global bans
POST <email address hidden> http://.../lists/list.example.com/bans -> adds <email address hidden> to <email address hidden>'s bans
DELETE http://.../<email address hidden> -> deletes <email address hidden> from <email address hidden>'s bans

I don't particularly like DELETE on the /bans resource to delete all the sub-resources, since the implication is that this will remove /bans, not everything underneath it. But it's the best I can come up with for now. Obviously DELETE on /bans doesn't delete the /bans resource.

Would you like to take a shot at making these changes? If so, please push an update and respond to this comment and I'd be happy to take another look. I'd like to get this into 3.0b3.

Thanks!

review: Needs Fixing
Revision history for this message
Jimmy Bergman (jimmy-sigint) wrote :

Hi

> Note that there is an IBanManager utility which you can use to add and remove
> list-specific or global bans. Probably better to use this utility in the REST
> API rather than adding a new interface to IMailingList.

You must have misread the patch, I actually use the IBanManager. I extended
it to be able to return a list of banned emails for a list, since
the semantics wouldn't be RESTy if i couldn't list.

> Also (perhaps because you didn't know about the IBanManager), your branch
> doesn't provide a way to set or delete global bans.

It is more the fact that I created the first version of the patch before
the IBanManager came to life, because our control panel software mailman 3
integration has this view that displays per list bans. I could
add global bans as well since it makes sense.

> What do you think about a resource tree that looks something like this:
>
> GET http://.../bans -> a list of all global bans
> DELETE http://.../bans -> deletes all global bans

Is this really required? I guess it might be nice of course, but
it feels like an uncommon operation (and one that can be done
using the other DELETE although less efficiently).

> POST <email address hidden> http://.../bans -> adds <email address hidden> to the global
> bans
> DELETE http://.../<email address hidden> -> deletes <email address hidden> from the
> global bans

> Then for list-specific bans, we would use
>
> GET http://.../lists/list.example.com/bans -> a list of all list-specific bans
> DELETE http://.../lists/list.example.com/bans -> delete all global bans
> POST <email address hidden> http://.../lists/list.example.com/bans -> adds
> <email address hidden> to <email address hidden>'s bans
> DELETE http://.../<email address hidden> -> deletes
> <email address hidden> from <email address hidden>'s bans

Sounds great (although with the same question re DELETE of all bans) although
in the current patch I also have
GET http://.../<email address hidden> -> check if <email address hidden> is banned on list.example.com

which I think should stay (also i named it ../banlist, but /bans sounds better).

> Would you like to take a shot at making these changes? If so, please push an
> update and respond to this comment and I'd be happy to take another look. I'd
> like to get this into 3.0b3.

Sure, I can do the above change once we agree on:
1. Should there be a DELETE for removing all bans, or is it enough with DELETE of a single ban + iterating if you need to clear the ban list?

2. Should I keep my extra GET on the banned email sub-resource (obviously a similar one would also be implemented for the global bans)?

And what is the timeframe for 3.0b3 so that I know when I need to push it?

Best regards,
Jimmy

Revision history for this message
Barry Warsaw (barry) wrote :
Download full text (4.3 KiB)

On Sep 24, 2012, at 06:00 AM, Jimmy Bergman wrote:

>> Note that there is an IBanManager utility which you can use to add and
>> remove list-specific or global bans. Probably better to use this utility
>> in the REST API rather than adding a new interface to IMailingList.
>
>You must have misread the patch, I actually use the IBanManager. I extended
>it to be able to return a list of banned emails for a list, since
>the semantics wouldn't be RESTy if i couldn't list.

You're right, sorry about that.

BTW, I'm probably going to change the way IBanManager works, to be a bit more
like IAcceptableAliasSet. This would mean you'd adapt an IMailingList to an
IBanManager, and then we could drop the mailing_list parameters to the various
calls. The one difference is that we'd allow you to adapt None to IBanManager
to get the global bans.

Don't worry about all that though. Once your branch is ready, I'll make those
changes as I merge in your work.

>> Also (perhaps because you didn't know about the IBanManager), your branch
>> doesn't provide a way to set or delete global bans.
>
>It is more the fact that I created the first version of the patch before
>the IBanManager came to life, because our control panel software mailman 3
>integration has this view that displays per list bans. I could
>add global bans as well since it makes sense.

Cool.

>> What do you think about a resource tree that looks something like this:
>>
>> GET http://.../bans -> a list of all global bans
>> DELETE http://.../bans -> deletes all global bans
>
>Is this really required? I guess it might be nice of course, but
>it feels like an uncommon operation (and one that can be done
>using the other DELETE although less efficiently).

True. I'm also not really happy with DELETE on .../bans because it doesn't
actually delete the resource. Okay, so let's forget DELETE on all. Do we
need GET on all?

>> POST <email address hidden> http://.../bans -> adds <email address hidden> to the global
>> bans
>> DELETE http://.../<email address hidden> -> deletes <email address hidden> from the
>> global bans
>
>
>> Then for list-specific bans, we would use
>>
>> GET http://.../lists/list.example.com/bans -> a list of all list-specific bans
>> DELETE http://.../lists/list.example.com/bans -> delete all global bans
>> POST <email address hidden> http://.../lists/list.example.com/bans -> adds
>> <email address hidden> to <email address hidden>'s bans
>> DELETE http://.../<email address hidden> -> deletes
>> <email address hidden> from <email address hidden>'s bans
>
>Sounds great (although with the same question re DELETE of all bans)

I would definitely want it to be symmetrical. So if we want to have DELETE
all global bans, we'd want the same for list bans, and if not, then neither
ban list would support DELETE. Same with GET.

>although in the current patch I also have GET
>http://.../<email address hidden> -> check if
><email address hidden> is banned on list.example.com
>
>which I think should stay (also i named it ../banlist, but /bans sounds
>better).

Agreed, let's keep GET on a specific address. I suppose it would 404 if the
given address had no ban, and probably 204 if there is a ban. It would wo...

Read more...

Revision history for this message
Barry Warsaw (barry) wrote :

On Oct 13, 2012, at 01:54 PM, Barry Warsaw wrote:

>BTW, I'm probably going to change the way IBanManager works, to be a bit more
>like IAcceptableAliasSet. This would mean you'd adapt an IMailingList to an
>IBanManager, and then we could drop the mailing_list parameters to the
>various calls. The one difference is that we'd allow you to adapt None to
>IBanManager to get the global bans.

I couldn't resist, so I made this change. I also realized that the ban table
was using mailing_list (i.e. the fqdn listname) as the cross-reference into
the mailinglist table. This is wrong because that can change. What can't
change though is the list-id, so I've added a schema migration to make that
change too (though it should all be hidden underneath the interface).

Still, you'll need to adjust your branch for the new API. If you can't do
that, please let me know, and I'll fix that when I merge your branch. (After
your response to my previous comments.)

Revision history for this message
Jimmy Bergman (jimmy-sigint) wrote :

Hi

On Wed, Oct 17, 2012 at 12:46 AM, Barry Warsaw <email address hidden> wrote:
> Still, you'll need to adjust your branch for the new API. If you can't do
> that, please let me know, and I'll fix that when I merge your branch. (After
> your response to my previous comments.)

I will do that at the same time as the other changes we discussed.

I'll hopefully be able to do the changes within a week or two.

Best regards,
Jimmy

Revision history for this message
Barry Warsaw (barry) wrote :

On Oct 17, 2012, at 08:25 AM, Jimmy Bergman wrote:

>I will do that at the same time as the other changes we discussed.
>
>I'll hopefully be able to do the changes within a week or two.

Sounds great, thanks.

Revision history for this message
Barry Warsaw (barry) wrote :

Hi Jimmy. I know it's been a while but do you have any interesting in updating this branch against the current git master branch, now hosted at gitlab?

https://gitlab.com/mailman/mailman

If so, please feel free to do a merge request over there and close this merge proposal. I think this would be useful to have access to the bans via REST.

review: Needs Information
Revision history for this message
Barry Warsaw (barry) wrote :

I created an issue over on gitlab: https://gitlab.com/mailman/mailman/issues/2

Unmerged revisions

7174. By Jimmy Bergman

Add support for handling per list bans in the REST API.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/mailman/interfaces/bans.py'
--- src/mailman/interfaces/bans.py 2012-01-01 19:14:46 +0000
+++ src/mailman/interfaces/bans.py 2012-09-21 09:06:20 +0000
@@ -108,3 +108,13 @@
108 or not.108 or not.
109 :rtype: bool109 :rtype: bool
110 """110 """
111
112 def ban_list(mailing_list):
113 """Retrieves a list of bans for a specific mailing list.
114
115 :param mailing_list: The fqdn name of the mailing list to retrieve
116 the ban list for.
117 :type mailing_list: string
118 :return: A list of banned emails.
119 :rtype: list
120 """
111121
=== modified file 'src/mailman/model/bans.py'
--- src/mailman/model/bans.py 2012-04-26 02:08:22 +0000
+++ src/mailman/model/bans.py 2012-09-21 09:06:20 +0000
@@ -109,3 +109,8 @@
109 re.match(ban.email, email, re.IGNORECASE) is not None):109 re.match(ban.email, email, re.IGNORECASE) is not None):
110 return True110 return True
111 return False111 return False
112
113 @dbconnection
114 def ban_list(self, store, mailing_list):
115 """See `IBanManager`."""
116 return [ ban.email for ban in store.find(Ban, mailing_list=mailing_list) ]
112117
=== modified file 'src/mailman/rest/docs/membership.rst'
--- src/mailman/rest/docs/membership.rst 2012-09-05 01:31:50 +0000
+++ src/mailman/rest/docs/membership.rst 2012-09-21 09:06:20 +0000
@@ -597,6 +597,51 @@
597 >>> set(member.mailing_list for member in elly.memberships.members)597 >>> set(member.mailing_list for member in elly.memberships.members)
598 set([])598 set([])
599599
600Handling the list of banned addresses
601=====================================
602
603To ban an address from subscribing you can POST to the /banlist child
604of any list using the REST API.
605::
606
607 # Ensure our previous reads don't keep the database lock.
608 >>> transaction.abort()
609 >>> dump_json('http://localhost:9001/3.0/lists/bee@example.com'
610 ... '/banlist', { 'address': 'someonebanned@example.com' })
611 content-length: 0
612 ...
613 status: 201
614
615This address is now banned, and you can get the list of banned addresses using
616the REST API:
617
618 # Ensure our previous reads don't keep the database lock.
619 >>> transaction.abort()
620 >>> dump_json('http://localhost:9001/3.0/lists/bee@example.com/banlist')
621 entry 0:
622 banned_email: someonebanned@example.com
623 ...
624
625Or checking if a single address is banned:
626
627 >>> dump_json('http://localhost:9001/3.0/lists/bee@example.com/banlist/someonebanned@example.com')
628 banned_email: someonebanned@example.com
629 http_etag: ...
630
631Unbanning addresses is also possible using the REST API.
632
633 >>> dump_json('http://localhost:9001/3.0/lists/bee@example.com/banlist'
634 ... '/someonebanned@example.com', method='DELETE')
635 content-length: 0
636 ...
637 status: 200
638
639After unbanning it shouldn't be available in the ban list:
640
641 >>> dump_json('http://localhost:9001/3.0/lists/bee@example.com/banlist/someonebanned@example.com')
642 Traceback (most recent call last):
643 ...
644 HTTPError: HTTP Error 404: 404 Not Found
600645
601Digest delivery646Digest delivery
602===============647===============
603648
=== modified file 'src/mailman/rest/lists.py'
--- src/mailman/rest/lists.py 2012-09-05 01:31:50 +0000
+++ src/mailman/rest/lists.py 2012-09-21 09:06:20 +0000
@@ -23,6 +23,8 @@
23__all__ = [23__all__ = [
24 'AList',24 'AList',
25 'AllLists',25 'AllLists',
26 'BannedAddress',
27 'BannedAddresses',
26 'ListConfiguration',28 'ListConfiguration',
27 'ListsForDomain',29 'ListsForDomain',
28 ]30 ]
@@ -33,6 +35,7 @@
33from zope.component import getUtility35from zope.component import getUtility
3436
35from mailman.app.lifecycle import create_list, remove_list37from mailman.app.lifecycle import create_list, remove_list
38from mailman.interfaces.bans import IBanManager
36from mailman.interfaces.domain import BadDomainSpecificationError39from mailman.interfaces.domain import BadDomainSpecificationError
37from mailman.interfaces.listmanager import (40from mailman.interfaces.listmanager import (
38 IListManager, ListAlreadyExistsError)41 IListManager, ListAlreadyExistsError)
@@ -82,6 +85,30 @@
8285
8386
84@restish_matcher87@restish_matcher
88def banlist_matcher(request, segments):
89 """A matcher of all banned addresses for a mailing list.
90
91 e.g. /banlist
92 """
93 if len(segments) != 1 or segments[0] != 'banlist':
94 return None
95 else:
96 return (), {}, ()
97
98
99@restish_matcher
100def banned_matcher(request, segments):
101 """A matcher of a single banned addresses for a mailing list.
102
103 e.g. /banlist/something@example.org
104 """
105 if len(segments) != 2 or segments[0] != 'banlist':
106 return None
107 else:
108 return (), dict(address=segments[1]), ()
109
110
111@restish_matcher
85def config_matcher(request, segments):112def config_matcher(request, segments):
86 """A matcher for a mailing list's configuration resource.113 """A matcher for a mailing list's configuration resource.
87114
@@ -173,6 +200,86 @@
173 return http.not_found()200 return http.not_found()
174 return HeldMessages(self._mlist)201 return HeldMessages(self._mlist)
175202
203 @resource.child(banlist_matcher)
204 def banlist(self, request, segments, attribute=None):
205 """Return a list of banned addresses."""
206 return BannedAddresses(self._mlist)
207
208 @resource.child(banned_matcher)
209 def banned(self, request, segments, address):
210 """Return a list of banned addresses."""
211 return BannedAddress(self._mlist, address)
212
213
214
176215
216class BannedAddress(resource.Resource, CollectionMixin):
217 """Class to represent a banned address."""
218
219 def __init__(self, mlist, address):
220 self._mlist = mlist
221 self._address = address
222 self.ban_manager = getUtility(IBanManager)
223
224 @resource.GET()
225 def container(self, request):
226 """/banlist/someone@example.com"""
227 if self._mlist is None or self._address is None or not self.ban_manager.is_banned(self._address, self._mlist.fqdn_listname):
228 return http.not_found()
229 resource = dict(banned_email=self._address)
230 return http.ok([], etag(resource))
231
232 @resource.DELETE()
233 def remove(self, request):
234 """Remove an address from the ban list."""
235 if self._mlist is None or self._address is None:
236 return http.not_found()
237 else:
238 if not self.ban_manager.is_banned(self._address, self._mlist.fqdn_listname):
239 return http.bad_request([], b'Address is not in ban list')
240
241 self.ban_manager.unban(self._address, self._mlist.fqdn_listname)
242 return http.ok([], '')
243
244class BannedAddresses(resource.Resource, CollectionMixin):
245 """Class to represent the list of all banned addresses."""
246
247 def __init__(self, mlist):
248 self._mlist = mlist
249 self.ban_manager = getUtility(IBanManager)
250
251 def _resource_as_dict(self, item):
252 """See `CollectionMixin`."""
253 return dict(
254 banned_email=item,
255 )
256
257 def _get_collection(self, request):
258 """See `CollectionMixin`."""
259 return self.ban_manager.ban_list(self._mlist.fqdn_listname)
260
261 @resource.GET()
262 def container(self, request):
263 """/banlist"""
264 resource = self._make_collection(request)
265 return http.ok([], etag(resource))
266
267 @resource.POST()
268 def create(self, request):
269 """Ban some address from subscribing."""
270 try:
271 validator = Validator(address=unicode)
272 converted = validator(request)
273
274 address = converted['address']
275 if self.ban_manager.is_banned(address, self._mlist.fqdn_listname):
276 return http.bad_request([], b'Address is already in ban list')
277
278 self.ban_manager.ban(address, self._mlist.fqdn_listname)
279 except ValueError as error:
280 return http.bad_request([], str(error))
281 location = path_to('lists/{0}/banlist'.format(self._mlist.fqdn_listname))
282 return http.created(location, [], None)
283
177284
178285
179286
180class AllLists(_ListBase):287class AllLists(_ListBase):