Merge lp:~cjwatson/launchpad/archive-get-signing-key-data into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18738
Proposed branch: lp:~cjwatson/launchpad/archive-get-signing-key-data
Merge into: lp:launchpad
Diff against target: 203 lines (+98/-2)
4 files modified
lib/lp/services/gpg/interfaces.py (+5/-1)
lib/lp/soyuz/interfaces/archive.py (+12/-1)
lib/lp/soyuz/model/archive.py (+9/-0)
lib/lp/soyuz/tests/test_archive.py (+72/-0)
To merge this branch: bzr merge lp:~cjwatson/launchpad/archive-get-signing-key-data
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+350431@code.launchpad.net

Commit message

Add Archive.getSigningKeyData, currently just proxying through to the keyserver.

Description of the change

GPGHandler.retrieveKey uses urlfetch and thus honours the webapp request timeout, so this shouldn't kill the webapp even if the keyserver is timing out.

At the moment, the only use for this will be making it possible to add a PPA from a restricted network with one fewer firewall egress rule. If we go ahead with the plan to store PPA signing keys in the LP database, it will become more useful as we'll be able to remove the keyserver as a point of unreliability.

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 'lib/lp/services/gpg/interfaces.py'
2--- lib/lp/services/gpg/interfaces.py 2018-03-02 16:17:35 +0000
3+++ lib/lp/services/gpg/interfaces.py 2018-07-27 00:05:56 +0000
4@@ -1,4 +1,4 @@
5-# Copyright 2009-2017 Canonical Ltd. This software is licensed under the
6+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
7 # GNU Affero General Public License version 3 (see the file LICENSE).
8
9 __all__ = [
10@@ -21,12 +21,14 @@
11 'valid_keyid',
12 ]
13
14+import httplib
15 import re
16
17 from lazr.enum import (
18 DBEnumeratedType,
19 DBItem,
20 )
21+from lazr.restful.declarations import error_status
22 from zope.interface import (
23 Attribute,
24 Interface,
25@@ -112,6 +114,7 @@
26 super(GPGKeyNotFoundError, self).__init__(message)
27
28
29+@error_status(httplib.INTERNAL_SERVER_ERROR)
30 class GPGKeyTemporarilyNotFoundError(GPGKeyNotFoundError):
31 """The GPG key with the given fingerprint was not found on the keyserver.
32
33@@ -126,6 +129,7 @@
34 fingerprint, message)
35
36
37+@error_status(httplib.NOT_FOUND)
38 class GPGKeyDoesNotExistOnServer(GPGKeyNotFoundError):
39 """The GPG key with the given fingerprint was not found on the keyserver.
40
41
42=== modified file 'lib/lp/soyuz/interfaces/archive.py'
43--- lib/lp/soyuz/interfaces/archive.py 2018-06-27 00:27:02 +0000
44+++ lib/lp/soyuz/interfaces/archive.py 2018-07-27 00:05:56 +0000
45@@ -462,7 +462,18 @@
46 series_with_sources = Attribute(
47 "DistroSeries to which this archive has published sources")
48 signing_key = Object(
49- title=_('Repository sigining key.'), required=False, schema=IGPGKey)
50+ title=_('Repository signing key.'), required=False, schema=IGPGKey)
51+
52+ @export_read_operation()
53+ @operation_for_version("devel")
54+ def getSigningKeyData():
55+ """Get the public key used to sign this repository.
56+
57+ If the repository has a signing key but it cannot be retrieved from
58+ the keyserver, then the response will have an appropriate 4xx or 5xx
59+ HTTP status code. Otherwise, returns the public key material as a
60+ byte string, or None if the repository has no signing key.
61+ """
62
63 def getAuthToken(person):
64 """Returns an IArchiveAuthToken for the archive in question for
65
66=== modified file 'lib/lp/soyuz/model/archive.py'
67--- lib/lp/soyuz/model/archive.py 2018-06-27 00:27:02 +0000
68+++ lib/lp/soyuz/model/archive.py 2018-07-27 00:05:56 +0000
69@@ -120,6 +120,7 @@
70 )
71 from lp.services.database.stormexpr import BulkUpdate
72 from lp.services.features import getFeatureFlag
73+from lp.services.gpg.interfaces import IGPGHandler
74 from lp.services.job.interfaces.job import JobStatus
75 from lp.services.librarian.model import (
76 LibraryFileAlias,
77@@ -409,6 +410,14 @@
78 return getUtility(IGPGKeySet).getByFingerprint(
79 self.signing_key_fingerprint)
80
81+ def getSigningKeyData(self):
82+ """See `IArchive`."""
83+ if self.signing_key_fingerprint is not None:
84+ # This may raise GPGKeyNotFoundError, which we allow to
85+ # propagate as an HTTP error.
86+ return getUtility(IGPGHandler).retrieveKey(
87+ self.signing_key_fingerprint).export()
88+
89 @property
90 def is_ppa(self):
91 """See `IArchive`."""
92
93=== modified file 'lib/lp/soyuz/tests/test_archive.py'
94--- lib/lp/soyuz/tests/test_archive.py 2018-05-04 21:59:32 +0000
95+++ lib/lp/soyuz/tests/test_archive.py 2018-07-27 00:05:56 +0000
96@@ -11,9 +11,11 @@
97 timedelta,
98 )
99 import doctest
100+import httplib
101 import os.path
102
103 from pytz import UTC
104+import responses
105 import six
106 from testtools.matchers import (
107 AllMatch,
108@@ -55,11 +57,17 @@
109 from lp.services.database.sqlbase import sqlvalues
110 from lp.services.features import getFeatureFlag
111 from lp.services.features.testing import FeatureFixture
112+from lp.services.gpg.interfaces import (
113+ GPGKeyDoesNotExistOnServer,
114+ GPGKeyTemporarilyNotFoundError,
115+ IGPGHandler,
116+ )
117 from lp.services.job.interfaces.job import JobStatus
118 from lp.services.propertycache import (
119 clear_property_cache,
120 get_property_cache,
121 )
122+from lp.services.timeout import default_timeout
123 from lp.services.webapp.interfaces import OAuthPermission
124 from lp.services.worlddata.interfaces.country import ICountrySet
125 from lp.soyuz.adapters.archivedependencies import (
126@@ -138,6 +146,7 @@
127 )
128 from lp.testing.matchers import HasQueryCount
129 from lp.testing.pages import webservice_for_person
130+from lp.testing.views import create_webservice_error_view
131
132
133 class TestGetPublicationsInArchive(TestCaseWithFactory):
134@@ -3789,6 +3798,69 @@
135 self.assertEqual(person.gpg_keys[0], ppa_with_key.signing_key)
136
137
138+class TestGetSigningKeyData(TestCaseWithFactory):
139+ """Test `Archive.getSigningKeyData`.
140+
141+ We just use `responses` to mock the keyserver here; the details of its
142+ implementation aren't especially important, we can't use
143+ `InProcessKeyServerFixture` because the keyserver operations are
144+ synchronous, and `responses` is much faster than `KeyServerTac`.
145+ """
146+
147+ layer = DatabaseFunctionalLayer
148+ run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=10)
149+
150+ def test_getSigningKeyData_no_fingerprint(self):
151+ ppa = self.factory.makeArchive(purpose=ArchivePurpose.PPA)
152+ self.assertIsNone(ppa.getSigningKeyData())
153+
154+ @responses.activate
155+ def test_getSigningKeyData_keyserver_success(self):
156+ ppa = self.factory.makeArchive(purpose=ArchivePurpose.PPA)
157+ key_path = os.path.join(gpgkeysdir, "ppa-sample@canonical.com.sec")
158+ gpghandler = getUtility(IGPGHandler)
159+ with open(key_path, "rb") as key_file:
160+ secret_key = gpghandler.importSecretKey(key_file.read())
161+ public_key = gpghandler.retrieveKey(secret_key.fingerprint)
162+ public_key_data = public_key.export()
163+ removeSecurityProxy(ppa).signing_key_fingerprint = (
164+ public_key.fingerprint)
165+ key_url = gpghandler.getURLForKeyInServer(
166+ public_key.fingerprint, action="get")
167+ responses.add("GET", key_url, body=public_key_data)
168+ gpghandler.resetLocalState()
169+ with default_timeout(5.0):
170+ self.assertEqual(public_key_data, ppa.getSigningKeyData())
171+
172+ @responses.activate
173+ def test_getSigningKeyData_not_found_on_keyserver(self):
174+ ppa = self.factory.makeArchive(purpose=ArchivePurpose.PPA)
175+ gpghandler = getUtility(IGPGHandler)
176+ removeSecurityProxy(ppa).signing_key_fingerprint = "dummy-fp"
177+ key_url = gpghandler.getURLForKeyInServer("dummy-fp", action="get")
178+ responses.add(
179+ "GET", key_url, status=404,
180+ body="No results found: No keys found")
181+ with default_timeout(5.0):
182+ error = self.assertRaises(
183+ GPGKeyDoesNotExistOnServer, ppa.getSigningKeyData)
184+ error_view = create_webservice_error_view(error)
185+ self.assertEqual(httplib.NOT_FOUND, error_view.status)
186+
187+ @responses.activate
188+ def test_getSigningKeyData_keyserver_failure(self):
189+ ppa = self.factory.makeArchive(purpose=ArchivePurpose.PPA)
190+ gpghandler = getUtility(IGPGHandler)
191+ removeSecurityProxy(ppa).signing_key_fingerprint = "dummy-fp"
192+ key_url = gpghandler.getURLForKeyInServer("dummy-fp", action="get")
193+ responses.add("GET", key_url, status=500)
194+ with default_timeout(5.0):
195+ error = self.assertRaises(
196+ GPGKeyTemporarilyNotFoundError, ppa.getSigningKeyData)
197+ error_view = create_webservice_error_view(error)
198+ self.assertEqual(httplib.INTERNAL_SERVER_ERROR, error_view.status)
199+
200+
201 class TestCountersAndSummaries(TestCaseWithFactory):
202
203 layer = LaunchpadFunctionalLayer