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

Proposed by j.c.sackett
Status: Superseded
Proposed branch: lp:~jcsackett/launchpad/blueprint-private-traversal
Merge into: lp:launchpad
Diff against target: 338 lines (+132/-45)
5 files modified
lib/lp/blueprints/browser/specification.py (+8/-8)
lib/lp/blueprints/doc/specgraph.txt (+20/-16)
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)
To merge this branch: bzr merge lp:~jcsackett/launchpad/blueprint-private-traversal
Reviewer Review Type Date Requested Status
Launchpad code reviewers Pending
Review via email: mp+137330@code.launchpad.net

This proposal has been superseded by a proposal from 2012-11-30.

Commit message

Strips nonpublic specifications from the dependency graph on Specification:+index if the user doesn't have permission to see them.

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.

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-11-30 21:39:22 +0000
3+++ lib/lp/blueprints/browser/specification.py 2012-11-30 21:39:22 +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('all_deps')
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('all_blocked')
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@@ -1197,10 +1200,7 @@
38 current_spec = to_search.pop()
39 visited.add(current_spec)
40 node = self.newOrExistingNode(current_spec)
41- try:
42- related_specs = set(get_related_specs_fn(current_spec))
43- except AttributeError:
44- import pdb; pdb.set_trace()
45+ related_specs = set(get_related_specs_fn(current_spec))
46 for related_spec in related_specs:
47 link_nodes_fn(node, self.newOrExistingNode(related_spec))
48 to_search.update(related_specs.difference(visited))
49@@ -1408,7 +1408,7 @@
50 def makeSpecGraph(self):
51 """Return a SpecGraph object rooted on the spec that is self.context.
52 """
53- graph = SpecGraph()
54+ graph = SpecGraph(self.user)
55 graph.newNode(self.context, root=True)
56 graph.addDependencyNodes(self.context)
57 graph.addBlockedNodes(self.context)
58
59=== modified file 'lib/lp/blueprints/doc/specgraph.txt'
60--- lib/lp/blueprints/doc/specgraph.txt 2012-11-30 21:39:22 +0000
61+++ lib/lp/blueprints/doc/specgraph.txt 2012-11-30 21:39:22 +0000
62@@ -27,12 +27,16 @@
63 ... self.title = title or name
64 ... self.is_complete = is_complete
65 ... self.assignee = assignee
66- ... # Use lists here to ensure that the code converts them
67- ... # to sets explicitly, like it has to do for SelectResults.
68- ... self.all_deps = []
69- ... # This is a hack for testing: we can set up dependencies,
70- ... # and simply use their "mirror image" for blocked specs.
71- ... self.all_blocked = self.all_deps
72+ ... # For purposes of this doc, we're only stubbing the
73+ ... # dependency methods.
74+ ... self._all_deps = []
75+ ... self._all_blocked = self._all_deps
76+ ...
77+ ... def all_deps(self, user):
78+ ... return self._all_deps
79+ ...
80+ ... def all_blocked(self, user):
81+ ... return self._all_blocked
82
83 >>> foo = Spec('foo', title='something with " and \n in it')
84 >>> root = g.newNode(foo, root=True)
85@@ -70,16 +74,16 @@
86 <fnord-foo>:
87
88 >>> foo1 = Spec('foo1')
89- >>> foo.all_deps.append(foo1)
90+ >>> foo._all_deps.append(foo1)
91 >>> foo2 = Spec('foo2')
92- >>> foo.all_deps.append(foo2)
93+ >>> foo._all_deps.append(foo2)
94 >>> foo11 = Spec('foo11')
95- >>> foo1.all_deps.append(foo11)
96+ >>> foo1._all_deps.append(foo11)
97 >>> foo111 = Spec('foo111')
98- >>> foo11.all_deps.append(foo111)
99+ >>> foo11._all_deps.append(foo111)
100
101 >>> def make_graph(dependency, blocked):
102- ... g = SpecGraph()
103+ ... g = SpecGraph(user=None)
104 ... g.url_pattern_for_testing = 'http://whatever/%s'
105 ... g.newNode(foo, root=True)
106 ... if dependency:
107@@ -175,11 +179,11 @@
108
109 The graph grows when specifications gain more dependencies.
110
111- >>> foo1.all_deps.append(foo)
112- >>> foo111.all_deps.append(foo1)
113- >>> foo111.all_deps.append(foo)
114- >>> foo2.all_deps.append(foo1)
115- >>> foo1.all_deps.append(foo2)
116+ >>> foo1._all_deps.append(foo)
117+ >>> foo111._all_deps.append(foo1)
118+ >>> foo111._all_deps.append(foo)
119+ >>> foo2._all_deps.append(foo1)
120+ >>> foo1._all_deps.append(foo2)
121 >>> print_graph()
122 Root is <fnord-foo>
123 <fnord-foo>:
124
125=== modified file 'lib/lp/blueprints/interfaces/specification.py'
126--- lib/lp/blueprints/interfaces/specification.py 2012-11-26 08:40:20 +0000
127+++ lib/lp/blueprints/interfaces/specification.py 2012-11-30 21:39:22 +0000
128@@ -404,10 +404,6 @@
129 readonly=True),
130 as_of="devel")
131 blocked_specs = Attribute('Specs for which this spec is a dependency.')
132- all_deps = Attribute(
133- "All the dependencies, including dependencies of dependencies.")
134- all_blocked = Attribute(
135- "All specs blocked on this, and those blocked on the blocked ones.")
136 linked_branches = exported(
137 CollectionField(
138 title=_("Branches associated with this spec, usually "
139@@ -453,6 +449,18 @@
140 readonly=True),
141 as_of="devel")
142
143+ def all_deps():
144+ """All the dependencies, including dependencies of dependencies.
145+
146+ If a user is provided, filters to only dependencies the user can see.
147+ """
148+ def all_blocked():
149+ """All specs blocked on this, and those blocked on the blocked ones.
150+
151+ If a user is provided, filters to only blocked dependencies the user
152+ can see.
153+ """
154+
155 def validateMove(target):
156 """Check that the specification can be moved to the target."""
157
158
159=== modified file 'lib/lp/blueprints/model/specification.py'
160--- lib/lp/blueprints/model/specification.py 2012-11-28 19:45:50 +0000
161+++ lib/lp/blueprints/model/specification.py 2012-11-30 21:39:22 +0000
162@@ -89,8 +89,8 @@
163 from lp.registry.enums import SpecificationSharingPolicy
164 from lp.registry.errors import CannotChangeInformationType
165 from lp.registry.interfaces.accesspolicy import (
166+ IAccessArtifactSource,
167 IAccessArtifactGrantSource,
168- IAccessArtifactSource,
169 )
170 from lp.registry.interfaces.distribution import IDistribution
171 from lp.registry.interfaces.distroseries import IDistroSeries
172@@ -822,25 +822,53 @@
173 SpecificationDependency.delete(deplink.id)
174 return deplink
175
176- @property
177- def all_deps(self):
178- return Store.of(self).with_(
179+ def all_deps(self, user=None):
180+ public_clause = True
181+ if user is None:
182+ public_clause = (
183+ Specification.information_type == InformationType.PUBLIC,
184+ )
185+
186+ results = Store.of(self).with_(
187 SQL(recursive_dependent_query(self))).find(
188 Specification,
189 Specification.id != self.id,
190- SQL('Specification.id in (select id from dependencies)')
191+ SQL('Specification.id in (select id from dependencies)'),
192+ public_clause,
193 ).order_by(Specification.name, Specification.id)
194
195- @property
196- def all_blocked(self):
197+ results = list(results)
198+
199+ if user:
200+ service = getUtility(IService, 'sharing')
201+ (ignore, ignore, results) = service.getVisibleArtifacts(
202+ user, specifications=results)
203+ return results
204+
205+ def all_blocked(self, user=None):
206 """See `ISpecification`."""
207- return Store.of(self).with_(
208+ public_clause = True
209+ if user is None:
210+ public_clause = (
211+ Specification.information_type == InformationType.PUBLIC,
212+ )
213+
214+ results = Store.of(self).with_(
215 SQL(recursive_blocked_query(self))).find(
216 Specification,
217 Specification.id != self.id,
218- SQL('Specification.id in (select id from blocked)')
219+ SQL('Specification.id in (select id from blocked)'),
220+ public_clause,
221 ).order_by(Specification.name, Specification.id)
222
223+ results = list(results)
224+
225+ if user:
226+ service = getUtility(IService, 'sharing')
227+ (ignore, ignore, results) = service.getVisibleArtifacts(
228+ user, specifications=results)
229+ return results
230+
231 # branches
232 def getBranchLink(self, branch):
233 return SpecificationBranch.selectOneBy(
234
235=== modified file 'lib/lp/blueprints/model/tests/test_specification.py'
236--- lib/lp/blueprints/model/tests/test_specification.py 2012-11-26 08:33:03 +0000
237+++ lib/lp/blueprints/model/tests/test_specification.py 2012-11-30 21:39:22 +0000
238@@ -53,18 +53,18 @@
239 def test_no_deps(self):
240 blueprint = self.factory.makeBlueprint()
241 self.assertThat(list(blueprint.dependencies), Equals([]))
242- self.assertThat(list(blueprint.all_deps), Equals([]))
243+ self.assertThat(list(blueprint.all_deps()), Equals([]))
244 self.assertThat(list(blueprint.blocked_specs), Equals([]))
245- self.assertThat(list(blueprint.all_blocked), Equals([]))
246+ self.assertThat(list(blueprint.all_blocked()), Equals([]))
247
248 def test_single_dependency(self):
249 do_first = self.factory.makeBlueprint()
250 do_next = self.factory.makeBlueprint()
251 do_next.createDependency(do_first)
252 self.assertThat(list(do_first.blocked_specs), Equals([do_next]))
253- self.assertThat(list(do_first.all_blocked), Equals([do_next]))
254+ self.assertThat(list(do_first.all_blocked()), Equals([do_next]))
255 self.assertThat(list(do_next.dependencies), Equals([do_first]))
256- self.assertThat(list(do_next.all_deps), Equals([do_first]))
257+ self.assertThat(list(do_next.all_deps()), Equals([do_first]))
258
259 def test_linear_dependency(self):
260 do_first = self.factory.makeBlueprint()
261@@ -74,11 +74,11 @@
262 do_last.createDependency(do_next)
263 self.assertThat(sorted(do_first.blocked_specs), Equals([do_next]))
264 self.assertThat(
265- sorted(do_first.all_blocked),
266+ sorted(do_first.all_blocked()),
267 Equals(sorted([do_next, do_last])))
268 self.assertThat(sorted(do_last.dependencies), Equals([do_next]))
269 self.assertThat(
270- sorted(do_last.all_deps),
271+ sorted(do_last.all_deps()),
272 Equals(sorted([do_first, do_next])))
273
274 def test_diamond_dependency(self):
275@@ -99,15 +99,62 @@
276 sorted(do_first.blocked_specs),
277 Equals(sorted([do_next_lhs, do_next_rhs])))
278 self.assertThat(
279- sorted(do_first.all_blocked),
280+ sorted(do_first.all_blocked()),
281 Equals(sorted([do_next_lhs, do_next_rhs, do_last])))
282 self.assertThat(
283 sorted(do_last.dependencies),
284 Equals(sorted([do_next_lhs, do_next_rhs])))
285 self.assertThat(
286- sorted(do_last.all_deps),
287+ sorted(do_last.all_deps()),
288 Equals(sorted([do_first, do_next_lhs, do_next_rhs])))
289
290+ def test_all_deps_filters(self):
291+ # all_deps, when provided a user, shows only the dependencies the user
292+ # can see.
293+ sharing_policy = SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY
294+ owner = self.factory.makePerson()
295+ product = self.factory.makeProduct(
296+ owner=owner, specification_sharing_policy=sharing_policy)
297+ root = self.factory.makeBlueprint(product=product)
298+ proprietary_dep = self.factory.makeBlueprint(
299+ product=product, information_type=InformationType.PROPRIETARY)
300+ public_dep = self.factory.makeBlueprint(product=product)
301+ root.createDependency(proprietary_dep)
302+ root.createDependency(public_dep)
303+ # Anonymous (no user) requests only get public dependencies
304+ self.assertEqual([public_dep], root.all_deps())
305+ # The owner of the product can see everything.
306+ self.assertEqual(
307+ [proprietary_dep, public_dep], root.all_deps(user=owner))
308+ # A random person can't see the proprietary dependency.
309+ self.assertEqual(
310+ [public_dep], root.all_deps(user=self.factory.makePerson()))
311+
312+ def test_all_blocked_filters(self):
313+ # all_blocked, when provided a user, shows only the blocked specs the
314+ # user can see.
315+ sharing_policy = SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY
316+ owner = self.factory.makePerson()
317+ product = self.factory.makeProduct(
318+ owner=owner, specification_sharing_policy=sharing_policy)
319+ root = self.factory.makeBlueprint(product=product)
320+ proprietary_blocked = self.factory.makeBlueprint(
321+ product=product, information_type=InformationType.PROPRIETARY)
322+ public_blocked = self.factory.makeBlueprint(product=product)
323+ proprietary_blocked.createDependency(root)
324+ public_blocked.createDependency(root)
325+ # Anonymous (no user) requests only get public blocked specs.
326+ self.assertEqual(
327+ [public_blocked], root.all_blocked())
328+ # The owner of the product can see everything.
329+ self.assertEqual(
330+ [proprietary_blocked, public_blocked],
331+ root.all_blocked(user=owner))
332+ # A random person can't see the proprietary blocked spec.
333+ self.assertEqual(
334+ [public_blocked],
335+ root.all_blocked(user=self.factory.makePerson()))
336+
337
338 class TestSpecificationSubscriptionSort(TestCaseWithFactory):
339