Merge lp:~timrchavez/launchpad/set_ppa_private_from_api_724740-4 into lp:launchpad

Proposed by Timothy R. Chavez
Status: Merged
Approved by: j.c.sackett
Approved revision: no longer in the source branch.
Merged at revision: 13276
Proposed branch: lp:~timrchavez/launchpad/set_ppa_private_from_api_724740-4
Merge into: lp:launchpad
Diff against target: 182 lines (+35/-15)
6 files modified
lib/lp/archivepublisher/scripts/generate_ppa_htaccess.py (+1/-1)
lib/lp/buildmaster/model/buildfarmjob.py (+3/-3)
lib/lp/registry/model/distributionsourcepackage.py (+1/-1)
lib/lp/soyuz/browser/archive.py (+2/-3)
lib/lp/soyuz/model/archive.py (+20/-3)
lib/lp/soyuz/tests/test_archive_privacy.py (+8/-4)
To merge this branch: bzr merge lp:~timrchavez/launchpad/set_ppa_private_from_api_724740-4
Reviewer Review Type Date Requested Status
j.c.sackett (community) Approve
Review via email: mp+64840@code.launchpad.net

This proposal supersedes a proposal from 2011-06-09.

Commit message

[r=jcsackett][bug=724740] Provides facility to set a PPA private via the API

Description of the change

SUMMARY

As described by LP #724740, making a PPA private via the API is currently not possible.

PROPOSED FIX

Making a PPA private or public is not simply a boolean assignment. When a PPA is made private, a buildd_secret must be set and if it is made public again, the buildd_secret must be discarded. To avoid leakage / exposure of the buildd_secret it must be auto-generated internally rather than provided by the user via the API.

PRE-IMPLEMENTATION NOTES

The original plan that I discussed with Julian was to make the private attribute on IArchivePublic read-only, add a mutator, push all the _validate_archive_privacy logic into it and add the ability to generate and set the buildd_secret.

After speaking with Robert, I went with his Python properties suggestion so that both internal and external had a common interface for setting PPA privacy.

IMPLEMENTATION DETAILS

Because making a PPA private is more than just a boolean assignment, we use a Python setter to make the PPA private or public and to generate a buildd_secret as necessary.

Some things I learned from this approach:

Thinking that the 'updateContextFromData' method Jelmer introduced[1] to fix this problem for the UI was no longer necessary, I removed it, only to find that the view test for private set / buildd_secret unset had started failing.

It turns out that the 'buildd_secret' attribute was being set to None (the value passed via the form), after the 'private' attribute was being set (and we generated and set a new buildd_secret). The solution to this problem was to just remove the token creation bit from 'updateContextFromData', rather than the entire method.

Using a Python getter and setter for the private attribute broke Storm expressions. The "workaround" for this problem was to provide the Storm expressions with the underlying _private attribute that the private getter and setter access / manipulate.

TESTS

testr run -- -t test_archive_admin_view -t test_archive_privacy

I wrote an additional two tests in test_archive_privacy.py[2] to verify the buildd_secret is getting generated and set when a PPA is made private and that it is discarded if it the PPA is made public.

DEMO AND Q/A

To demo this you could write a small app which sets the 'private' attribute to True for a public PPA. Without this change, after calling lp_save() the value will revert back to False. With this change it should remain True. For example,

from launchpadlib.launchpad import Launchpad

launchpad = Launchpad.login_with(
        'test', service_root='https://api.launchpad.dev', version='devel')
user = launchpad.people['name16']
ppa = user.getPPAByName(name="test2")
ppa.private = True
ppa.lp_save()
print ppa.private

You could also go into the Archive web view and verify that the "Private" field is checked.

To verify there is no UI regression, you can go into the Archive web view, check the "Private" field, click Save, and then go back into the Archive web view to verify a buildd_secret has been generated and set for you (this sort of begs the question why this field even exists, though :))

LINT

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/soyuz/browser/archive.py
  lib/lp/soyuz/model/archive.py
  lib/lp/soyuz/tests/test_archive_privacy.py

./lib/lp/soyuz/model/archive.py
     344: redefinition of function 'private' from line 340
make: *** [lint] Error 1

Apparently lint doesn't like this way of defining Python getters and setters?

