Merge lp:~wallyworld/launchpad/no-private-bug-rss into lp:launchpad

Proposed by Ian Booth
Status: Merged
Approved by: Ian Booth
Approved revision: no longer in the source branch.
Merged at revision: 13110
Proposed branch: lp:~wallyworld/launchpad/no-private-bug-rss
Merge into: lp:launchpad
Diff against target: 306 lines (+119/-45)
6 files modified
lib/canonical/launchpad/browser/feeds.py (+29/-2)
lib/canonical/launchpad/pagetests/feeds/xx-authentication.txt (+0/-26)
lib/canonical/launchpad/pagetests/feeds/xx-links.txt (+49/-0)
lib/canonical/launchpad/pagetests/feeds/xx-security.txt (+3/-10)
lib/lp/bugs/feed/bug.py (+16/-4)
lib/lp/code/feed/branch.py (+22/-3)
To merge this branch: bzr merge lp:~wallyworld/launchpad/no-private-bug-rss
Reviewer Review Type Date Requested Status
Jeroen T. Vermeulen (community) code Approve
Review via email: mp+61718@code.launchpad.net

This proposal supersedes a proposal from 2011-05-19.

Commit message

[r=jtv][bug=302097] Do not allow private rss feeds and provide a better error message

Description of the change

I was going to land this branch in two parts -

1. prevent the rss feed from being enabled when a private bug/branch pages is loaded
2. handle the case where inline ajax editing is used to change the bug/branch status

Part one was already put up as a mp and approved.
However, turns out you can't disable the rss icon dynamically so I've updated the original branch with some code to better handle the case where a user url hacks or attempts to get to the feed page for a private bug/branch.

== Implementation ==

There was already code in BugFeed and BranchFeed to raise an Unauthorised exception if an attempt was made to get to the feed page for a private bug/branch. This lead the user to a login page and it all went south from there as described in the bug report.

The new implementation doesn't raise an exception - it simply adds an error notification to the request response and redirects back to the page for the bug/branch. In the case of branches, the user may not have permission to see the branch so the lp code index page is used instead. For bugs, users who cannot see the bug are directed to the lp bugs index page with the error notification. So they are taken somewhere sensible and given an error message. This last bit is more of an edge case.

== Demo ==

Try and enter an rss feed url directly for a private bug or branch eg
http://feeds.launchpad.dev/bugs/14/bug.atom
or
Log in as a user (eg daf) and navigate to bug 14. The rss feed icon in the browser should be disabled.

== Tests ==

I added extra lines to the existing xx-links.txt doc test to check the two items listed at the beginning of this mp.

== Lint ==

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/canonical/launchpad/browser/feeds.py
  lib/canonical/launchpad/pagetests/feeds/xx-links.txt
  lib/lp/bugs/feed/bug.py
  lib/lp/code/feed/branch.py

./lib/canonical/launchpad/pagetests/feeds/xx-links.txt
       1: narrative uses a moin header.
       9: narrative uses a moin header.
      33: narrative uses a moin header.
      77: narrative uses a moin header.
     107: narrative uses a moin header.
     141: narrative uses a moin header.
     168: want exceeds 78 characters.
     171: want exceeds 78 characters.
     174: want exceeds 78 characters.
     177: want exceeds 78 characters.
     179: narrative uses a moin header.
     227: narrative uses a moin header.
     254: narrative uses a moin header.
     267: narrative uses a moin header.
     280: narrative uses a moin header.
     289: want exceeds 78 characters.
     293: narrative uses a moin header.
     309: narrative uses a moin header.
     325: narrative uses a moin header.
     341: narrative uses a moin header.
     351: want exceeds 78 characters.
     352: want exceeds 78 characters.
     369: source exceeds 78 characters.
./lib/lp/bugs/feed/bug.py
      80: E251 no spaces around keyword / parameter equals

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) : Posted in a previous version of this proposal
review: Approve
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :
Download full text (5.5 KiB)

Hi Ian,

