Merge lp:~wgrant/launchpad/export-archive-dependencies into lp:launchpad

Proposed by William Grant
Status: Merged
Merged at revision: not available
Proposed branch: lp:~wgrant/launchpad/export-archive-dependencies
Merge into: lp:launchpad
Diff against target: None lines
To merge this branch: bzr merge lp:~wgrant/launchpad/export-archive-dependencies
Reviewer Review Type Date Requested Status
Celso Providelo (community) code Approve
Review via email: mp+9823@code.launchpad.net
To post a comment you must log in.
Revision history for this message
William Grant (wgrant) wrote :

= Summary =

Bug #385129 requests that archive dependencies be exported on the webservice.
This branch gives read-only access to ArchiveDependency records through the
API.

== Implementation details ==

I've given IArchiveDependency a URL and navigation
(<dependent>/+dependency/<dependency ID>). To get hold of the dependencies,
I also moved IArchive.{dependencies,getArchiveDependency} from IArchivePublic
to IArchiveView and exported them. This required some interface patches in
c.l.i._schema_circular_imports.

IComponent isn't exposed, so IArchiveDependency.component could not be
exposed directly. I followed the example of similar interfaces, instead
creating and exporting IArchiveDependency.component_name.

The other changes to IArchiveDependency are just decoration, with Choices
and Objects replaced with ReferenceChoices and References to coerce
lazr.restful into working.

== Tests ==

$ bin/test -vv -t xx-archive.txt
Running tests at level 1
Running canonical.testing.layers.PageTestLayer tests:
  Set up canonical.testing.layers.BaseLayer in 0.024 seconds.
  Set up canonical.testing.layers.DatabaseLayer in 0.521 seconds.
  Set up canonical.testing.layers.LibrarianLayer in 8.932 seconds.
  Set up canonical.testing.layers.LaunchpadLayer in 0.000 seconds.
  Set up canonical.testing.layers.FunctionalLayer in 4.414 seconds.
  Set up canonical.testing.layers.GoogleServiceLayer in 1.358 seconds.
  Set up canonical.testing.layers.LaunchpadFunctionalLayer in 0.000 seconds.
  Set up canonical.testing.layers.PageTestLayer in 0.000 seconds.
  Running:
 lib/lp/soyuz/tests/../stories/webservice/xx-archive.txt
  Ran 77 tests with 0 failures and 0 errors in 10.408 seconds.
Tearing down left over layers:
  Tear down canonical.testing.layers.PageTestLayer in 0.000 seconds.
  Tear down canonical.testing.layers.LaunchpadFunctionalLayer in 0.000 seconds.
  Tear down canonical.testing.layers.LaunchpadLayer in 0.000 seconds.
  Tear down canonical.testing.layers.LibrarianLayer in 0.312 seconds.
  Tear down canonical.testing.layers.GoogleServiceLayer in 0.051 seconds.
  Tear down canonical.testing.layers.FunctionalLayer ... not supported
  Tear down canonical.testing.layers.DatabaseLayer in 0.020 seconds.
  Tear down canonical.testing.layers.BaseLayer in 0.000 seconds.

== lint ==

Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.

Linting changed files:
  lib/canonical/launchpad/interfaces/_schema_circular_imports.py
  lib/lp/soyuz/browser/archive.py
  lib/lp/soyuz/browser/configure.zcml
  lib/lp/soyuz/interfaces/archive.py
  lib/lp/soyuz/interfaces/archivedependency.py
  lib/lp/soyuz/model/archivedependency.py
  lib/lp/soyuz/stories/webservice/xx-archive.txt

Revision history for this message
Celso Providelo (cprov) wrote :
Download full text (19.4 KiB)

On Fri, Aug 7, 2009 at 10:05 AM, William Grant<email address hidden> wrote:
> William Grant has proposed merging lp:~wgrant/launchpad/export-archive-dependencies into lp:launchpad/devel.
>
> Requested reviews:
>    Celso Providelo (cprov)
>
> = Summary =
>
> Bug #385129 requests that archive dependencies be exported on the webservice.
> This branch gives read-only access to ArchiveDependency records through the
> API.
>
>
> == Implementation details ==
>
> I've given IArchiveDependency a URL and navigation
> (<dependent>/+dependency/<dependency ID>). To get hold of the dependencies,
> I also moved IArchive.{dependencies,getArchiveDependency} from IArchivePublic
> to IArchiveView and exported them. This required some interface patches in
> c.l.i._schema_circular_imports.
>
> IComponent isn't exposed, so IArchiveDependency.component could not be
> exposed directly. I followed the example of similar interfaces, instead
> creating and exporting IArchiveDependency.component_name.
>
> The other changes to IArchiveDependency are just decoration, with Choices
> and Objects replaced with ReferenceChoices and References to coerce
> lazr.restful into working.

Hi Willian,

Thanks for working on this, You've done a fantastic job!

Comments inline.

> === modified file 'lib/canonical/launchpad/interfaces/_schema_circular_imports.py'

Nice that you've found your way through the monkey-patches :)

> === modified file 'lib/lp/soyuz/browser/archive.py'
> --- lib/lp/soyuz/browser/archive.py 2009-08-05 11:33:16 +0000
> +++ lib/lp/soyuz/browser/archive.py 2009-08-07 14:05:54 +0000
> @@ -34,6 +34,8 @@
> from zope.schema import Choice, List
> from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
>
> +from sqlobject import SQLObjectNotFound
> +
> from canonical.cachedproperty import cachedproperty
> from canonical.launchpad import _
> from lp.soyuz.browser.build import BuildRecordsView
> @@ -285,6 +287,20 @@
> else:
> return None
>
> + @stepthrough('+dependency')
> + def traverse_dependency(self, id):
> + try:
> + id = int(id)
> + except ValueError:
> + # Not a number.
> + return None
> +
> + try:
> + archive = getUtility(IArchiveSet).get(id)
> + except SQLObjectNotFound:
> + return None
> +
> + return self.context.getArchiveDependency(archive)

