Merge lp:~wgrant/launchpad/devel-add-read-ff into lp:launchpad
- devel-add-read-ff
- Merge into devel
Proposed by
William Grant
Status: | Merged |
---|---|
Merged at revision: | 17956 |
Proposed branch: | lp:~wgrant/launchpad/devel-add-read-ff |
Merge into: | lp:launchpad |
Diff against target: |
1185 lines (+420/-236) 20 files modified
database/schema/security.cfg (+8/-0) lib/lp/registry/browser/tests/test_gpgkey.py (+112/-8) lib/lp/registry/configure.zcml (+5/-0) lib/lp/registry/doc/gpgkey.txt (+0/-33) lib/lp/registry/interfaces/gpg.py (+3/-1) lib/lp/registry/interfaces/person.py (+2/-2) lib/lp/registry/model/gpgkey.py (+153/-27) lib/lp/registry/stories/gpg-coc/gpg-with-gpgservice-ff.txt (+6/-2) lib/lp/services/gpg/configure.zcml (+1/-1) lib/lp/services/gpg/handler.py (+6/-131) lib/lp/services/gpg/interfaces.py (+10/-9) lib/lp/services/gpg/tests/test_gpghandler.py (+81/-9) lib/lp/services/verification/browser/tests/test_logintoken.py (+6/-4) lib/lp/soyuz/tests/test_archive_subscriptions.py (+5/-2) lib/lp/testing/factory.py (+8/-0) lib/lp/testing/gpgkeys/__init__.py (+1/-0) lib/lp/testing/gpgservice/_fixture.py (+7/-4) lib/lp/testing/gpgservice/tests/test_fixture.py (+4/-3) setup.py (+1/-0) versions.cfg (+1/-0) |
To merge this branch: | bzr merge lp:~wgrant/launchpad/devel-add-read-ff |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email: mp+289603@code.launchpad.net |
Commit message
Add feature flag to read GPG keys from gpgservice.
Description of the change
Polished version of https:/
To post a comment you must log in.
Revision history for this message
William Grant (wgrant) : | # |
review:
Approve
(code)
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'database/schema/security.cfg' |
2 | --- database/schema/security.cfg 2016-03-14 19:40:14 +0000 |
3 | +++ database/schema/security.cfg 2016-03-21 05:51:58 +0000 |
4 | @@ -975,6 +975,7 @@ |
5 | public.livefs = SELECT |
6 | public.livefsbuild = SELECT, UPDATE |
7 | public.livefsfile = SELECT |
8 | +public.openididentifier = SELECT |
9 | public.packageset = SELECT |
10 | public.packagesetgroup = SELECT |
11 | public.packagesetinclusion = SELECT |
12 | @@ -1056,6 +1057,7 @@ |
13 | public.job = SELECT, INSERT, UPDATE |
14 | public.libraryfilealias = SELECT |
15 | public.libraryfilecontent = SELECT |
16 | +public.openididentifier = SELECT |
17 | public.packagecopyjob = SELECT |
18 | public.packageset = SELECT, INSERT |
19 | public.packagesetgroup = SELECT, INSERT |
20 | @@ -1113,6 +1115,7 @@ |
21 | public.libraryfilecontent = SELECT, INSERT |
22 | public.message = SELECT, INSERT, UPDATE |
23 | public.messagechunk = SELECT, INSERT, UPDATE |
24 | +public.openididentifier = SELECT |
25 | public.packagecopyjob = SELECT, UPDATE |
26 | public.packagediff = SELECT, INSERT |
27 | public.packageset = SELECT, INSERT |
28 | @@ -1216,6 +1219,7 @@ |
29 | public.logintoken = SELECT, INSERT, UPDATE |
30 | public.message = SELECT, INSERT, UPDATE |
31 | public.milestone = SELECT, INSERT, UPDATE |
32 | +public.openididentifier = SELECT |
33 | public.packageupload = SELECT, INSERT, UPDATE |
34 | public.packageuploadbuild = SELECT, INSERT, UPDATE |
35 | public.packageuploadcustom = SELECT, INSERT, UPDATE |
36 | @@ -1390,6 +1394,7 @@ |
37 | public.messagechunk = SELECT, INSERT |
38 | public.milestone = SELECT |
39 | public.milestonetag = SELECT |
40 | +public.openididentifier = SELECT |
41 | public.packagecopyjob = SELECT, INSERT |
42 | public.packagediff = SELECT, INSERT, UPDATE, DELETE |
43 | public.packageset = SELECT |
44 | @@ -1502,6 +1507,7 @@ |
45 | public.messagechunk = SELECT, INSERT |
46 | public.milestone = SELECT |
47 | public.milestonetag = SELECT |
48 | +public.openididentifier = SELECT |
49 | public.packagecopyjob = SELECT, INSERT, UPDATE |
50 | public.packagediff = SELECT, UPDATE |
51 | public.packageset = SELECT |
52 | @@ -1804,6 +1810,7 @@ |
53 | public.messagechunk = SELECT, INSERT |
54 | public.milestone = SELECT |
55 | public.milestonetag = SELECT, INSERT, DELETE |
56 | +public.openididentifier = SELECT |
57 | public.packageset = SELECT |
58 | public.packagesetgroup = SELECT |
59 | public.packagesetinclusion = SELECT |
60 | @@ -1922,6 +1929,7 @@ |
61 | public.libraryfilecontent = SELECT, INSERT |
62 | public.message = SELECT, INSERT |
63 | public.messagechunk = SELECT, INSERT |
64 | +public.openididentifier = SELECT |
65 | public.person = SELECT |
66 | public.product = SELECT |
67 | public.productseries = SELECT |
68 | |
69 | === modified file 'lib/lp/registry/browser/tests/test_gpgkey.py' |
70 | --- lib/lp/registry/browser/tests/test_gpgkey.py 2016-02-10 00:51:55 +0000 |
71 | +++ lib/lp/registry/browser/tests/test_gpgkey.py 2016-03-21 05:51:58 +0000 |
72 | @@ -6,28 +6,39 @@ |
73 | __metaclass__ = type |
74 | |
75 | from testtools.matchers import ( |
76 | + Contains, |
77 | + Equals, |
78 | + HasLength, |
79 | Not, |
80 | Raises, |
81 | raises, |
82 | ) |
83 | +from zope.component import getUtility |
84 | |
85 | +from lp.registry.interfaces.person import IPersonSet |
86 | +from lp.registry.interfaces.gpg import IGPGKeySet |
87 | from lp.services.features.testing import FeatureFixture |
88 | from lp.services.gpg.interfaces import ( |
89 | + GPG_DATABASE_READONLY_FEATURE_FLAG, |
90 | + GPG_READ_FROM_GPGSERVICE_FEATURE_FLAG, |
91 | + GPG_WRITE_TO_GPGSERVICE_FEATURE_FLAG, |
92 | + GPGKeyAlgorithm, |
93 | GPGReadOnly, |
94 | - GPG_DATABASE_READONLY_FEATURE_FLAG, |
95 | ) |
96 | +from lp.services.verification.interfaces.authtoken import LoginTokenType |
97 | +from lp.services.verification.interfaces.logintoken import ILoginTokenSet |
98 | from lp.services.webapp import canonical_url |
99 | from lp.testing import ( |
100 | login_person, |
101 | TestCaseWithFactory, |
102 | ) |
103 | -from lp.testing.layers import DatabaseFunctionalLayer |
104 | +from lp.testing.layers import LaunchpadFunctionalLayer |
105 | from lp.testing.views import create_initialized_view |
106 | |
107 | |
108 | class TestCanonicalUrl(TestCaseWithFactory): |
109 | |
110 | - layer = DatabaseFunctionalLayer |
111 | + layer = LaunchpadFunctionalLayer |
112 | |
113 | def test_canonical_url(self): |
114 | # The canonical URL of a GPG key is ??? |
115 | @@ -41,7 +52,7 @@ |
116 | |
117 | class TestPersonGPGView(TestCaseWithFactory): |
118 | |
119 | - layer = DatabaseFunctionalLayer |
120 | + layer = LaunchpadFunctionalLayer |
121 | |
122 | def test_edit_pgp_keys_login_redirect(self): |
123 | """+editpgpkeys should redirect to force you to re-authenticate.""" |
124 | @@ -60,8 +71,9 @@ |
125 | })) |
126 | person = self.factory.makePerson() |
127 | login_person(person) |
128 | - view = create_initialized_view(person, "+editpgpkeys", principal=person, |
129 | - method='POST', have_fresh_login=True) |
130 | + view = create_initialized_view( |
131 | + person, "+editpgpkeys", principal=person, method='POST', |
132 | + have_fresh_login=True) |
133 | self.assertThat(view.render, raises(GPGReadOnly)) |
134 | |
135 | def test_gpgkeys_GET_readonly_with_feature_flag_set(self): |
136 | @@ -70,6 +82,98 @@ |
137 | })) |
138 | person = self.factory.makePerson() |
139 | login_person(person) |
140 | - view = create_initialized_view(person, "+editpgpkeys", principal=person, |
141 | - method='GET', have_fresh_login=True) |
142 | + view = create_initialized_view( |
143 | + person, "+editpgpkeys", principal=person, method='GET', |
144 | + have_fresh_login=True) |
145 | self.assertThat(view.render, Not(Raises())) |
146 | + |
147 | + |
148 | +class GPGKeySetTests(TestCaseWithFactory): |
149 | + |
150 | + layer = LaunchpadFunctionalLayer |
151 | + |
152 | + def test_can_add_keys_for_test(self): |
153 | + # IGPGKeySet.new _only_ writes to the launchpad database, so this test |
154 | + # only works if the read_from_gpgservice FF is *not* set. Once this is |
155 | + # the default codepath this test should be deleted. |
156 | + self.useFixture(FeatureFixture({ |
157 | + GPG_READ_FROM_GPGSERVICE_FEATURE_FLAG: None, |
158 | + })) |
159 | + keyset = getUtility(IGPGKeySet) |
160 | + person = self.factory.makePerson() |
161 | + fingerprint = "DEADBEEF12345678DEADBEEF12345678DEADBEEF" |
162 | + keyset.new(person.id, "F0A432C2", fingerprint, 4096, |
163 | + GPGKeyAlgorithm.R, True, True) |
164 | + |
165 | + keys = keyset.getGPGKeysForPerson(person) |
166 | + |
167 | + self.assertThat(keys, HasLength(1)) |
168 | + self.assertThat(keys[0].fingerprint, Equals(fingerprint)) |
169 | + |
170 | + def test_sampledata_contains_gpgkeys(self): |
171 | + keyset = getUtility(IGPGKeySet) |
172 | + personset = getUtility(IPersonSet) |
173 | + foobar = personset.getByName('name16') |
174 | + keys = keyset.getGPGKeysForPerson(foobar) |
175 | + |
176 | + self.assertThat(keys, HasLength(1)) |
177 | + self.assertThat(keys[0].keyid, Equals('12345678')) |
178 | + self.assertThat(keys[0].fingerprint, |
179 | + Equals('ABCDEF0123456789ABCDDCBA0000111112345678')) |
180 | + |
181 | + def test_can_retrieve_keys_by_fingerprint(self): |
182 | + keyset = getUtility(IGPGKeySet) |
183 | + person = self.factory.makePerson() |
184 | + key = self.factory.makeGPGKey(person) |
185 | + |
186 | + retrieved_key = keyset.getByFingerprint(key.fingerprint) |
187 | + |
188 | + self.assertThat(retrieved_key.owner.name, Equals(person.name)) |
189 | + self.assertThat(retrieved_key.fingerprint, Equals(key.fingerprint)) |
190 | + |
191 | + def test_getGPGKeysForPerson_retrieves_active_keys(self): |
192 | + keyset = getUtility(IGPGKeySet) |
193 | + person = self.factory.makePerson() |
194 | + key = self.factory.makeGPGKey(person) |
195 | + |
196 | + keys = keyset.getGPGKeysForPerson(person) |
197 | + |
198 | + self.assertThat(keys, HasLength(1)) |
199 | + self.assertThat(keys, Contains(key)) |
200 | + |
201 | + def test_getGPGKeysForPerson_retrieves_inactive_keys(self): |
202 | + keyset = getUtility(IGPGKeySet) |
203 | + person = self.factory.makePerson() |
204 | + key = self.factory.makeGPGKey(person) |
205 | + keyset.deactivate(key) |
206 | + |
207 | + active_keys = keyset.getGPGKeysForPerson(person) |
208 | + inactive_keys = keyset.getGPGKeysForPerson(person, active=False) |
209 | + |
210 | + self.assertThat(active_keys, HasLength(0)) |
211 | + self.assertThat(inactive_keys, HasLength(1)) |
212 | + self.assertThat(inactive_keys, Contains(key)) |
213 | + |
214 | + def test_getGPGKeysForPerson_excludes_keys_with_logintoken(self): |
215 | + keyset = getUtility(IGPGKeySet) |
216 | + email = 'foo@bar.com' |
217 | + person = self.factory.makePerson(email) |
218 | + key = self.factory.makeGPGKey(person) |
219 | + keyset.deactivate(key) |
220 | + getUtility(ILoginTokenSet).new( |
221 | + person, email, email, LoginTokenType.VALIDATEGPG, key.fingerprint) |
222 | + |
223 | + inactive_keys = keyset.getGPGKeysForPerson(person, active=False) |
224 | + self.assertThat(inactive_keys, HasLength(0)) |
225 | + |
226 | + |
227 | +class GPGKeySetWithGPGServiceTests(GPGKeySetTests): |
228 | + |
229 | + """A copy of GPGKeySetTests, but with gpgservice used.""" |
230 | + |
231 | + def setUp(self): |
232 | + super(GPGKeySetWithGPGServiceTests, self).setUp() |
233 | + self.useFixture(FeatureFixture({ |
234 | + GPG_READ_FROM_GPGSERVICE_FEATURE_FLAG: True, |
235 | + GPG_WRITE_TO_GPGSERVICE_FEATURE_FLAG: True, |
236 | + })) |
237 | |
238 | === modified file 'lib/lp/registry/configure.zcml' |
239 | --- lib/lp/registry/configure.zcml 2016-02-28 19:13:17 +0000 |
240 | +++ lib/lp/registry/configure.zcml 2016-03-21 05:51:58 +0000 |
241 | @@ -1284,6 +1284,11 @@ |
242 | permission="launchpad.Edit" |
243 | set_attributes="active can_encrypt"/> |
244 | </class> |
245 | + <class |
246 | + class="lp.registry.model.gpgkey.GPGServiceKey"> |
247 | + <allow |
248 | + interface="lp.registry.interfaces.gpg.IGPGKey"/> |
249 | + </class> |
250 | |
251 | <!-- GPGKeySet --> |
252 | |
253 | |
254 | === removed file 'lib/lp/registry/doc/gpgkey.txt' |
255 | --- lib/lp/registry/doc/gpgkey.txt 2016-03-01 12:46:32 +0000 |
256 | +++ lib/lp/registry/doc/gpgkey.txt 1970-01-01 00:00:00 +0000 |
257 | @@ -1,33 +0,0 @@ |
258 | -= GPG Keys = |
259 | - |
260 | -Launchpad models GPG keys in a GPGKey class. |
261 | - |
262 | - >>> from zope.component import getUtility |
263 | - >>> from lp.registry.interfaces.gpg import IGPGKeySet |
264 | - >>> from lp.registry.interfaces.person import IPersonSet |
265 | - >>> from lp.services.gpg.interfaces import GPGKeyAlgorithm |
266 | - >>> personset = getUtility(IPersonSet) |
267 | - >>> foobar = personset.getByName('name16') |
268 | - >>> gpgkeyset = getUtility(IGPGKeySet) |
269 | - >>> keys = gpgkeyset.getGPGKeysForPerson(foobar) |
270 | - >>> [key.keyid for key in keys] |
271 | - [u'12345678'] |
272 | - |
273 | -Adding new keys is pretty easy: |
274 | - |
275 | - >>> name12 = personset.getByName('name12') |
276 | - >>> gpgkeyset.new(name12.id, u'DEADBEEF', |
277 | - ... 'DEADBEEF12345678DEADBEEF12345678DEADBEEF', 1024, |
278 | - ... GPGKeyAlgorithm.LITTLE_G) |
279 | - <GPGKey...> |
280 | - >>> gpgkeyset.new(name12.id, u'DEADBEED', |
281 | - ... 'DEADBEED12345678DEADBEED12345678DEADBEED', 2048, |
282 | - ... GPGKeyAlgorithm.G) |
283 | - <GPGKey...> |
284 | - |
285 | -As is pulling it based on the key ID: |
286 | - |
287 | - >>> key = gpgkeyset.getByFingerprint( |
288 | - ... 'DEADBEEF12345678DEADBEEF12345678DEADBEEF') |
289 | - >>> key.owner.name |
290 | - u'name12' |
291 | |
292 | === modified file 'lib/lp/registry/interfaces/gpg.py' |
293 | --- lib/lp/registry/interfaces/gpg.py 2016-03-14 19:40:14 +0000 |
294 | +++ lib/lp/registry/interfaces/gpg.py 2016-03-21 05:51:58 +0000 |
295 | @@ -39,7 +39,6 @@ |
296 | |
297 | export_as_webservice_entry('gpg_key') |
298 | |
299 | - id = Int(title=_("Database id"), required=True, readonly=True) |
300 | keysize = Int(title=_("Keysize"), required=True) |
301 | algorithm = Choice(title=_("Algorithm"), required=True, |
302 | vocabulary='GpgAlgorithm') |
303 | @@ -84,6 +83,9 @@ |
304 | inactive ones. |
305 | """ |
306 | |
307 | + def getOwnerIdForPerson(person): |
308 | + """return an owner id string suitable for sending to gpgservice.""" |
309 | + |
310 | def getByFingerprints(fingerprints): |
311 | """Get multiple OpenPGP keys by their fingerprints.""" |
312 | |
313 | |
314 | === modified file 'lib/lp/registry/interfaces/person.py' |
315 | --- lib/lp/registry/interfaces/person.py 2016-01-26 15:47:37 +0000 |
316 | +++ lib/lp/registry/interfaces/person.py 2016-03-21 05:51:58 +0000 |
317 | @@ -775,10 +775,10 @@ |
318 | inactivesignatures = Attribute("Retrieve own Inactive CoC Signatures.") |
319 | signedcocs = Attribute("List of Signed Code Of Conduct") |
320 | gpg_keys = exported( |
321 | - CollectionField( |
322 | + doNotSnapshot(CollectionField( |
323 | title=_("List of valid OpenPGP keys ordered by ID"), |
324 | readonly=False, required=False, |
325 | - value_type=Reference(schema=IGPGKey))) |
326 | + value_type=Reference(schema=IGPGKey)))) |
327 | pending_gpg_keys = CollectionField( |
328 | title=_("Set of fingerprints pending confirmation"), |
329 | readonly=False, required=False, |
330 | |
331 | === modified file 'lib/lp/registry/model/gpgkey.py' |
332 | --- lib/lp/registry/model/gpgkey.py 2016-03-16 00:32:05 +0000 |
333 | +++ lib/lp/registry/model/gpgkey.py 2016-03-21 05:51:58 +0000 |
334 | @@ -8,7 +8,6 @@ |
335 | BoolCol, |
336 | ForeignKey, |
337 | IntCol, |
338 | - SQLObjectNotFound, |
339 | StringCol, |
340 | ) |
341 | from zope.component import getUtility |
342 | @@ -18,6 +17,8 @@ |
343 | IGPGKey, |
344 | IGPGKeySet, |
345 | ) |
346 | +from lp.registry.interfaces.person import IPersonSet |
347 | +from lp.services.config import config |
348 | from lp.services.database.enumcol import EnumCol |
349 | from lp.services.database.interfaces import IStore |
350 | from lp.services.database.sqlbase import ( |
351 | @@ -27,12 +28,14 @@ |
352 | from lp.services.features import getFeatureFlag |
353 | from lp.services.gpg.interfaces import ( |
354 | GPG_WRITE_TO_GPGSERVICE_FEATURE_FLAG, |
355 | + GPG_READ_FROM_GPGSERVICE_FEATURE_FLAG, |
356 | GPGKeyAlgorithm, |
357 | IGPGClient, |
358 | IGPGHandler, |
359 | ) |
360 | from lp.services.openid.interfaces.openid import IOpenIDPersistentIdentity |
361 | from lp.services.openid.model.openididentifier import OpenIdIdentifier |
362 | +from lp.services.verification.interfaces.logintoken import ILoginTokenSet |
363 | |
364 | |
365 | @implementer(IGPGKey) |
366 | @@ -65,6 +68,58 @@ |
367 | return '%s%s/%s' % (self.keysize, self.algorithm.title, self.keyid) |
368 | |
369 | |
370 | +@implementer(IGPGKey) |
371 | +class GPGServiceKey: |
372 | + |
373 | + def __init__(self, key_data): |
374 | + self._key_data = key_data |
375 | + |
376 | + @property |
377 | + def active(self): |
378 | + return self._key_data['enabled'] |
379 | + |
380 | + @property |
381 | + def keysize(self): |
382 | + return self._key_data['size'] |
383 | + |
384 | + @property |
385 | + def algorithm(self): |
386 | + return GPGKeyAlgorithm.items[self._key_data['algorithm']] |
387 | + |
388 | + @property |
389 | + def keyid(self): |
390 | + return self._key_data['id'] |
391 | + |
392 | + @property |
393 | + def fingerprint(self): |
394 | + return self._key_data['fingerprint'] |
395 | + |
396 | + @property |
397 | + def displayname(self): |
398 | + return '%s%s/%s' % (self.keysize, self.algorithm.title, self.keyid) |
399 | + |
400 | + @property |
401 | + def keyserverURL(self): |
402 | + return getUtility( |
403 | + IGPGHandler).getURLForKeyInServer(self.fingerprint, public=True) |
404 | + |
405 | + @property |
406 | + def can_encrypt(self): |
407 | + return self._key_data['can_encrypt'] |
408 | + |
409 | + @property |
410 | + def owner(self): |
411 | + return getUtility(IPersonSet).getByOpenIDIdentifier( |
412 | + self._key_data['owner']) |
413 | + |
414 | + @property |
415 | + def ownerID(self): |
416 | + return self.owner.id |
417 | + |
418 | + def __eq__(self, other): |
419 | + return self.fingerprint == other.fingerprint |
420 | + |
421 | + |
422 | @implementer(IGPGKeySet) |
423 | class GPGKeySet: |
424 | |
425 | @@ -79,8 +134,13 @@ |
426 | def activate(self, requester, key, can_encrypt): |
427 | """See `IGPGKeySet`.""" |
428 | fingerprint = key.fingerprint |
429 | - lp_key = self.getByFingerprint(fingerprint) |
430 | + # XXX: This is a little ugly - we can't use getByFingerprint |
431 | + # here since if the READ_FROM_GPGSERVICE FF is set we'll get a |
432 | + # GPGServiceKey object instead of a GPGKey object, and we need |
433 | + # to change the database representation in all cases. |
434 | + lp_key = GPGKey.selectOneBy(fingerprint=fingerprint) |
435 | if lp_key: |
436 | + assert lp_key.owner == requester |
437 | is_new = False |
438 | # Then the key already exists, so let's reactivate it. |
439 | lp_key.active = True |
440 | @@ -95,50 +155,116 @@ |
441 | ownerID, keyid, fingerprint, keysize, algorithm, |
442 | can_encrypt=can_encrypt) |
443 | if getFeatureFlag(GPG_WRITE_TO_GPGSERVICE_FEATURE_FLAG): |
444 | + # XXX: Further to the comment above, if READ_FROM_GPGSERVICE FF is |
445 | + # set then we need to duplicate the block above but reading from |
446 | + # the gpgservice instead of the database: |
447 | client = getUtility(IGPGClient) |
448 | - openid_identifier = self.getOwnerIdForPerson(lp_key.owner) |
449 | - client.addKeyForOwner(openid_identifier, key.fingerprint) |
450 | + owner_id = self.getOwnerIdForPerson(requester) |
451 | + # Users with more than one openid identifier may be re-activating |
452 | + # a key that was previously deactivated with their non-default |
453 | + # openid identifier. If that's the case, use the same openid |
454 | + # identifier rather than the default one - this happens even if the |
455 | + # read FF is not set: |
456 | + key_data = client.getKeyByFingerprint(fingerprint) |
457 | + if key_data: |
458 | + owner_id = key_data['owner'] |
459 | + allowed_owner_ids = self._getAllOwnerIdsForPerson(requester) |
460 | + assert owner_id in allowed_owner_ids |
461 | + gpgservice_key = GPGServiceKey( |
462 | + client.addKeyForOwner(owner_id, key.fingerprint)) |
463 | + if getFeatureFlag(GPG_READ_FROM_GPGSERVICE_FEATURE_FLAG): |
464 | + is_new = key_data is None |
465 | + lp_key = gpgservice_key |
466 | return lp_key, is_new |
467 | |
468 | def deactivate(self, key): |
469 | - key.active = False |
470 | + # key could be a GPGServiceKey, which doesn't allow us to set it's |
471 | + # active attribute. Retrieve it by fingerprint: |
472 | + lp_key = GPGKey.selectOneBy(fingerprint=key.fingerprint) |
473 | + lp_key.active = False |
474 | if getFeatureFlag(GPG_WRITE_TO_GPGSERVICE_FEATURE_FLAG): |
475 | + # Users with more than one openid identifier may be |
476 | + # deactivating a key that is associated with their |
477 | + # non-default openid identifier. If that's the case, use |
478 | + # the same openid identifier rather than the default one: |
479 | client = getUtility(IGPGClient) |
480 | - openid_identifier = self.getOwnerIdForPerson(key.owner) |
481 | + key_data = client.getKeyByFingerprint(key.fingerprint) |
482 | + if not key_data: |
483 | + # We get here if we're asked to deactivate a key that was never |
484 | + # activated. This should probably never happen. |
485 | + return |
486 | + openid_identifier = key_data['owner'] |
487 | client.disableKeyForOwner(openid_identifier, key.fingerprint) |
488 | |
489 | def getByFingerprint(self, fingerprint, default=None): |
490 | """See `IGPGKeySet`""" |
491 | - result = GPGKey.selectOneBy(fingerprint=fingerprint) |
492 | - if result is None: |
493 | - return default |
494 | - return result |
495 | + if getFeatureFlag(GPG_READ_FROM_GPGSERVICE_FEATURE_FLAG): |
496 | + key_data = getUtility(IGPGClient).getKeyByFingerprint(fingerprint) |
497 | + return GPGServiceKey(key_data) if key_data else default |
498 | + else: |
499 | + result = GPGKey.selectOneBy(fingerprint=fingerprint) |
500 | + if result is None: |
501 | + return default |
502 | + return result |
503 | |
504 | def getByFingerprints(self, fingerprints): |
505 | """See `IGPGKeySet`""" |
506 | - return IStore(GPGKey).find( |
507 | - GPGKey, GPGKey.fingerprint.is_in(fingerprints)) |
508 | + fingerprints = list(fingerprints) |
509 | + if getFeatureFlag(GPG_READ_FROM_GPGSERVICE_FEATURE_FLAG): |
510 | + client = getUtility(IGPGClient) |
511 | + return [ |
512 | + GPGServiceKey(key_data) |
513 | + for key_data in client.getKeysByFingerprints(fingerprints)] |
514 | + else: |
515 | + return list(IStore(GPGKey).find( |
516 | + GPGKey, GPGKey.fingerprint.is_in(fingerprints))) |
517 | |
518 | def getGPGKeysForPerson(self, owner, active=True): |
519 | - if active is False: |
520 | - query = """ |
521 | - active = false |
522 | - AND fingerprint NOT IN |
523 | - (SELECT fingerprint FROM LoginToken |
524 | - WHERE fingerprint IS NOT NULL |
525 | - AND requester = %s |
526 | - AND date_consumed is NULL |
527 | - ) |
528 | - """ % sqlvalues(owner.id) |
529 | + if getFeatureFlag(GPG_READ_FROM_GPGSERVICE_FEATURE_FLAG): |
530 | + client = getUtility(IGPGClient) |
531 | + owner_ids = self._getAllOwnerIdsForPerson(owner) |
532 | + if not owner_ids: |
533 | + return [] |
534 | + gpg_keys = [] |
535 | + for owner_id in owner_ids: |
536 | + key_data_list = client.getKeysForOwner(owner_id)['keys'] |
537 | + gpg_keys.extend([ |
538 | + GPGServiceKey(d) for d in key_data_list |
539 | + if d['enabled'] == active]) |
540 | + if active is False: |
541 | + login_tokens = getUtility(ILoginTokenSet).getPendingGPGKeys( |
542 | + owner.id) |
543 | + token_fingerprints = [t.fingerprint for t in login_tokens] |
544 | + return [ |
545 | + k for k in gpg_keys |
546 | + if k.fingerprint not in token_fingerprints] |
547 | + return gpg_keys |
548 | else: |
549 | - query = 'active=true' |
550 | - |
551 | - query += ' AND owner=%s' % sqlvalues(owner.id) |
552 | - |
553 | - return list(GPGKey.select(query, orderBy='id')) |
554 | + if active is False: |
555 | + query = """ |
556 | + active = false |
557 | + AND fingerprint NOT IN |
558 | + (SELECT fingerprint FROM LoginToken |
559 | + WHERE fingerprint IS NOT NULL |
560 | + AND requester = %s |
561 | + AND date_consumed is NULL |
562 | + ) |
563 | + """ % sqlvalues(owner.id) |
564 | + else: |
565 | + query = 'active=true' |
566 | + query += ' AND owner=%s' % sqlvalues(owner.id) |
567 | + return list(GPGKey.select(query, orderBy='id')) |
568 | |
569 | def getOwnerIdForPerson(self, owner): |
570 | """See IGPGKeySet.""" |
571 | url = IOpenIDPersistentIdentity(owner).openid_identity_url |
572 | assert url is not None |
573 | return url |
574 | + |
575 | + def _getAllOwnerIdsForPerson(self, owner): |
576 | + identifiers = IStore(OpenIdIdentifier).find( |
577 | + OpenIdIdentifier, account=owner.account) |
578 | + openid_provider_root = config.launchpad.openid_provider_root |
579 | + return [ |
580 | + openid_provider_root + '+id/' + i.identifier.encode('ascii') |
581 | + for i in identifiers] |
582 | |
583 | === modified file 'lib/lp/registry/stories/gpg-coc/gpg-with-gpgservice-ff.txt' |
584 | --- lib/lp/registry/stories/gpg-coc/gpg-with-gpgservice-ff.txt 2016-02-22 23:45:24 +0000 |
585 | +++ lib/lp/registry/stories/gpg-coc/gpg-with-gpgservice-ff.txt 2016-03-21 05:51:58 +0000 |
586 | @@ -7,9 +7,13 @@ |
587 | tests will be deleted. |
588 | |
589 | >>> from lp.services.features.testing import FeatureFixture |
590 | - >>> from lp.services.gpg.interfaces import GPG_WRITE_TO_GPGSERVICE_FEATURE_FLAG |
591 | + >>> from lp.services.gpg.interfaces import ( |
592 | + ... GPG_WRITE_TO_GPGSERVICE_FEATURE_FLAG, |
593 | + ... GPG_READ_FROM_GPGSERVICE_FEATURE_FLAG, |
594 | + ... ) |
595 | >>> feature_fixture = FeatureFixture( |
596 | - ... {GPG_WRITE_TO_GPGSERVICE_FEATURE_FLAG: True}) |
597 | + ... {GPG_WRITE_TO_GPGSERVICE_FEATURE_FLAG: True, |
598 | + ... GPG_READ_FROM_GPGSERVICE_FEATURE_FLAG: True}) |
599 | >>> feature_fixture.setUp() |
600 | |
601 | == Setup == |
602 | |
603 | === modified file 'lib/lp/services/gpg/configure.zcml' |
604 | --- lib/lp/services/gpg/configure.zcml 2016-02-18 03:35:43 +0000 |
605 | +++ lib/lp/services/gpg/configure.zcml 2016-03-21 05:51:58 +0000 |
606 | @@ -24,7 +24,7 @@ |
607 | </class> |
608 | |
609 | <securedutility |
610 | - class="lp.services.gpg.handler.GPGClient" |
611 | + class="lp.services.gpg.handler.LPGPGClient" |
612 | provides="lp.services.gpg.interfaces.IGPGClient"> |
613 | <allow interface="lp.services.gpg.interfaces.IGPGClient" /> |
614 | </securedutility> |
615 | |
616 | === modified file 'lib/lp/services/gpg/handler.py' |
617 | --- lib/lp/services/gpg/handler.py 2016-03-18 00:48:59 +0000 |
618 | +++ lib/lp/services/gpg/handler.py 2016-03-21 05:51:58 +0000 |
619 | @@ -11,7 +11,6 @@ |
620 | ] |
621 | |
622 | import atexit |
623 | -import base64 |
624 | import httplib |
625 | import os |
626 | import shutil |
627 | @@ -22,12 +21,10 @@ |
628 | import tempfile |
629 | import urllib |
630 | import urllib2 |
631 | -from urlparse import urljoin |
632 | |
633 | import gpgme |
634 | +from gpgservice_client import GPGClient |
635 | from lazr.restful.utils import get_current_browser_request |
636 | -import requests |
637 | -from requests.status_codes import codes as http_codes |
638 | from zope.interface import implementer |
639 | |
640 | from lp.app.validators.email import valid_email |
641 | @@ -39,7 +36,6 @@ |
642 | GPGKeyNotFoundError, |
643 | GPGKeyRevoked, |
644 | GPGKeyTemporarilyNotFoundError, |
645 | - GPGServiceException, |
646 | GPGUploadFailure, |
647 | GPGVerificationError, |
648 | IGPGClient, |
649 | @@ -51,7 +47,6 @@ |
650 | SecretGPGKeyImportDetected, |
651 | valid_fingerprint, |
652 | ) |
653 | -from lp.services.openid.model.openididentifier import OpenIdIdentifier |
654 | from lp.services.timeline.requesttimeline import get_request_timeline |
655 | from lp.services.timeout import ( |
656 | TimeoutError, |
657 | @@ -640,132 +635,12 @@ |
658 | return fingerprint |
659 | |
660 | |
661 | -def sanitize_fingerprint_or_raise(fingerprint): |
662 | - """Check the sanity of 'fingerprint'. |
663 | - |
664 | - If 'fingerprint' is a valid fingerprint, the sanitised version will be |
665 | - returned (see sanitize_fingerprint). |
666 | - |
667 | - Otherwise, a ValueError will be raised. |
668 | - """ |
669 | - sane_fingerprint = sanitize_fingerprint(fingerprint) |
670 | - if sane_fingerprint is None: |
671 | - raise ValueError("Invalid fingerprint: %r." % fingerprint) |
672 | - return sane_fingerprint |
673 | - |
674 | - |
675 | @implementer(IGPGClient) |
676 | -class GPGClient: |
677 | +class LPGPGClient(GPGClient): |
678 | """See IGPGClient.""" |
679 | |
680 | - def __init__(self): |
681 | - self.write_hooks = set() |
682 | - |
683 | - def getKeysForOwner(self, owner_id): |
684 | - """See IGPGClient.""" |
685 | - path = '/users/%s/keys' % self._encode_owner_id(owner_id) |
686 | - resp = self._request('get', path) |
687 | - if resp.status_code != http_codes['OK']: |
688 | - self.raise_for_error(resp) |
689 | - return resp.json() |
690 | - |
691 | - def addKeyForOwner(self, owner_id, fingerprint): |
692 | - """See IGPGClient.""" |
693 | - fingerprint = sanitize_fingerprint_or_raise(fingerprint) |
694 | - path = '/users/%s/keys' % self._encode_owner_id(owner_id) |
695 | - data = dict(fingerprint=fingerprint) |
696 | - resp = self._request('post', path, data) |
697 | - if resp.status_code == http_codes['CREATED']: |
698 | - self._notify_writes() |
699 | - # status 200 (OK) is returned if the key was already enabled: |
700 | - elif resp.status_code != http_codes['OK']: |
701 | - self.raise_for_error(resp) |
702 | - |
703 | - def disableKeyForOwner(self, owner_id, fingerprint): |
704 | - """See IGPGClient.""" |
705 | - fingerprint = sanitize_fingerprint_or_raise(fingerprint) |
706 | - path = '/users/%s/keys/%s' % (self._encode_owner_id(owner_id), fingerprint) |
707 | - resp = self._request('delete', path) |
708 | - if resp.status_code == http_codes['OK']: |
709 | - self._notify_writes() |
710 | - else: |
711 | - self.raise_for_error(resp) |
712 | - |
713 | - def getKeyByFingerprint(self, fingerprint): |
714 | - fingerprint = sanitize_fingerprint_or_raise(fingerprint) |
715 | - path = '/keys/%s' % fingerprint |
716 | - resp = self._request('get', path) |
717 | - if resp.status_code == http_codes['OK']: |
718 | - return resp.json() |
719 | - elif resp.status_code == http_codes['NOT_FOUND']: |
720 | - return None |
721 | - else: |
722 | - self.raise_for_error(resp) |
723 | - |
724 | - def registerWriteHook(self, hook_callable): |
725 | - """See IGPGClient.""" |
726 | - if not callable(hook_callable): |
727 | - raise TypeError("'hook_callable' parameter must be a callable.") |
728 | - self.write_hooks.add(hook_callable) |
729 | - |
730 | - def unregisterWriteHook(self, hook_callable): |
731 | - """See IGPGClient.""" |
732 | - if hook_callable not in self.write_hooks: |
733 | - raise ValueError("%r not registered.") |
734 | - self.write_hooks.remove(hook_callable) |
735 | - |
736 | - def addKeyForTest(self, owner_id, keyid, fingerprint, keysize, algorithm, enabled, |
737 | - can_encrypt): |
738 | - """See IGPGClient.""" |
739 | - document = {'keys': [{ |
740 | - 'owner': owner_id, |
741 | - 'id': keyid, |
742 | - 'fingerprint': fingerprint, |
743 | - 'size': keysize, |
744 | - 'algorithm': algorithm, |
745 | - 'enabled': enabled, |
746 | - 'can_encrypt': can_encrypt}]} |
747 | - path = '/test/add_keys' |
748 | - resp = self._request('post', path, document) |
749 | - if resp.status_code == http_codes['NOT_FOUND']: |
750 | - raise RuntimeError( |
751 | - "gpgservice was not configured with test endpoints enabled.") |
752 | - elif resp.status_code != http_codes['OK']: |
753 | - self.raise_for_error(resp) |
754 | - |
755 | - def _notify_writes(self): |
756 | - errors = [] |
757 | - for hook in self.write_hooks: |
758 | - try: |
759 | - hook() |
760 | - except Exception as e: |
761 | - errors.append(str(e)) |
762 | - if errors: |
763 | - raise Exception("The operation succeeded, but one or more write" |
764 | - " hooks failed: %s" % ', '.join(errors)) |
765 | - |
766 | - def _encode_owner_id(self, owner_id): |
767 | - return base64.b64encode(owner_id, altchars='-_') |
768 | - |
769 | - def raise_for_error(self, response): |
770 | - """Raise GPGServiceException based on what's in 'response'.""" |
771 | - if response.headers['Content-Type'] == 'application/json': |
772 | - message = response.json()['status'] |
773 | - else: |
774 | - message = "Unhandled service error. HTTP Status: %d HTTP Body: %s" % ( |
775 | - response.status_code, response.content) |
776 | - raise GPGServiceException(message) |
777 | - |
778 | - @property |
779 | - def timeout(self): |
780 | - # Perhaps this should be from config? |
781 | + def get_endpoint(self): |
782 | + return "http://{}".format(config.gpgservice.api_endpoint) |
783 | + |
784 | + def get_timeout(self): |
785 | return 30.0 |
786 | - |
787 | - @property |
788 | - def endpoint(self): |
789 | - return "http://{}".format(config.gpgservice.api_endpoint) |
790 | - |
791 | - def _request(self, method, path, data=None, **kwargs): |
792 | - response = getattr(requests, method)( |
793 | - urljoin(self.endpoint, path), json=data, timeout=self.timeout, **kwargs) |
794 | - return response |
795 | |
796 | === modified file 'lib/lp/services/gpg/interfaces.py' |
797 | --- lib/lp/services/gpg/interfaces.py 2016-03-16 00:32:46 +0000 |
798 | +++ lib/lp/services/gpg/interfaces.py 2016-03-21 05:51:58 +0000 |
799 | @@ -3,6 +3,7 @@ |
800 | |
801 | __all__ = [ |
802 | 'GPG_DATABASE_READONLY_FEATURE_FLAG', |
803 | + 'GPG_READ_FROM_GPGSERVICE_FEATURE_FLAG', |
804 | 'GPG_WRITE_TO_GPGSERVICE_FEATURE_FLAG', |
805 | 'GPGKeyAlgorithm', |
806 | 'GPGKeyDoesNotExistOnServer', |
807 | @@ -28,6 +29,7 @@ |
808 | import httplib |
809 | import re |
810 | |
811 | +from gpgservice_client import GPGServiceException |
812 | from lazr.enum import ( |
813 | DBEnumeratedType, |
814 | DBItem, |
815 | @@ -52,6 +54,7 @@ |
816 | |
817 | GPG_DATABASE_READONLY_FEATURE_FLAG = u"gpg.database_read_only" |
818 | GPG_WRITE_TO_GPGSERVICE_FEATURE_FLAG = u"gpg.write_to_gpgservice" |
819 | +GPG_READ_FROM_GPGSERVICE_FEATURE_FLAG = u"gpg.read_from_gpgservice" |
820 | |
821 | |
822 | def valid_fingerprint(fingerprint): |
823 | @@ -425,15 +428,6 @@ |
824 | comment = Attribute("The comment portion of this user ID") |
825 | |
826 | |
827 | -class GPGServiceException(Exception): |
828 | - |
829 | - """Raised when we get an error from the gpgservice. |
830 | - |
831 | - More specific errors for commonly encountered errors may be added once we |
832 | - actually integrate gpgservice with the rest of launchpad. |
833 | - """ |
834 | - |
835 | - |
836 | class IGPGClient(Interface): |
837 | |
838 | """A client for querying a gpgservice instance.""" |
839 | @@ -467,6 +461,13 @@ |
840 | :raises ValueError: if the fingerprint isn't valid. |
841 | """ |
842 | |
843 | + def getKeysByFingerprints(fingerprints): |
844 | + """Bulk retrieve GPG keys by a list of fingerprints. |
845 | + |
846 | + :param fingerprints: A list of fingerprints to retrieve. |
847 | + :returns: A list of keys that were found. |
848 | + """ |
849 | + |
850 | def registerWriteHook(hook_callable): |
851 | """Register a write hook. |
852 | |
853 | |
854 | === modified file 'lib/lp/services/gpg/tests/test_gpghandler.py' |
855 | --- lib/lp/services/gpg/tests/test_gpghandler.py 2016-03-18 00:48:59 +0000 |
856 | +++ lib/lp/services/gpg/tests/test_gpghandler.py 2016-03-21 05:51:58 +0000 |
857 | @@ -16,6 +16,11 @@ |
858 | from zope.component import getUtility |
859 | from zope.security.proxy import removeSecurityProxy |
860 | |
861 | +from lp.registry.interfaces.gpg import IGPGKeySet |
862 | +from lp.registry.interfaces.person import IPersonSet |
863 | +from lp.services.database.constants import THIRTY_DAYS_AGO |
864 | +from lp.services.database.interfaces import IMasterStore |
865 | +from lp.services.features.testing import FeatureFixture |
866 | from lp.services.gpg.interfaces import ( |
867 | GPGKeyDoesNotExistOnServer, |
868 | GPGKeyTemporarilyNotFoundError, |
869 | @@ -24,6 +29,7 @@ |
870 | IGPGHandler, |
871 | ) |
872 | from lp.services.log.logger import BufferLogger |
873 | +from lp.services.openid.model.openididentifier import OpenIdIdentifier |
874 | from lp.services.timeout import ( |
875 | get_default_timeout_function, |
876 | set_default_timeout_function, |
877 | @@ -277,7 +283,9 @@ |
878 | |
879 | def test_get_key_for_user_with_sampledata(self): |
880 | client = getUtility(IGPGClient) |
881 | - data = client.getKeysForOwner('name16_oid') |
882 | + person = getUtility(IPersonSet).getByName('name16') |
883 | + openid_id = getUtility(IGPGKeySet).getOwnerIdForPerson(person) |
884 | + data = client.getKeysForOwner(openid_id) |
885 | self.assertThat(data, ContainsDict({'keys': HasLength(1)})) |
886 | |
887 | def test_get_key_for_unknown_user(self): |
888 | @@ -339,7 +347,8 @@ |
889 | def test_adding_invalid_fingerprint_raises_ValueError(self): |
890 | client = getUtility(IGPGClient) |
891 | self.assertThat( |
892 | - lambda: client.addKeyForOwner(self.get_random_owner_id_string(), ''), |
893 | + lambda: client.addKeyForOwner( |
894 | + self.get_random_owner_id_string(), ''), |
895 | raises(ValueError("Invalid fingerprint: ''."))) |
896 | |
897 | def test_adding_duplicate_fingerprint_raises_GPGServiceException(self): |
898 | @@ -351,7 +360,8 @@ |
899 | client.addKeyForOwner(user_one, fingerprint) |
900 | self.assertThat( |
901 | lambda: client.addKeyForOwner(user_two, fingerprint), |
902 | - raises(GPGServiceException("Error: Fingerprint already in database."))) |
903 | + raises(GPGServiceException( |
904 | + "Error: Fingerprint already in database."))) |
905 | |
906 | def test_disabling_active_key(self): |
907 | self.useFixture(KeyServerTac()) |
908 | @@ -382,7 +392,8 @@ |
909 | def test_disabling_invalid_fingerprint_raises_ValueError(self): |
910 | client = getUtility(IGPGClient) |
911 | self.assertThat( |
912 | - lambda: client.disableKeyForOwner(self.get_random_owner_id_string(), ''), |
913 | + lambda: client.disableKeyForOwner( |
914 | + self.get_random_owner_id_string(), ''), |
915 | raises(ValueError("Invalid fingerprint: ''.")) |
916 | ) |
917 | |
918 | @@ -409,15 +420,76 @@ |
919 | raises(ValueError)) |
920 | |
921 | def test_can_add_IGPGKey_to_test_enabled_gpgservice(self): |
922 | + self.useFixture( |
923 | + FeatureFixture({'gpg.write_to_gpgservice': True})) |
924 | client = getUtility(IGPGClient) |
925 | person = self.factory.makePerson() |
926 | + # With the feature flag enabled, the following creates a |
927 | + # gpg key on the gpgservice. |
928 | gpgkey = self.factory.makeGPGKey(person) |
929 | - user = self.get_random_owner_id_string() |
930 | - client.addKeyForTest(user, gpgkey.keyid, gpgkey.fingerprint, |
931 | - gpgkey.keysize, gpgkey.algorithm.name, |
932 | - gpgkey.active, gpgkey.can_encrypt) |
933 | - |
934 | + user = getUtility(IGPGKeySet).getOwnerIdForPerson(person) |
935 | key = client.getKeyByFingerprint(gpgkey.fingerprint) |
936 | self.assertThat( |
937 | key, ContainsDict({'owner': Equals(user), |
938 | 'fingerprint': Equals(gpgkey.fingerprint)})) |
939 | + |
940 | + def makePersonWithMultipleGPGKeysInDifferentOpenids(self): |
941 | + """Make a person with multiple GPG keys owned by |
942 | + different openid identifiers. This happens as a result |
943 | + of an account merge. |
944 | + |
945 | + :returns: an IPerson instance with two keys under |
946 | + different openid identifiers. |
947 | + """ |
948 | + person = self.factory.makePerson() |
949 | + self.factory.makeGPGKey(person) |
950 | + # Create a second openid identifier from 30 days ago. |
951 | + # This simulates the account merge: |
952 | + identifier = OpenIdIdentifier() |
953 | + identifier.account = person.account |
954 | + identifier.identifier = u'openid_identifier' |
955 | + identifier.date_created = THIRTY_DAYS_AGO |
956 | + IMasterStore(OpenIdIdentifier).add(identifier) |
957 | + self.factory.makeGPGKey(person) |
958 | + return person |
959 | + |
960 | + def test_can_retrieve_keys_for_all_openid_identifiers(self): |
961 | + person = self.makePersonWithMultipleGPGKeysInDifferentOpenids() |
962 | + keys = getUtility(IGPGKeySet).getGPGKeysForPerson(person) |
963 | + self.assertThat(keys, HasLength(2)) |
964 | + |
965 | + def test_can_deactivate_all_keys_with_multiple_openid_identifiers(self): |
966 | + person = self.makePersonWithMultipleGPGKeysInDifferentOpenids() |
967 | + keyset = getUtility(IGPGKeySet) |
968 | + key_one, key_two = keyset.getGPGKeysForPerson(person) |
969 | + keyset.deactivate(key_one) |
970 | + keyset.deactivate(key_two) |
971 | + key_one, key_two = keyset.getGPGKeysForPerson(person, active=False) |
972 | + |
973 | + self.assertFalse(key_one.active) |
974 | + self.assertFalse(key_two.active) |
975 | + |
976 | + def test_can_reactivate_all_keys_with_multiple_openid_identifiers(self): |
977 | + person = self.makePersonWithMultipleGPGKeysInDifferentOpenids() |
978 | + keyset = getUtility(IGPGKeySet) |
979 | + for k in keyset.getGPGKeysForPerson(person): |
980 | + keyset.deactivate(k) |
981 | + for k in keyset.getGPGKeysForPerson(person, active=False): |
982 | + keyset.activate(person, k, k.can_encrypt) |
983 | + key_one, key_two = keyset.getGPGKeysForPerson(person) |
984 | + |
985 | + self.assertTrue(key_one.active) |
986 | + self.assertTrue(key_two.active) |
987 | + |
988 | + def test_cannot_reactivate_someone_elses_key(self): |
989 | + person1 = self.factory.makePerson() |
990 | + key = self.factory.makeGPGKey(person1) |
991 | + person2 = self.factory.makePerson() |
992 | + |
993 | + keyset = getUtility(IGPGKeySet) |
994 | + keyset.deactivate(key) |
995 | + self.assertRaises( |
996 | + AssertionError, |
997 | + keyset.activate, |
998 | + person2, key, key.can_encrypt |
999 | + ) |
1000 | |
1001 | === modified file 'lib/lp/services/verification/browser/tests/test_logintoken.py' |
1002 | --- lib/lp/services/verification/browser/tests/test_logintoken.py 2016-02-10 02:37:07 +0000 |
1003 | +++ lib/lp/services/verification/browser/tests/test_logintoken.py 2016-03-21 05:51:58 +0000 |
1004 | @@ -21,12 +21,14 @@ |
1005 | from lp.services.verification.interfaces.authtoken import LoginTokenType |
1006 | from lp.services.verification.interfaces.logintoken import ILoginTokenSet |
1007 | from lp.testing import ( |
1008 | - login_person, |
1009 | person_logged_in, |
1010 | TestCaseWithFactory, |
1011 | ) |
1012 | from lp.testing.deprecated import LaunchpadFormHarness |
1013 | -from lp.testing.layers import DatabaseFunctionalLayer |
1014 | +from lp.testing.layers import ( |
1015 | + DatabaseFunctionalLayer, |
1016 | + LaunchpadFunctionalLayer, |
1017 | + ) |
1018 | from lp.testing.views import create_initialized_view |
1019 | |
1020 | |
1021 | @@ -37,7 +39,7 @@ |
1022 | token to be consumed (so it can't be used again) when the user hits |
1023 | Cancel. |
1024 | """ |
1025 | - layer = DatabaseFunctionalLayer |
1026 | + layer = LaunchpadFunctionalLayer |
1027 | |
1028 | def setUp(self): |
1029 | TestCaseWithFactory.setUp(self) |
1030 | @@ -82,7 +84,7 @@ |
1031 | |
1032 | class LoginTokenReadOnlyTests(TestCaseWithFactory): |
1033 | |
1034 | - layer = DatabaseFunctionalLayer |
1035 | + layer = LaunchpadFunctionalLayer |
1036 | |
1037 | def test_continue_action_failed_with_gpg_database_in_ro_mode(self): |
1038 | self.useFixture(FeatureFixture({ |
1039 | |
1040 | === modified file 'lib/lp/soyuz/tests/test_archive_subscriptions.py' |
1041 | --- lib/lp/soyuz/tests/test_archive_subscriptions.py 2015-09-24 11:30:24 +0000 |
1042 | +++ lib/lp/soyuz/tests/test_archive_subscriptions.py 2016-03-21 05:51:58 +0000 |
1043 | @@ -21,7 +21,10 @@ |
1044 | StormStatementRecorder, |
1045 | TestCaseWithFactory, |
1046 | ) |
1047 | -from lp.testing.layers import DatabaseFunctionalLayer |
1048 | +from lp.testing.layers import ( |
1049 | + DatabaseFunctionalLayer, |
1050 | + LaunchpadFunctionalLayer, |
1051 | + ) |
1052 | from lp.testing.mail_helpers import pop_notifications |
1053 | from lp.testing.matchers import HasQueryCount |
1054 | from lp.testing.pages import ( |
1055 | @@ -129,7 +132,7 @@ |
1056 | class PrivateArtifactsViewTestCase(BrowserTestCase): |
1057 | """ Tests that private team archives can be viewed.""" |
1058 | |
1059 | - layer = DatabaseFunctionalLayer |
1060 | + layer = LaunchpadFunctionalLayer |
1061 | |
1062 | def setUp(self): |
1063 | """Create a test archive.""" |
1064 | |
1065 | === modified file 'lib/lp/testing/factory.py' |
1066 | --- lib/lp/testing/factory.py 2016-03-09 01:37:56 +0000 |
1067 | +++ lib/lp/testing/factory.py 2016-03-21 05:51:58 +0000 |
1068 | @@ -178,6 +178,7 @@ |
1069 | ) |
1070 | from lp.registry.interfaces.distroseriesparent import IDistroSeriesParentSet |
1071 | from lp.registry.interfaces.gpg import IGPGKeySet |
1072 | +from lp.registry.model.gpgkey import GPGServiceKey |
1073 | from lp.registry.interfaces.mailinglist import ( |
1074 | IMailingListSet, |
1075 | MailingListStatus, |
1076 | @@ -233,8 +234,10 @@ |
1077 | from lp.services.database.sqlbase import flush_database_updates |
1078 | from lp.services.features import getFeatureFlag |
1079 | from lp.services.gpg.interfaces import ( |
1080 | + GPG_READ_FROM_GPGSERVICE_FEATURE_FLAG, |
1081 | GPG_WRITE_TO_GPGSERVICE_FEATURE_FLAG, |
1082 | GPGKeyAlgorithm, |
1083 | + IGPGClient, |
1084 | IGPGHandler, |
1085 | ) |
1086 | from lp.services.identity.interfaces.account import ( |
1087 | @@ -603,6 +606,11 @@ |
1088 | client.addKeyForTest( |
1089 | openid_identifier, key.keyid, key.fingerprint, key.keysize, |
1090 | key.algorithm.name, key.active, key.can_encrypt) |
1091 | + # Sadly client.addKeyForTest does not return the key that |
1092 | + # was added: |
1093 | + if getFeatureFlag(GPG_READ_FROM_GPGSERVICE_FEATURE_FLAG): |
1094 | + return GPGServiceKey( |
1095 | + client.getKeyByFingerprint(key.fingerprint)) |
1096 | return key |
1097 | |
1098 | def makePerson( |
1099 | |
1100 | === modified file 'lib/lp/testing/gpgkeys/__init__.py' |
1101 | --- lib/lp/testing/gpgkeys/__init__.py 2016-02-24 22:43:07 +0000 |
1102 | +++ lib/lp/testing/gpgkeys/__init__.py 2016-03-21 05:51:58 +0000 |
1103 | @@ -31,6 +31,7 @@ |
1104 | from lp.services.gpg.interfaces import ( |
1105 | GPG_WRITE_TO_GPGSERVICE_FEATURE_FLAG, |
1106 | GPGKeyAlgorithm, |
1107 | + IGPGClient, |
1108 | IGPGHandler, |
1109 | ) |
1110 | |
1111 | |
1112 | === modified file 'lib/lp/testing/gpgservice/_fixture.py' |
1113 | --- lib/lp/testing/gpgservice/_fixture.py 2016-02-16 05:36:37 +0000 |
1114 | +++ lib/lp/testing/gpgservice/_fixture.py 2016-03-21 05:51:58 +0000 |
1115 | @@ -36,8 +36,8 @@ |
1116 | |
1117 | def setUp(self): |
1118 | super(GPGKeyServiceFixture, self).setUp() |
1119 | - # Write service config to a file on disk. This file gets deleted when the |
1120 | - # fixture ends. |
1121 | + # Write service config to a file on disk. This file gets deleted |
1122 | + # when the fixture ends. |
1123 | service_config = _get_default_service_config() |
1124 | self._config_file = NamedTemporaryFile() |
1125 | self.addCleanup(self._config_file.close) |
1126 | @@ -94,12 +94,15 @@ |
1127 | raise RuntimeError("Service not responding: %r" % errors) |
1128 | |
1129 | def reset_service_database(self): |
1130 | - """Reset the gpgservice instance database to the launchpad sampledata.""" |
1131 | + """Reset the gpgservice instance database to the launchpad sampledata. |
1132 | + """ |
1133 | conn = httplib.HTTPConnection(self.bind_address) |
1134 | test_data = { |
1135 | 'keys': [ |
1136 | { |
1137 | - 'owner': 'name16_oid', |
1138 | + 'owner': |
1139 | + config.launchpad.openid_provider_root |
1140 | + + '+id/name16_oid', |
1141 | 'id': '12345678', |
1142 | 'fingerprint': 'ABCDEF0123456789ABCDDCBA0000111112345678', |
1143 | 'size': 1024, |
1144 | |
1145 | === modified file 'lib/lp/testing/gpgservice/tests/test_fixture.py' |
1146 | --- lib/lp/testing/gpgservice/tests/test_fixture.py 2016-03-16 20:41:49 +0000 |
1147 | +++ lib/lp/testing/gpgservice/tests/test_fixture.py 2016-03-21 05:51:58 +0000 |
1148 | @@ -47,9 +47,10 @@ |
1149 | def test_fixture_can_create_test_data(self): |
1150 | fixture = self.useFixture(GPGKeyServiceFixture()) |
1151 | conn = httplib.HTTPConnection(fixture.bind_address) |
1152 | - conn.request( |
1153 | - 'GET', |
1154 | - '/users/%s/keys' % base64.b64encode('name16_oid', altchars='-_')) |
1155 | + user = base64.b64encode( |
1156 | + config.launchpad.openid_provider_root + '+id/name16_oid', |
1157 | + altchars='-_') |
1158 | + conn.request('GET', '/users/%s/keys' % user) |
1159 | resp = conn.getresponse() |
1160 | self.assertEqual(200, resp.status) |
1161 | data = json.loads(resp.read()) |
1162 | |
1163 | === modified file 'setup.py' |
1164 | --- setup.py 2016-02-05 20:28:29 +0000 |
1165 | +++ setup.py 2016-03-21 05:51:58 +0000 |
1166 | @@ -41,6 +41,7 @@ |
1167 | 'FeedParser', |
1168 | 'feedvalidator', |
1169 | 'funkload', |
1170 | + 'gpgservice-client', |
1171 | 'html5browser', |
1172 | 'httmock', |
1173 | 'pygpgme', |
1174 | |
1175 | === modified file 'versions.cfg' |
1176 | --- versions.cfg 2016-03-14 02:01:04 +0000 |
1177 | +++ versions.cfg 2016-03-21 05:51:58 +0000 |
1178 | @@ -39,6 +39,7 @@ |
1179 | FormEncode = 1.2.4 |
1180 | funkload = 1.16.1 |
1181 | gpgservice = 0.1.2 |
1182 | +gpgservice-client = 0.0.2 |
1183 | grokcore.component = 1.6 |
1184 | gunicorn = 19.4.5 |
1185 | html5browser = 0.0.9 |