Merge lp:~jcsackett/launchpad/blueprint-private-traversal into lp:launchpad

Proposed by j.c.sackett
Status: Merged
Approved by: j.c.sackett
Approved revision: no longer in the source branch.
Merged at revision: 16333
Proposed branch: lp:~jcsackett/launchpad/blueprint-private-traversal
Merge into: lp:launchpad
Diff against target: 393 lines (+140/-47)
8 files modified
lib/lp/blueprints/browser/specification.py (+7/-4)
lib/lp/blueprints/doc/specgraph.txt (+20/-16)
lib/lp/blueprints/doc/specification.txt (+4/-4)
lib/lp/blueprints/interfaces/specification.py (+12/-4)
lib/lp/blueprints/model/specification.py (+37/-9)
lib/lp/blueprints/model/tests/test_specification.py (+55/-8)
lib/lp/blueprints/vocabularies/specification.py (+2/-1)
lib/lp/blueprints/vocabularies/specificationdependency.py (+3/-1)
To merge this branch: bzr merge lp:~jcsackett/launchpad/blueprint-private-traversal
Reviewer Review Type Date Requested Status
Deryck Hodge (community) Approve
Review via email: mp+137333@code.launchpad.net

This proposal supersedes a proposal from 2012-11-30.

Description of the change

Summary
=======
Specifications can have dependencies with different permissions (either
information_type or grants) than the root node; likewise, it can block
specifications with the same conditions.

In this situation the page currently 403s. Per our usual rules we want to show
the page without the information a user shouldn't see.

Preimp
======
Spoke with Orange Squad and William Grant.

Implementation
==============
The SpecGraph is updated to use all_deps and all_blocked; these are easy to
modify to filter out blueprints the user can't see.

Those two properties are converted to methods that take the user as a
parameter, which is then used with the sharing service to modify the results
of the query to only include the ones the user can see.

The SpecGraph object is modified to properly use the all_deps/all_blocked
methods, and to recieve the user as an argument to __init__.

Tests
=====
bin/test -vvc -t TestSpecificationDependencies -t specgraph

QA
==
Follow the setup in the bug; you should see the page without a 403, and
without see nonpublic data that you have no grant for.

LoC
===
Part of private projects.

Lint
====

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/blueprints/doc/specgraph.txt
  lib/lp/blueprints/model/tests/test_specification.py
  lib/lp/blueprints/model/specification.py
  lib/lp/blueprints/browser/specification.py
  lib/lp/blueprints/interfaces/specification.py