Okay, there are few subtle details in this travesal that I'd like to
clarify with you.

1. The protection against leaking dependencies of P3As exists because
IArchive.getArchiveDependencies() requires launchpad.View.

2. By design we don't allow public PPAs to depend on P3As, so attempts
to reach a P3A by querying its ID as a dependency of a public PPA will
never be an option (not that it would expose much of the dependant P3A
either, but ...)

Can you summarize those aspects in the traversal docstring ?

It is still possible, though, that a P3A B added as dependency of
another P3A A isn't accessible to all people with launchpad.View on A.
This situation would break most of the code in this area, including
the page rendering ... We should file a bug about it.

> class ArchiveContextMenu(Cont...

Revision history for this message
Celso Providelo (cprov) :
review: Needs Information (code)
Revision history for this message
Francis J. Lacoste (flacoste) wrote :

On August 7, 2009, Celso Providelo wrote:
> Any archive purpose can contain dependencies, not only PPAs. The
> restriction can remain in the form interface
> (IArchiveEditDependenciesForm), so you don't need to modify any test.
>
> I believe you can test this by creating a dependency for a COPY
> (rebuild) archive. In the best case, I think ReferenceChoice() is
> doing nothing and could be replaced with a simple Reference() mainly
> for simplicity.

Well, if you want to check the value against a vocabulary, you need
REferenceChoice, if a simple interface check is sufficent, Reference is the
thing to use.

--
Francis J. Lacoste
<email address hidden>

Revision history for this message
Francis J. Lacoste (flacoste) wrote :

On August 7, 2009, Celso Providelo wrote:
> > class IArchiveDependency(Interface):
> > """ArchiveDependency interface."""
> > + export_as_webservice_entry()
> >
> > id = Int(title=_("The archive ID."), readonly=True)
> >
> > - date_created = Datetime(
> > - title=_("Instant when the dependency was created."),
> > - required=False, readonly=True)
> > -
> > - archive = Choice(
> > - title=_('Target archive'),
> > - required=True,
> > - vocabulary='PPA',
> > - description=_("The PPA affected by this dependecy."))
> > -
> > - dependency = Object(
> > - schema=IArchive,
> > - title=_("The archive set as a dependency."),
> > - required=False)
> > -
> > - pocket = Choice(
> > - title=_("Pocket"), required=True,
> > vocabulary=PackagePublishingPocket) + date_created = exported(
> > + Datetime(
> > + title=_("Instant when the dependency was created."),
> > + required=False, readonly=True))
> > +
> > + archive = exported(
> > + ReferenceChoice(
> > + title=_('Target archive'),
> > + required=True,
> > + vocabulary='PPA',
> > + schema=IArchive,
> > + description=_("The PPA affected by this dependecy.")))
> > +
>
> This was a previous design slip, not your fault, but you can probably fix
> it.
>
> Any archive purpose can contain dependencies, not only PPAs. The
> restriction can remain in the form interface
> (IArchiveEditDependenciesForm), so you don't need to modify any test.
>
> I believe you can test this by creating a dependency for a COPY
> (rebuild) archive. In the best case, I think ReferenceChoice() is
> doing nothing and could be replaced with a simple Reference() mainly
> for simplicity.
>
> > + dependency = exported(
> > + Reference(
> > + schema=IArchive,
> > + title=_("The archive set as a dependency."),
> > + required=False))
> > +
> > + pocket = exported(
> > + Choice(
> > + title=_("Pocket"), required=True,
> > + vocabulary=PackagePublishingPocket))
> >
> > component = Choice(
> > title=_("Component"), required=True, vocabulary='Component')

Another thing to watch out for here is that most of these attributes are
read/write. Is that really intented?

Shouldn't a IArchiveDependency object be a relatively immutable objects that
are created/destroyed, but not modified?

Does it make sense to change the dependency and archive attribute directly? If
not, you need to put readonly=True on those attributes.

--
Francis J. Lacoste
<email address hidden>

Revision history for this message
William Grant (wgrant) wrote :
Download full text (11.8 KiB)

On Fri, 2009-08-07 at 21:54 +0000, Celso Providelo wrote:
> [snip]
>
> Hi Willian,
>
> Thanks for working on this, You've done a fantastic job!
>
> Comments inline.

Thanks.

> > === modified file 'lib/canonical/launchpad/interfaces/_schema_circular_imports.py'
>
> Nice that you've found your way through the monkey-patches :)

Myes. A little messy.

> > === modified file 'lib/lp/soyuz/browser/archive.py'
> > [snip]
> > + @stepthrough('+dependency')
> > + def traverse_dependency(self, id):
> > + try:
> > + id = int(id)
> > + except ValueError:
> > + # Not a number.
> > + return None
> > +
> > + try:
> > + archive = getUtility(IArchiveSet).get(id)
> > + except SQLObjectNotFound:
> > + return None
> > +
> > + return self.context.getArchiveDependency(archive)
>
> Okay, there are few subtle details in this travesal that I'd like to
> clarify with you.
>
> 1. The protection against leaking dependencies of P3As exists because
> IArchive.getArchiveDependencies() requires launchpad.View.
>
> 2. By design we don't allow public PPAs to depend on P3As, so attempts
> to reach a P3A by querying its ID as a dependency of a public PPA will
> never be an option (not that it would expose much of the dependant P3A
> either, but ...)
>
> Can you summarize those aspects in the traversal docstring ?
>
> It is still possible, though, that a P3A B added as dependency of
> another P3A A isn't accessible to all people with launchpad.View on A.
> This situation would break most of the code in this area, including
> the page rendering ... We should file a bug about it.

I've addressed point 1, but omitted point 2 as discussed on IRC.

> [snip]
> > === modified file 'lib/lp/soyuz/interfaces/archivedependency.py'
> > [snip]
> > + archive = exported(
> > + ReferenceChoice(
> > + title=_('Target archive'),
> > + required=True,
> > + vocabulary='PPA',
> > + schema=IArchive,
> > + description=_("The PPA affected by this dependecy.")))
> > +
>
> This was a previous design slip, not your fault, but you can probably fix it.
>
> Any archive purpose can contain dependencies, not only PPAs. The
> restriction can remain in the form interface
> (IArchiveEditDependenciesForm), so you don't need to modify any test.
>
> I believe you can test this by creating a dependency for a COPY
> (rebuild) archive. In the best case, I think ReferenceChoice() is
> doing nothing and could be replaced with a simple Reference() mainly
> for simplicity.

I wondered about that, but decided to trust the old interface. I've
replaced it with just a Reference now.

> [snip]
> > === modified file 'lib/lp/soyuz/model/archivedependency.py'
> > --- lib/lp/soyuz/model/archivedependency.py 2009-06-25 04:06:00 +0000
> > +++ lib/lp/soyuz/model/archivedependency.py 2009-08-07 14:05:54 +0000
> > @@ -48,6 +48,14 @@
> > foreignKey='Component', dbName='component')
> >
> > @property
> > + def component_name(self):
> > + """See `IArchiveDependency`"""
> > + if self.component:
> > + return self.component.name
> ...

