Merge lp:~jimmy-sigint/mailman/rest_api_ban_management into lp:mailman
- rest_api_ban_management
- Merge into 3.0
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Barry Warsaw | Needs Information | ||
Review via email:
|
Commit message
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/

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://
> DELETE http://
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
> DELETE http://
> global bans
> Then for list-specific bans, we would use
>
> GET http://
> DELETE http://
> POST <email address hidden> http://
> <email address hidden> to <email address hidden>'s bans
> DELETE http://
> <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://
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

Barry Warsaw (barry) wrote : | # |
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 IAcceptableAlia
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://
>> DELETE http://
>
>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
>> DELETE http://
>> global bans
>
>
>> Then for list-specific bans, we would use
>>
>> GET http://
>> DELETE http://
>> POST <email address hidden> http://
>> <email address hidden> to <email address hidden>'s bans
>> DELETE http://
>> <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> 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...

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 IAcceptableAlia
>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.)

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

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.

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:/
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.

Barry Warsaw (barry) wrote : | # |
I created an issue over on gitlab: https:/
Unmerged revisions
- 7174. By Jimmy Bergman
-
Add support for handling per list bans in the REST API.
Preview Diff
1 | === modified file 'src/mailman/interfaces/bans.py' |
2 | --- src/mailman/interfaces/bans.py 2012-01-01 19:14:46 +0000 |
3 | +++ src/mailman/interfaces/bans.py 2012-09-21 09:06:20 +0000 |
4 | @@ -108,3 +108,13 @@ |
5 | or not. |
6 | :rtype: bool |
7 | """ |
8 | + |
9 | + def ban_list(mailing_list): |
10 | + """Retrieves a list of bans for a specific mailing list. |
11 | + |
12 | + :param mailing_list: The fqdn name of the mailing list to retrieve |
13 | + the ban list for. |
14 | + :type mailing_list: string |
15 | + :return: A list of banned emails. |
16 | + :rtype: list |
17 | + """ |
18 | |
19 | === modified file 'src/mailman/model/bans.py' |
20 | --- src/mailman/model/bans.py 2012-04-26 02:08:22 +0000 |
21 | +++ src/mailman/model/bans.py 2012-09-21 09:06:20 +0000 |
22 | @@ -109,3 +109,8 @@ |
23 | re.match(ban.email, email, re.IGNORECASE) is not None): |
24 | return True |
25 | return False |
26 | + |
27 | + @dbconnection |
28 | + def ban_list(self, store, mailing_list): |
29 | + """See `IBanManager`.""" |
30 | + return [ ban.email for ban in store.find(Ban, mailing_list=mailing_list) ] |
31 | |
32 | === modified file 'src/mailman/rest/docs/membership.rst' |
33 | --- src/mailman/rest/docs/membership.rst 2012-09-05 01:31:50 +0000 |
34 | +++ src/mailman/rest/docs/membership.rst 2012-09-21 09:06:20 +0000 |
35 | @@ -597,6 +597,51 @@ |
36 | >>> set(member.mailing_list for member in elly.memberships.members) |
37 | set([]) |
38 | |
39 | +Handling the list of banned addresses |
40 | +===================================== |
41 | + |
42 | +To ban an address from subscribing you can POST to the /banlist child |
43 | +of any list using the REST API. |
44 | +:: |
45 | + |
46 | + # Ensure our previous reads don't keep the database lock. |
47 | + >>> transaction.abort() |
48 | + >>> dump_json('http://localhost:9001/3.0/lists/bee@example.com' |
49 | + ... '/banlist', { 'address': 'someonebanned@example.com' }) |
50 | + content-length: 0 |
51 | + ... |
52 | + status: 201 |
53 | + |
54 | +This address is now banned, and you can get the list of banned addresses using |
55 | +the REST API: |
56 | + |
57 | + # Ensure our previous reads don't keep the database lock. |
58 | + >>> transaction.abort() |
59 | + >>> dump_json('http://localhost:9001/3.0/lists/bee@example.com/banlist') |
60 | + entry 0: |
61 | + banned_email: someonebanned@example.com |
62 | + ... |
63 | + |
64 | +Or checking if a single address is banned: |
65 | + |
66 | + >>> dump_json('http://localhost:9001/3.0/lists/bee@example.com/banlist/someonebanned@example.com') |
67 | + banned_email: someonebanned@example.com |
68 | + http_etag: ... |
69 | + |
70 | +Unbanning addresses is also possible using the REST API. |
71 | + |
72 | + >>> dump_json('http://localhost:9001/3.0/lists/bee@example.com/banlist' |
73 | + ... '/someonebanned@example.com', method='DELETE') |
74 | + content-length: 0 |
75 | + ... |
76 | + status: 200 |
77 | + |
78 | +After unbanning it shouldn't be available in the ban list: |
79 | + |
80 | + >>> dump_json('http://localhost:9001/3.0/lists/bee@example.com/banlist/someonebanned@example.com') |
81 | + Traceback (most recent call last): |
82 | + ... |
83 | + HTTPError: HTTP Error 404: 404 Not Found |
84 | |
85 | Digest delivery |
86 | =============== |
87 | |
88 | === modified file 'src/mailman/rest/lists.py' |
89 | --- src/mailman/rest/lists.py 2012-09-05 01:31:50 +0000 |
90 | +++ src/mailman/rest/lists.py 2012-09-21 09:06:20 +0000 |
91 | @@ -23,6 +23,8 @@ |
92 | __all__ = [ |
93 | 'AList', |
94 | 'AllLists', |
95 | + 'BannedAddress', |
96 | + 'BannedAddresses', |
97 | 'ListConfiguration', |
98 | 'ListsForDomain', |
99 | ] |
100 | @@ -33,6 +35,7 @@ |
101 | from zope.component import getUtility |
102 | |
103 | from mailman.app.lifecycle import create_list, remove_list |
104 | +from mailman.interfaces.bans import IBanManager |
105 | from mailman.interfaces.domain import BadDomainSpecificationError |
106 | from mailman.interfaces.listmanager import ( |
107 | IListManager, ListAlreadyExistsError) |
108 | @@ -82,6 +85,30 @@ |
109 | |
110 | |
111 | @restish_matcher |
112 | +def banlist_matcher(request, segments): |
113 | + """A matcher of all banned addresses for a mailing list. |
114 | + |
115 | + e.g. /banlist |
116 | + """ |
117 | + if len(segments) != 1 or segments[0] != 'banlist': |
118 | + return None |
119 | + else: |
120 | + return (), {}, () |
121 | + |
122 | + |
123 | +@restish_matcher |
124 | +def banned_matcher(request, segments): |
125 | + """A matcher of a single banned addresses for a mailing list. |
126 | + |
127 | + e.g. /banlist/something@example.org |
128 | + """ |
129 | + if len(segments) != 2 or segments[0] != 'banlist': |
130 | + return None |
131 | + else: |
132 | + return (), dict(address=segments[1]), () |
133 | + |
134 | + |
135 | +@restish_matcher |
136 | def config_matcher(request, segments): |
137 | """A matcher for a mailing list's configuration resource. |
138 | |
139 | @@ -173,6 +200,86 @@ |
140 | return http.not_found() |
141 | return HeldMessages(self._mlist) |
142 | |
143 | + @resource.child(banlist_matcher) |
144 | + def banlist(self, request, segments, attribute=None): |
145 | + """Return a list of banned addresses.""" |
146 | + return BannedAddresses(self._mlist) |
147 | + |
148 | + @resource.child(banned_matcher) |
149 | + def banned(self, request, segments, address): |
150 | + """Return a list of banned addresses.""" |
151 | + return BannedAddress(self._mlist, address) |
152 | + |
153 | + |
154 | + |
155 | |
156 | +class BannedAddress(resource.Resource, CollectionMixin): |
157 | + """Class to represent a banned address.""" |
158 | + |
159 | + def __init__(self, mlist, address): |
160 | + self._mlist = mlist |
161 | + self._address = address |
162 | + self.ban_manager = getUtility(IBanManager) |
163 | + |
164 | + @resource.GET() |
165 | + def container(self, request): |
166 | + """/banlist/someone@example.com""" |
167 | + if self._mlist is None or self._address is None or not self.ban_manager.is_banned(self._address, self._mlist.fqdn_listname): |
168 | + return http.not_found() |
169 | + resource = dict(banned_email=self._address) |
170 | + return http.ok([], etag(resource)) |
171 | + |
172 | + @resource.DELETE() |
173 | + def remove(self, request): |
174 | + """Remove an address from the ban list.""" |
175 | + if self._mlist is None or self._address is None: |
176 | + return http.not_found() |
177 | + else: |
178 | + if not self.ban_manager.is_banned(self._address, self._mlist.fqdn_listname): |
179 | + return http.bad_request([], b'Address is not in ban list') |
180 | + |
181 | + self.ban_manager.unban(self._address, self._mlist.fqdn_listname) |
182 | + return http.ok([], '') |
183 | + |
184 | +class BannedAddresses(resource.Resource, CollectionMixin): |
185 | + """Class to represent the list of all banned addresses.""" |
186 | + |
187 | + def __init__(self, mlist): |
188 | + self._mlist = mlist |
189 | + self.ban_manager = getUtility(IBanManager) |
190 | + |
191 | + def _resource_as_dict(self, item): |
192 | + """See `CollectionMixin`.""" |
193 | + return dict( |
194 | + banned_email=item, |
195 | + ) |
196 | + |
197 | + def _get_collection(self, request): |
198 | + """See `CollectionMixin`.""" |
199 | + return self.ban_manager.ban_list(self._mlist.fqdn_listname) |
200 | + |
201 | + @resource.GET() |
202 | + def container(self, request): |
203 | + """/banlist""" |
204 | + resource = self._make_collection(request) |
205 | + return http.ok([], etag(resource)) |
206 | + |
207 | + @resource.POST() |
208 | + def create(self, request): |
209 | + """Ban some address from subscribing.""" |
210 | + try: |
211 | + validator = Validator(address=unicode) |
212 | + converted = validator(request) |
213 | + |
214 | + address = converted['address'] |
215 | + if self.ban_manager.is_banned(address, self._mlist.fqdn_listname): |
216 | + return http.bad_request([], b'Address is already in ban list') |
217 | + |
218 | + self.ban_manager.ban(address, self._mlist.fqdn_listname) |
219 | + except ValueError as error: |
220 | + return http.bad_request([], str(error)) |
221 | + location = path_to('lists/{0}/banlist'.format(self._mlist.fqdn_listname)) |
222 | + return http.created(location, [], None) |
223 | + |
224 | |
225 | |
226 | |
227 | class AllLists(_ListBase): |
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 .../bans -> deletes all global bans .../bans -> adds <email address hidden> to the global bans .../<email address hidden> -> deletes <email address hidden> from the global bans
DELETE http://
POST <email address hidden> http://
DELETE http://
Then for list-specific bans, we would use
GET http:// .../lists/ list.example. com/bans -> a list of all list-specific bans .../lists/ list.example. com/bans -> delete all global bans .../lists/ list.example. com/bans -> adds <email address hidden> to <email address hidden>'s bans .../<email address hidden> -> deletes <email address hidden> from <email address hidden>'s bans
DELETE http://
POST <email address hidden> http://
DELETE http://
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!