It generally looks good but as always I have some notes. And while I'm here, thanks for the little drive-by cleanups!

=== modified file 'lib/canonical/launchpad/browser/feeds.py'
--- lib/canonical/launchpad/browser/feeds.py 2010-11-08 12:52:43 +0000
+++ lib/canonical/launchpad/browser/feeds.py 2011-05-20 09:01:26 +0000
@@ -177,6 +177,14 @@
             "Context %r does not provide interface %r"
             % (context, self.usedfor))

+ @classmethod
+ def feed_allowed(cls, context):
+ """Return True if a feed is allowed for the given context.

We normally use dromedaryCase for methods, where the first word is expected to be a verb. Skimming over this change at first I thought it was a property!

The "verb" part has a corollary: "feed allowed" can be misread as "feed those that are allowed." It's not hard to figure out that that's not what you mean, but it's a small distraction that can help slow down and confuse the reader.

@@ -346,4 +368,5 @@
     def feed_links(self):
         return [feed_type(self.context)
                 for feed_type in self.feed_types
- if feed_type.usedfor.providedBy(self.context)]
+ if feed_type.usedfor.providedBy(self.context) and
+ feed_type.feed_allowed(self.context)]

The indentation of that last clause threw me off a bit. Is there some more legible way to write this?

=== modified file 'lib/canonical/launchpad/pagetests/feeds/xx-links.txt'
--- lib/canonical/launchpad/pagetests/feeds/xx-links.txt 2011-05-05 05:46:41 +0000
+++ lib/canonical/launchpad/pagetests/feeds/xx-links.txt 2011-05-20 09:01:26 +0000
@@ -42,6 +42,37 @@
         href="http://feeds.launchpad.dev/bugs/1/bug.atom"
         title="Bug 1 Feed" />]

+But if the bug is private, there should be no link.
+
+ >>> from canonical.launchpad.testing.pages import setupBrowserForUser
+ >>> from zope.component import getUtility
+ >>> from lp.registry.interfaces.person import IPersonSet
+ >>> login(ANONYMOUS)
+ >>> user = getUtility(IPersonSet).getByEmail('<email address hidden>')
+ >>> logout()
+ >>> auth_browser = setupBrowserForUser(user, 'daf')
+ >>> auth_browser.open('http://bugs.launchpad.dev/jokosher/+bug/14')
+ >>> soup = BeautifulSoup(auth_browser.contents)
+ >>> soup.head.findAll('link', type='application/atom+xml')
+ []

What exactly is auth_browser for? If you merely need a brower for a logged-in user, there's no need to construct one; use user_browser which is set up implicitly for this usage.

+But if they somehow manage to hack the url or use inline ajax editing of the
+bug status and attempt to subscribe, they are redirected to the bug page:

You're "But"-ing your own "But"!

+ >>> def print_notifications(browser):
+ ... tags = find_tags_by_class(
+ ... browser.contents, 'error message')
+ ... if len(tags) == 0:
+ ... print 'No messages.'
+ ... else:
+ ... for tag in tags:
+ ... print extract_text(tag)

We have a helper for this: get_feedback_messages.

@@ -319,3 +350,24 @@
     [<link rel="alternate" type="application/atom+xml"
         href="http://feeds.launch...

Read more...

review: Approve (code)
Revision history for this message
Ian Booth (wallyworld) wrote :
Download full text (5.2 KiB)

Yello

>
> It generally looks good but as always I have some notes. And while I'm here, thanks for the little drive-by cleanups!
>

I always appreciate the thoroughness of your reviews.

>
>
> We normally use dromedaryCase for methods, where the first word is expected to be a verb. Skimming over this change at first I thought it was a property!
>
> The "verb" part has a corollary: "feed allowed" can be misread as "feed those that are allowed." It's not hard to figure out that that's not what you mean, but it's a small distraction that can help slow down and confuse the reader.
>

Good point. Will fix.

>
>
> @@ -346,4 +368,5 @@
> def feed_links(self):
> return [feed_type(self.context)
> for feed_type in self.feed_types
> - if feed_type.usedfor.providedBy(self.context)]
> + if feed_type.usedfor.providedBy(self.context) and
> + feed_type.feed_allowed(self.context)]
>
>
> The indentation of that last clause threw me off a bit. Is there some more legible way to write this?
>

I'll find something.

>
> === modified file 'lib/canonical/launchpad/pagetests/feeds/xx-links.txt'
> --- lib/canonical/launchpad/pagetests/feeds/xx-links.txt 2011-05-05 05:46:41 +0000
> +++ lib/canonical/launchpad/pagetests/feeds/xx-links.txt 2011-05-20 09:01:26 +0000
> @@ -42,6 +42,37 @@
> href="http://feeds.launchpad.dev/bugs/1/bug.atom"
> title="Bug 1 Feed" />]
>
> +But if the bug is private, there should be no link.
> +
> + >>> from canonical.launchpad.testing.pages import setupBrowserForUser
> + >>> from zope.component import getUtility
> + >>> from lp.registry.interfaces.person import IPersonSet
> + >>> login(ANONYMOUS)
> + >>> user = getUtility(IPersonSet).getByEmail('<email address hidden>')
> + >>> logout()
> + >>> auth_browser = setupBrowserForUser(user, 'daf')
> + >>> auth_browser.open('http://bugs.launchpad.dev/jokosher/+bug/14')
> + >>> soup = BeautifulSoup(auth_browser.contents)
> + >>> soup.head.findAll('link', type='application/atom+xml')
> + []
>
> What exactly is auth_browser for? If you merely need a brower for a logged-in user, there's no need to construct one; use user_browser which is set up implicitly for this usage.
>

I'll take another look. There's already a "browser" instance which is
used for (I think) non-authenticated access but I didn't realise there
was already a "user_browser" instance.

>
> +But if they somehow manage to hack the url or use inline ajax editing of the
> +bug status and attempt to subscribe, they are redirected to the bug page:
>
>
> You're "But"-ing your own "But"!
>
>
> + >>> def print_notifications(browser):
> + ... tags = find_tags_by_class(
> + ... browser.contents, 'error message')
> + ... if len(tags) == 0:
> + ... print 'No messages.'
> + ... else:
> + ... for tag in tags:
> + ... print extract_text(tag)
>
> We have a helper for this: get_feedback_messages.
>

Really? I copied the above code from elsewhere :-)
I'll fix it.

>
> === modified file 'lib/lp/bugs/feed/bug.py'
> --- lib/lp/bugs/feed/bug.py 2...

Read more...

Revision history for this message
Ian Booth (wallyworld) wrote :

>
> What exactly is auth_browser for? If you merely need a brower for a logged-in user, there's no need to construct one; use user_browser which is set up implicitly for this usage.
>

I looked at the code again in more detail. I need more than just "a"
logged in user. I need a browser logged in for a specific user so that a
private bug and a private branch can be accessed. Hence I get the user I
need:

user = getUtility(IPersonSet).getByEmail('<email address hidden>')

and then get the logged in browser for that user, using the password =
'daf':

auth_browser = setupBrowserForUser(user, 'daf')

So I think the use of auth_browser is ok unless there is something I am
missing.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/browser/feeds.py'
2--- lib/canonical/launchpad/browser/feeds.py 2010-11-08 12:52:43 +0000
3+++ lib/canonical/launchpad/browser/feeds.py 2011-05-24 03:31:14 +0000
4@@ -177,6 +177,14 @@
5 "Context %r does not provide interface %r"
6 % (context, self.usedfor))
7
8+ @classmethod
9+ def allowFeed(cls, context):
10+ """Return True if a feed is allowed for the given context.
11+
12+ Subclasses should override this method as necessary.
13+ """
14+ return True
15+
16
17 class BugFeedLink(FeedLinkBase):
18 usedfor = IBugTask
19@@ -190,6 +198,12 @@
20 return urlappend(self.rooturl,
21 'bugs/' + str(self.context.bug.id) + '/bug.atom')
22
23+ @classmethod
24+ def allowFeed(cls, context):
25+ """See `FeedLinkBase`"""
26+ # No feeds for private bugs.
27+ return not context.bug.private
28+
29
30 class BugTargetLatestBugsFeedLink(FeedLinkBase):
31 usedfor = IHasBugs
32@@ -222,6 +236,7 @@
33 return urlappend(canonical_url(self.context, rootsite='feeds'),
34 'announcements.atom')
35
36+
37 class RootAnnouncementsFeedLink(AnnouncementsFeedLink):
38 usedfor = ILaunchpadRoot
39
40@@ -296,11 +311,18 @@
41 @property
42 def title(self):
43 return 'Latest Revisions for Branch %s' % self.context.displayname
44+
45 @property
46 def href(self):
47 return urlappend(canonical_url(self.context, rootsite="feeds"),
48 'branch.atom')
49
50+ @classmethod
51+ def allowFeed(cls, context):
52+ """See `FeedLinkBase`"""
53+ # No feeds for private branches.
54+ return not context.private
55+
56
57 class PersonRevisionsFeedLink(FeedLinkBase):
58 """Feed links for revisions created by a person."""
59@@ -344,6 +366,11 @@
60
61 @property
62 def feed_links(self):
63+
64+ def allowFeed(feed_type, context):
65+ return (feed_type.usedfor.providedBy(context) and
66+ feed_type.allowFeed(context))
67+
68 return [feed_type(self.context)
69- for feed_type in self.feed_types
70- if feed_type.usedfor.providedBy(self.context)]
71+ for feed_type in self.feed_types
72+ if allowFeed(feed_type, self.context)]
73
74=== removed file 'lib/canonical/launchpad/pagetests/feeds/xx-authentication.txt'
75--- lib/canonical/launchpad/pagetests/feeds/xx-authentication.txt 2009-10-22 13:02:12 +0000
76+++ lib/canonical/launchpad/pagetests/feeds/xx-authentication.txt 1970-01-01 00:00:00 +0000
77@@ -1,26 +0,0 @@
78-= Authentication for feeds =
79-
80-Requests made to feeds.launchpad.dev are not challenged for
81-authentication, so most access is done anonymously. In the event a
82-request provides basic authentication, even though not requested by
83-the server, the web application will nonetheless use the
84-unauthenticated anonymous user for all interaction.
85-
86- # Setup a Foo Bar user and let Zope handle the errors.
87- >>> browser = setupBrowser(auth='Basic foo.bar@canonical.com:test')
88- >>> browser.handleErrors = True
89-
90- # First, get the bug through the web site, to ensure the
91- # bug exists and is private.
92- >>> browser.open('http://launchpad.dev/bugs/14')
93- >>> print browser.url
94- http://bugs.launchpad.dev/jokosher/+bug/14
95-
96- # Traversing to the private bug will trigger an Unauthorized error.
97- # For feeds, this will result in a Forbidden error. This test
98- # ensures FeedsUnauthorizedError works and that the unauthorized
99- # user is the current user.
100- >>> browser.open('http://feeds.launchpad.dev/bugs/14/bug.atom')
101- Traceback (most recent call last):
102- ...
103- HTTPError: HTTP Error 403: Forbidden
104
105=== modified file 'lib/canonical/launchpad/pagetests/feeds/xx-links.txt'
106--- lib/canonical/launchpad/pagetests/feeds/xx-links.txt 2011-05-05 05:46:41 +0000
107+++ lib/canonical/launchpad/pagetests/feeds/xx-links.txt 2011-05-24 03:31:14 +0000
108@@ -42,6 +42,34 @@
109 href="http://feeds.launchpad.dev/bugs/1/bug.atom"
110 title="Bug 1 Feed" />]
111
112+But if the bug is private, there should be no link.
113+
114+ # Set up an authenticated browser.
115+ >>> from canonical.launchpad.testing.pages import setupBrowserForUser
116+ >>> from zope.component import getUtility
117+ >>> from lp.registry.interfaces.person import IPersonSet
118+ >>> login(ANONYMOUS)
119+ >>> user = getUtility(IPersonSet).getByEmail('daf@canonical.com')
120+ >>> logout()
121+ >>> auth_browser = setupBrowserForUser(user, 'daf')
122+
123+ # First check that the bug exists.
124+ >>> auth_browser.open('http://launchpad.dev/bugs/14')
125+ >>> print auth_browser.url
126+ http://bugs.launchpad.dev/jokosher/+bug/14
127+
128+ >>> soup = BeautifulSoup(auth_browser.contents)
129+ >>> soup.head.findAll('link', type='application/atom+xml')
130+ []
131+
132+Even so, if they somehow manage to hack the url or use inline ajax editing of
133+the bug status and attempt to subscribe, they are redirected to the bug page:
134+
135+ >>> auth_browser.open('http://feeds.launchpad.dev/bugs/14/bug.atom')
136+ >>> print auth_browser.url
137+ http://bugs.launchpad.dev/
138+ >>> print get_feedback_messages(auth_browser.contents)
139+ [u'The requested bug is private. Feeds do not serve private bugs.']
140
141 == Latest Bugs and Branches for a Person ==
142
143@@ -319,3 +347,24 @@
144 [<link rel="alternate" type="application/atom+xml"
145 href="http://feeds.launchpad.dev/~mark/firefox/release--0.9.1/branch.atom"
146 title="Latest Revisions for Branch lp://dev/~mark/firefox/release--0.9.1" />]
147+
148+But if the branch is private, there should be no link.
149+
150+ >>> login(ANONYMOUS)
151+ >>> user = getUtility(IPersonSet).getByEmail('test@canonical.com')
152+ >>> logout()
153+ >>> auth_browser = setupBrowserForUser(user)
154+ >>> auth_browser.open(
155+ ... 'https://code.launchpad.dev/~name12/landscape/feature-x')
156+ >>> soup = BeautifulSoup(auth_browser.contents)
157+ >>> soup.head.findAll('link', type='application/atom+xml')
158+ []
159+
160+Even so, if they somehow manage to hack the url, they are redirected to a page
161+with an error notification:
162+
163+ >>> browser.open('http://feeds.launchpad.dev/~name12/landscape/feature-x/branch.atom')
164+ >>> print browser.url
165+ http://code.launchpad.dev/
166+ >>> print get_feedback_messages(browser.contents)
167+ [u'The requested branch is private. Feeds do not serve private branches.']
168
169=== modified file 'lib/canonical/launchpad/pagetests/feeds/xx-security.txt'
170--- lib/canonical/launchpad/pagetests/feeds/xx-security.txt 2011-05-05 05:46:41 +0000
171+++ lib/canonical/launchpad/pagetests/feeds/xx-security.txt 2011-05-24 03:31:14 +0000
172@@ -42,13 +42,6 @@
173 >>> BSS(browser.contents)('entry')
174 []
175
176-Looking up a single bug raises an Unauthorized error if it is private.
177-
178- >>> browser.open('http://feeds.launchpad.dev/bugs/1/bug.atom')
179- Traceback (most recent call last):
180- ...
181- Unauthorized: ...
182-
183 There should be just one <tr> elements for the table header in
184 these HTML feeds, since all the bugs are private.
185
186@@ -89,9 +82,9 @@
187 >>> try:
188 ... browser.open('http://feeds.launchpad.dev/bugs/1/bug.html')
189 ... except Unauthorized:
190- ... pass
191- ... else:
192- ... print "Should raise Unauthorized exception"
193+ ... print "Shouldn't raise Unauthorized exception"
194+ >>> BSS(browser.contents)('entry')
195+ []
196
197 Revert configuration change after tests are finished.
198
199
200=== modified file 'lib/lp/bugs/feed/bug.py'
201--- lib/lp/bugs/feed/bug.py 2010-08-20 20:31:18 +0000
202+++ lib/lp/bugs/feed/bug.py 2011-05-24 03:31:14 +0000
203@@ -14,13 +14,14 @@
204
205 from z3c.ptcompat import ViewPageTemplateFile
206 from zope.component import getUtility
207-from zope.security.interfaces import Unauthorized
208
209 from canonical.config import config
210 from canonical.launchpad.webapp import (
211 canonical_url,
212 urlparse,
213 )
214+from canonical.launchpad.webapp.authorization import check_permission
215+from canonical.launchpad.webapp.interfaces import ILaunchpadRoot
216 from canonical.launchpad.webapp.publisher import LaunchpadView
217 from canonical.lazr.feed import (
218 FeedBase,
219@@ -141,8 +142,8 @@
220 def getBugsFromBugTasks(self, tasks):
221 """Given a list of BugTasks return the list of associated bugs.
222
223- Since a Bug can have multiple BugTasks, we only select bugs that have not
224- yet been seen.
225+ Since a Bug can have multiple BugTasks, we only select bugs that have
226+ not yet been seen.
227 """
228 bug_ids = []
229 for task in tasks:
230@@ -171,7 +172,18 @@
231 # For a `BugFeed` we must ensure that the bug is not private.
232 super(BugFeed, self).initialize()
233 if self.context.private:
234- raise Unauthorized("Feeds do not serve private bugs")
235+ if check_permission("launchpad.View", self.context):
236+ message_prefix = "This bug is private."
237+ redirect_url = canonical_url(self.context)
238+ else:
239+ # Bug cannot be seen so redirect to the bugs index page.
240+ message_prefix = "The requested bug is private."
241+ root = getUtility(ILaunchpadRoot)
242+ redirect_url = canonical_url(root, rootsite='bugs')
243+
244+ self.request.response.addErrorNotification(
245+ message_prefix + " Feeds do not serve private bugs.")
246+ self.request.response.redirect(redirect_url)
247
248 @property
249 def title(self):
250
251=== modified file 'lib/lp/code/feed/branch.py'
252--- lib/lp/code/feed/branch.py 2011-03-03 01:13:47 +0000
253+++ lib/lp/code/feed/branch.py 2011-05-24 03:31:14 +0000
254@@ -30,6 +30,7 @@
255 LaunchpadView,
256 urlparse,
257 )
258+from canonical.launchpad.webapp.interfaces import ILaunchpadRoot
259 from canonical.lazr.feed import (
260 FeedBase,
261 FeedEntry,
262@@ -74,6 +75,7 @@
263 super(BranchFeedContentView, self).__init__(context, request)
264 self.feed = feed
265 self.template_ = template
266+
267 def render(self):
268 """Render the view."""
269 return ViewPageTemplateFile(self.template_)(self)
270@@ -375,7 +377,7 @@
271
272 def __init__(self, person, rootsite):
273
274- no_email = person.name_without_email
275+ no_email = person.name_without_email
276 if no_email:
277 self.name = no_email
278 else:
279@@ -400,8 +402,25 @@
280 """See `IFeed`."""
281 # For a `BranchFeed` we must ensure that the branch is not private.
282 super(BranchFeed, self).initialize()
283- if self.context.private:
284- raise Unauthorized("Feeds do not serve private branches")
285+ try:
286+ feed_allowed = not self.context.private
287+ if not feed_allowed:
288+ # We are logged in and can see the branch so redirect to the
289+ # branch index page.
290+ message_prefix = "This branch is private."
291+ redirect_url = canonical_url(self.context)
292+ except Unauthorized:
293+ # Branch cannot be seen so redirect to the code index page.
294+ feed_allowed = False
295+ message_prefix = "The requested branch is private."
296+ root = getUtility(ILaunchpadRoot)
297+ redirect_url = canonical_url(root, rootsite='code')
298+
299+ if not feed_allowed:
300+ self.request.response.addErrorNotification(
301+ message_prefix +
302+ " Feeds do not serve private branches.")
303+ self.request.response.redirect(redirect_url)
304
305 @property
306 def title(self):