1=== modified file 'lib/lp/soyuz/browser/archive.py'
2--- lib/lp/soyuz/browser/archive.py 2009-08-04 03:55:04 +0000
3+++ lib/lp/soyuz/browser/archive.py 2009-08-07 22:51:18 +0000
4@@ -289,6 +289,12 @@
5
6 @stepthrough('+dependency')
7 def traverse_dependency(self, id):
8+ """Traverse to an archive dependency by archive ID.
9+
10+ We use IArchive.getArchiveDependency here, which is protected by
11+ launchpad.View, so you cannot get to a dependency of a private
12+ archive that you can't see.
13+ """
14 try:
15 id = int(id)
16 except ValueError:
17
18=== modified file 'lib/lp/soyuz/doc/archive.txt'
19--- lib/lp/soyuz/doc/archive.txt 2009-07-31 13:49:20 +0000
20+++ lib/lp/soyuz/doc/archive.txt 2009-08-07 13:30:27 +0000
21@@ -1143,6 +1143,11 @@
22 >>> print primary_dependency.title
23 Primary Archive for Ubuntu Linux - UPDATES (main, universe)
24
25+They also expose the name of the component directly, for use in the API.
26+
27+ >>> print primary_dependency.component_name
28+ universe
29+
30 See further implications of archive dependencies in
31 doc/archive-dependencies.txt.
32
33@@ -1170,6 +1175,11 @@
34 >>> print primary_component_dep.title
35 Primary Archive for Ubuntu Linux - SECURITY
36
37+In this case the component name is None.
38+
39+ >>> print primary_component_dep.component_name
40+ None
41+
42 However only PRIMARY archive dependencies support pockets other than
43 RELEASE or other components than 'main'.
44
45
46=== modified file 'lib/lp/soyuz/interfaces/archivedependency.py'
47--- lib/lp/soyuz/interfaces/archivedependency.py 2009-08-07 12:44:29 +0000
48+++ lib/lp/soyuz/interfaces/archivedependency.py 2009-08-07 23:22:53 +0000
49@@ -17,7 +17,7 @@
50 from canonical.launchpad import _
51 from lp.soyuz.interfaces.archive import IArchive
52 from lp.soyuz.interfaces.publishing import PackagePublishingPocket
53-from lazr.restful.fields import Reference, ReferenceChoice
54+from lazr.restful.fields import Reference
55 from lazr.restful.declarations import (
56 export_as_webservice_entry, exported)
57
58@@ -34,31 +34,30 @@
59 required=False, readonly=True))
60
61 archive = exported(
62- ReferenceChoice(
63+ Reference(
64+ schema=IArchive, required=True, readonly=True,
65 title=_('Target archive'),
66- required=True,
67- vocabulary='PPA',
68- schema=IArchive,
69- description=_("The PPA affected by this dependecy.")))
70+ description=_("The archive affected by this dependecy.")))
71
72 dependency = exported(
73 Reference(
74- schema=IArchive,
75- title=_("The archive set as a dependency."),
76- required=False))
77+ schema=IArchive, required=False, readonly=True,
78+ title=_("The archive set as a dependency.")))
79
80 pocket = exported(
81 Choice(
82- title=_("Pocket"), required=True,
83+ title=_("Pocket"), required=True, readonly=True,
84 vocabulary=PackagePublishingPocket))
85
86 component = Choice(
87- title=_("Component"), required=True, vocabulary='Component')
88+ title=_("Component"), required=True, readonly=True,
89+ vocabulary='Component')
90
91 # We don't want to export IComponent, so the name is exported specially.
92 component_name = exported(
93 TextLine(
94 title=_("Component name"),
95- required=True))
96+ required=True, readonly=True))
97
98- title = exported(TextLine(title=_("Archive dependency title.")))
99+ title = exported(
100+ TextLine(title=_("Archive dependency title."), readonly=True))
101
102=== modified file 'lib/lp/soyuz/stories/webservice/xx-archive.txt'
103--- lib/lp/soyuz/stories/webservice/xx-archive.txt 2009-08-07 11:16:45 +0000
104+++ lib/lp/soyuz/stories/webservice/xx-archive.txt 2009-08-07 23:20:11 +0000
105@@ -946,90 +946,9 @@
106 HTTP/1.1 404 Not Found
107 ...
108
109-== Security ==
110-
111-Any user can retrieve a public PPA's dependencies.
112-
113- >>> login('foo.bar@canonical.com')
114- >>> cprov_ppa_db.private = False
115- >>> logout()
116-
117- >>> print user_webservice.get(
118- ... '/~cprov/+archive/ppa/dependencies')
119- HTTP/1.1 200 Ok
120- ...
121-
122- >>> print user_webservice.get(
123- ... '/~cprov/+archive/ppa/+dependency/1')
124- HTTP/1.1 200 Ok
125- ...
126-
127-The dependencies of a private archive are private. Let's make
128-Celso's PPA private.
129-
130- >>> login('foo.bar@canonical.com')
131- >>> cprov_ppa_db.private = True
132- >>> logout()
133-
134-Now our unprivileged user can't get a list of the dependencies.
135-
136- >>> print user_webservice.get(
137- ... '/~cprov/+archive/ppa/dependencies')
138- HTTP/1.1 401 Unauthorized
139- ...
140- Unauthorized: (<Archive at ...>, 'dependencies', 'launchpad.View')
141- <BLANKLINE>
142-
143-Nor can said user craft a URL to a dependency.
144-
145- >>> print user_webservice.get(
146- ... '/~cprov/+archive/ppa/+dependency/1')
147- HTTP/1.1 401 Unauthorized
148- ...
149- Unauthorized: (<Archive at ...>, 'getArchiveDependency', 'launchpad.View')
150- <BLANKLINE>
151-
152-Celso can still see them if we grant private permissions, of course.
153-
154- >>> cprov_webservice = webservice_for_person(
155- ... cprov, permission=OAuthPermission.WRITE_PRIVATE)
156- >>> print cprov_webservice.get(
157- ... '/~cprov/+archive/ppa/dependencies')
158- HTTP/1.1 200 Ok
159- ...
160- >>> print cprov_webservice.get(
161- ... '/~cprov/+archive/ppa/+dependency/1')
162- HTTP/1.1 200 Ok
163- ...
164-
165- >>> login('foo.bar@canonical.com')
166- >>> cprov_ppa_db.private = False
167- >>> logout()
168-
169-But even he can't write to a dependency.
170-
171- >>> sabdfl_ppa = cprov_webservice.get(
172- ... '/~sabdfl/+archive/ppa').jsonBody()
173- >>> print cprov_webservice.patch(
174- ... '/~cprov/+archive/ppa/+dependency/1', 'application/json',
175- ... simplejson.dumps({'archive_link': sabdfl_ppa['self_link']}))
176- HTTP/1.1 ...
177- ...
178- ForbiddenAttribute: ('archive', <ArchiveDependency at ...>)
179- <BLANKLINE>
180-
181- >>> print cprov_webservice.patch(
182- ... '/~cprov/+archive/ppa/+dependency/1', 'application/json',
183- ... simplejson.dumps({'dependency_link': sabdfl_ppa['self_link']}))
184- HTTP/1.1 ...
185- ...
186- ForbiddenAttribute: ('dependency', <ArchiveDependency at ...>)
187- <BLANKLINE>
188-
189- >>> print cprov_webservice.patch(
190- ... '/~cprov/+archive/ppa/+dependency/1', 'application/json',
191- ... simplejson.dumps({'pocket': 'Security'}))
192- HTTP/1.1 ...
193- ...
194- ForbiddenAttribute: ('pocket', <ArchiveDependency at ...>)
195- <BLANKLINE>
196+And even if we ask for a non-integral archive ID.
197+
198+ >>> print webservice.get(
199+ ... '/~cprov/+archive/ppa/+dependency/foo')
200+ HTTP/1.1 404 Not Found
201+ ...
202
203=== added file 'lib/lp/soyuz/stories/webservice/xx-archivedependency.txt'
204--- lib/lp/soyuz/stories/webservice/xx-archivedependency.txt 1970-01-01 00:00:00 +0000
205+++ lib/lp/soyuz/stories/webservice/xx-archivedependency.txt 2009-08-07 23:26:06 +0000
206@@ -0,0 +1,114 @@
207+= Archive dependencies =
208+
209+`ArchiveDependency` records represent build-dependencies between
210+archives, and are exposed through the API.
211+
212+Most of the tests live in
213+lib/lp/soyuz/stories/webservice/xx-archive.txt.
214+
215+Firstly we need to set some things up: we need a PPA with a dependency.
216+We'll use Celso's PPA, and give it a custom dependency on the primary
217+archive.
218+
219+ >>> import simplejson
220+ >>> from zope.component import getUtility
221+ >>> from lp.registry.interfaces.person import IPersonSet
222+ >>> from lp.soyuz.interfaces.component import IComponentSet
223+ >>> from lp.soyuz.interfaces.publishing import PackagePublishingPocket
224+ >>> login('foo.bar@canonical.com')
225+ >>> cprov_db = getUtility(IPersonSet).getByName('cprov')
226+ >>> cprov_ppa_db = cprov_db.archive
227+ >>> dep = cprov_ppa_db.addArchiveDependency(
228+ ... cprov_ppa_db.distribution.main_archive,
229+ ... PackagePublishingPocket.RELEASE,
230+ ... component=getUtility(IComponentSet)['universe'])
231+ >>> logout()
232+
233+Any user can retrieve a public PPA's dependencies.
234+
235+ >>> login('foo.bar@canonical.com')
236+ >>> cprov_ppa_db.private = False
237+ >>> logout()
238+
239+ >>> print user_webservice.get(
240+ ... '/~cprov/+archive/ppa/dependencies')
241+ HTTP/1.1 200 Ok
242+ ...
243+
244+ >>> print user_webservice.get(
245+ ... '/~cprov/+archive/ppa/+dependency/1')
246+ HTTP/1.1 200 Ok
247+ ...
248+
249+The dependencies of a private archive are private. Let's make
250+Celso's PPA private.
251+
252+ >>> login('foo.bar@canonical.com')
253+ >>> cprov_ppa_db.private = True
254+ >>> cprov_ppa_db.buildd_secret = 'foobar'
255+ >>> logout()
256+
257+Now our unprivileged user can't get a list of the dependencies.
258+
259+ >>> print user_webservice.get(
260+ ... '/~cprov/+archive/ppa/dependencies')
261+ HTTP/1.1 401 Unauthorized
262+ ...
263+ Unauthorized: (<Archive at ...>, 'dependencies', 'launchpad.View')
264+ <BLANKLINE>
265+
266+Nor can said user craft a URL to a dependency.
267+
268+ >>> print user_webservice.get(
269+ ... '/~cprov/+archive/ppa/+dependency/1')
270+ HTTP/1.1 401 Unauthorized
271+ ...
272+ Unauthorized: (<Archive at ...>, 'getArchiveDependency', 'launchpad.View')
273+ <BLANKLINE>
274+
275+Celso can still see them if we grant private permissions, of course.
276+
277+ >>> from canonical.launchpad.testing.pages import webservice_for_person
278+ >>> from canonical.launchpad.webapp.interfaces import OAuthPermission
279+ >>> cprov_webservice = webservice_for_person(
280+ ... cprov_db, permission=OAuthPermission.WRITE_PRIVATE)
281+ >>> print cprov_webservice.get(
282+ ... '/~cprov/+archive/ppa/dependencies')
283+ HTTP/1.1 200 Ok
284+ ...
285+ >>> print cprov_webservice.get(
286+ ... '/~cprov/+archive/ppa/+dependency/1')
287+ HTTP/1.1 200 Ok
288+ ...
289+
290+ >>> login('foo.bar@canonical.com')
291+ >>> cprov_ppa_db.private = False
292+ >>> logout()
293+
294+But even he can't write to a dependency.
295+
296+ >>> sabdfl_ppa = cprov_webservice.get(
297+ ... '/~sabdfl/+archive/ppa').jsonBody()
298+ >>> print cprov_webservice.patch(
299+ ... '/~cprov/+archive/ppa/+dependency/1', 'application/json',
300+ ... simplejson.dumps({'archive_link': sabdfl_ppa['self_link']}))
301+ HTTP/1.1 400 Bad Request
302+ ...
303+ archive_link: You tried to modify a read-only attribute.
304+ <BLANKLINE>
305+
306+ >>> print cprov_webservice.patch(
307+ ... '/~cprov/+archive/ppa/+dependency/1', 'application/json',
308+ ... simplejson.dumps({'dependency_link': sabdfl_ppa['self_link']}))
309+ HTTP/1.1 400 Bad Request
310+ ...
311+ dependency_link: You tried to modify a read-only attribute.
312+ <BLANKLINE>
313+
314+ >>> print cprov_webservice.patch(
315+ ... '/~cprov/+archive/ppa/+dependency/1', 'application/json',
316+ ... simplejson.dumps({'pocket': 'Security'}))
317+ HTTP/1.1 400 Bad Request
318+ ...
319+ pocket: You tried to modify a read-only attribute.
320+ <BLANKLINE>
Revision history for this message
Celso Providelo (cprov) wrote :

