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
> ...

=== modified file 'lib/lp/soyuz/browser/archive.py'
--- lib/lp/soyuz/browser/archive.py 2009-08-04 03:55:04 +0000
+++ lib/lp/soyuz/browser/archive.py 2009-08-07 22:51:18 +0000
@@ -289,6 +289,12 @@
289289
290 @stepthrough('+dependency')290 @stepthrough('+dependency')
291 def traverse_dependency(self, id):291 def traverse_dependency(self, id):
292 """Traverse to an archive dependency by archive ID.
293
294 We use IArchive.getArchiveDependency here, which is protected by
295 launchpad.View, so you cannot get to a dependency of a private
296 archive that you can't see.
297 """
292 try:298 try:
293 id = int(id)299 id = int(id)
294 except ValueError:300 except ValueError:
295301
=== modified file 'lib/lp/soyuz/doc/archive.txt'
--- lib/lp/soyuz/doc/archive.txt 2009-07-31 13:49:20 +0000
+++ lib/lp/soyuz/doc/archive.txt 2009-08-07 13:30:27 +0000
@@ -1143,6 +1143,11 @@
1143 >>> print primary_dependency.title1143 >>> print primary_dependency.title
1144 Primary Archive for Ubuntu Linux - UPDATES (main, universe)1144 Primary Archive for Ubuntu Linux - UPDATES (main, universe)
11451145
1146They also expose the name of the component directly, for use in the API.
1147
1148 >>> print primary_dependency.component_name
1149 universe
1150
1146See further implications of archive dependencies in1151See further implications of archive dependencies in
1147doc/archive-dependencies.txt.1152doc/archive-dependencies.txt.
11481153
@@ -1170,6 +1175,11 @@
1170 >>> print primary_component_dep.title1175 >>> print primary_component_dep.title
1171 Primary Archive for Ubuntu Linux - SECURITY1176 Primary Archive for Ubuntu Linux - SECURITY
11721177
1178In this case the component name is None.
1179
1180 >>> print primary_component_dep.component_name
1181 None
1182
1173However only PRIMARY archive dependencies support pockets other than1183However only PRIMARY archive dependencies support pockets other than
1174RELEASE or other components than 'main'.1184RELEASE or other components than 'main'.
11751185
11761186
=== modified file 'lib/lp/soyuz/interfaces/archivedependency.py'
--- lib/lp/soyuz/interfaces/archivedependency.py 2009-08-07 12:44:29 +0000
+++ lib/lp/soyuz/interfaces/archivedependency.py 2009-08-07 23:22:53 +0000
@@ -17,7 +17,7 @@
17from canonical.launchpad import _17from canonical.launchpad import _
18from lp.soyuz.interfaces.archive import IArchive18from lp.soyuz.interfaces.archive import IArchive
19from lp.soyuz.interfaces.publishing import PackagePublishingPocket19from lp.soyuz.interfaces.publishing import PackagePublishingPocket
20from lazr.restful.fields import Reference, ReferenceChoice20from lazr.restful.fields import Reference
21from lazr.restful.declarations import (21from lazr.restful.declarations import (
22 export_as_webservice_entry, exported)22 export_as_webservice_entry, exported)
2323
@@ -34,31 +34,30 @@
34 required=False, readonly=True))34 required=False, readonly=True))
3535
36 archive = exported(36 archive = exported(
37 ReferenceChoice(37 Reference(
38 schema=IArchive, required=True, readonly=True,
38 title=_('Target archive'),39 title=_('Target archive'),
39 required=True,40 description=_("The archive affected by this dependecy.")))
40 vocabulary='PPA',
41 schema=IArchive,
42 description=_("The PPA affected by this dependecy.")))
4341
44 dependency = exported(42 dependency = exported(
45 Reference(43 Reference(
46 schema=IArchive,44 schema=IArchive, required=False, readonly=True,
47 title=_("The archive set as a dependency."),45 title=_("The archive set as a dependency.")))
48 required=False))
4946
50 pocket = exported(47 pocket = exported(
51 Choice(48 Choice(
52 title=_("Pocket"), required=True,49 title=_("Pocket"), required=True, readonly=True,
53 vocabulary=PackagePublishingPocket))50 vocabulary=PackagePublishingPocket))
5451
55 component = Choice(52 component = Choice(
56 title=_("Component"), required=True, vocabulary='Component')53 title=_("Component"), required=True, readonly=True,
54 vocabulary='Component')
5755
58 # We don't want to export IComponent, so the name is exported specially.56 # We don't want to export IComponent, so the name is exported specially.
59 component_name = exported(57 component_name = exported(
60 TextLine(58 TextLine(
61 title=_("Component name"),59 title=_("Component name"),
62 required=True))60 required=True, readonly=True))
6361
64 title = exported(TextLine(title=_("Archive dependency title.")))62 title = exported(
63 TextLine(title=_("Archive dependency title."), readonly=True))
6564
=== modified file 'lib/lp/soyuz/stories/webservice/xx-archive.txt'
--- lib/lp/soyuz/stories/webservice/xx-archive.txt 2009-08-07 11:16:45 +0000
+++ lib/lp/soyuz/stories/webservice/xx-archive.txt 2009-08-07 23:20:11 +0000
@@ -946,90 +946,9 @@
946 HTTP/1.1 404 Not Found946 HTTP/1.1 404 Not Found
947 ...947 ...
948948
949== Security ==949And even if we ask for a non-integral archive ID.
950950
951Any user can retrieve a public PPA's dependencies.951 >>> print webservice.get(
952952 ... '/~cprov/+archive/ppa/+dependency/foo')
953 >>> login('foo.bar@canonical.com')953 HTTP/1.1 404 Not Found
954 >>> cprov_ppa_db.private = False954 ...
955 >>> logout()
956
957 >>> print user_webservice.get(
958 ... '/~cprov/+archive/ppa/dependencies')
959 HTTP/1.1 200 Ok
960 ...
961
962 >>> print user_webservice.get(
963 ... '/~cprov/+archive/ppa/+dependency/1')
964 HTTP/1.1 200 Ok
965 ...
966
967The dependencies of a private archive are private. Let's make
968Celso's PPA private.
969
970 >>> login('foo.bar@canonical.com')
971 >>> cprov_ppa_db.private = True
972 >>> logout()
973
974Now our unprivileged user can't get a list of the dependencies.
975
976 >>> print user_webservice.get(
977 ... '/~cprov/+archive/ppa/dependencies')
978 HTTP/1.1 401 Unauthorized
979 ...
980 Unauthorized: (<Archive at ...>, 'dependencies', 'launchpad.View')
981 <BLANKLINE>
982
983Nor can said user craft a URL to a dependency.
984
985 >>> print user_webservice.get(
986 ... '/~cprov/+archive/ppa/+dependency/1')
987 HTTP/1.1 401 Unauthorized
988 ...
989 Unauthorized: (<Archive at ...>, 'getArchiveDependency', 'launchpad.View')
990 <BLANKLINE>
991
992Celso can still see them if we grant private permissions, of course.
993
994 >>> cprov_webservice = webservice_for_person(
995 ... cprov, permission=OAuthPermission.WRITE_PRIVATE)
996 >>> print cprov_webservice.get(
997 ... '/~cprov/+archive/ppa/dependencies')
998 HTTP/1.1 200 Ok
999 ...
1000 >>> print cprov_webservice.get(
1001 ... '/~cprov/+archive/ppa/+dependency/1')
1002 HTTP/1.1 200 Ok
1003 ...
1004
1005 >>> login('foo.bar@canonical.com')
1006 >>> cprov_ppa_db.private = False
1007 >>> logout()
1008
1009But even he can't write to a dependency.
1010
1011 >>> sabdfl_ppa = cprov_webservice.get(
1012 ... '/~sabdfl/+archive/ppa').jsonBody()
1013 >>> print cprov_webservice.patch(
1014 ... '/~cprov/+archive/ppa/+dependency/1', 'application/json',
1015 ... simplejson.dumps({'archive_link': sabdfl_ppa['self_link']}))
1016 HTTP/1.1 ...
1017 ...
1018 ForbiddenAttribute: ('archive', <ArchiveDependency at ...>)
1019 <BLANKLINE>
1020
1021 >>> print cprov_webservice.patch(
1022 ... '/~cprov/+archive/ppa/+dependency/1', 'application/json',
1023 ... simplejson.dumps({'dependency_link': sabdfl_ppa['self_link']}))
1024 HTTP/1.1 ...
1025 ...
1026 ForbiddenAttribute: ('dependency', <ArchiveDependency at ...>)
1027 <BLANKLINE>
1028
1029 >>> print cprov_webservice.patch(
1030 ... '/~cprov/+archive/ppa/+dependency/1', 'application/json',
1031 ... simplejson.dumps({'pocket': 'Security'}))
1032 HTTP/1.1 ...
1033 ...
1034 ForbiddenAttribute: ('pocket', <ArchiveDependency at ...>)
1035 <BLANKLINE>
1036955
=== added file 'lib/lp/soyuz/stories/webservice/xx-archivedependency.txt'
--- lib/lp/soyuz/stories/webservice/xx-archivedependency.txt 1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/stories/webservice/xx-archivedependency.txt 2009-08-07 23:26:06 +0000
@@ -0,0 +1,114 @@
1= Archive dependencies =
2
3`ArchiveDependency` records represent build-dependencies between
4archives, and are exposed through the API.
5
6Most of the tests live in
7lib/lp/soyuz/stories/webservice/xx-archive.txt.
8
9Firstly we need to set some things up: we need a PPA with a dependency.
10We'll use Celso's PPA, and give it a custom dependency on the primary
11archive.
12
13 >>> import simplejson
14 >>> from zope.component import getUtility
15 >>> from lp.registry.interfaces.person import IPersonSet
16 >>> from lp.soyuz.interfaces.component import IComponentSet
17 >>> from lp.soyuz.interfaces.publishing import PackagePublishingPocket
18 >>> login('foo.bar@canonical.com')
19 >>> cprov_db = getUtility(IPersonSet).getByName('cprov')
20 >>> cprov_ppa_db = cprov_db.archive
21 >>> dep = cprov_ppa_db.addArchiveDependency(
22 ... cprov_ppa_db.distribution.main_archive,
23 ... PackagePublishingPocket.RELEASE,
24 ... component=getUtility(IComponentSet)['universe'])
25 >>> logout()
26
27Any user can retrieve a public PPA's dependencies.
28
29 >>> login('foo.bar@canonical.com')
30 >>> cprov_ppa_db.private = False
31 >>> logout()
32
33 >>> print user_webservice.get(
34 ... '/~cprov/+archive/ppa/dependencies')
35 HTTP/1.1 200 Ok
36 ...
37
38 >>> print user_webservice.get(
39 ... '/~cprov/+archive/ppa/+dependency/1')
40 HTTP/1.1 200 Ok
41 ...
42
43The dependencies of a private archive are private. Let's make
44Celso's PPA private.
45
46 >>> login('foo.bar@canonical.com')
47 >>> cprov_ppa_db.private = True
48 >>> cprov_ppa_db.buildd_secret = 'foobar'
49 >>> logout()
50
51Now our unprivileged user can't get a list of the dependencies.
52
53 >>> print user_webservice.get(
54 ... '/~cprov/+archive/ppa/dependencies')
55 HTTP/1.1 401 Unauthorized
56 ...
57 Unauthorized: (<Archive at ...>, 'dependencies', 'launchpad.View')
58 <BLANKLINE>
59
60Nor can said user craft a URL to a dependency.
61
62 >>> print user_webservice.get(
63 ... '/~cprov/+archive/ppa/+dependency/1')
64 HTTP/1.1 401 Unauthorized
65 ...
66 Unauthorized: (<Archive at ...>, 'getArchiveDependency', 'launchpad.View')
67 <BLANKLINE>
68
69Celso can still see them if we grant private permissions, of course.
70
71 >>> from canonical.launchpad.testing.pages import webservice_for_person
72 >>> from canonical.launchpad.webapp.interfaces import OAuthPermission
73 >>> cprov_webservice = webservice_for_person(
74 ... cprov_db, permission=OAuthPermission.WRITE_PRIVATE)
75 >>> print cprov_webservice.get(
76 ... '/~cprov/+archive/ppa/dependencies')
77 HTTP/1.1 200 Ok
78 ...
79 >>> print cprov_webservice.get(
80 ... '/~cprov/+archive/ppa/+dependency/1')
81 HTTP/1.1 200 Ok
82 ...
83
84 >>> login('foo.bar@canonical.com')
85 >>> cprov_ppa_db.private = False
86 >>> logout()
87
88But even he can't write to a dependency.
89
90 >>> sabdfl_ppa = cprov_webservice.get(
91 ... '/~sabdfl/+archive/ppa').jsonBody()
92 >>> print cprov_webservice.patch(
93 ... '/~cprov/+archive/ppa/+dependency/1', 'application/json',
94 ... simplejson.dumps({'archive_link': sabdfl_ppa['self_link']}))
95 HTTP/1.1 400 Bad Request
96 ...
97 archive_link: You tried to modify a read-only attribute.
98 <BLANKLINE>
99
100 >>> print cprov_webservice.patch(
101 ... '/~cprov/+archive/ppa/+dependency/1', 'application/json',
102 ... simplejson.dumps({'dependency_link': sabdfl_ppa['self_link']}))
103 HTTP/1.1 400 Bad Request
104 ...
105 dependency_link: You tried to modify a read-only attribute.
106 <BLANKLINE>
107
108 >>> print cprov_webservice.patch(
109 ... '/~cprov/+archive/ppa/+dependency/1', 'application/json',
110 ... simplejson.dumps({'pocket': 'Security'}))
111 HTTP/1.1 400 Bad Request
112 ...
113 pocket: You tried to modify a read-only attribute.
114 <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
=== modified file 'lib/canonical/launchpad/interfaces/_schema_circular_imports.py'
--- lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2009-07-17 00:26:05 +0000
+++ lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2009-08-04 12:20:53 +0000
@@ -52,6 +52,8 @@
52 IArchivePermission)52 IArchivePermission)
53from lp.soyuz.interfaces.archivesubscriber import (53from lp.soyuz.interfaces.archivesubscriber import (
54 IArchiveSubscriber)54 IArchiveSubscriber)
55from lp.soyuz.interfaces.archivedependency import (
56 IArchiveDependency)
55from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries57from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries
56from lp.soyuz.interfaces.publishing import (58from lp.soyuz.interfaces.publishing import (
57 IBinaryPackagePublishingHistory, ISecureBinaryPackagePublishingHistory,59 IBinaryPackagePublishingHistory, ISecureBinaryPackagePublishingHistory,
@@ -161,6 +163,7 @@
161163
162# IArchive apocalypse.164# IArchive apocalypse.
163patch_reference_property(IArchive, 'distribution', IDistribution)165patch_reference_property(IArchive, 'distribution', IDistribution)
166patch_collection_property(IArchive, 'dependencies', IArchiveDependency)
164patch_collection_return_type(167patch_collection_return_type(
165 IArchive, 'getPermissionsForPerson', IArchivePermission)168 IArchive, 'getPermissionsForPerson', IArchivePermission)
166patch_collection_return_type(169patch_collection_return_type(
@@ -187,6 +190,9 @@
187patch_plain_parameter_type(IArchive, 'syncSource', 'from_archive', IArchive)190patch_plain_parameter_type(IArchive, 'syncSource', 'from_archive', IArchive)
188patch_entry_return_type(IArchive, 'newSubscription', IArchiveSubscriber)191patch_entry_return_type(IArchive, 'newSubscription', IArchiveSubscriber)
189patch_plain_parameter_type(192patch_plain_parameter_type(
193 IArchive, 'getArchiveDependency', 'dependency', IArchive)
194patch_entry_return_type(IArchive, 'getArchiveDependency', IArchiveDependency)
195patch_plain_parameter_type(
190 IArchive, 'getPublishedSources', 'distroseries', IDistroSeries)196 IArchive, 'getPublishedSources', 'distroseries', IDistroSeries)
191patch_collection_return_type(197patch_collection_return_type(
192 IArchive, 'getPublishedSources', ISourcePackagePublishingHistory)198 IArchive, 'getPublishedSources', ISourcePackagePublishingHistory)
193199
=== modified file 'lib/lp/soyuz/browser/archive.py'
--- lib/lp/soyuz/browser/archive.py 2009-07-30 01:24:18 +0000
+++ lib/lp/soyuz/browser/archive.py 2009-08-04 03:55:04 +0000
@@ -34,6 +34,8 @@
34from zope.schema import Choice, List34from zope.schema import Choice, List
35from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm35from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
3636
37from sqlobject import SQLObjectNotFound
38
37from canonical.cachedproperty import cachedproperty39from canonical.cachedproperty import cachedproperty
38from canonical.launchpad import _40from canonical.launchpad import _
39from lp.soyuz.browser.build import BuildRecordsView41from lp.soyuz.browser.build import BuildRecordsView
@@ -285,6 +287,20 @@
285 else:287 else:
286 return None288 return None
287289
290 @stepthrough('+dependency')
291 def traverse_dependency(self, id):
292 try:
293 id = int(id)
294 except ValueError:
295 # Not a number.
296 return None
297
298 try:
299 archive = getUtility(IArchiveSet).get(id)
300 except SQLObjectNotFound:
301 return None
302
303 return self.context.getArchiveDependency(archive)
288304
289class ArchiveContextMenu(ContextMenu):305class ArchiveContextMenu(ContextMenu):
290 """Overview Menu for IArchive."""306 """Overview Menu for IArchive."""
291307
=== modified file 'lib/lp/soyuz/browser/configure.zcml'
--- lib/lp/soyuz/browser/configure.zcml 2009-07-18 00:05:49 +0000
+++ lib/lp/soyuz/browser/configure.zcml 2009-08-04 03:55:04 +0000
@@ -834,4 +834,9 @@
834 path_expression="string:+upload/${id}"834 path_expression="string:+upload/${id}"
835 attribute_to_parent="distroseries"835 attribute_to_parent="distroseries"
836 />836 />
837 <browser:url
838 for="lp.soyuz.interfaces.archivedependency.IArchiveDependency"
839 path_expression="string:+dependency/${dependency/id}"
840 attribute_to_parent="archive"
841 />
837</configure>842</configure>
838843
=== modified file 'lib/lp/soyuz/interfaces/archive.py'
--- lib/lp/soyuz/interfaces/archive.py 2009-07-30 00:20:01 +0000
+++ lib/lp/soyuz/interfaces/archive.py 2009-08-07 12:44:22 +0000
@@ -55,8 +55,9 @@
55 REQUEST_USER, call_with, export_as_webservice_entry, exported,55 REQUEST_USER, call_with, export_as_webservice_entry, exported,
56 export_read_operation, export_factory_operation, export_operation_as,56 export_read_operation, export_factory_operation, export_operation_as,
57 export_write_operation, operation_parameters,57 export_write_operation, operation_parameters,
58 operation_returns_collection_of, rename_parameters_as, webservice_error)58 operation_returns_collection_of, operation_returns_entry,
59from lazr.restful.fields import Reference59 rename_parameters_as, webservice_error)
60from lazr.restful.fields import CollectionField, Reference
6061
6162
62class ArchiveDependencyError(Exception):63class ArchiveDependencyError(Exception):
@@ -184,10 +185,6 @@
184 signing_key = Object(185 signing_key = Object(
185 title=_('Repository sigining key.'), required=False, schema=IGPGKey)186 title=_('Repository sigining key.'), required=False, schema=IGPGKey)
186187
187 dependencies = Attribute(
188 "Archive dependencies recorded for this archive and ordered by owner "
189 "displayname.")
190
191 expanded_archive_dependencies = Attribute(188 expanded_archive_dependencies = Attribute(
192 "The expanded list of archive dependencies. It includes the implicit "189 "The expanded list of archive dependencies. It includes the implicit "
193 "PRIMARY archive dependency for PPAs.")190 "PRIMARY archive dependency for PPAs.")
@@ -310,15 +307,6 @@
310 not found.307 not found.
311 """308 """
312309
313 def getArchiveDependency(dependency):
314 """Return the `IArchiveDependency` object for the given dependency.
315
316 :param dependency: is an `IArchive` object.
317
318 :return: `IArchiveDependency` or None if a corresponding object
319 could not be found.
320 """
321
322 def removeArchiveDependency(dependency):310 def removeArchiveDependency(dependency):
323 """Remove the `IArchiveDependency` record for the given dependency.311 """Remove the `IArchiveDependency` record for the given dependency.
324312
@@ -761,6 +749,12 @@
761 description=_("The password used by the builder to access the "749 description=_("The password used by the builder to access the "
762 "archive."))750 "archive."))
763751
752 dependencies = exported(
753 CollectionField(
754 title=_("Archive dependencies recorded for this archive."),
755 value_type=Reference(schema=Interface), #Really IArchiveDependency
756 readonly=True))
757
764 description = exported(758 description = exported(
765 Text(759 Text(
766 title=_("Archive contents description"), required=False,760 title=_("Archive contents description"), required=False,
@@ -920,6 +914,19 @@
920 given ids that belong in the archive.914 given ids that belong in the archive.
921 """915 """
922916
917 @operation_parameters(
918 dependency=Reference(schema=Interface)) #Really IArchive. See below.
919 @operation_returns_entry(schema=Interface) #Really IArchiveDependency.
920 @export_read_operation()
921 def getArchiveDependency(dependency):
922 """Return the `IArchiveDependency` object for the given dependency.
923
924 :param dependency: is an `IArchive` object.
925
926 :return: `IArchiveDependency` or None if a corresponding object
927 could not be found.
928 """
929
923930
924class IArchiveAppend(Interface):931class IArchiveAppend(Interface):
925 """Archive interface for operations restricted by append privilege."""932 """Archive interface for operations restricted by append privilege."""
926933
=== modified file 'lib/lp/soyuz/interfaces/archivedependency.py'
--- lib/lp/soyuz/interfaces/archivedependency.py 2009-06-25 04:06:00 +0000
+++ lib/lp/soyuz/interfaces/archivedependency.py 2009-08-07 12:44:29 +0000
@@ -11,38 +11,54 @@
11 'IArchiveDependency',11 'IArchiveDependency',
12 ]12 ]
1313
14from zope.interface import Attribute, Interface14from zope.interface import Interface
15from zope.schema import Choice, Datetime, Int, Object15from zope.schema import Choice, Datetime, Int, TextLine
1616
17from canonical.launchpad import _17from canonical.launchpad import _
18from lp.soyuz.interfaces.archive import IArchive18from lp.soyuz.interfaces.archive import IArchive
19from lp.soyuz.interfaces.publishing import PackagePublishingPocket19from lp.soyuz.interfaces.publishing import PackagePublishingPocket
20from lazr.restful.fields import Reference, ReferenceChoice
21from lazr.restful.declarations import (
22 export_as_webservice_entry, exported)
2023
2124
22class IArchiveDependency(Interface):25class IArchiveDependency(Interface):
23 """ArchiveDependency interface."""26 """ArchiveDependency interface."""
27 export_as_webservice_entry()
2428
25 id = Int(title=_("The archive ID."), readonly=True)29 id = Int(title=_("The archive ID."), readonly=True)
2630
27 date_created = Datetime(31 date_created = exported(
28 title=_("Instant when the dependency was created."),32 Datetime(
29 required=False, readonly=True)33 title=_("Instant when the dependency was created."),
3034 required=False, readonly=True))
31 archive = Choice(35
32 title=_('Target archive'),36 archive = exported(
33 required=True,37 ReferenceChoice(
34 vocabulary='PPA',38 title=_('Target archive'),
35 description=_("The PPA affected by this dependecy."))39 required=True,
3640 vocabulary='PPA',
37 dependency = Object(41 schema=IArchive,
38 schema=IArchive,42 description=_("The PPA affected by this dependecy.")))
39 title=_("The archive set as a dependency."),43
40 required=False)44 dependency = exported(
4145 Reference(
42 pocket = Choice(46 schema=IArchive,
43 title=_("Pocket"), required=True, vocabulary=PackagePublishingPocket)47 title=_("The archive set as a dependency."),
48 required=False))
49
50 pocket = exported(
51 Choice(
52 title=_("Pocket"), required=True,
53 vocabulary=PackagePublishingPocket))
4454
45 component = Choice(55 component = Choice(
46 title=_("Component"), required=True, vocabulary='Component')56 title=_("Component"), required=True, vocabulary='Component')
4757
48 title = Attribute("Archive dependency title.")58 # We don't want to export IComponent, so the name is exported specially.
59 component_name = exported(
60 TextLine(
61 title=_("Component name"),
62 required=True))
63
64 title = exported(TextLine(title=_("Archive dependency title.")))
4965
=== 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-04 03:32:34 +0000
@@ -48,6 +48,14 @@
48 foreignKey='Component', dbName='component')48 foreignKey='Component', dbName='component')
4949
50 @property50 @property
51 def component_name(self):
52 """See `IArchiveDependency`"""
53 if self.component:
54 return self.component.name
55 else:
56 return None
57
58 @property
51 def title(self):59 def title(self):
52 """See `IArchiveDependency`."""60 """See `IArchiveDependency`."""
53 if self.dependency.is_ppa:61 if self.dependency.is_ppa:
5462
=== modified file 'lib/lp/soyuz/stories/webservice/xx-archive.txt'
--- lib/lp/soyuz/stories/webservice/xx-archive.txt 2009-07-18 11:51:59 +0000
+++ lib/lp/soyuz/stories/webservice/xx-archive.txt 2009-08-07 11:16:45 +0000
@@ -15,6 +15,7 @@
1515
16 >>> from lazr.restful.testing.webservice import pprint_entry16 >>> from lazr.restful.testing.webservice import pprint_entry
17 >>> pprint_entry(cprov_archive)17 >>> pprint_entry(cprov_archive)
18 dependencies_collection_link: u'http://.../~cprov/+archive/ppa/dependencies'
18 description: u'packages to help my friends.'19 description: u'packages to help my friends.'
19 displayname: u'PPA for Celso Providelo'20 displayname: u'PPA for Celso Providelo'
20 distribution_link: u'http://.../ubuntu'21 distribution_link: u'http://.../ubuntu'
@@ -77,6 +78,7 @@
77 >>> main_archive = webservice.get(78 >>> main_archive = webservice.get(
78 ... ubuntutest['main_archive_link']).jsonBody()79 ... ubuntutest['main_archive_link']).jsonBody()
79 >>> pprint_entry(main_archive)80 >>> pprint_entry(main_archive)
81 dependencies_collection_link: u'http://.../ubuntutest/+archive/primary/dependencies'
80 description: None82 description: None
81 displayname: u'Primary Archive for Ubuntu Test'83 displayname: u'Primary Archive for Ubuntu Test'
82 distribution_link: u'http://.../ubuntutest'84 distribution_link: u'http://.../ubuntutest'
@@ -704,6 +706,7 @@
704the IArchive context, in this case only Celso has it.706the IArchive context, in this case only Celso has it.
705707
706 >>> pprint_entry(user_webservice.get("/~cprov/+archive/ppa").jsonBody())708 >>> pprint_entry(user_webservice.get("/~cprov/+archive/ppa").jsonBody())
709 dependencies_collection_link: u'http://.../~cprov/+archive/ppa/dependencies'
707 description: u'tag:launchpad.net:2008:redacted'710 description: u'tag:launchpad.net:2008:redacted'
708 displayname: u'PPA for Celso Providelo'711 displayname: u'PPA for Celso Providelo'
709 distribution_link: u'http://.../ubuntu'712 distribution_link: u'http://.../ubuntu'
@@ -715,6 +718,7 @@
715 signing_key_fingerprint: u'tag:launchpad.net:2008:redacted'718 signing_key_fingerprint: u'tag:launchpad.net:2008:redacted'
716719
717 >>> pprint_entry(cprov_webservice.get("/~cprov/+archive/ppa").jsonBody())720 >>> pprint_entry(cprov_webservice.get("/~cprov/+archive/ppa").jsonBody())
721 dependencies_collection_link: u'http://.../~cprov/+archive/ppa/dependencies'
718 description: u'packages to help my friends.'722 description: u'packages to help my friends.'
719 displayname: u'PPA for Celso Providelo'723 displayname: u'PPA for Celso Providelo'
720 distribution_link: u'http://.../ubuntu'724 distribution_link: u'http://.../ubuntu'
@@ -878,3 +882,154 @@
878 ...882 ...
879 CannotCopy: private 1.1 in hoary883 CannotCopy: private 1.1 in hoary
880 (same version already uploaded and waiting in ACCEPTED queue)884 (same version already uploaded and waiting in ACCEPTED queue)
885
886
887= Archive dependencies =
888
889Archives can specify dependencies on pockets and components of other
890archives. Found at <dependentarchive.id>/+dependency/<dependencyarchive.id>,
891these IArchiveDependency records can be retrieved through the API.
892
893First we'll add an explicit dependency on the primary archive to
894cprov's PPA. We can't do this through the webservice yet.
895
896 >>> from lp.soyuz.interfaces.component import IComponentSet
897 >>> from lp.soyuz.interfaces.publishing import PackagePublishingPocket
898 >>> login('foo.bar@canonical.com')
899 >>> cprov_ppa_db = getUtility(IPersonSet).getByName('cprov').archive
900 >>> dep = cprov_ppa_db.addArchiveDependency(
901 ... cprov_ppa_db.distribution.main_archive,
902 ... PackagePublishingPocket.RELEASE,
903 ... component=getUtility(IComponentSet)['universe'])
904 >>> logout()
905
906We can then request that dependency, and see that we get all of its
907attributes.
908
909 >>> cprov_main_dependency = webservice.named_get(
910 ... '/~cprov/+archive/ppa', 'getArchiveDependency',
911 ... dependency=ubuntu['main_archive_link']).jsonBody()
912 >>> pprint_entry(cprov_main_dependency)
913 archive_link: u'http://.../~cprov/+archive/ppa'
914 component_name: u'universe'
915 date_created: ...
916 dependency_link: u'http://.../ubuntu/+archive/primary'
917 pocket: u'Release'
918 resource_type_link: u'http://.../#archive_dependency'
919 self_link: u'http://.../~cprov/+archive/ppa/+dependency/1'
920 title: u'Primary Archive for Ubuntu Linux - RELEASE (main, universe)'
921
922Asking for an archive on which there is no dependency returns None.
923
924 >>> debian = webservice.get('/debian').jsonBody()
925 >>> webservice.named_get(
926 ... '/~cprov/+archive/ppa', 'getArchiveDependency',
927 ... dependency=debian['main_archive_link']).jsonBody()
928
929Archives will also give us a list of their custom dependencies.
930
931 >>> print_self_link_of_entries(
932 ... webservice.get('/~cprov/+archive/ppa/dependencies').jsonBody())
933 http://.../~cprov/+archive/ppa/+dependency/1
934
935Crafting a URL to a non-dependency 404s:
936
937 >>> print webservice.get(
938 ... '/~cprov/+archive/ppa/+dependency/2')
939 HTTP/1.1 404 Not Found
940 ...
941
942A 404 also occurs if we ask for an archive that doesn't exist.
943
944 >>> print webservice.get(
945 ... '/~cprov/+archive/ppa/+dependency/123456')
946 HTTP/1.1 404 Not Found
947 ...
948
949== Security ==
950
951Any user can retrieve a public PPA's dependencies.
952
953 >>> login('foo.bar@canonical.com')
954 >>> cprov_ppa_db.private = False
955 >>> logout()
956
957 >>> print user_webservice.get(
958 ... '/~cprov/+archive/ppa/dependencies')
959 HTTP/1.1 200 Ok
960 ...
961
962 >>> print user_webservice.get(
963 ... '/~cprov/+archive/ppa/+dependency/1')
964 HTTP/1.1 200 Ok
965 ...
966
967The dependencies of a private archive are private. Let's make
968Celso's PPA private.
969
970 >>> login('foo.bar@canonical.com')
971 >>> cprov_ppa_db.private = True
972 >>> logout()
973
974Now our unprivileged user can't get a list of the dependencies.
975
976 >>> print user_webservice.get(
977 ... '/~cprov/+archive/ppa/dependencies')
978 HTTP/1.1 401 Unauthorized
979 ...
980 Unauthorized: (<Archive at ...>, 'dependencies', 'launchpad.View')
981 <BLANKLINE>
982
983Nor can said user craft a URL to a dependency.
984
985 >>> print user_webservice.get(
986 ... '/~cprov/+archive/ppa/+dependency/1')
987 HTTP/1.1 401 Unauthorized
988 ...
989 Unauthorized: (<Archive at ...>, 'getArchiveDependency', 'launchpad.View')
990 <BLANKLINE>
991
992Celso can still see them if we grant private permissions, of course.
993
994 >>> cprov_webservice = webservice_for_person(
995 ... cprov, permission=OAuthPermission.WRITE_PRIVATE)
996 >>> print cprov_webservice.get(
997 ... '/~cprov/+archive/ppa/dependencies')
998 HTTP/1.1 200 Ok
999 ...
1000 >>> print cprov_webservice.get(
1001 ... '/~cprov/+archive/ppa/+dependency/1')
1002 HTTP/1.1 200 Ok
1003 ...
1004
1005 >>> login('foo.bar@canonical.com')
1006 >>> cprov_ppa_db.private = False
1007 >>> logout()
1008
1009But even he can't write to a dependency.
1010
1011 >>> sabdfl_ppa = cprov_webservice.get(
1012 ... '/~sabdfl/+archive/ppa').jsonBody()
1013 >>> print cprov_webservice.patch(
1014 ... '/~cprov/+archive/ppa/+dependency/1', 'application/json',
1015 ... simplejson.dumps({'archive_link': sabdfl_ppa['self_link']}))
1016 HTTP/1.1 ...
1017 ...
1018 ForbiddenAttribute: ('archive', <ArchiveDependency at ...>)
1019 <BLANKLINE>
1020
1021 >>> print cprov_webservice.patch(
1022 ... '/~cprov/+archive/ppa/+dependency/1', 'application/json',
1023 ... simplejson.dumps({'dependency_link': sabdfl_ppa['self_link']}))
1024 HTTP/1.1 ...
1025 ...
1026 ForbiddenAttribute: ('dependency', <ArchiveDependency at ...>)
1027 <BLANKLINE>
1028
1029 >>> print cprov_webservice.patch(
1030 ... '/~cprov/+archive/ppa/+dependency/1', 'application/json',
1031 ... simplejson.dumps({'pocket': 'Security'}))
1032 HTTP/1.1 ...
1033 ...
1034 ForbiddenAttribute: ('pocket', <ArchiveDependency at ...>)
1035 <BLANKLINE>