Merge lp:~wgrant/launchpad/export-archive-dependencies into lp:launchpad
- export-archive-dependencies
- Merge into devel
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 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Celso Providelo (community) | code | Approve | |
Review via email: mp+9823@code.launchpad.net |
Commit message
Description of the change
William Grant (wgrant) wrote : | # |
Celso Providelo (cprov) wrote : | # |
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>
> I also moved IArchive.
> to IArchiveView and exported them. This required some interface patches in
> c.l.i._
>
> IComponent isn't exposed, so IArchiveDepende
> exposed directly. I followed the example of similar interfaces, instead
> creating and exporting IArchiveDepende
>
> 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/
Nice that you've found your way through the monkey-patches :)
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
> @@ -34,6 +34,8 @@
> from zope.schema import Choice, List
> from zope.schema.
>
> +from sqlobject import SQLObjectNotFound
> +
> from canonical.
> from canonical.launchpad import _
> from lp.soyuz.
> @@ -285,6 +287,20 @@
> else:
> return None
>
> + @stepthrough(
> + def traverse_
> + try:
> + id = int(id)
> + except ValueError:
> + # Not a number.
> + return None
> +
> + try:
> + archive = getUtility(
> + except SQLObjectNotFound:
> + return None
> +
> + return self.context.
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.
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 ArchiveContextM
Celso Providelo (cprov) : | # |
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
> (IArchiveEditDe
>
> 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>
Francis J. Lacoste (flacoste) wrote : | # |
On August 7, 2009, Celso Providelo wrote:
> > class IArchiveDepende
> > """ArchiveDepen
> > + export_
> >
> > 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=
> > + 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
> (IArchiveEditDe
>
> 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=
> >
> > component = Choice(
> > title=_
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>
William Grant (wgrant) wrote : | # |
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/
>
> Nice that you've found your way through the monkey-patches :)
Myes. A little messy.
> > === modified file 'lib/lp/
> > [snip]
> > + @stepthrough(
> > + def traverse_
> > + try:
> > + id = int(id)
> > + except ValueError:
> > + # Not a number.
> > + return None
> > +
> > + try:
> > + archive = getUtility(
> > + except SQLObjectNotFound:
> > + return None
> > +
> > + return self.context.
>
> 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.
>
> 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/
> > [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
> (IArchiveEditDe
>
> 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/
> > --- lib/lp/
> > +++ lib/lp/
> > @@ -48,6 +48,14 @@
> > foreignKey=
> >
> > @property
> > + def component_
> > + """See `IArchiveDepend
> > + 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> |
Celso Providelo (cprov) wrote : | # |
Willian,
Thanks for addressing my comments. Not only a nice new feature but also existing code got improved.
r=me
Preview Diff
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> |
= 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 /+dependency/ <dependency ID>). To get hold of the dependencies, {dependencies, getArchiveDepen dency} from IArchivePublic schema_ circular_ imports.
(<dependent>
I also moved IArchive.
to IArchiveView and exported them. This required some interface patches in
c.l.i._
IComponent isn't exposed, so IArchiveDepende ncy.component could not be ncy.component_ name.
exposed directly. I followed the example of similar interfaces, instead
creating and exporting IArchiveDepende
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 testing. layers. PageTestLayer tests: testing. layers. BaseLayer in 0.024 seconds. testing. layers. DatabaseLayer in 0.521 seconds. testing. layers. LibrarianLayer in 8.932 seconds. testing. layers. LaunchpadLayer in 0.000 seconds. testing. layers. FunctionalLayer in 4.414 seconds. testing. layers. GoogleServiceLa yer in 1.358 seconds. testing. layers. LaunchpadFuncti onalLayer in 0.000 seconds. testing. layers. PageTestLayer in 0.000 seconds. soyuz/tests/ ../stories/ webservice/ xx-archive. txt testing. layers. PageTestLayer in 0.000 seconds. testing. layers. LaunchpadFuncti onalLayer in 0.000 seconds. testing. layers. LaunchpadLayer in 0.000 seconds. testing. layers. LibrarianLayer in 0.312 seconds. testing. layers. GoogleServiceLa yer in 0.051 seconds. testing. layers. FunctionalLayer ... not supported testing. layers. DatabaseLayer in 0.020 seconds. testing. layers. BaseLayer in 0.000 seconds.
Running tests at level 1
Running canonical.
Set up canonical.
Set up canonical.
Set up canonical.
Set up canonical.
Set up canonical.
Set up canonical.
Set up canonical.
Set up canonical.
Running:
lib/lp/
Ran 77 tests with 0 failures and 0 errors in 10.408 seconds.
Tearing down left over layers:
Tear down canonical.
Tear down canonical.
Tear down canonical.
Tear down canonical.
Tear down canonical.
Tear down canonical.
Tear down canonical.
Tear down canonical.
== lint ==
Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.
Linting changed files: /launchpad/ interfaces/ _schema_ circular_ imports. py soyuz/browser/ archive. py soyuz/browser/ configure. zcml soyuz/interface s/archive. py soyuz/interface s/archivedepend ency.py soyuz/model/ archivedependen cy.py soyuz/stories/ webservice/ xx-archive. txt
lib/canonical
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/