Willian,

Thanks for addressing my comments. Not only a nice new feature but also existing code got improved.

r=me

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/interfaces/_schema_circular_imports.py'
2--- lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2009-07-17 00:26:05 +0000
3+++ lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2009-08-04 12:20:53 +0000
4@@ -52,6 +52,8 @@
5 IArchivePermission)
6 from lp.soyuz.interfaces.archivesubscriber import (
7 IArchiveSubscriber)
8+from lp.soyuz.interfaces.archivedependency import (
9+ IArchiveDependency)
10 from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries
11 from lp.soyuz.interfaces.publishing import (
12 IBinaryPackagePublishingHistory, ISecureBinaryPackagePublishingHistory,
13@@ -161,6 +163,7 @@
14
15 # IArchive apocalypse.
16 patch_reference_property(IArchive, 'distribution', IDistribution)
17+patch_collection_property(IArchive, 'dependencies', IArchiveDependency)
18 patch_collection_return_type(
19 IArchive, 'getPermissionsForPerson', IArchivePermission)
20 patch_collection_return_type(
21@@ -187,6 +190,9 @@
22 patch_plain_parameter_type(IArchive, 'syncSource', 'from_archive', IArchive)
23 patch_entry_return_type(IArchive, 'newSubscription', IArchiveSubscriber)
24 patch_plain_parameter_type(
25+ IArchive, 'getArchiveDependency', 'dependency', IArchive)
26+patch_entry_return_type(IArchive, 'getArchiveDependency', IArchiveDependency)
27+patch_plain_parameter_type(
28 IArchive, 'getPublishedSources', 'distroseries', IDistroSeries)
29 patch_collection_return_type(
30 IArchive, 'getPublishedSources', ISourcePackagePublishingHistory)
31
32=== modified file 'lib/lp/soyuz/browser/archive.py'
33--- lib/lp/soyuz/browser/archive.py 2009-07-30 01:24:18 +0000
34+++ lib/lp/soyuz/browser/archive.py 2009-08-04 03:55:04 +0000
35@@ -34,6 +34,8 @@
36 from zope.schema import Choice, List
37 from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
38
39+from sqlobject import SQLObjectNotFound
40+
41 from canonical.cachedproperty import cachedproperty
42 from canonical.launchpad import _
43 from lp.soyuz.browser.build import BuildRecordsView
44@@ -285,6 +287,20 @@
45 else:
46 return None
47
48+ @stepthrough('+dependency')
49+ def traverse_dependency(self, id):
50+ try:
51+ id = int(id)
52+ except ValueError:
53+ # Not a number.
54+ return None
55+
56+ try:
57+ archive = getUtility(IArchiveSet).get(id)
58+ except SQLObjectNotFound:
59+ return None
60+
61+ return self.context.getArchiveDependency(archive)
62
63 class ArchiveContextMenu(ContextMenu):
64 """Overview Menu for IArchive."""
65
66=== modified file 'lib/lp/soyuz/browser/configure.zcml'
67--- lib/lp/soyuz/browser/configure.zcml 2009-07-18 00:05:49 +0000
68+++ lib/lp/soyuz/browser/configure.zcml 2009-08-04 03:55:04 +0000
69@@ -834,4 +834,9 @@
70 path_expression="string:+upload/${id}"
71 attribute_to_parent="distroseries"
72 />
73+ <browser:url
74+ for="lp.soyuz.interfaces.archivedependency.IArchiveDependency"
75+ path_expression="string:+dependency/${dependency/id}"
76+ attribute_to_parent="archive"
77+ />
78 </configure>
79
80=== modified file 'lib/lp/soyuz/interfaces/archive.py'
81--- lib/lp/soyuz/interfaces/archive.py 2009-07-30 00:20:01 +0000
82+++ lib/lp/soyuz/interfaces/archive.py 2009-08-07 12:44:22 +0000
83@@ -55,8 +55,9 @@
84 REQUEST_USER, call_with, export_as_webservice_entry, exported,
85 export_read_operation, export_factory_operation, export_operation_as,
86 export_write_operation, operation_parameters,
87- operation_returns_collection_of, rename_parameters_as, webservice_error)
88-from lazr.restful.fields import Reference
89+ operation_returns_collection_of, operation_returns_entry,
90+ rename_parameters_as, webservice_error)
91+from lazr.restful.fields import CollectionField, Reference
92
93
94 class ArchiveDependencyError(Exception):
95@@ -184,10 +185,6 @@
96 signing_key = Object(
97 title=_('Repository sigining key.'), required=False, schema=IGPGKey)
98
99- dependencies = Attribute(
100- "Archive dependencies recorded for this archive and ordered by owner "
101- "displayname.")
102-
103 expanded_archive_dependencies = Attribute(
104 "The expanded list of archive dependencies. It includes the implicit "
105 "PRIMARY archive dependency for PPAs.")
106@@ -310,15 +307,6 @@
107 not found.
108 """
109
110- def getArchiveDependency(dependency):
111- """Return the `IArchiveDependency` object for the given dependency.
112-
113- :param dependency: is an `IArchive` object.
114-
115- :return: `IArchiveDependency` or None if a corresponding object
116- could not be found.
117- """
118-
119 def removeArchiveDependency(dependency):
120 """Remove the `IArchiveDependency` record for the given dependency.
121
122@@ -761,6 +749,12 @@
123 description=_("The password used by the builder to access the "
124 "archive."))
125
126+ dependencies = exported(
127+ CollectionField(
128+ title=_("Archive dependencies recorded for this archive."),
129+ value_type=Reference(schema=Interface), #Really IArchiveDependency
130+ readonly=True))
131+
132 description = exported(
133 Text(
134 title=_("Archive contents description"), required=False,
135@@ -920,6 +914,19 @@
136 given ids that belong in the archive.
137 """
138
139+ @operation_parameters(
140+ dependency=Reference(schema=Interface)) #Really IArchive. See below.
141+ @operation_returns_entry(schema=Interface) #Really IArchiveDependency.
142+ @export_read_operation()
143+ def getArchiveDependency(dependency):
144+ """Return the `IArchiveDependency` object for the given dependency.
145+
146+ :param dependency: is an `IArchive` object.
147+
148+ :return: `IArchiveDependency` or None if a corresponding object
149+ could not be found.
150+ """
151+
152
153 class IArchiveAppend(Interface):
154 """Archive interface for operations restricted by append privilege."""
155
156=== modified file 'lib/lp/soyuz/interfaces/archivedependency.py'
157--- lib/lp/soyuz/interfaces/archivedependency.py 2009-06-25 04:06:00 +0000
158+++ lib/lp/soyuz/interfaces/archivedependency.py 2009-08-07 12:44:29 +0000
159@@ -11,38 +11,54 @@
160 'IArchiveDependency',
161 ]
162
163-from zope.interface import Attribute, Interface
164-from zope.schema import Choice, Datetime, Int, Object
165+from zope.interface import Interface
166+from zope.schema import Choice, Datetime, Int, TextLine
167
168 from canonical.launchpad import _
169 from lp.soyuz.interfaces.archive import IArchive
170 from lp.soyuz.interfaces.publishing import PackagePublishingPocket
171+from lazr.restful.fields import Reference, ReferenceChoice
172+from lazr.restful.declarations import (
173+ export_as_webservice_entry, exported)
174
175
176 class IArchiveDependency(Interface):
177 """ArchiveDependency interface."""
178+ export_as_webservice_entry()
179
180 id = Int(title=_("The archive ID."), readonly=True)
181
182- date_created = Datetime(
183- title=_("Instant when the dependency was created."),
184- required=False, readonly=True)
185-
186- archive = Choice(
187- title=_('Target archive'),
188- required=True,
189- vocabulary='PPA',
190- description=_("The PPA affected by this dependecy."))
191-
192- dependency = Object(
193- schema=IArchive,
194- title=_("The archive set as a dependency."),
195- required=False)
196-
197- pocket = Choice(
198- title=_("Pocket"), required=True, vocabulary=PackagePublishingPocket)
199+ date_created = exported(
200+ Datetime(
201+ title=_("Instant when the dependency was created."),
202+ required=False, readonly=True))
203+
204+ archive = exported(
205+ ReferenceChoice(
206+ title=_('Target archive'),
207+ required=True,
208+ vocabulary='PPA',
209+ schema=IArchive,
210+ description=_("The PPA affected by this dependecy.")))
211+
212+ dependency = exported(
213+ Reference(
214+ schema=IArchive,
215+ title=_("The archive set as a dependency."),
216+ required=False))
217+
218+ pocket = exported(
219+ Choice(
220+ title=_("Pocket"), required=True,
221+ vocabulary=PackagePublishingPocket))
222
223 component = Choice(
224 title=_("Component"), required=True, vocabulary='Component')
225
226- title = Attribute("Archive dependency title.")
227+ # We don't want to export IComponent, so the name is exported specially.
228+ component_name = exported(
229+ TextLine(
230+ title=_("Component name"),
231+ required=True))
232+
233+ title = exported(TextLine(title=_("Archive dependency title.")))
234
235=== modified file 'lib/lp/soyuz/model/archivedependency.py'
236--- lib/lp/soyuz/model/archivedependency.py 2009-06-25 04:06:00 +0000
237+++ lib/lp/soyuz/model/archivedependency.py 2009-08-04 03:32:34 +0000
238@@ -48,6 +48,14 @@
239 foreignKey='Component', dbName='component')
240
241 @property
242+ def component_name(self):
243+ """See `IArchiveDependency`"""
244+ if self.component:
245+ return self.component.name
246+ else:
247+ return None
248+
249+ @property
250 def title(self):
251 """See `IArchiveDependency`."""
252 if self.dependency.is_ppa:
253
254=== modified file 'lib/lp/soyuz/stories/webservice/xx-archive.txt'
255--- lib/lp/soyuz/stories/webservice/xx-archive.txt 2009-07-18 11:51:59 +0000
256+++ lib/lp/soyuz/stories/webservice/xx-archive.txt 2009-08-07 11:16:45 +0000
257@@ -15,6 +15,7 @@
258
259 >>> from lazr.restful.testing.webservice import pprint_entry
260 >>> pprint_entry(cprov_archive)
261+ dependencies_collection_link: u'http://.../~cprov/+archive/ppa/dependencies'
262 description: u'packages to help my friends.'
263 displayname: u'PPA for Celso Providelo'
264 distribution_link: u'http://.../ubuntu'
265@@ -77,6 +78,7 @@
266 >>> main_archive = webservice.get(
267 ... ubuntutest['main_archive_link']).jsonBody()
268 >>> pprint_entry(main_archive)
269+ dependencies_collection_link: u'http://.../ubuntutest/+archive/primary/dependencies'
270 description: None
271 displayname: u'Primary Archive for Ubuntu Test'
272 distribution_link: u'http://.../ubuntutest'
273@@ -704,6 +706,7 @@
274 the IArchive context, in this case only Celso has it.
275
276 >>> pprint_entry(user_webservice.get("/~cprov/+archive/ppa").jsonBody())
277+ dependencies_collection_link: u'http://.../~cprov/+archive/ppa/dependencies'
278 description: u'tag:launchpad.net:2008:redacted'
279 displayname: u'PPA for Celso Providelo'
280 distribution_link: u'http://.../ubuntu'
281@@ -715,6 +718,7 @@
282 signing_key_fingerprint: u'tag:launchpad.net:2008:redacted'
283
284 >>> pprint_entry(cprov_webservice.get("/~cprov/+archive/ppa").jsonBody())
285+ dependencies_collection_link: u'http://.../~cprov/+archive/ppa/dependencies'
286 description: u'packages to help my friends.'
287 displayname: u'PPA for Celso Providelo'
288 distribution_link: u'http://.../ubuntu'
289@@ -878,3 +882,154 @@
290 ...
291 CannotCopy: private 1.1 in hoary
292 (same version already uploaded and waiting in ACCEPTED queue)
293+
294+
295+= Archive dependencies =
296+
297+Archives can specify dependencies on pockets and components of other
298+archives. Found at <dependentarchive.id>/+dependency/<dependencyarchive.id>,
299+these IArchiveDependency records can be retrieved through the API.
300+
301+First we'll add an explicit dependency on the primary archive to
302+cprov's PPA. We can't do this through the webservice yet.
303+
304+ >>> from lp.soyuz.interfaces.component import IComponentSet
305+ >>> from lp.soyuz.interfaces.publishing import PackagePublishingPocket
306+ >>> login('foo.bar@canonical.com')
307+ >>> cprov_ppa_db = getUtility(IPersonSet).getByName('cprov').archive
308+ >>> dep = cprov_ppa_db.addArchiveDependency(
309+ ... cprov_ppa_db.distribution.main_archive,
310+ ... PackagePublishingPocket.RELEASE,
311+ ... component=getUtility(IComponentSet)['universe'])
312+ >>> logout()
313+
314+We can then request that dependency, and see that we get all of its
315+attributes.
316+
317+ >>> cprov_main_dependency = webservice.named_get(
318+ ... '/~cprov/+archive/ppa', 'getArchiveDependency',
319+ ... dependency=ubuntu['main_archive_link']).jsonBody()
320+ >>> pprint_entry(cprov_main_dependency)
321+ archive_link: u'http://.../~cprov/+archive/ppa'
322+ component_name: u'universe'
323+ date_created: ...
324+ dependency_link: u'http://.../ubuntu/+archive/primary'
325+ pocket: u'Release'
326+ resource_type_link: u'http://.../#archive_dependency'
327+ self_link: u'http://.../~cprov/+archive/ppa/+dependency/1'
328+ title: u'Primary Archive for Ubuntu Linux - RELEASE (main, universe)'
329+
330+Asking for an archive on which there is no dependency returns None.
331+
332+ >>> debian = webservice.get('/debian').jsonBody()
333+ >>> webservice.named_get(
334+ ... '/~cprov/+archive/ppa', 'getArchiveDependency',
335+ ... dependency=debian['main_archive_link']).jsonBody()
336+
337+Archives will also give us a list of their custom dependencies.
338+
339+ >>> print_self_link_of_entries(
340+ ... webservice.get('/~cprov/+archive/ppa/dependencies').jsonBody())
341+ http://.../~cprov/+archive/ppa/+dependency/1
342+
343+Crafting a URL to a non-dependency 404s:
344+
345+ >>> print webservice.get(
346+ ... '/~cprov/+archive/ppa/+dependency/2')
347+ HTTP/1.1 404 Not Found
348+ ...
349+
350+A 404 also occurs if we ask for an archive that doesn't exist.
351+
352+ >>> print webservice.get(
353+ ... '/~cprov/+archive/ppa/+dependency/123456')
354+ HTTP/1.1 404 Not Found
355+ ...
356+
357+== Security ==
358+
359+Any user can retrieve a public PPA's dependencies.
360+
361+ >>> login('foo.bar@canonical.com')
362+ >>> cprov_ppa_db.private = False
363+ >>> logout()
364+
365+ >>> print user_webservice.get(
366+ ... '/~cprov/+archive/ppa/dependencies')
367+ HTTP/1.1 200 Ok
368+ ...
369+
370+ >>> print user_webservice.get(
371+ ... '/~cprov/+archive/ppa/+dependency/1')
372+ HTTP/1.1 200 Ok
373+ ...
374+
375+The dependencies of a private archive are private. Let's make
376+Celso's PPA private.
377+
378+ >>> login('foo.bar@canonical.com')
379+ >>> cprov_ppa_db.private = True
380+ >>> logout()
381+
382+Now our unprivileged user can't get a list of the dependencies.
383+
384+ >>> print user_webservice.get(
385+ ... '/~cprov/+archive/ppa/dependencies')
386+ HTTP/1.1 401 Unauthorized
387+ ...
388+ Unauthorized: (<Archive at ...>, 'dependencies', 'launchpad.View')
389+ <BLANKLINE>
390+
391+Nor can said user craft a URL to a dependency.
392+
393+ >>> print user_webservice.get(
394+ ... '/~cprov/+archive/ppa/+dependency/1')
395+ HTTP/1.1 401 Unauthorized
396+ ...
397+ Unauthorized: (<Archive at ...>, 'getArchiveDependency', 'launchpad.View')
398+ <BLANKLINE>
399+
400+Celso can still see them if we grant private permissions, of course.
401+
402+ >>> cprov_webservice = webservice_for_person(
403+ ... cprov, permission=OAuthPermission.WRITE_PRIVATE)
404+ >>> print cprov_webservice.get(
405+ ... '/~cprov/+archive/ppa/dependencies')
406+ HTTP/1.1 200 Ok
407+ ...
408+ >>> print cprov_webservice.get(
409+ ... '/~cprov/+archive/ppa/+dependency/1')
410+ HTTP/1.1 200 Ok
411+ ...
412+
413+ >>> login('foo.bar@canonical.com')
414+ >>> cprov_ppa_db.private = False
415+ >>> logout()
416+
417+But even he can't write to a dependency.
418+
419+ >>> sabdfl_ppa = cprov_webservice.get(
420+ ... '/~sabdfl/+archive/ppa').jsonBody()
421+ >>> print cprov_webservice.patch(
422+ ... '/~cprov/+archive/ppa/+dependency/1', 'application/json',
423+ ... simplejson.dumps({'archive_link': sabdfl_ppa['self_link']}))
424+ HTTP/1.1 ...
425+ ...
426+ ForbiddenAttribute: ('archive', <ArchiveDependency at ...>)
427+ <BLANKLINE>
428+
429+ >>> print cprov_webservice.patch(
430+ ... '/~cprov/+archive/ppa/+dependency/1', 'application/json',
431+ ... simplejson.dumps({'dependency_link': sabdfl_ppa['self_link']}))
432+ HTTP/1.1 ...
433+ ...
434+ ForbiddenAttribute: ('dependency', <ArchiveDependency at ...>)
435+ <BLANKLINE>
436+
437+ >>> print cprov_webservice.patch(
438+ ... '/~cprov/+archive/ppa/+dependency/1', 'application/json',
439+ ... simplejson.dumps({'pocket': 'Security'}))
440+ HTTP/1.1 ...
441+ ...
442+ ForbiddenAttribute: ('pocket', <ArchiveDependency at ...>)
443+ <BLANKLINE>