[1] https://code.launchpad.net/~jelmer/launchpad/394798-auto-buildd-secret
[2] lib/lp/soyuz/tests/test_archive_privacy.py

To post a comment you must log in.
Revision history for this message
j.c.sackett (jcsackett) wrote : Posted in a previous version of this proposal
Download full text (3.6 KiB)

Hi Tim--

This looks pretty good, I have one comment (regarding the lint issue you pointed out) and one question regarding sourcedeps.cache

> === modified file 'lib/lp/soyuz/model/archive.py'
> --- lib/lp/soyuz/model/archive.py 2011-06-07 05:02:53 +0000
> +++ lib/lp/soyuz/model/archive.py 2011-06-09 03:09:34 +0000
> @@ -259,7 +260,7 @@
>
> publish = BoolCol(dbName='publish', notNull=True, default=True)
>
> - private = BoolCol(dbName='private', notNull=True, default=False,
> + _private = BoolCol(dbName='private', notNull=True, default=False,
> storm_validator=_validate_archive_privacy)
>
> require_virtualized = BoolCol(
> @@ -324,6 +325,19 @@
> alsoProvides(self, IDistributionArchive)
>
> @property
> + def private(self):
> + return self._private
> +
> + @private.setter
> + def private(self, private):
> + self._private = private
> + if private:
> + if not self.buildd_secret:
> + self.buildd_secret = create_token(20)
> + else:
> + self.buildd_secret = Nonek
> +
> + @property
> def title(self):
> """See `IArchive`."""
> return self.displayname

I may be out of date, but the way I've usually seen this done is

+ def _get_private(self):
+ return self._private
+
+ def _set_private(self):
+ self._private = private
+ if private:
+ if not self.buildd_secret:
+ self.buildd_secret = create_token(20)
+ else:
+ self.buildd_secret = None
+ private = property(_get_private, _set_private)

I see that .setter attr was introduced on the @property decorator
in 2.6; it's possible lint wasn't updated to catch this.

As @property.setter works, there's no need to change this, just providing an explanation. It may be worth it to add a comment indicating lint will complain so other people changing this file don't have to figure it out again themselves.

> === modified file 'utilities/sourcedeps.cache'
> --- utilities/sourcedeps.cache 2011-06-01 18:58:15 +0000
> +++ utilities/sourcedeps.cache 2011-06-09 03:09:34 +0000
> @@ -1,4 +1,8 @@
> {
> + "bzr-builder": [
> + 68,
> + "<email address hidden>"
> + ],
> "testresources": [
> 16,
> "<email address hidden>"
> @@ -27,18 +31,14 @@
> 24,
> "<email address hidden>"
> ],
> + "lpreview": [
> + 23,
> + "<email address hidden>"
> + ],
> "bzr-git": [
> 259,
> "<email address hidden>"
> ],
> - "loggerhead": [
> - 445,
> - "<email address hidden>"
> - ],
> - "bzr-builder": [
> - 68,
> - "<email address hidden>"
> - ],
> "bzr-loom": [
> 49,
> "<email address hidden>"
> @@ -47,9 +47,9 @@
> 4,
> "sinzui-20...

Read more...

review: Needs Information
Revision history for this message
Timothy R. Chavez (timrchavez) wrote : Posted in a previous version of this proposal

The sourcedeps.cache looks like my bad. Not actually sure how this happens. Launchpad is still a somewhat mystical beast to me :) I think it needs to be reverted back to whatever is in trunk.

I can go ahead and add the comment to the setter this evening as well. Thanks for the review.

Revision history for this message
j.c.sackett (jcsackett) wrote : Posted in a previous version of this proposal

Just add the comment for the setter; having investigated the sourcedeps.cache thing it looks like it's just an artifact of when update-sourcecode updates it, and nothing to do with your branch. I saw the same phenomenon in another branch I reviewed a bit after yours.

I'll mark this approved, with the understanding that you're going to add that comment. Thanks, Tim.

review: Approve
Revision history for this message
j.c.sackett (jcsackett) wrote : Posted in a previous version of this proposal

As a note, Tim, you'll need to find someone else to land this for you; normally as the reviewer I would, but I'll be unavailable shortly, so I'll likely not be around once you've added that comment.

Revision history for this message
Timothy R. Chavez (timrchavez) wrote : Posted in a previous version of this proposal

I've added the note. I'll try to get Cody to land this if you are unavailable. Thanks (and my apologies for the slow turnaround)

Revision history for this message
Timothy R. Chavez (timrchavez) wrote :

Using Python properties to get/set the private attribute broke Storm expressions that were using Archive.private. To fix this, I changed these Storm expressions to use the underlying _private attribute.

Revision history for this message
j.c.sackett (jcsackett) wrote :

Tim--

This looks good. Thanks.

review: Approve
Revision history for this message
Timothy R. Chavez (timrchavez) wrote :

I believe I've identified the problem that was causing those test case failures. When going through and updating Storm expressions, I accidentally updated an SQL statement as well. Once fix, all tests that were failing ran successfully :)

Revision history for this message
j.c.sackett (jcsackett) wrote :

Yup, that seems to be the problem.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/archivepublisher/scripts/generate_ppa_htaccess.py'
2--- lib/lp/archivepublisher/scripts/generate_ppa_htaccess.py 2010-12-02 16:13:51 +0000
3+++ lib/lp/archivepublisher/scripts/generate_ppa_htaccess.py 2011-06-21 17:46:08 +0000
4@@ -271,7 +271,7 @@
5 Archive.date_created >= last_success.date_completed]
6
7 return store.find(
8- Archive, Archive.private == True, *extra_expr)
9+ Archive, Archive._private == True, *extra_expr)
10
11 def main(self):
12 """Script entry point."""
13
14=== modified file 'lib/lp/buildmaster/model/buildfarmjob.py'
15--- lib/lp/buildmaster/model/buildfarmjob.py 2011-05-27 21:12:25 +0000
16+++ lib/lp/buildmaster/model/buildfarmjob.py 2011-06-21 17:46:08 +0000
17@@ -417,13 +417,13 @@
18 Select(
19 Archive.id,
20 tables=(Archive,),
21- where=(Archive.private == False)
22+ where=(Archive._private == False)
23 ),
24 Select(
25 Archive.id,
26 tables=(Archive,),
27 where=And(
28- Archive.private == True,
29+ Archive._private == True,
30 Archive.ownerID.is_in(
31 Select(
32 TeamParticipation.teamID,
33@@ -445,7 +445,7 @@
34 Select(
35 Archive.id,
36 tables=(Archive,),
37- where=(Archive.private == False)
38+ where=(Archive._private == False)
39 )
40 )
41 )
42
43=== modified file 'lib/lp/registry/model/distributionsourcepackage.py'
44--- lib/lp/registry/model/distributionsourcepackage.py 2011-06-07 03:30:25 +0000
45+++ lib/lp/registry/model/distributionsourcepackage.py 2011-06-21 17:46:08 +0000
46@@ -349,7 +349,7 @@
47 Archive,
48 Archive.distribution == self.distribution,
49 Archive._enabled == True,
50- Archive.private == False,
51+ Archive._private == False,
52 SourcePackagePublishingHistory.archive == Archive.id,
53 (SourcePackagePublishingHistory.status ==
54 PackagePublishingStatus.PUBLISHED),
55
56=== modified file 'lib/lp/soyuz/browser/archive.py'
57--- lib/lp/soyuz/browser/archive.py 2011-06-11 00:49:33 +0000
58+++ lib/lp/soyuz/browser/archive.py 2011-06-21 17:46:08 +0000
59@@ -59,7 +59,6 @@
60
61 from canonical.launchpad import _
62 from canonical.launchpad.browser.librarian import FileNavigationMixin
63-from canonical.launchpad.components.tokens import create_token
64 from canonical.launchpad.helpers import english_list
65 from canonical.launchpad.webapp import (
66 canonical_url,
67@@ -2059,8 +2058,8 @@
68 archive as private, generate a secret for them.
69 """
70 if data['private'] and data['buildd_secret'] is None:
71- # buildd secrets are only used by builders, autogenerate one.
72- self.context.buildd_secret = create_token(16)
73+ # The buildd secret is auto-generated and set when 'private'
74+ # is set to True
75 del(data['buildd_secret'])
76 super(ArchiveAdminView, self).updateContextFromData(data)
77
78
79=== modified file 'lib/lp/soyuz/model/archive.py'
80--- lib/lp/soyuz/model/archive.py 2011-06-16 20:12:00 +0000
81+++ lib/lp/soyuz/model/archive.py 2011-06-21 17:46:08 +0000
82@@ -59,6 +59,7 @@
83 )
84 from canonical.launchpad.components.tokens import (
85 create_unique_token_for_table,
86+ create_token,
87 )
88 from canonical.launchpad.database.librarian import (
89 LibraryFileAlias,
90@@ -271,7 +272,7 @@
91
92 publish = BoolCol(dbName='publish', notNull=True, default=True)
93
94- private = BoolCol(dbName='private', notNull=True, default=False,
95+ _private = BoolCol(dbName='private', notNull=True, default=False,
96 storm_validator=_validate_archive_privacy)
97
98 require_virtualized = BoolCol(
99@@ -336,6 +337,22 @@
100 else:
101 alsoProvides(self, IDistributionArchive)
102
103+ # Note: You may safely ignore lint when it complains about this
104+ # declaration. As of Python 2.6, this is a perfectly valid way
105+ # of adding a setter
106+ @property
107+ def private(self):
108+ return self._private
109+
110+ @private.setter
111+ def private(self, private):
112+ self._private = private
113+ if private:
114+ if not self.buildd_secret:
115+ self.buildd_secret = create_token(20)
116+ else:
117+ self.buildd_secret = None
118+
119 @property
120 def title(self):
121 """See `IArchive`."""
122@@ -2205,7 +2222,7 @@
123 store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
124 return store.find(
125 Archive,
126- Archive.private == True,
127+ Archive._private == True,
128 Archive.purpose == ArchivePurpose.PPA)
129
130 def getCommercialPPAs(self):
131@@ -2233,7 +2250,7 @@
132 if name is not None:
133 extra_exprs.append(Archive.name == name)
134
135- public_archive = And(Archive.private == False,
136+ public_archive = And(Archive._private == False,
137 Archive._enabled == True)
138
139 if user is not None:
140
141=== modified file 'lib/lp/soyuz/tests/test_archive_privacy.py'
142--- lib/lp/soyuz/tests/test_archive_privacy.py 2010-10-04 19:50:45 +0000
143+++ lib/lp/soyuz/tests/test_archive_privacy.py 2011-06-21 17:46:08 +0000
144@@ -29,7 +29,6 @@
145 super(TestArchivePrivacy, self).setUp()
146 self.private_ppa = self.factory.makeArchive(description='Foo')
147 login('admin@canonical.com')
148- self.private_ppa.buildd_secret = 'blah'
149 self.private_ppa.private = True
150 self.joe = self.factory.makePerson(name='joe')
151 self.fred = self.factory.makePerson(name='fred')
152@@ -59,18 +58,15 @@
153 super(TestArchivePrivacySwitching, self).setUp()
154 self.public_ppa = self.factory.makeArchive()
155 self.private_ppa = self.factory.makeArchive()
156- self.private_ppa.buildd_secret = 'blah'
157 self.private_ppa.private = True
158
159 def make_ppa_private(self, ppa):
160 """Helper method to privatise a ppa."""
161 ppa.private = True
162- ppa.buildd_secret = "secret"
163
164 def make_ppa_public(self, ppa):
165 """Helper method to make a PPA public (and use for assertRaises)."""
166 ppa.private = False
167- ppa.buildd_secret = ''
168
169 def test_switch_privacy_no_pubs_succeeds(self):
170 # Changing the privacy is fine if there are no publishing
171@@ -94,3 +90,11 @@
172
173 self.assertRaises(
174 CannotSwitchPrivacy, self.make_ppa_public, self.private_ppa)
175+
176+ def test_buildd_secret_was_generated(self):
177+ self.make_ppa_private(self.public_ppa)
178+ self.assertNotEqual(self.public_ppa.buildd_secret, None)
179+
180+ def test_discard_buildd_was_discarded(self):
181+ self.make_ppa_public(self.private_ppa)
182+ self.assertEqual(self.private_ppa.buildd_secret, None)