To post a comment you must log in.
Revision history for this message
Deryck Hodge (deryck) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/blueprints/browser/specification.py'
2--- lib/lp/blueprints/browser/specification.py 2012-10-30 22:37:17 +0000
3+++ lib/lp/blueprints/browser/specification.py 2012-12-01 23:02:20 +0000
4@@ -1112,10 +1112,11 @@
5 # containing one '%s' replacement marker.
6 url_pattern_for_testing = None
7
8- def __init__(self):
9+ def __init__(self, user=None):
10 self.nodes = set()
11 self.edges = set()
12 self.root_node = None
13+ self.user = user
14
15 def newNode(self, spec, root=False):
16 """Return a new node based on the given spec.
17@@ -1166,7 +1167,8 @@
18 """Add nodes for the specs that the given spec depends on,
19 transitively.
20 """
21- get_related_specs_fn = attrgetter('dependencies')
22+ def get_related_specs_fn(spec):
23+ return spec.all_deps(user=self.user)
24
25 def link_nodes_fn(node, dependency):
26 self.link(dependency, node)
27@@ -1174,7 +1176,8 @@
28
29 def addBlockedNodes(self, spec):
30 """Add nodes for specs that the given spec blocks, transitively."""
31- get_related_specs_fn = attrgetter('blocked_specs')
32+ def get_related_specs_fn(spec):
33+ return spec.all_blocked(user=self.user)
34
35 def link_nodes_fn(node, blocked_spec):
36 self.link(node, blocked_spec)
37@@ -1405,7 +1408,7 @@
38 def makeSpecGraph(self):
39 """Return a SpecGraph object rooted on the spec that is self.context.
40 """
41- graph = SpecGraph()
42+ graph = SpecGraph(self.user)
43 graph.newNode(self.context, root=True)
44 graph.addDependencyNodes(self.context)
45 graph.addBlockedNodes(self.context)
46
47=== modified file 'lib/lp/blueprints/doc/specgraph.txt'
48--- lib/lp/blueprints/doc/specgraph.txt 2012-09-12 19:04:40 +0000
49+++ lib/lp/blueprints/doc/specgraph.txt 2012-12-01 23:02:20 +0000
50@@ -27,12 +27,16 @@
51 ... self.title = title or name
52 ... self.is_complete = is_complete
53 ... self.assignee = assignee
54- ... # Use lists here to ensure that the code converts them
55- ... # to sets explicitly, like it has to do for SelectResults.
56- ... self.dependencies = []
57- ... # This is a hack for testing: we can set up dependencies,
58- ... # and simply use their "mirror image" for blocked specs.
59- ... self.blocked_specs = self.dependencies
60+ ... # For purposes of this doc, we're only stubbing the
61+ ... # dependency methods.
62+ ... self._all_deps = []
63+ ... self._all_blocked = self._all_deps
64+ ...
65+ ... def all_deps(self, user):
66+ ... return self._all_deps
67+ ...
68+ ... def all_blocked(self, user):
69+ ... return self._all_blocked
70
71 >>> foo = Spec('foo', title='something with " and \n in it')
72 >>> root = g.newNode(foo, root=True)
73@@ -70,16 +74,16 @@
74 <fnord-foo>:
75
76 >>> foo1 = Spec('foo1')
77- >>> foo.dependencies.append(foo1)
78+ >>> foo._all_deps.append(foo1)
79 >>> foo2 = Spec('foo2')
80- >>> foo.dependencies.append(foo2)
81+ >>> foo._all_deps.append(foo2)
82 >>> foo11 = Spec('foo11')
83- >>> foo1.dependencies.append(foo11)
84+ >>> foo1._all_deps.append(foo11)
85 >>> foo111 = Spec('foo111')
86- >>> foo11.dependencies.append(foo111)
87+ >>> foo11._all_deps.append(foo111)
88
89 >>> def make_graph(dependency, blocked):
90- ... g = SpecGraph()
91+ ... g = SpecGraph(user=None)
92 ... g.url_pattern_for_testing = 'http://whatever/%s'
93 ... g.newNode(foo, root=True)
94 ... if dependency:
95@@ -175,11 +179,11 @@
96
97 The graph grows when specifications gain more dependencies.
98
99- >>> foo1.dependencies.append(foo)
100- >>> foo111.dependencies.append(foo1)
101- >>> foo111.dependencies.append(foo)
102- >>> foo2.dependencies.append(foo1)
103- >>> foo1.dependencies.append(foo2)
104+ >>> foo1._all_deps.append(foo)
105+ >>> foo111._all_deps.append(foo1)
106+ >>> foo111._all_deps.append(foo)
107+ >>> foo2._all_deps.append(foo1)
108+ >>> foo1._all_deps.append(foo2)
109 >>> print_graph()
110 Root is <fnord-foo>
111 <fnord-foo>:
112
113=== modified file 'lib/lp/blueprints/doc/specification.txt'
114--- lib/lp/blueprints/doc/specification.txt 2012-09-27 19:05:50 +0000
115+++ lib/lp/blueprints/doc/specification.txt 2012-12-01 23:02:20 +0000
116@@ -265,22 +265,22 @@
117 >>> for spec in efourx.dependencies: print spec.name
118 svg-support
119
120- >>> for spec in efourx.all_deps: print spec.name
121+ >>> for spec in efourx.all_deps(): print spec.name
122 svg-support
123
124 >>> for spec in efourx.blocked_specs: print spec.name
125 canvas
126
127- >>> for spec in efourx.all_blocked: print spec.name
128+ >>> for spec in efourx.all_blocked(): print spec.name
129 canvas
130
131 >>> canvas = efourx.blocked_specs[0]
132 >>> svg = efourx.dependencies[0]
133- >>> for spec in svg.all_blocked: print spec.name
134+ >>> for spec in svg.all_blocked(): print spec.name
135 canvas
136 e4x
137
138- >>> for spec in canvas.all_deps: print spec.name
139+ >>> for spec in canvas.all_deps(): print spec.name
140 e4x
141 svg-support
142
143
144=== modified file 'lib/lp/blueprints/interfaces/specification.py'
145--- lib/lp/blueprints/interfaces/specification.py 2012-11-26 08:40:20 +0000
146+++ lib/lp/blueprints/interfaces/specification.py 2012-12-01 23:02:20 +0000
147@@ -404,10 +404,6 @@
148 readonly=True),
149 as_of="devel")
150 blocked_specs = Attribute('Specs for which this spec is a dependency.')
151- all_deps = Attribute(
152- "All the dependencies, including dependencies of dependencies.")
153- all_blocked = Attribute(
154- "All specs blocked on this, and those blocked on the blocked ones.")
155 linked_branches = exported(
156 CollectionField(
157 title=_("Branches associated with this spec, usually "
158@@ -453,6 +449,18 @@
159 readonly=True),
160 as_of="devel")
161
162+ def all_deps():
163+ """All the dependencies, including dependencies of dependencies.
164+
165+ If a user is provided, filters to only dependencies the user can see.
166+ """
167+ def all_blocked():
168+ """All specs blocked on this, and those blocked on the blocked ones.
169+
170+ If a user is provided, filters to only blocked dependencies the user
171+ can see.
172+ """
173+
174 def validateMove(target):
175 """Check that the specification can be moved to the target."""
176
177
178=== modified file 'lib/lp/blueprints/model/specification.py'
179--- lib/lp/blueprints/model/specification.py 2012-11-28 19:45:50 +0000
180+++ lib/lp/blueprints/model/specification.py 2012-12-01 23:02:20 +0000
181@@ -89,8 +89,8 @@
182 from lp.registry.enums import SpecificationSharingPolicy
183 from lp.registry.errors import CannotChangeInformationType
184 from lp.registry.interfaces.accesspolicy import (
185+ IAccessArtifactSource,
186 IAccessArtifactGrantSource,
187- IAccessArtifactSource,
188 )
189 from lp.registry.interfaces.distribution import IDistribution
190 from lp.registry.interfaces.distroseries import IDistroSeries
191@@ -822,25 +822,53 @@
192 SpecificationDependency.delete(deplink.id)
193 return deplink
194
195- @property
196- def all_deps(self):
197- return Store.of(self).with_(
198+ def all_deps(self, user=None):
199+ public_clause = True
200+ if user is None:
201+ public_clause = (
202+ Specification.information_type == InformationType.PUBLIC,
203+ )
204+
205+ results = Store.of(self).with_(
206 SQL(recursive_dependent_query(self))).find(
207 Specification,
208 Specification.id != self.id,
209- SQL('Specification.id in (select id from dependencies)')
210+ SQL('Specification.id in (select id from dependencies)'),
211+ public_clause,
212 ).order_by(Specification.name, Specification.id)
213
214- @property
215- def all_blocked(self):
216+ results = list(results)
217+
218+ if user:
219+ service = getUtility(IService, 'sharing')
220+ (ignore, ignore, results) = service.getVisibleArtifacts(
221+ user, specifications=results)
222+ return results
223+
224+ def all_blocked(self, user=None):
225 """See `ISpecification`."""
226- return Store.of(self).with_(
227+ public_clause = True
228+ if user is None:
229+ public_clause = (
230+ Specification.information_type == InformationType.PUBLIC,
231+ )
232+
233+ results = Store.of(self).with_(
234 SQL(recursive_blocked_query(self))).find(
235 Specification,
236 Specification.id != self.id,
237- SQL('Specification.id in (select id from blocked)')
238+ SQL('Specification.id in (select id from blocked)'),
239+ public_clause,
240 ).order_by(Specification.name, Specification.id)
241
242+ results = list(results)
243+
244+ if user:
245+ service = getUtility(IService, 'sharing')
246+ (ignore, ignore, results) = service.getVisibleArtifacts(
247+ user, specifications=results)
248+ return results
249+
250 # branches
251 def getBranchLink(self, branch):
252 return SpecificationBranch.selectOneBy(
253
254=== modified file 'lib/lp/blueprints/model/tests/test_specification.py'
255--- lib/lp/blueprints/model/tests/test_specification.py 2012-11-26 08:33:03 +0000
256+++ lib/lp/blueprints/model/tests/test_specification.py 2012-12-01 23:02:20 +0000
257@@ -53,18 +53,18 @@
258 def test_no_deps(self):
259 blueprint = self.factory.makeBlueprint()
260 self.assertThat(list(blueprint.dependencies), Equals([]))
261- self.assertThat(list(blueprint.all_deps), Equals([]))
262+ self.assertThat(list(blueprint.all_deps()), Equals([]))
263 self.assertThat(list(blueprint.blocked_specs), Equals([]))
264- self.assertThat(list(blueprint.all_blocked), Equals([]))
265+ self.assertThat(list(blueprint.all_blocked()), Equals([]))
266
267 def test_single_dependency(self):
268 do_first = self.factory.makeBlueprint()
269 do_next = self.factory.makeBlueprint()
270 do_next.createDependency(do_first)
271 self.assertThat(list(do_first.blocked_specs), Equals([do_next]))
272- self.assertThat(list(do_first.all_blocked), Equals([do_next]))
273+ self.assertThat(list(do_first.all_blocked()), Equals([do_next]))
274 self.assertThat(list(do_next.dependencies), Equals([do_first]))
275- self.assertThat(list(do_next.all_deps), Equals([do_first]))
276+ self.assertThat(list(do_next.all_deps()), Equals([do_first]))
277
278 def test_linear_dependency(self):
279 do_first = self.factory.makeBlueprint()
280@@ -74,11 +74,11 @@
281 do_last.createDependency(do_next)
282 self.assertThat(sorted(do_first.blocked_specs), Equals([do_next]))
283 self.assertThat(
284- sorted(do_first.all_blocked),
285+ sorted(do_first.all_blocked()),
286 Equals(sorted([do_next, do_last])))
287 self.assertThat(sorted(do_last.dependencies), Equals([do_next]))
288 self.assertThat(
289- sorted(do_last.all_deps),
290+ sorted(do_last.all_deps()),
291 Equals(sorted([do_first, do_next])))
292
293 def test_diamond_dependency(self):
294@@ -99,15 +99,62 @@
295 sorted(do_first.blocked_specs),
296 Equals(sorted([do_next_lhs, do_next_rhs])))
297 self.assertThat(
298- sorted(do_first.all_blocked),
299+ sorted(do_first.all_blocked()),
300 Equals(sorted([do_next_lhs, do_next_rhs, do_last])))
301 self.assertThat(
302 sorted(do_last.dependencies),
303 Equals(sorted([do_next_lhs, do_next_rhs])))
304 self.assertThat(
305- sorted(do_last.all_deps),
306+ sorted(do_last.all_deps()),
307 Equals(sorted([do_first, do_next_lhs, do_next_rhs])))
308
309+ def test_all_deps_filters(self):
310+ # all_deps, when provided a user, shows only the dependencies the user
311+ # can see.
312+ sharing_policy = SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY
313+ owner = self.factory.makePerson()
314+ product = self.factory.makeProduct(
315+ owner=owner, specification_sharing_policy=sharing_policy)
316+ root = self.factory.makeBlueprint(product=product)
317+ proprietary_dep = self.factory.makeBlueprint(
318+ product=product, information_type=InformationType.PROPRIETARY)
319+ public_dep = self.factory.makeBlueprint(product=product)
320+ root.createDependency(proprietary_dep)
321+ root.createDependency(public_dep)
322+ # Anonymous (no user) requests only get public dependencies
323+ self.assertEqual([public_dep], root.all_deps())
324+ # The owner of the product can see everything.
325+ self.assertEqual(
326+ [proprietary_dep, public_dep], root.all_deps(user=owner))
327+ # A random person can't see the proprietary dependency.
328+ self.assertEqual(
329+ [public_dep], root.all_deps(user=self.factory.makePerson()))
330+
331+ def test_all_blocked_filters(self):
332+ # all_blocked, when provided a user, shows only the blocked specs the
333+ # user can see.
334+ sharing_policy = SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY
335+ owner = self.factory.makePerson()
336+ product = self.factory.makeProduct(
337+ owner=owner, specification_sharing_policy=sharing_policy)
338+ root = self.factory.makeBlueprint(product=product)
339+ proprietary_blocked = self.factory.makeBlueprint(
340+ product=product, information_type=InformationType.PROPRIETARY)
341+ public_blocked = self.factory.makeBlueprint(product=product)
342+ proprietary_blocked.createDependency(root)
343+ public_blocked.createDependency(root)
344+ # Anonymous (no user) requests only get public blocked specs.
345+ self.assertEqual(
346+ [public_blocked], root.all_blocked())
347+ # The owner of the product can see everything.
348+ self.assertEqual(
349+ [proprietary_blocked, public_blocked],
350+ root.all_blocked(user=owner))
351+ # A random person can't see the proprietary blocked spec.
352+ self.assertEqual(
353+ [public_blocked],
354+ root.all_blocked(user=self.factory.makePerson()))
355+
356
357 class TestSpecificationSubscriptionSort(TestCaseWithFactory):
358
359
360=== modified file 'lib/lp/blueprints/vocabularies/specification.py'
361--- lib/lp/blueprints/vocabularies/specification.py 2012-09-28 14:35:35 +0000
362+++ lib/lp/blueprints/vocabularies/specification.py 2012-12-01 23:02:20 +0000
363@@ -50,6 +50,7 @@
364 # the widget is currently used to select new dependencies,
365 # and we do not want to introduce circular dependencies.
366 if launchbag.specification is not None:
367- if spec in launchbag.specification.all_blocked:
368+ user = getattr(launchbag, 'user', None)
369+ if spec in launchbag.specification.all_blocked(user=user):
370 continue
371 yield SimpleTerm(spec, spec.name, spec.title)
372
373=== modified file 'lib/lp/blueprints/vocabularies/specificationdependency.py'
374--- lib/lp/blueprints/vocabularies/specificationdependency.py 2011-12-30 06:14:56 +0000
375+++ lib/lp/blueprints/vocabularies/specificationdependency.py 2012-12-01 23:02:20 +0000
376@@ -29,6 +29,7 @@
377 canonical_url,
378 urlparse,
379 )
380+from lp.services.webapp.interfaces import ILaunchBag
381 from lp.services.webapp.vocabulary import (
382 CountableIterator,
383 IHugeVocabulary,
384@@ -77,7 +78,8 @@
385 """
386 if spec is None or spec == self.context:
387 return False
388- return spec not in set(self.context.all_blocked)
389+ user = getattr(getUtility(ILaunchBag), 'user', None)
390+ return spec not in set(self.context.all_blocked(user=user))
391
392 def _order_by(self):
393 """Look at the context to provide grouping.