Merge lp:~cjwatson/launchpad/snap-build-macaroon into lp:launchpad
- snap-build-macaroon
- Merge into devel
Proposed by
Colin Watson
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 18942 | ||||
Proposed branch: | lp:~cjwatson/launchpad/snap-build-macaroon | ||||
Merge into: | lp:launchpad | ||||
Prerequisite: | lp:~cjwatson/launchpad/librarian-accept-macaroon | ||||
Diff against target: |
977 lines (+469/-210) 11 files modified
lib/lp/code/model/codeimportjob.py (+30/-42) lib/lp/code/model/tests/test_codeimportjob.py (+27/-22) lib/lp/code/xmlrpc/git.py (+2/-2) lib/lp/services/authserver/tests/test_authserver.py (+23/-38) lib/lp/services/macaroons/interfaces.py (+19/-10) lib/lp/services/macaroons/model.py (+132/-0) lib/lp/snappy/configure.zcml (+8/-0) lib/lp/snappy/model/snapbuild.py (+58/-1) lib/lp/snappy/tests/test_snapbuild.py (+121/-1) lib/lp/soyuz/model/binarypackagebuild.py (+43/-72) lib/lp/soyuz/tests/test_binarypackagebuild.py (+6/-22) |
||||
To merge this branch: | bzr merge lp:~cjwatson/launchpad/snap-build-macaroon | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant (community) | code | Approve | |
Review via email: mp+364333@code.launchpad.net |
Commit message
Add a snap-build macaroon issuer.
Description of the change
I factored out some common code into a base class, since there are now three rather similar implementations.
Nothing uses this yet, but I'm planning for SnapBuildBehaviour to do so via an extension to the authserver.
To post a comment you must log in.
Revision history for this message
William Grant (wgrant) : | # |
review:
Approve
(code)
Revision history for this message
Colin Watson (cjwatson) : | # |
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'lib/lp/code/model/codeimportjob.py' | |||
2 | --- lib/lp/code/model/codeimportjob.py 2018-11-01 18:03:06 +0000 | |||
3 | +++ lib/lp/code/model/codeimportjob.py 2019-04-24 16:19:42 +0000 | |||
4 | @@ -1,4 +1,4 @@ | |||
6 | 1 | # Copyright 2009-2018 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2019 Canonical Ltd. This software is licensed under the |
7 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
8 | 3 | 3 | ||
9 | 4 | """Database classes for the CodeImportJob table.""" | 4 | """Database classes for the CodeImportJob table.""" |
10 | @@ -12,10 +12,6 @@ | |||
11 | 12 | 12 | ||
12 | 13 | import datetime | 13 | import datetime |
13 | 14 | 14 | ||
14 | 15 | from pymacaroons import ( | ||
15 | 16 | Macaroon, | ||
16 | 17 | Verifier, | ||
17 | 18 | ) | ||
18 | 19 | from sqlobject import ( | 15 | from sqlobject import ( |
19 | 20 | ForeignKey, | 16 | ForeignKey, |
20 | 21 | IntCol, | 17 | IntCol, |
21 | @@ -58,7 +54,11 @@ | |||
22 | 58 | SQLBase, | 54 | SQLBase, |
23 | 59 | sqlvalues, | 55 | sqlvalues, |
24 | 60 | ) | 56 | ) |
26 | 61 | from lp.services.macaroons.interfaces import IMacaroonIssuer | 57 | from lp.services.macaroons.interfaces import ( |
27 | 58 | BadMacaroonContext, | ||
28 | 59 | IMacaroonIssuer, | ||
29 | 60 | ) | ||
30 | 61 | from lp.services.macaroons.model import MacaroonIssuerBase | ||
31 | 62 | 62 | ||
32 | 63 | 63 | ||
33 | 64 | @implementer(ICodeImportJob) | 64 | @implementer(ICodeImportJob) |
34 | @@ -409,7 +409,9 @@ | |||
35 | 409 | 409 | ||
36 | 410 | 410 | ||
37 | 411 | @implementer(IMacaroonIssuer) | 411 | @implementer(IMacaroonIssuer) |
39 | 412 | class CodeImportJobMacaroonIssuer: | 412 | class CodeImportJobMacaroonIssuer(MacaroonIssuerBase): |
40 | 413 | |||
41 | 414 | identifier = "code-import-job" | ||
42 | 413 | 415 | ||
43 | 414 | @property | 416 | @property |
44 | 415 | def _root_secret(self): | 417 | def _root_secret(self): |
45 | @@ -423,38 +425,24 @@ | |||
46 | 423 | "launchpad.internal_macaroon_secret_key not configured.") | 425 | "launchpad.internal_macaroon_secret_key not configured.") |
47 | 424 | return secret | 426 | return secret |
48 | 425 | 427 | ||
84 | 426 | def issueMacaroon(self, context): | 428 | def checkIssuingContext(self, context): |
85 | 427 | """See `IMacaroonIssuer`.""" | 429 | """See `MacaroonIssuerBase`.""" |
86 | 428 | assert context.code_import.git_repository is not None | 430 | if context.code_import.git_repository is None: |
87 | 429 | macaroon = Macaroon( | 431 | raise BadMacaroonContext( |
88 | 430 | location=config.vhost.mainsite.hostname, | 432 | context, "context.code_import.git_repository is None") |
89 | 431 | identifier="code-import-job", key=self._root_secret) | 433 | return context.id |
90 | 432 | macaroon.add_first_party_caveat("lp.code-import-job %s" % context.id) | 434 | |
91 | 433 | return macaroon | 435 | def checkVerificationContext(self, context): |
92 | 434 | 436 | """See `MacaroonIssuerBase`.""" | |
93 | 435 | def checkMacaroonIssuer(self, macaroon): | 437 | if (not ICodeImportJob.providedBy(context) or |
94 | 436 | """See `IMacaroonIssuer`.""" | 438 | context.state != CodeImportJobState.RUNNING): |
95 | 437 | if macaroon.location != config.vhost.mainsite.hostname: | 439 | raise BadMacaroonContext(context) |
96 | 438 | return False | 440 | return context |
97 | 439 | try: | 441 | |
98 | 440 | verifier = Verifier() | 442 | def verifyPrimaryCaveat(self, caveat_value, context): |
99 | 441 | verifier.satisfy_general( | 443 | """See `MacaroonIssuerBase`.""" |
100 | 442 | lambda caveat: caveat.startswith("lp.code-import-job ")) | 444 | if context is None: |
101 | 443 | return verifier.verify(macaroon, self._root_secret) | 445 | # We're only verifying that the macaroon could be valid for some |
102 | 444 | except Exception: | 446 | # context. |
103 | 445 | return False | 447 | return True |
104 | 446 | 448 | return caveat_value == str(context.id) | |
70 | 447 | def verifyMacaroon(self, macaroon, context): | ||
71 | 448 | """See `IMacaroonIssuer`.""" | ||
72 | 449 | if not ICodeImportJob.providedBy(context): | ||
73 | 450 | return False | ||
74 | 451 | if not self.checkMacaroonIssuer(macaroon): | ||
75 | 452 | return False | ||
76 | 453 | try: | ||
77 | 454 | verifier = Verifier() | ||
78 | 455 | verifier.satisfy_exact("lp.code-import-job %s" % context.id) | ||
79 | 456 | return ( | ||
80 | 457 | verifier.verify(macaroon, self._root_secret) and | ||
81 | 458 | context.state == CodeImportJobState.RUNNING) | ||
82 | 459 | except Exception: | ||
83 | 460 | return False | ||
105 | 461 | 449 | ||
106 | === modified file 'lib/lp/code/model/tests/test_codeimportjob.py' | |||
107 | --- lib/lp/code/model/tests/test_codeimportjob.py 2018-11-01 18:03:06 +0000 | |||
108 | +++ lib/lp/code/model/tests/test_codeimportjob.py 2019-04-24 16:19:42 +0000 | |||
109 | @@ -1,4 +1,4 @@ | |||
111 | 1 | # Copyright 2009-2018 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2019 Canonical Ltd. This software is licensed under the |
112 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
113 | 3 | 3 | ||
114 | 4 | """Unit tests for CodeImportJob and CodeImportJobWorkflow.""" | 4 | """Unit tests for CodeImportJob and CodeImportJobWorkflow.""" |
115 | @@ -59,7 +59,10 @@ | |||
116 | 59 | from lp.services.database.sqlbase import get_transaction_timestamp | 59 | from lp.services.database.sqlbase import get_transaction_timestamp |
117 | 60 | from lp.services.librarian.interfaces import ILibraryFileAliasSet | 60 | from lp.services.librarian.interfaces import ILibraryFileAliasSet |
118 | 61 | from lp.services.librarian.interfaces.client import ILibrarianClient | 61 | from lp.services.librarian.interfaces.client import ILibrarianClient |
120 | 62 | from lp.services.macaroons.interfaces import IMacaroonIssuer | 62 | from lp.services.macaroons.interfaces import ( |
121 | 63 | BadMacaroonContext, | ||
122 | 64 | IMacaroonIssuer, | ||
123 | 65 | ) | ||
124 | 63 | from lp.services.webapp import canonical_url | 66 | from lp.services.webapp import canonical_url |
125 | 64 | from lp.testing import ( | 67 | from lp.testing import ( |
126 | 65 | ANONYMOUS, | 68 | ANONYMOUS, |
127 | @@ -1285,7 +1288,7 @@ | |||
128 | 1285 | job = self.makeJob(target_rcs_type=TargetRevisionControlSystems.BZR) | 1288 | job = self.makeJob(target_rcs_type=TargetRevisionControlSystems.BZR) |
129 | 1286 | issuer = getUtility(IMacaroonIssuer, "code-import-job") | 1289 | issuer = getUtility(IMacaroonIssuer, "code-import-job") |
130 | 1287 | self.assertRaises( | 1290 | self.assertRaises( |
132 | 1288 | AssertionError, removeSecurityProxy(issuer).issueMacaroon, job) | 1291 | BadMacaroonContext, removeSecurityProxy(issuer).issueMacaroon, job) |
133 | 1289 | 1292 | ||
134 | 1290 | def test_issueMacaroon_good(self): | 1293 | def test_issueMacaroon_good(self): |
135 | 1291 | job = self.makeJob() | 1294 | job = self.makeJob() |
136 | @@ -1303,25 +1306,6 @@ | |||
137 | 1303 | self.pushConfig("codeimport", macaroon_secret_key="some-secret") | 1306 | self.pushConfig("codeimport", macaroon_secret_key="some-secret") |
138 | 1304 | self.test_issueMacaroon_good() | 1307 | self.test_issueMacaroon_good() |
139 | 1305 | 1308 | ||
140 | 1306 | def test_checkMacaroonIssuer_good(self): | ||
141 | 1307 | job = self.makeJob() | ||
142 | 1308 | issuer = getUtility(IMacaroonIssuer, "code-import-job") | ||
143 | 1309 | macaroon = removeSecurityProxy(issuer).issueMacaroon(job) | ||
144 | 1310 | self.assertTrue(issuer.checkMacaroonIssuer(macaroon)) | ||
145 | 1311 | |||
146 | 1312 | def test_checkMacaroonIssuer_wrong_location(self): | ||
147 | 1313 | issuer = getUtility(IMacaroonIssuer, "code-import-job") | ||
148 | 1314 | macaroon = Macaroon( | ||
149 | 1315 | location="another-location", | ||
150 | 1316 | key=removeSecurityProxy(issuer)._root_secret) | ||
151 | 1317 | self.assertFalse(issuer.checkMacaroonIssuer(macaroon)) | ||
152 | 1318 | |||
153 | 1319 | def test_checkMacaroonIssuer_wrong_key(self): | ||
154 | 1320 | issuer = getUtility(IMacaroonIssuer, "code-import-job") | ||
155 | 1321 | macaroon = Macaroon( | ||
156 | 1322 | location=config.vhost.mainsite.hostname, key="another-secret") | ||
157 | 1323 | self.assertFalse(issuer.checkMacaroonIssuer(macaroon)) | ||
158 | 1324 | |||
159 | 1325 | def test_verifyMacaroon_good(self): | 1309 | def test_verifyMacaroon_good(self): |
160 | 1326 | machine = self.factory.makeCodeImportMachine(set_online=True) | 1310 | machine = self.factory.makeCodeImportMachine(set_online=True) |
161 | 1327 | job = self.makeJob() | 1311 | job = self.makeJob() |
162 | @@ -1330,6 +1314,23 @@ | |||
163 | 1330 | macaroon = removeSecurityProxy(issuer).issueMacaroon(job) | 1314 | macaroon = removeSecurityProxy(issuer).issueMacaroon(job) |
164 | 1331 | self.assertTrue(issuer.verifyMacaroon(macaroon, job)) | 1315 | self.assertTrue(issuer.verifyMacaroon(macaroon, job)) |
165 | 1332 | 1316 | ||
166 | 1317 | def test_verifyMacaroon_good_no_context(self): | ||
167 | 1318 | machine = self.factory.makeCodeImportMachine(set_online=True) | ||
168 | 1319 | job = self.makeJob() | ||
169 | 1320 | issuer = getUtility(IMacaroonIssuer, "code-import-job") | ||
170 | 1321 | getUtility(ICodeImportJobWorkflow).startJob(job, machine) | ||
171 | 1322 | macaroon = removeSecurityProxy(issuer).issueMacaroon(job) | ||
172 | 1323 | self.assertTrue( | ||
173 | 1324 | issuer.verifyMacaroon(macaroon, None, require_context=False)) | ||
174 | 1325 | |||
175 | 1326 | def test_verifyMacaroon_no_context_but_require_context(self): | ||
176 | 1327 | machine = self.factory.makeCodeImportMachine(set_online=True) | ||
177 | 1328 | job = self.makeJob() | ||
178 | 1329 | issuer = getUtility(IMacaroonIssuer, "code-import-job") | ||
179 | 1330 | getUtility(ICodeImportJobWorkflow).startJob(job, machine) | ||
180 | 1331 | macaroon = removeSecurityProxy(issuer).issueMacaroon(job) | ||
181 | 1332 | self.assertFalse(issuer.verifyMacaroon(macaroon, None)) | ||
182 | 1333 | |||
183 | 1333 | def test_verifyMacaroon_wrong_location(self): | 1334 | def test_verifyMacaroon_wrong_location(self): |
184 | 1334 | machine = self.factory.makeCodeImportMachine(set_online=True) | 1335 | machine = self.factory.makeCodeImportMachine(set_online=True) |
185 | 1335 | job = self.makeJob() | 1336 | job = self.makeJob() |
186 | @@ -1339,6 +1340,8 @@ | |||
187 | 1339 | location="another-location", | 1340 | location="another-location", |
188 | 1340 | key=removeSecurityProxy(issuer)._root_secret) | 1341 | key=removeSecurityProxy(issuer)._root_secret) |
189 | 1341 | self.assertFalse(issuer.verifyMacaroon(macaroon, job)) | 1342 | self.assertFalse(issuer.verifyMacaroon(macaroon, job)) |
190 | 1343 | self.assertFalse( | ||
191 | 1344 | issuer.verifyMacaroon(macaroon, None, require_context=False)) | ||
192 | 1342 | 1345 | ||
193 | 1343 | def test_verifyMacaroon_wrong_key(self): | 1346 | def test_verifyMacaroon_wrong_key(self): |
194 | 1344 | machine = self.factory.makeCodeImportMachine(set_online=True) | 1347 | machine = self.factory.makeCodeImportMachine(set_online=True) |
195 | @@ -1348,6 +1351,8 @@ | |||
196 | 1348 | macaroon = Macaroon( | 1351 | macaroon = Macaroon( |
197 | 1349 | location=config.vhost.mainsite.hostname, key="another-secret") | 1352 | location=config.vhost.mainsite.hostname, key="another-secret") |
198 | 1350 | self.assertFalse(issuer.verifyMacaroon(macaroon, job)) | 1353 | self.assertFalse(issuer.verifyMacaroon(macaroon, job)) |
199 | 1354 | self.assertFalse( | ||
200 | 1355 | issuer.verifyMacaroon(macaroon, None, require_context=False)) | ||
201 | 1351 | 1356 | ||
202 | 1352 | def test_verifyMacaroon_not_running(self): | 1357 | def test_verifyMacaroon_not_running(self): |
203 | 1353 | job = self.makeJob() | 1358 | job = self.makeJob() |
204 | 1354 | 1359 | ||
205 | === modified file 'lib/lp/code/xmlrpc/git.py' | |||
206 | --- lib/lp/code/xmlrpc/git.py 2019-04-23 12:30:16 +0000 | |||
207 | +++ lib/lp/code/xmlrpc/git.py 2019-04-24 16:19:42 +0000 | |||
208 | @@ -101,9 +101,9 @@ | |||
209 | 101 | job = code_import.import_job | 101 | job = code_import.import_job |
210 | 102 | if job is None: | 102 | if job is None: |
211 | 103 | return False | 103 | return False |
212 | 104 | return issuer.verifyMacaroon(macaroon, job) | ||
213 | 105 | else: | 104 | else: |
215 | 106 | return issuer.checkMacaroonIssuer(macaroon) | 105 | job = None |
216 | 106 | return issuer.verifyMacaroon(macaroon, job, require_context=False) | ||
217 | 107 | 107 | ||
218 | 108 | def _performLookup(self, requester, path, auth_params): | 108 | def _performLookup(self, requester, path, auth_params): |
219 | 109 | repository, extra_path = getUtility(IGitLookup).getByPath(path) | 109 | repository, extra_path = getUtility(IGitLookup).getByPath(path) |
220 | 110 | 110 | ||
221 | === modified file 'lib/lp/services/authserver/tests/test_authserver.py' | |||
222 | --- lib/lp/services/authserver/tests/test_authserver.py 2019-04-23 13:50:35 +0000 | |||
223 | +++ lib/lp/services/authserver/tests/test_authserver.py 2019-04-24 16:19:42 +0000 | |||
224 | @@ -5,10 +5,7 @@ | |||
225 | 5 | 5 | ||
226 | 6 | __metaclass__ = type | 6 | __metaclass__ = type |
227 | 7 | 7 | ||
232 | 8 | from pymacaroons import ( | 8 | from pymacaroons import Macaroon |
229 | 9 | Macaroon, | ||
230 | 10 | Verifier, | ||
231 | 11 | ) | ||
233 | 12 | from storm.sqlobject import SQLObjectNotFound | 9 | from storm.sqlobject import SQLObjectNotFound |
234 | 13 | from testtools.matchers import Is | 10 | from testtools.matchers import Is |
235 | 14 | from zope.component import getUtility | 11 | from zope.component import getUtility |
236 | @@ -21,7 +18,11 @@ | |||
237 | 21 | ILibraryFileAlias, | 18 | ILibraryFileAlias, |
238 | 22 | ILibraryFileAliasSet, | 19 | ILibraryFileAliasSet, |
239 | 23 | ) | 20 | ) |
241 | 24 | from lp.services.macaroons.interfaces import IMacaroonIssuer | 21 | from lp.services.macaroons.interfaces import ( |
242 | 22 | BadMacaroonContext, | ||
243 | 23 | IMacaroonIssuer, | ||
244 | 24 | ) | ||
245 | 25 | from lp.services.macaroons.model import MacaroonIssuerBase | ||
246 | 25 | from lp.testing import ( | 26 | from lp.testing import ( |
247 | 26 | person_logged_in, | 27 | person_logged_in, |
248 | 27 | TestCase, | 28 | TestCase, |
249 | @@ -78,42 +79,26 @@ | |||
250 | 78 | 79 | ||
251 | 79 | 80 | ||
252 | 80 | @implementer(IMacaroonIssuer) | 81 | @implementer(IMacaroonIssuer) |
254 | 81 | class DummyMacaroonIssuer: | 82 | class DummyMacaroonIssuer(MacaroonIssuerBase): |
255 | 82 | 83 | ||
256 | 84 | identifier = 'test' | ||
257 | 83 | _root_secret = 'test' | 85 | _root_secret = 'test' |
258 | 84 | 86 | ||
291 | 85 | def issueMacaroon(self, context): | 87 | def checkIssuingContext(self, context): |
292 | 86 | """See `IMacaroonIssuer`.""" | 88 | """See `MacaroonIssuerBase`.""" |
293 | 87 | macaroon = Macaroon( | 89 | if not ILibraryFileAlias.providedBy(context): |
294 | 88 | location=config.vhost.mainsite.hostname, identifier='test', | 90 | raise BadMacaroonContext(context) |
295 | 89 | key=self._root_secret) | 91 | return context.id |
296 | 90 | macaroon.add_first_party_caveat('test %s' % context.id) | 92 | |
297 | 91 | return macaroon | 93 | def checkVerificationContext(self, context): |
298 | 92 | 94 | """See `IMacaroonIssuerBase`.""" | |
299 | 93 | def checkMacaroonIssuer(self, macaroon): | 95 | if not ILibraryFileAlias.providedBy(context): |
300 | 94 | """See `IMacaroonIssuer`.""" | 96 | raise BadMacaroonContext(context) |
301 | 95 | if macaroon.location != config.vhost.mainsite.hostname: | 97 | return context |
302 | 96 | return False | 98 | |
303 | 97 | try: | 99 | def verifyPrimaryCaveat(self, caveat_value, context): |
304 | 98 | verifier = Verifier() | 100 | """See `MacaroonIssuerBase`.""" |
305 | 99 | verifier.satisfy_general( | 101 | return caveat_value == str(context.id) |
274 | 100 | lambda caveat: caveat.startswith('test ')) | ||
275 | 101 | return verifier.verify(macaroon, self._root_secret) | ||
276 | 102 | except Exception: | ||
277 | 103 | return False | ||
278 | 104 | |||
279 | 105 | def verifyMacaroon(self, macaroon, context): | ||
280 | 106 | """See `IMacaroonIssuer`.""" | ||
281 | 107 | if not ILibraryFileAlias.providedBy(context): | ||
282 | 108 | return False | ||
283 | 109 | if not self.checkMacaroonIssuer(macaroon): | ||
284 | 110 | return False | ||
285 | 111 | try: | ||
286 | 112 | verifier = Verifier() | ||
287 | 113 | verifier.satisfy_exact('test %s' % context.id) | ||
288 | 114 | return verifier.verify(macaroon, self._root_secret) | ||
289 | 115 | except Exception: | ||
290 | 116 | return False | ||
306 | 117 | 102 | ||
307 | 118 | 103 | ||
308 | 119 | class VerifyMacaroonTests(TestCase): | 104 | class VerifyMacaroonTests(TestCase): |
309 | 120 | 105 | ||
310 | === modified file 'lib/lp/services/macaroons/interfaces.py' | |||
311 | --- lib/lp/services/macaroons/interfaces.py 2016-10-07 15:50:10 +0000 | |||
312 | +++ lib/lp/services/macaroons/interfaces.py 2019-04-24 16:19:42 +0000 | |||
313 | @@ -1,4 +1,4 @@ | |||
315 | 1 | # Copyright 2016 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2016-2019 Canonical Ltd. This software is licensed under the |
316 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
317 | 3 | 3 | ||
318 | 4 | """Interface to a policy for issuing and verifying macaroons.""" | 4 | """Interface to a policy for issuing and verifying macaroons.""" |
319 | @@ -7,28 +7,36 @@ | |||
320 | 7 | 7 | ||
321 | 8 | __metaclass__ = type | 8 | __metaclass__ = type |
322 | 9 | __all__ = [ | 9 | __all__ = [ |
323 | 10 | 'BadMacaroonContext', | ||
324 | 10 | 'IMacaroonIssuer', | 11 | 'IMacaroonIssuer', |
325 | 11 | ] | 12 | ] |
326 | 12 | 13 | ||
327 | 13 | from zope.interface import Interface | 14 | from zope.interface import Interface |
328 | 14 | 15 | ||
329 | 15 | 16 | ||
330 | 17 | class BadMacaroonContext(Exception): | ||
331 | 18 | """The requested context is unsuitable.""" | ||
332 | 19 | |||
333 | 20 | def __init__(self, context, message=None): | ||
334 | 21 | if message is None: | ||
335 | 22 | message = "Cannot handle context %r." % context | ||
336 | 23 | super(BadMacaroonContext, self).__init__(message) | ||
337 | 24 | self.context = context | ||
338 | 25 | |||
339 | 26 | |||
340 | 16 | class IMacaroonIssuerPublic(Interface): | 27 | class IMacaroonIssuerPublic(Interface): |
341 | 17 | """Public interface to a policy for verifying macaroons.""" | 28 | """Public interface to a policy for verifying macaroons.""" |
342 | 18 | 29 | ||
352 | 19 | def checkMacaroonIssuer(macaroon): | 30 | def verifyMacaroon(macaroon, context, require_context=True): |
344 | 20 | """Check that `macaroon` was issued by this issuer. | ||
345 | 21 | |||
346 | 22 | This does not verify that the macaroon is valid for a given context, | ||
347 | 23 | only that it could be valid for some context. Use this in the | ||
348 | 24 | authentication part of an authentication/authorisation API. | ||
349 | 25 | """ | ||
350 | 26 | |||
351 | 27 | def verifyMacaroon(macaroon, context): | ||
353 | 28 | """Verify that `macaroon` is valid for `context`. | 31 | """Verify that `macaroon` is valid for `context`. |
354 | 29 | 32 | ||
355 | 30 | :param macaroon: A `Macaroon`. | 33 | :param macaroon: A `Macaroon`. |
356 | 31 | :param context: The context to check. | 34 | :param context: The context to check. |
357 | 35 | :param require_context: If True (the default), fail verification if | ||
358 | 36 | the context is None. If False and the context is None, only | ||
359 | 37 | verify that the macaroon could be valid for some context. Use | ||
360 | 38 | this in the authentication part of an | ||
361 | 39 | authentication/authorisation API. | ||
362 | 32 | :return: True if `macaroon` is valid for `context`, otherwise False. | 40 | :return: True if `macaroon` is valid for `context`, otherwise False. |
363 | 33 | """ | 41 | """ |
364 | 34 | 42 | ||
365 | @@ -41,5 +49,6 @@ | |||
366 | 41 | 49 | ||
367 | 42 | :param context: The context that the returned macaroon should relate | 50 | :param context: The context that the returned macaroon should relate |
368 | 43 | to. | 51 | to. |
369 | 52 | :raises ValueError: if the context is unsuitable. | ||
370 | 44 | :return: A macaroon. | 53 | :return: A macaroon. |
371 | 45 | """ | 54 | """ |
372 | 46 | 55 | ||
373 | === added file 'lib/lp/services/macaroons/model.py' | |||
374 | --- lib/lp/services/macaroons/model.py 1970-01-01 00:00:00 +0000 | |||
375 | +++ lib/lp/services/macaroons/model.py 2019-04-24 16:19:42 +0000 | |||
376 | @@ -0,0 +1,132 @@ | |||
377 | 1 | # Copyright 2016-2019 Canonical Ltd. This software is licensed under the | ||
378 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
379 | 3 | |||
380 | 4 | """Policies for issuing and verifying macaroons.""" | ||
381 | 5 | |||
382 | 6 | from __future__ import absolute_import, print_function, unicode_literals | ||
383 | 7 | |||
384 | 8 | __metaclass__ = type | ||
385 | 9 | __all__ = [ | ||
386 | 10 | "MacaroonIssuerBase", | ||
387 | 11 | ] | ||
388 | 12 | |||
389 | 13 | from pymacaroons import ( | ||
390 | 14 | Macaroon, | ||
391 | 15 | Verifier, | ||
392 | 16 | ) | ||
393 | 17 | from pymacaroons.exceptions import MacaroonVerificationFailedException | ||
394 | 18 | |||
395 | 19 | from lp.services.config import config | ||
396 | 20 | from lp.services.macaroons.interfaces import BadMacaroonContext | ||
397 | 21 | |||
398 | 22 | |||
399 | 23 | class MacaroonIssuerBase: | ||
400 | 24 | """See `IMacaroonIssuer`.""" | ||
401 | 25 | |||
402 | 26 | @property | ||
403 | 27 | def identifier(self): | ||
404 | 28 | """An identifying name for this issuer.""" | ||
405 | 29 | raise NotImplementedError | ||
406 | 30 | |||
407 | 31 | @property | ||
408 | 32 | def _primary_caveat_name(self): | ||
409 | 33 | """The name of the primary context caveat issued by this issuer.""" | ||
410 | 34 | return "lp.%s" % self.identifier | ||
411 | 35 | |||
412 | 36 | @property | ||
413 | 37 | def _root_secret(self): | ||
414 | 38 | secret = config.launchpad.internal_macaroon_secret_key | ||
415 | 39 | if not secret: | ||
416 | 40 | raise RuntimeError( | ||
417 | 41 | "launchpad.internal_macaroon_secret_key not configured.") | ||
418 | 42 | return secret | ||
419 | 43 | |||
420 | 44 | def checkIssuingContext(self, context): | ||
421 | 45 | """Check that the issuing context is suitable. | ||
422 | 46 | |||
423 | 47 | Concrete implementations may implement this method to check that the | ||
424 | 48 | context of a macaroon issuance is suitable. The returned context is | ||
425 | 49 | used to create the primary caveat, and may be the same context that | ||
426 | 50 | was passed in or an adapted one. | ||
427 | 51 | |||
428 | 52 | :param context: The context to check. | ||
429 | 53 | :raises BadMacaroonContext: if the context is unsuitable. | ||
430 | 54 | :return: The context to use to create the primary caveat. | ||
431 | 55 | """ | ||
432 | 56 | return context | ||
433 | 57 | |||
434 | 58 | def issueMacaroon(self, context): | ||
435 | 59 | """See `IMacaroonIssuer`. | ||
436 | 60 | |||
437 | 61 | Concrete implementations should normally wrap this with some | ||
438 | 62 | additional checks of and/or changes to the context. | ||
439 | 63 | """ | ||
440 | 64 | context = self.checkIssuingContext(context) | ||
441 | 65 | macaroon = Macaroon( | ||
442 | 66 | location=config.vhost.mainsite.hostname, | ||
443 | 67 | identifier=self.identifier, key=self._root_secret) | ||
444 | 68 | macaroon.add_first_party_caveat( | ||
445 | 69 | "%s %s" % (self._primary_caveat_name, context)) | ||
446 | 70 | return macaroon | ||
447 | 71 | |||
448 | 72 | def checkVerificationContext(self, context): | ||
449 | 73 | """Check that the verification context is suitable. | ||
450 | 74 | |||
451 | 75 | Concrete implementations may implement this method to check that the | ||
452 | 76 | context of a macaroon verification is suitable. The returned | ||
453 | 77 | context is passed to individual caveat checkers, and may be the same | ||
454 | 78 | context that was passed in or an adapted one. | ||
455 | 79 | |||
456 | 80 | :param context: The context to check. | ||
457 | 81 | :raises BadMacaroonContext: if the context is unsuitable. | ||
458 | 82 | :return: The context to pass to individual caveat checkers. | ||
459 | 83 | """ | ||
460 | 84 | return context | ||
461 | 85 | |||
462 | 86 | def verifyPrimaryCaveat(self, caveat_value, context): | ||
463 | 87 | """Verify the primary context caveat on one of this issuer's macaroons. | ||
464 | 88 | |||
465 | 89 | :param caveat_value: The text of the caveat, with this issuer's | ||
466 | 90 | prefix removed. | ||
467 | 91 | :param context: The context to check. | ||
468 | 92 | :return: True if this caveat is allowed, otherwise False. | ||
469 | 93 | """ | ||
470 | 94 | raise NotImplementedError | ||
471 | 95 | |||
472 | 96 | def verifyMacaroon(self, macaroon, context, require_context=True): | ||
473 | 97 | """See `IMacaroonIssuer`.""" | ||
474 | 98 | if macaroon.location != config.vhost.mainsite.hostname: | ||
475 | 99 | return False | ||
476 | 100 | if require_context and context is None: | ||
477 | 101 | return False | ||
478 | 102 | if context is not None: | ||
479 | 103 | try: | ||
480 | 104 | context = self.checkVerificationContext(context) | ||
481 | 105 | except BadMacaroonContext: | ||
482 | 106 | return False | ||
483 | 107 | |||
484 | 108 | def verify(caveat): | ||
485 | 109 | try: | ||
486 | 110 | caveat_name, caveat_value = caveat.split(" ", 1) | ||
487 | 111 | except ValueError: | ||
488 | 112 | return False | ||
489 | 113 | if caveat_name == self._primary_caveat_name: | ||
490 | 114 | checker = self.verifyPrimaryCaveat | ||
491 | 115 | else: | ||
492 | 116 | # XXX cjwatson 2019-04-09: For now we just fail closed if | ||
493 | 117 | # there are any other caveats, which is good enough for | ||
494 | 118 | # internal use. | ||
495 | 119 | return False | ||
496 | 120 | return checker(caveat_value, context) | ||
497 | 121 | |||
498 | 122 | try: | ||
499 | 123 | verifier = Verifier() | ||
500 | 124 | verifier.satisfy_general(verify) | ||
501 | 125 | return verifier.verify(macaroon, self._root_secret) | ||
502 | 126 | # XXX cjwatson 2019-04-24: This can currently raise a number of | ||
503 | 127 | # other exceptions in the presence of non-well-formed input data, | ||
504 | 128 | # but most of them are too broad to reasonably catch so we let them | ||
505 | 129 | # turn into OOPSes for now. Revisit this once | ||
506 | 130 | # https://github.com/ecordell/pymacaroons/issues/51 is fixed. | ||
507 | 131 | except MacaroonVerificationFailedException: | ||
508 | 132 | return False | ||
509 | 0 | 133 | ||
510 | === modified file 'lib/lp/snappy/configure.zcml' | |||
511 | --- lib/lp/snappy/configure.zcml 2019-02-11 13:23:34 +0000 | |||
512 | +++ lib/lp/snappy/configure.zcml 2019-04-24 16:19:42 +0000 | |||
513 | @@ -83,6 +83,14 @@ | |||
514 | 83 | <allow interface="lp.buildmaster.interfaces.buildfarmjob.ISpecificBuildFarmJobSource" /> | 83 | <allow interface="lp.buildmaster.interfaces.buildfarmjob.ISpecificBuildFarmJobSource" /> |
515 | 84 | </securedutility> | 84 | </securedutility> |
516 | 85 | 85 | ||
517 | 86 | <!-- SnapBuildMacaroonIssuer --> | ||
518 | 87 | <securedutility | ||
519 | 88 | class="lp.snappy.model.snapbuild.SnapBuildMacaroonIssuer" | ||
520 | 89 | provides="lp.services.macaroons.interfaces.IMacaroonIssuer" | ||
521 | 90 | name="snap-build"> | ||
522 | 91 | <allow interface="lp.services.macaroons.interfaces.IMacaroonIssuerPublic"/> | ||
523 | 92 | </securedutility> | ||
524 | 93 | |||
525 | 86 | <!-- SnapBuildBehaviour --> | 94 | <!-- SnapBuildBehaviour --> |
526 | 87 | <adapter | 95 | <adapter |
527 | 88 | for="lp.snappy.interfaces.snapbuild.ISnapBuild" | 96 | for="lp.snappy.interfaces.snapbuild.ISnapBuild" |
528 | 89 | 97 | ||
529 | === modified file 'lib/lp/snappy/model/snapbuild.py' | |||
530 | --- lib/lp/snappy/model/snapbuild.py 2018-12-18 18:14:37 +0000 | |||
531 | +++ lib/lp/snappy/model/snapbuild.py 2019-04-24 16:19:42 +0000 | |||
532 | @@ -1,4 +1,4 @@ | |||
534 | 1 | # Copyright 2015-2018 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2015-2019 Canonical Ltd. This software is licensed under the |
535 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
536 | 3 | 3 | ||
537 | 4 | __metaclass__ = type | 4 | __metaclass__ = type |
538 | @@ -35,6 +35,7 @@ | |||
539 | 35 | from zope.component.interfaces import ObjectEvent | 35 | from zope.component.interfaces import ObjectEvent |
540 | 36 | from zope.event import notify | 36 | from zope.event import notify |
541 | 37 | from zope.interface import implementer | 37 | from zope.interface import implementer |
542 | 38 | from zope.security.proxy import removeSecurityProxy | ||
543 | 38 | 39 | ||
544 | 39 | from lp.app.errors import NotFoundError | 40 | from lp.app.errors import NotFoundError |
545 | 40 | from lp.buildmaster.enums import ( | 41 | from lp.buildmaster.enums import ( |
546 | @@ -45,6 +46,7 @@ | |||
547 | 45 | from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJobSource | 46 | from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJobSource |
548 | 46 | from lp.buildmaster.model.buildfarmjob import SpecificBuildFarmJobSourceMixin | 47 | from lp.buildmaster.model.buildfarmjob import SpecificBuildFarmJobSourceMixin |
549 | 47 | from lp.buildmaster.model.packagebuild import PackageBuildMixin | 48 | from lp.buildmaster.model.packagebuild import PackageBuildMixin |
550 | 49 | from lp.code.interfaces.gitrepository import IGitRepository | ||
551 | 48 | from lp.registry.interfaces.pocket import PackagePublishingPocket | 50 | from lp.registry.interfaces.pocket import PackagePublishingPocket |
552 | 49 | from lp.registry.model.distribution import Distribution | 51 | from lp.registry.model.distribution import Distribution |
553 | 50 | from lp.registry.model.distroseries import DistroSeries | 52 | from lp.registry.model.distroseries import DistroSeries |
554 | @@ -65,6 +67,11 @@ | |||
555 | 65 | LibraryFileAlias, | 67 | LibraryFileAlias, |
556 | 66 | LibraryFileContent, | 68 | LibraryFileContent, |
557 | 67 | ) | 69 | ) |
558 | 70 | from lp.services.macaroons.interfaces import ( | ||
559 | 71 | BadMacaroonContext, | ||
560 | 72 | IMacaroonIssuer, | ||
561 | 73 | ) | ||
562 | 74 | from lp.services.macaroons.model import MacaroonIssuerBase | ||
563 | 68 | from lp.services.propertycache import ( | 75 | from lp.services.propertycache import ( |
564 | 69 | cachedproperty, | 76 | cachedproperty, |
565 | 70 | get_property_cache, | 77 | get_property_cache, |
566 | @@ -584,3 +591,53 @@ | |||
567 | 584 | SnapBuild, SnapBuild.build_farm_job_id.is_in( | 591 | SnapBuild, SnapBuild.build_farm_job_id.is_in( |
568 | 585 | bfj.id for bfj in build_farm_jobs)) | 592 | bfj.id for bfj in build_farm_jobs)) |
569 | 586 | return DecoratedResultSet(rows, pre_iter_hook=self.preloadBuildsData) | 593 | return DecoratedResultSet(rows, pre_iter_hook=self.preloadBuildsData) |
570 | 594 | |||
571 | 595 | |||
572 | 596 | @implementer(IMacaroonIssuer) | ||
573 | 597 | class SnapBuildMacaroonIssuer(MacaroonIssuerBase): | ||
574 | 598 | |||
575 | 599 | identifier = "snap-build" | ||
576 | 600 | |||
577 | 601 | def checkIssuingContext(self, context): | ||
578 | 602 | """See `MacaroonIssuerBase`. | ||
579 | 603 | |||
580 | 604 | For issuing, the context is an `ISnapBuild` or its ID. | ||
581 | 605 | """ | ||
582 | 606 | if ISnapBuild.providedBy(context): | ||
583 | 607 | pass | ||
584 | 608 | elif isinstance(context, int): | ||
585 | 609 | context = getUtility(ISnapBuildSet).getByID(context) | ||
586 | 610 | else: | ||
587 | 611 | raise BadMacaroonContext(context) | ||
588 | 612 | if not removeSecurityProxy(context).is_private: | ||
589 | 613 | raise BadMacaroonContext( | ||
590 | 614 | context, "Refusing to issue macaroon for public build.") | ||
591 | 615 | return removeSecurityProxy(context).id | ||
592 | 616 | |||
593 | 617 | def checkVerificationContext(self, context): | ||
594 | 618 | """See `MacaroonIssuerBase`.""" | ||
595 | 619 | if not IGitRepository.providedBy(context): | ||
596 | 620 | raise BadMacaroonContext(context) | ||
597 | 621 | return context | ||
598 | 622 | |||
599 | 623 | def verifyPrimaryCaveat(self, caveat_value, context): | ||
600 | 624 | """See `MacaroonIssuerBase`. | ||
601 | 625 | |||
602 | 626 | For verification, the context is an `IGitRepository`. We check that | ||
603 | 627 | the repository is needed to build the `ISnapBuild` that is the | ||
604 | 628 | context of the macaroon, and that the context build is currently | ||
605 | 629 | building. | ||
606 | 630 | """ | ||
607 | 631 | # Circular import. | ||
608 | 632 | from lp.snappy.model.snap import Snap | ||
609 | 633 | |||
610 | 634 | try: | ||
611 | 635 | build_id = int(caveat_value) | ||
612 | 636 | except ValueError: | ||
613 | 637 | return False | ||
614 | 638 | return not IStore(SnapBuild).find( | ||
615 | 639 | SnapBuild, | ||
616 | 640 | SnapBuild.id == build_id, | ||
617 | 641 | SnapBuild.snap_id == Snap.id, | ||
618 | 642 | Snap.git_repository == context, | ||
619 | 643 | SnapBuild.status == BuildStatus.BUILDING).is_empty() | ||
620 | 587 | 644 | ||
621 | === modified file 'lib/lp/snappy/tests/test_snapbuild.py' | |||
622 | --- lib/lp/snappy/tests/test_snapbuild.py 2018-12-18 18:23:52 +0000 | |||
623 | +++ lib/lp/snappy/tests/test_snapbuild.py 2019-04-24 16:19:42 +0000 | |||
624 | @@ -1,4 +1,4 @@ | |||
626 | 1 | # Copyright 2015-2018 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2015-2019 Canonical Ltd. This software is licensed under the |
627 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
628 | 3 | 3 | ||
629 | 4 | """Test snap package build features.""" | 4 | """Test snap package build features.""" |
630 | @@ -22,11 +22,13 @@ | |||
631 | 22 | Equals, | 22 | Equals, |
632 | 23 | Is, | 23 | Is, |
633 | 24 | MatchesDict, | 24 | MatchesDict, |
634 | 25 | MatchesListwise, | ||
635 | 25 | MatchesStructure, | 26 | MatchesStructure, |
636 | 26 | ) | 27 | ) |
637 | 27 | from zope.component import getUtility | 28 | from zope.component import getUtility |
638 | 28 | from zope.security.proxy import removeSecurityProxy | 29 | from zope.security.proxy import removeSecurityProxy |
639 | 29 | 30 | ||
640 | 31 | from lp.app.enums import InformationType | ||
641 | 30 | from lp.app.errors import NotFoundError | 32 | from lp.app.errors import NotFoundError |
642 | 31 | from lp.app.interfaces.launchpad import ILaunchpadCelebrities | 33 | from lp.app.interfaces.launchpad import ILaunchpadCelebrities |
643 | 32 | from lp.buildmaster.enums import BuildStatus | 34 | from lp.buildmaster.enums import BuildStatus |
644 | @@ -38,6 +40,10 @@ | |||
645 | 38 | from lp.services.features.testing import FeatureFixture | 40 | from lp.services.features.testing import FeatureFixture |
646 | 39 | from lp.services.job.interfaces.job import JobStatus | 41 | from lp.services.job.interfaces.job import JobStatus |
647 | 40 | from lp.services.librarian.browser import ProxiedLibraryFileAlias | 42 | from lp.services.librarian.browser import ProxiedLibraryFileAlias |
648 | 43 | from lp.services.macaroons.interfaces import ( | ||
649 | 44 | BadMacaroonContext, | ||
650 | 45 | IMacaroonIssuer, | ||
651 | 46 | ) | ||
652 | 41 | from lp.services.propertycache import clear_property_cache | 47 | from lp.services.propertycache import clear_property_cache |
653 | 42 | from lp.services.webapp.interfaces import OAuthPermission | 48 | from lp.services.webapp.interfaces import OAuthPermission |
654 | 43 | from lp.services.webapp.publisher import canonical_url | 49 | from lp.services.webapp.publisher import canonical_url |
655 | @@ -774,3 +780,117 @@ | |||
656 | 774 | browser = self.getNonRedirectingBrowser(user=self.person) | 780 | browser = self.getNonRedirectingBrowser(user=self.person) |
657 | 775 | for file_url in file_urls: | 781 | for file_url in file_urls: |
658 | 776 | self.assertCanOpenRedirectedUrl(browser, file_url) | 782 | self.assertCanOpenRedirectedUrl(browser, file_url) |
659 | 783 | |||
660 | 784 | |||
661 | 785 | class TestSnapBuildMacaroonIssuer(TestCaseWithFactory): | ||
662 | 786 | """Test SnapBuild macaroon issuing and verification.""" | ||
663 | 787 | |||
664 | 788 | layer = LaunchpadZopelessLayer | ||
665 | 789 | |||
666 | 790 | def setUp(self): | ||
667 | 791 | super(TestSnapBuildMacaroonIssuer, self).setUp() | ||
668 | 792 | self.useFixture(FeatureFixture(SNAP_TESTING_FLAGS)) | ||
669 | 793 | self.pushConfig( | ||
670 | 794 | "launchpad", internal_macaroon_secret_key="some-secret") | ||
671 | 795 | |||
672 | 796 | def test_issueMacaroon_refuses_public_snap(self): | ||
673 | 797 | build = self.factory.makeSnapBuild() | ||
674 | 798 | issuer = getUtility(IMacaroonIssuer, "snap-build") | ||
675 | 799 | self.assertRaises( | ||
676 | 800 | BadMacaroonContext, removeSecurityProxy(issuer).issueMacaroon, | ||
677 | 801 | build) | ||
678 | 802 | |||
679 | 803 | def test_issueMacaroon_good(self): | ||
680 | 804 | build = self.factory.makeSnapBuild( | ||
681 | 805 | snap=self.factory.makeSnap(private=True)) | ||
682 | 806 | issuer = getUtility(IMacaroonIssuer, "snap-build") | ||
683 | 807 | macaroon = removeSecurityProxy(issuer).issueMacaroon(build) | ||
684 | 808 | self.assertThat(macaroon, MatchesStructure( | ||
685 | 809 | location=Equals("launchpad.dev"), | ||
686 | 810 | identifier=Equals("snap-build"), | ||
687 | 811 | caveats=MatchesListwise([ | ||
688 | 812 | MatchesStructure.byEquality( | ||
689 | 813 | caveat_id="lp.snap-build %s" % build.id), | ||
690 | 814 | ]))) | ||
691 | 815 | |||
692 | 816 | def test_verifyMacaroon_good(self): | ||
693 | 817 | [ref] = self.factory.makeGitRefs( | ||
694 | 818 | information_type=InformationType.USERDATA) | ||
695 | 819 | build = self.factory.makeSnapBuild( | ||
696 | 820 | snap=self.factory.makeSnap(git_ref=ref, private=True)) | ||
697 | 821 | build.updateStatus(BuildStatus.BUILDING) | ||
698 | 822 | issuer = removeSecurityProxy( | ||
699 | 823 | getUtility(IMacaroonIssuer, "snap-build")) | ||
700 | 824 | macaroon = issuer.issueMacaroon(build) | ||
701 | 825 | self.assertTrue(issuer.verifyMacaroon(macaroon, ref.repository)) | ||
702 | 826 | |||
703 | 827 | def test_verifyMacaroon_wrong_location(self): | ||
704 | 828 | [ref] = self.factory.makeGitRefs( | ||
705 | 829 | information_type=InformationType.USERDATA) | ||
706 | 830 | build = self.factory.makeSnapBuild( | ||
707 | 831 | snap=self.factory.makeSnap(git_ref=ref, private=True)) | ||
708 | 832 | build.updateStatus(BuildStatus.BUILDING) | ||
709 | 833 | issuer = removeSecurityProxy( | ||
710 | 834 | getUtility(IMacaroonIssuer, "snap-build")) | ||
711 | 835 | macaroon = Macaroon( | ||
712 | 836 | location="another-location", key=issuer._root_secret) | ||
713 | 837 | self.assertFalse(issuer.verifyMacaroon(macaroon, ref.repository)) | ||
714 | 838 | |||
715 | 839 | def test_verifyMacaroon_wrong_key(self): | ||
716 | 840 | [ref] = self.factory.makeGitRefs( | ||
717 | 841 | information_type=InformationType.USERDATA) | ||
718 | 842 | build = self.factory.makeSnapBuild( | ||
719 | 843 | snap=self.factory.makeSnap(git_ref=ref, private=True)) | ||
720 | 844 | build.updateStatus(BuildStatus.BUILDING) | ||
721 | 845 | issuer = removeSecurityProxy( | ||
722 | 846 | getUtility(IMacaroonIssuer, "snap-build")) | ||
723 | 847 | macaroon = Macaroon( | ||
724 | 848 | location=config.vhost.mainsite.hostname, key="another-secret") | ||
725 | 849 | self.assertFalse(issuer.verifyMacaroon(macaroon, ref.repository)) | ||
726 | 850 | |||
727 | 851 | def test_verifyMacaroon_refuses_branch(self): | ||
728 | 852 | branch = self.factory.makeAnyBranch( | ||
729 | 853 | information_type=InformationType.USERDATA) | ||
730 | 854 | build = self.factory.makeSnapBuild( | ||
731 | 855 | snap=self.factory.makeSnap(branch=branch, private=True)) | ||
732 | 856 | build.updateStatus(BuildStatus.BUILDING) | ||
733 | 857 | issuer = removeSecurityProxy( | ||
734 | 858 | getUtility(IMacaroonIssuer, "snap-build")) | ||
735 | 859 | macaroon = issuer.issueMacaroon(build) | ||
736 | 860 | self.assertFalse(issuer.verifyMacaroon(macaroon, branch)) | ||
737 | 861 | |||
738 | 862 | def test_verifyMacaroon_not_building(self): | ||
739 | 863 | [ref] = self.factory.makeGitRefs( | ||
740 | 864 | information_type=InformationType.USERDATA) | ||
741 | 865 | build = self.factory.makeSnapBuild( | ||
742 | 866 | snap=self.factory.makeSnap(git_ref=ref, private=True)) | ||
743 | 867 | issuer = removeSecurityProxy( | ||
744 | 868 | getUtility(IMacaroonIssuer, "snap-build")) | ||
745 | 869 | macaroon = issuer.issueMacaroon(build) | ||
746 | 870 | self.assertFalse(issuer.verifyMacaroon(macaroon, ref.repository)) | ||
747 | 871 | |||
748 | 872 | def test_verifyMacaroon_wrong_build(self): | ||
749 | 873 | [ref] = self.factory.makeGitRefs( | ||
750 | 874 | information_type=InformationType.USERDATA) | ||
751 | 875 | build = self.factory.makeSnapBuild( | ||
752 | 876 | snap=self.factory.makeSnap(git_ref=ref, private=True)) | ||
753 | 877 | build.updateStatus(BuildStatus.BUILDING) | ||
754 | 878 | other_build = self.factory.makeSnapBuild( | ||
755 | 879 | snap=self.factory.makeSnap(private=True)) | ||
756 | 880 | other_build.updateStatus(BuildStatus.BUILDING) | ||
757 | 881 | issuer = removeSecurityProxy( | ||
758 | 882 | getUtility(IMacaroonIssuer, "snap-build")) | ||
759 | 883 | macaroon = issuer.issueMacaroon(other_build) | ||
760 | 884 | self.assertFalse(issuer.verifyMacaroon(macaroon, ref.repository)) | ||
761 | 885 | |||
762 | 886 | def test_verifyMacaroon_wrong_repository(self): | ||
763 | 887 | [ref] = self.factory.makeGitRefs( | ||
764 | 888 | information_type=InformationType.USERDATA) | ||
765 | 889 | build = self.factory.makeSnapBuild( | ||
766 | 890 | snap=self.factory.makeSnap(git_ref=ref, private=True)) | ||
767 | 891 | other_repository = self.factory.makeGitRepository() | ||
768 | 892 | build.updateStatus(BuildStatus.BUILDING) | ||
769 | 893 | issuer = removeSecurityProxy( | ||
770 | 894 | getUtility(IMacaroonIssuer, "snap-build")) | ||
771 | 895 | macaroon = issuer.issueMacaroon(build) | ||
772 | 896 | self.assertFalse(issuer.verifyMacaroon(macaroon, other_repository)) | ||
773 | 777 | 897 | ||
774 | === modified file 'lib/lp/soyuz/model/binarypackagebuild.py' | |||
775 | --- lib/lp/soyuz/model/binarypackagebuild.py 2019-04-23 14:00:43 +0000 | |||
776 | +++ lib/lp/soyuz/model/binarypackagebuild.py 2019-04-24 16:19:42 +0000 | |||
777 | @@ -21,10 +21,6 @@ | |||
778 | 21 | 21 | ||
779 | 22 | import apt_pkg | 22 | import apt_pkg |
780 | 23 | from debian.deb822 import PkgRelation | 23 | from debian.deb822 import PkgRelation |
781 | 24 | from pymacaroons import ( | ||
782 | 25 | Macaroon, | ||
783 | 26 | Verifier, | ||
784 | 27 | ) | ||
785 | 28 | import pytz | 24 | import pytz |
786 | 29 | from sqlobject import SQLObjectNotFound | 25 | from sqlobject import SQLObjectNotFound |
787 | 30 | from storm.expr import ( | 26 | from storm.expr import ( |
788 | @@ -86,7 +82,11 @@ | |||
789 | 86 | LibraryFileAlias, | 82 | LibraryFileAlias, |
790 | 87 | LibraryFileContent, | 83 | LibraryFileContent, |
791 | 88 | ) | 84 | ) |
793 | 89 | from lp.services.macaroons.interfaces import IMacaroonIssuer | 85 | from lp.services.macaroons.interfaces import ( |
794 | 86 | BadMacaroonContext, | ||
795 | 87 | IMacaroonIssuer, | ||
796 | 88 | ) | ||
797 | 89 | from lp.services.macaroons.model import MacaroonIssuerBase | ||
798 | 90 | from lp.soyuz.adapters.buildarch import determine_architectures_to_build | 90 | from lp.soyuz.adapters.buildarch import determine_architectures_to_build |
799 | 91 | from lp.soyuz.enums import ( | 91 | from lp.soyuz.enums import ( |
800 | 92 | ArchivePurpose, | 92 | ArchivePurpose, |
801 | @@ -1374,52 +1374,39 @@ | |||
802 | 1374 | 1374 | ||
803 | 1375 | 1375 | ||
804 | 1376 | @implementer(IMacaroonIssuer) | 1376 | @implementer(IMacaroonIssuer) |
806 | 1377 | class BinaryPackageBuildMacaroonIssuer: | 1377 | class BinaryPackageBuildMacaroonIssuer(MacaroonIssuerBase): |
807 | 1378 | |||
808 | 1379 | identifier = "binary-package-build" | ||
809 | 1378 | 1380 | ||
810 | 1379 | @property | 1381 | @property |
828 | 1380 | def _root_secret(self): | 1382 | def _primary_caveat_name(self): |
829 | 1381 | secret = config.launchpad.internal_macaroon_secret_key | 1383 | """See `MacaroonIssuerBase`.""" |
813 | 1382 | if not secret: | ||
814 | 1383 | raise RuntimeError( | ||
815 | 1384 | "launchpad.internal_macaroon_secret_key not configured.") | ||
816 | 1385 | return secret | ||
817 | 1386 | |||
818 | 1387 | def issueMacaroon(self, context): | ||
819 | 1388 | """See `IMacaroonIssuer`. | ||
820 | 1389 | |||
821 | 1390 | For issuing, the context is an `IBinaryPackageBuild`. | ||
822 | 1391 | """ | ||
823 | 1392 | if not removeSecurityProxy(context).archive.private: | ||
824 | 1393 | raise ValueError("Refusing to issue macaroon for public build.") | ||
825 | 1394 | macaroon = Macaroon( | ||
826 | 1395 | location=config.vhost.mainsite.hostname, | ||
827 | 1396 | identifier="binary-package-build", key=self._root_secret) | ||
830 | 1397 | # The "lp.principal" prefix indicates that this caveat constrains | 1384 | # The "lp.principal" prefix indicates that this caveat constrains |
831 | 1398 | # the macaroon to access only resources that should be accessible | 1385 | # the macaroon to access only resources that should be accessible |
832 | 1399 | # when acting on behalf of the named build, rather than to access | 1386 | # when acting on behalf of the named build, rather than to access |
833 | 1400 | # the named build directly. | 1387 | # the named build directly. |
856 | 1401 | macaroon.add_first_party_caveat( | 1388 | return "lp.principal.binary-package-build" |
857 | 1402 | "lp.principal.binary-package-build %s" % | 1389 | |
858 | 1403 | removeSecurityProxy(context).id) | 1390 | def checkIssuingContext(self, context): |
859 | 1404 | return macaroon | 1391 | """See `MacaroonIssuerBase`. |
860 | 1405 | 1392 | ||
861 | 1406 | def checkMacaroonIssuer(self, macaroon): | 1393 | For issuing, the context is an `IBinaryPackageBuild`. |
862 | 1407 | """See `IMacaroonIssuer`.""" | 1394 | """ |
863 | 1408 | if macaroon.location != config.vhost.mainsite.hostname: | 1395 | if not removeSecurityProxy(context).archive.private: |
864 | 1409 | return False | 1396 | raise BadMacaroonContext( |
865 | 1410 | try: | 1397 | context, "Refusing to issue macaroon for public build.") |
866 | 1411 | verifier = Verifier() | 1398 | return removeSecurityProxy(context).id |
867 | 1412 | verifier.satisfy_general( | 1399 | |
868 | 1413 | lambda caveat: caveat.startswith( | 1400 | def checkVerificationContext(self, context): |
869 | 1414 | "lp.principal.binary-package-build ")) | 1401 | """See `MacaroonIssuerBase`.""" |
870 | 1415 | return verifier.verify(macaroon, self._root_secret) | 1402 | if not ILibraryFileAlias.providedBy(context): |
871 | 1416 | except Exception: | 1403 | raise BadMacaroonContext(context) |
872 | 1417 | return False | 1404 | return context |
873 | 1418 | 1405 | ||
874 | 1419 | def verifyMacaroon(self, macaroon, context): | 1406 | def verifyPrimaryCaveat(self, caveat_value, context): |
875 | 1420 | """See `IMacaroonIssuer`. | 1407 | """See `MacaroonIssuerBase`. |
876 | 1421 | 1408 | ||
877 | 1422 | For verification, the context is a `LibraryFileAlias`. We check | 1409 | For verification, the context is an `ILibraryFileAlias`. We check |
878 | 1423 | that the file is one of those required to build the | 1410 | that the file is one of those required to build the |
879 | 1424 | `IBinaryPackageBuild` that is the context of the macaroon, and that | 1411 | `IBinaryPackageBuild` that is the context of the macaroon, and that |
880 | 1425 | the context build is currently building. | 1412 | the context build is currently building. |
881 | @@ -1427,32 +1414,16 @@ | |||
882 | 1427 | # Circular import. | 1414 | # Circular import. |
883 | 1428 | from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease | 1415 | from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease |
884 | 1429 | 1416 | ||
885 | 1430 | if not ILibraryFileAlias.providedBy(context): | ||
886 | 1431 | return False | ||
887 | 1432 | if not self.checkMacaroonIssuer(macaroon): | ||
888 | 1433 | return False | ||
889 | 1434 | |||
890 | 1435 | def verify_build(caveat): | ||
891 | 1436 | prefix = "lp.principal.binary-package-build " | ||
892 | 1437 | if not caveat.startswith(prefix): | ||
893 | 1438 | return False | ||
894 | 1439 | try: | ||
895 | 1440 | build_id = int(caveat[len(prefix):]) | ||
896 | 1441 | except ValueError: | ||
897 | 1442 | return False | ||
898 | 1443 | return not IStore(BinaryPackageBuild).find( | ||
899 | 1444 | BinaryPackageBuild, | ||
900 | 1445 | BinaryPackageBuild.id == build_id, | ||
901 | 1446 | BinaryPackageBuild.source_package_release_id == | ||
902 | 1447 | SourcePackageRelease.id, | ||
903 | 1448 | SourcePackageReleaseFile.sourcepackagereleaseID == | ||
904 | 1449 | SourcePackageRelease.id, | ||
905 | 1450 | SourcePackageReleaseFile.libraryfile == context, | ||
906 | 1451 | BinaryPackageBuild.status == BuildStatus.BUILDING).is_empty() | ||
907 | 1452 | |||
908 | 1453 | try: | 1417 | try: |
913 | 1454 | verifier = Verifier() | 1418 | build_id = int(caveat_value) |
914 | 1455 | verifier.satisfy_general(verify_build) | 1419 | except ValueError: |
911 | 1456 | return verifier.verify(macaroon, self._root_secret) | ||
912 | 1457 | except Exception: | ||
915 | 1458 | return False | 1420 | return False |
916 | 1421 | return not IStore(BinaryPackageBuild).find( | ||
917 | 1422 | BinaryPackageBuild, | ||
918 | 1423 | BinaryPackageBuild.id == build_id, | ||
919 | 1424 | BinaryPackageBuild.source_package_release_id == | ||
920 | 1425 | SourcePackageRelease.id, | ||
921 | 1426 | SourcePackageReleaseFile.sourcepackagereleaseID == | ||
922 | 1427 | SourcePackageRelease.id, | ||
923 | 1428 | SourcePackageReleaseFile.libraryfile == context, | ||
924 | 1429 | BinaryPackageBuild.status == BuildStatus.BUILDING).is_empty() | ||
925 | 1459 | 1430 | ||
926 | === modified file 'lib/lp/soyuz/tests/test_binarypackagebuild.py' | |||
927 | --- lib/lp/soyuz/tests/test_binarypackagebuild.py 2019-04-23 14:00:43 +0000 | |||
928 | +++ lib/lp/soyuz/tests/test_binarypackagebuild.py 2019-04-24 16:19:42 +0000 | |||
929 | @@ -29,7 +29,10 @@ | |||
930 | 29 | from lp.registry.interfaces.sourcepackage import SourcePackageUrgency | 29 | from lp.registry.interfaces.sourcepackage import SourcePackageUrgency |
931 | 30 | from lp.services.config import config | 30 | from lp.services.config import config |
932 | 31 | from lp.services.log.logger import DevNullLogger | 31 | from lp.services.log.logger import DevNullLogger |
934 | 32 | from lp.services.macaroons.interfaces import IMacaroonIssuer | 32 | from lp.services.macaroons.interfaces import ( |
935 | 33 | BadMacaroonContext, | ||
936 | 34 | IMacaroonIssuer, | ||
937 | 35 | ) | ||
938 | 33 | from lp.services.webapp.interaction import ANONYMOUS | 36 | from lp.services.webapp.interaction import ANONYMOUS |
939 | 34 | from lp.services.webapp.interfaces import OAuthPermission | 37 | from lp.services.webapp.interfaces import OAuthPermission |
940 | 35 | from lp.soyuz.enums import ( | 38 | from lp.soyuz.enums import ( |
941 | @@ -920,7 +923,8 @@ | |||
942 | 920 | build = self.factory.makeBinaryPackageBuild() | 923 | build = self.factory.makeBinaryPackageBuild() |
943 | 921 | issuer = getUtility(IMacaroonIssuer, "binary-package-build") | 924 | issuer = getUtility(IMacaroonIssuer, "binary-package-build") |
944 | 922 | self.assertRaises( | 925 | self.assertRaises( |
946 | 923 | ValueError, removeSecurityProxy(issuer).issueMacaroon, build) | 926 | BadMacaroonContext, removeSecurityProxy(issuer).issueMacaroon, |
947 | 927 | build) | ||
948 | 924 | 928 | ||
949 | 925 | def test_issueMacaroon_good(self): | 929 | def test_issueMacaroon_good(self): |
950 | 926 | build = self.factory.makeBinaryPackageBuild( | 930 | build = self.factory.makeBinaryPackageBuild( |
951 | @@ -934,26 +938,6 @@ | |||
952 | 934 | caveat_id="lp.principal.binary-package-build %s" % build.id), | 938 | caveat_id="lp.principal.binary-package-build %s" % build.id), |
953 | 935 | ])) | 939 | ])) |
954 | 936 | 940 | ||
955 | 937 | def test_checkMacaroonIssuer_good(self): | ||
956 | 938 | build = self.factory.makeBinaryPackageBuild( | ||
957 | 939 | archive=self.factory.makeArchive(private=True)) | ||
958 | 940 | issuer = getUtility(IMacaroonIssuer, "binary-package-build") | ||
959 | 941 | macaroon = removeSecurityProxy(issuer).issueMacaroon(build) | ||
960 | 942 | self.assertTrue(issuer.checkMacaroonIssuer(macaroon)) | ||
961 | 943 | |||
962 | 944 | def test_checkMacaroonIssuer_wrong_location(self): | ||
963 | 945 | issuer = getUtility(IMacaroonIssuer, "binary-package-build") | ||
964 | 946 | macaroon = Macaroon( | ||
965 | 947 | location="another-location", | ||
966 | 948 | key=removeSecurityProxy(issuer)._root_secret) | ||
967 | 949 | self.assertFalse(issuer.checkMacaroonIssuer(macaroon)) | ||
968 | 950 | |||
969 | 951 | def test_checkMacaroonIssuer_wrong_key(self): | ||
970 | 952 | issuer = getUtility(IMacaroonIssuer, "binary-package-build") | ||
971 | 953 | macaroon = Macaroon( | ||
972 | 954 | location=config.vhost.mainsite.hostname, key="another-secret") | ||
973 | 955 | self.assertFalse(issuer.checkMacaroonIssuer(macaroon)) | ||
974 | 956 | |||
975 | 957 | def test_verifyMacaroon_good(self): | 941 | def test_verifyMacaroon_good(self): |
976 | 958 | build = self.factory.makeBinaryPackageBuild( | 942 | build = self.factory.makeBinaryPackageBuild( |
977 | 959 | archive=self.factory.makeArchive(private=True)) | 943 | archive=self.factory.makeArchive(private=True)) |