Merge ~cjwatson/launchpad:cibuild-macaroons into launchpad:master
- Git
- lp:~cjwatson/launchpad
- cibuild-macaroons
- Merge into master
Proposed by
Colin Watson
Status: | Merged |
---|---|
Approved by: | Colin Watson |
Approved revision: | 163146dfdb0fde5d0a9496a5729c0e73ab08c020 |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~cjwatson/launchpad:cibuild-macaroons |
Merge into: | launchpad:master |
Diff against target: |
466 lines (+315/-5) 6 files modified
lib/lp/code/configure.zcml (+8/-0) lib/lp/code/model/cibuild.py (+63/-0) lib/lp/code/model/tests/test_cibuild.py (+149/-0) lib/lp/code/xmlrpc/tests/test_git.py (+84/-0) lib/lp/services/authserver/interfaces.py (+4/-3) lib/lp/services/authserver/xmlrpc.py (+7/-2) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jürgen Gmach | Approve | ||
Review via email: mp+419976@code.launchpad.net |
Commit message
Add CIBuild macaroons
Description of the change
This implements the same macaroon-based authorization protocol used by several other build types, allowing builders to clone private repositories associated with the CI build they're running.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/lib/lp/code/configure.zcml b/lib/lp/code/configure.zcml | |||
2 | index 882d3da..99a5bf0 100644 | |||
3 | --- a/lib/lp/code/configure.zcml | |||
4 | +++ b/lib/lp/code/configure.zcml | |||
5 | @@ -1307,6 +1307,14 @@ | |||
6 | 1307 | <allow interface="lp.buildmaster.interfaces.buildfarmjob.ISpecificBuildFarmJobSource" /> | 1307 | <allow interface="lp.buildmaster.interfaces.buildfarmjob.ISpecificBuildFarmJobSource" /> |
7 | 1308 | </securedutility> | 1308 | </securedutility> |
8 | 1309 | 1309 | ||
9 | 1310 | <!-- CIBuildMacaroonIssuer --> | ||
10 | 1311 | <securedutility | ||
11 | 1312 | class="lp.code.model.cibuild.CIBuildMacaroonIssuer" | ||
12 | 1313 | provides="lp.services.macaroons.interfaces.IMacaroonIssuer" | ||
13 | 1314 | name="ci-build"> | ||
14 | 1315 | <allow interface="lp.services.macaroons.interfaces.IMacaroonIssuerPublic" /> | ||
15 | 1316 | </securedutility> | ||
16 | 1317 | |||
17 | 1310 | <!-- CIBuildBehaviour --> | 1318 | <!-- CIBuildBehaviour --> |
18 | 1311 | <adapter | 1319 | <adapter |
19 | 1312 | for="lp.code.interfaces.cibuild.ICIBuild" | 1320 | for="lp.code.interfaces.cibuild.ICIBuild" |
20 | diff --git a/lib/lp/code/model/cibuild.py b/lib/lp/code/model/cibuild.py | |||
21 | index 912d31f..dd2390f 100644 | |||
22 | --- a/lib/lp/code/model/cibuild.py | |||
23 | +++ b/lib/lp/code/model/cibuild.py | |||
24 | @@ -25,6 +25,7 @@ from storm.store import EmptyResultSet | |||
25 | 25 | from zope.component import getUtility | 25 | from zope.component import getUtility |
26 | 26 | from zope.event import notify | 26 | from zope.event import notify |
27 | 27 | from zope.interface import implementer | 27 | from zope.interface import implementer |
28 | 28 | from zope.security.proxy import removeSecurityProxy | ||
29 | 28 | 29 | ||
30 | 29 | from lp.app.errors import NotFoundError | 30 | from lp.app.errors import NotFoundError |
31 | 30 | from lp.app.interfaces.launchpad import ILaunchpadCelebrities | 31 | from lp.app.interfaces.launchpad import ILaunchpadCelebrities |
32 | @@ -51,6 +52,7 @@ from lp.code.interfaces.cibuild import ( | |||
33 | 51 | MissingConfiguration, | 52 | MissingConfiguration, |
34 | 52 | ) | 53 | ) |
35 | 53 | from lp.code.interfaces.githosting import IGitHostingClient | 54 | from lp.code.interfaces.githosting import IGitHostingClient |
36 | 55 | from lp.code.interfaces.gitrepository import IGitRepository | ||
37 | 54 | from lp.code.interfaces.revisionstatus import IRevisionStatusReportSet | 56 | from lp.code.interfaces.revisionstatus import IRevisionStatusReportSet |
38 | 55 | from lp.code.model.gitref import GitRef | 57 | from lp.code.model.gitref import GitRef |
39 | 56 | from lp.code.model.lpcraft import load_configuration | 58 | from lp.code.model.lpcraft import load_configuration |
40 | @@ -72,6 +74,12 @@ from lp.services.librarian.model import ( | |||
41 | 72 | LibraryFileAlias, | 74 | LibraryFileAlias, |
42 | 73 | LibraryFileContent, | 75 | LibraryFileContent, |
43 | 74 | ) | 76 | ) |
44 | 77 | from lp.services.macaroons.interfaces import ( | ||
45 | 78 | BadMacaroonContext, | ||
46 | 79 | IMacaroonIssuer, | ||
47 | 80 | NO_USER, | ||
48 | 81 | ) | ||
49 | 82 | from lp.services.macaroons.model import MacaroonIssuerBase | ||
50 | 75 | from lp.services.propertycache import cachedproperty | 83 | from lp.services.propertycache import cachedproperty |
51 | 76 | from lp.soyuz.model.distroarchseries import DistroArchSeries | 84 | from lp.soyuz.model.distroarchseries import DistroArchSeries |
52 | 77 | 85 | ||
53 | @@ -596,3 +604,58 @@ class CIBuildSet(SpecificBuildFarmJobSourceMixin): | |||
54 | 596 | def deleteByGitRepository(self, git_repository): | 604 | def deleteByGitRepository(self, git_repository): |
55 | 597 | """See `ICIBuildSet`.""" | 605 | """See `ICIBuildSet`.""" |
56 | 598 | self.findByGitRepository(git_repository).remove() | 606 | self.findByGitRepository(git_repository).remove() |
57 | 607 | |||
58 | 608 | |||
59 | 609 | @implementer(IMacaroonIssuer) | ||
60 | 610 | class CIBuildMacaroonIssuer(MacaroonIssuerBase): | ||
61 | 611 | |||
62 | 612 | identifier = "ci-build" | ||
63 | 613 | issuable_via_authserver = True | ||
64 | 614 | |||
65 | 615 | def checkIssuingContext(self, context, **kwargs): | ||
66 | 616 | """See `MacaroonIssuerBase`. | ||
67 | 617 | |||
68 | 618 | For issuing, the context is an `ICIBuild`. | ||
69 | 619 | """ | ||
70 | 620 | if not ICIBuild.providedBy(context): | ||
71 | 621 | raise BadMacaroonContext(context) | ||
72 | 622 | return removeSecurityProxy(context).id | ||
73 | 623 | |||
74 | 624 | def checkVerificationContext(self, context, **kwargs): | ||
75 | 625 | """See `MacaroonIssuerBase`.""" | ||
76 | 626 | if not IGitRepository.providedBy(context): | ||
77 | 627 | raise BadMacaroonContext(context) | ||
78 | 628 | return context | ||
79 | 629 | |||
80 | 630 | def verifyPrimaryCaveat(self, verified, caveat_value, context, user=None, | ||
81 | 631 | **kwargs): | ||
82 | 632 | """See `MacaroonIssuerBase`. | ||
83 | 633 | |||
84 | 634 | For verification, the context is an `IGitRepository`. We check that | ||
85 | 635 | the repository or archive is needed to build the `ICIBuild` that is | ||
86 | 636 | the context of the macaroon, and that the context build is currently | ||
87 | 637 | building. | ||
88 | 638 | """ | ||
89 | 639 | # CI builds only support free-floating macaroons for Git | ||
90 | 640 | # authentication, not ones bound to a user. | ||
91 | 641 | if user: | ||
92 | 642 | return False | ||
93 | 643 | verified.user = NO_USER | ||
94 | 644 | |||
95 | 645 | if context is None: | ||
96 | 646 | # We're only verifying that the macaroon could be valid for some | ||
97 | 647 | # context. | ||
98 | 648 | return True | ||
99 | 649 | if not IGitRepository.providedBy(context): | ||
100 | 650 | return False | ||
101 | 651 | |||
102 | 652 | try: | ||
103 | 653 | build_id = int(caveat_value) | ||
104 | 654 | except ValueError: | ||
105 | 655 | return False | ||
106 | 656 | clauses = [ | ||
107 | 657 | CIBuild.id == build_id, | ||
108 | 658 | CIBuild.status == BuildStatus.BUILDING, | ||
109 | 659 | CIBuild.git_repository == context, | ||
110 | 660 | ] | ||
111 | 661 | return not IStore(CIBuild).find(CIBuild, *clauses).is_empty() | ||
112 | diff --git a/lib/lp/code/model/tests/test_cibuild.py b/lib/lp/code/model/tests/test_cibuild.py | |||
113 | index d4cc4ec..3f9682e 100644 | |||
114 | --- a/lib/lp/code/model/tests/test_cibuild.py | |||
115 | +++ b/lib/lp/code/model/tests/test_cibuild.py | |||
116 | @@ -12,14 +12,17 @@ from textwrap import dedent | |||
117 | 12 | from unittest.mock import Mock | 12 | from unittest.mock import Mock |
118 | 13 | 13 | ||
119 | 14 | from fixtures import MockPatchObject | 14 | from fixtures import MockPatchObject |
120 | 15 | from pymacaroons import Macaroon | ||
121 | 15 | import pytz | 16 | import pytz |
122 | 16 | from storm.locals import Store | 17 | from storm.locals import Store |
123 | 17 | from testtools.matchers import ( | 18 | from testtools.matchers import ( |
124 | 18 | Equals, | 19 | Equals, |
125 | 20 | MatchesListwise, | ||
126 | 19 | MatchesSetwise, | 21 | MatchesSetwise, |
127 | 20 | MatchesStructure, | 22 | MatchesStructure, |
128 | 21 | ) | 23 | ) |
129 | 22 | from zope.component import getUtility | 24 | from zope.component import getUtility |
130 | 25 | from zope.publisher.xmlrpc import TestRequest | ||
131 | 23 | from zope.security.proxy import removeSecurityProxy | 26 | from zope.security.proxy import removeSecurityProxy |
132 | 24 | 27 | ||
133 | 25 | from lp.app.enums import InformationType | 28 | from lp.app.enums import InformationType |
134 | @@ -53,7 +56,11 @@ from lp.code.model.cibuild import ( | |||
135 | 53 | from lp.code.model.lpcraft import load_configuration | 56 | from lp.code.model.lpcraft import load_configuration |
136 | 54 | from lp.code.tests.helpers import GitHostingFixture | 57 | from lp.code.tests.helpers import GitHostingFixture |
137 | 55 | from lp.registry.interfaces.series import SeriesStatus | 58 | from lp.registry.interfaces.series import SeriesStatus |
138 | 59 | from lp.services.authserver.xmlrpc import AuthServerAPIView | ||
139 | 60 | from lp.services.config import config | ||
140 | 56 | from lp.services.log.logger import BufferLogger | 61 | from lp.services.log.logger import BufferLogger |
141 | 62 | from lp.services.macaroons.interfaces import IMacaroonIssuer | ||
142 | 63 | from lp.services.macaroons.testing import MacaroonTestMixin | ||
143 | 57 | from lp.services.propertycache import clear_property_cache | 64 | from lp.services.propertycache import clear_property_cache |
144 | 58 | from lp.testing import ( | 65 | from lp.testing import ( |
145 | 59 | person_logged_in, | 66 | person_logged_in, |
146 | @@ -62,6 +69,7 @@ from lp.testing import ( | |||
147 | 62 | ) | 69 | ) |
148 | 63 | from lp.testing.layers import LaunchpadZopelessLayer | 70 | from lp.testing.layers import LaunchpadZopelessLayer |
149 | 64 | from lp.testing.matchers import HasQueryCount | 71 | from lp.testing.matchers import HasQueryCount |
150 | 72 | from lp.xmlrpc.interfaces import IPrivateApplication | ||
151 | 65 | 73 | ||
152 | 66 | 74 | ||
153 | 67 | class TestGetAllCommitsForPaths(TestCaseWithFactory): | 75 | class TestGetAllCommitsForPaths(TestCaseWithFactory): |
154 | @@ -983,3 +991,144 @@ class TestDetermineDASesToBuild(TestCaseWithFactory): | |||
155 | 983 | "name in Ubuntu %s\n" % distro_series.name, | 991 | "name in Ubuntu %s\n" % distro_series.name, |
156 | 984 | logger.getLogBuffer() | 992 | logger.getLogBuffer() |
157 | 985 | ) | 993 | ) |
158 | 994 | |||
159 | 995 | |||
160 | 996 | class TestCIBuildMacaroonIssuer(MacaroonTestMixin, TestCaseWithFactory): | ||
161 | 997 | """Test CIBuild macaroon issuing and verification.""" | ||
162 | 998 | |||
163 | 999 | layer = LaunchpadZopelessLayer | ||
164 | 1000 | |||
165 | 1001 | def setUp(self): | ||
166 | 1002 | super().setUp() | ||
167 | 1003 | self.pushConfig( | ||
168 | 1004 | "launchpad", internal_macaroon_secret_key="some-secret") | ||
169 | 1005 | |||
170 | 1006 | def test_issueMacaroon_good(self): | ||
171 | 1007 | build = self.factory.makeCIBuild( | ||
172 | 1008 | git_repository=self.factory.makeGitRepository( | ||
173 | 1009 | information_type=InformationType.USERDATA)) | ||
174 | 1010 | issuer = getUtility(IMacaroonIssuer, "ci-build") | ||
175 | 1011 | macaroon = removeSecurityProxy(issuer).issueMacaroon(build) | ||
176 | 1012 | self.assertThat(macaroon, MatchesStructure( | ||
177 | 1013 | location=Equals("launchpad.test"), | ||
178 | 1014 | identifier=Equals("ci-build"), | ||
179 | 1015 | caveats=MatchesListwise([ | ||
180 | 1016 | MatchesStructure.byEquality( | ||
181 | 1017 | caveat_id="lp.ci-build %s" % build.id), | ||
182 | 1018 | ]))) | ||
183 | 1019 | |||
184 | 1020 | def test_issueMacaroon_via_authserver(self): | ||
185 | 1021 | build = self.factory.makeCIBuild( | ||
186 | 1022 | git_repository=self.factory.makeGitRepository( | ||
187 | 1023 | information_type=InformationType.USERDATA)) | ||
188 | 1024 | private_root = getUtility(IPrivateApplication) | ||
189 | 1025 | authserver = AuthServerAPIView(private_root.authserver, TestRequest()) | ||
190 | 1026 | macaroon = Macaroon.deserialize( | ||
191 | 1027 | authserver.issueMacaroon("ci-build", "CIBuild", build.id)) | ||
192 | 1028 | self.assertThat(macaroon, MatchesStructure( | ||
193 | 1029 | location=Equals("launchpad.test"), | ||
194 | 1030 | identifier=Equals("ci-build"), | ||
195 | 1031 | caveats=MatchesListwise([ | ||
196 | 1032 | MatchesStructure.byEquality( | ||
197 | 1033 | caveat_id="lp.ci-build %s" % build.id), | ||
198 | 1034 | ]))) | ||
199 | 1035 | |||
200 | 1036 | def test_verifyMacaroon_good_repository(self): | ||
201 | 1037 | build = self.factory.makeCIBuild( | ||
202 | 1038 | git_repository=self.factory.makeGitRepository( | ||
203 | 1039 | information_type=InformationType.USERDATA)) | ||
204 | 1040 | build.updateStatus(BuildStatus.BUILDING) | ||
205 | 1041 | issuer = removeSecurityProxy(getUtility(IMacaroonIssuer, "ci-build")) | ||
206 | 1042 | macaroon = issuer.issueMacaroon(build) | ||
207 | 1043 | self.assertMacaroonVerifies(issuer, macaroon, build.git_repository) | ||
208 | 1044 | |||
209 | 1045 | def test_verifyMacaroon_good_no_context(self): | ||
210 | 1046 | build = self.factory.makeCIBuild( | ||
211 | 1047 | git_repository=self.factory.makeGitRepository( | ||
212 | 1048 | information_type=InformationType.USERDATA)) | ||
213 | 1049 | build.updateStatus(BuildStatus.BUILDING) | ||
214 | 1050 | issuer = removeSecurityProxy(getUtility(IMacaroonIssuer, "ci-build")) | ||
215 | 1051 | macaroon = issuer.issueMacaroon(build) | ||
216 | 1052 | self.assertMacaroonVerifies( | ||
217 | 1053 | issuer, macaroon, None, require_context=False) | ||
218 | 1054 | self.assertMacaroonVerifies( | ||
219 | 1055 | issuer, macaroon, build.git_repository, require_context=False) | ||
220 | 1056 | |||
221 | 1057 | def test_verifyMacaroon_no_context_but_require_context(self): | ||
222 | 1058 | build = self.factory.makeCIBuild( | ||
223 | 1059 | git_repository=self.factory.makeGitRepository( | ||
224 | 1060 | information_type=InformationType.USERDATA)) | ||
225 | 1061 | build.updateStatus(BuildStatus.BUILDING) | ||
226 | 1062 | issuer = removeSecurityProxy(getUtility(IMacaroonIssuer, "ci-build")) | ||
227 | 1063 | macaroon = issuer.issueMacaroon(build) | ||
228 | 1064 | self.assertMacaroonDoesNotVerify( | ||
229 | 1065 | ["Expected macaroon verification context but got None."], | ||
230 | 1066 | issuer, macaroon, None) | ||
231 | 1067 | |||
232 | 1068 | def test_verifyMacaroon_wrong_location(self): | ||
233 | 1069 | build = self.factory.makeCIBuild( | ||
234 | 1070 | git_repository=self.factory.makeGitRepository( | ||
235 | 1071 | information_type=InformationType.USERDATA)) | ||
236 | 1072 | build.updateStatus(BuildStatus.BUILDING) | ||
237 | 1073 | issuer = removeSecurityProxy(getUtility(IMacaroonIssuer, "ci-build")) | ||
238 | 1074 | macaroon = Macaroon( | ||
239 | 1075 | location="another-location", key=issuer._root_secret) | ||
240 | 1076 | self.assertMacaroonDoesNotVerify( | ||
241 | 1077 | ["Macaroon has unknown location 'another-location'."], | ||
242 | 1078 | issuer, macaroon, build.git_repository) | ||
243 | 1079 | self.assertMacaroonDoesNotVerify( | ||
244 | 1080 | ["Macaroon has unknown location 'another-location'."], | ||
245 | 1081 | issuer, macaroon, build.git_repository, require_context=False) | ||
246 | 1082 | |||
247 | 1083 | def test_verifyMacaroon_wrong_key(self): | ||
248 | 1084 | build = self.factory.makeCIBuild( | ||
249 | 1085 | git_repository=self.factory.makeGitRepository( | ||
250 | 1086 | information_type=InformationType.USERDATA)) | ||
251 | 1087 | build.updateStatus(BuildStatus.BUILDING) | ||
252 | 1088 | issuer = removeSecurityProxy(getUtility(IMacaroonIssuer, "ci-build")) | ||
253 | 1089 | macaroon = Macaroon( | ||
254 | 1090 | location=config.vhost.mainsite.hostname, key="another-secret") | ||
255 | 1091 | self.assertMacaroonDoesNotVerify( | ||
256 | 1092 | ["Signatures do not match"], | ||
257 | 1093 | issuer, macaroon, build.git_repository) | ||
258 | 1094 | self.assertMacaroonDoesNotVerify( | ||
259 | 1095 | ["Signatures do not match"], | ||
260 | 1096 | issuer, macaroon, build.git_repository, require_context=False) | ||
261 | 1097 | |||
262 | 1098 | def test_verifyMacaroon_not_building(self): | ||
263 | 1099 | build = self.factory.makeCIBuild( | ||
264 | 1100 | git_repository=self.factory.makeGitRepository( | ||
265 | 1101 | information_type=InformationType.USERDATA)) | ||
266 | 1102 | issuer = removeSecurityProxy( | ||
267 | 1103 | getUtility(IMacaroonIssuer, "ci-build")) | ||
268 | 1104 | macaroon = issuer.issueMacaroon(build) | ||
269 | 1105 | self.assertMacaroonDoesNotVerify( | ||
270 | 1106 | ["Caveat check for 'lp.ci-build %s' failed." % build.id], | ||
271 | 1107 | issuer, macaroon, build.git_repository) | ||
272 | 1108 | |||
273 | 1109 | def test_verifyMacaroon_wrong_build(self): | ||
274 | 1110 | build = self.factory.makeCIBuild( | ||
275 | 1111 | git_repository=self.factory.makeGitRepository( | ||
276 | 1112 | information_type=InformationType.USERDATA)) | ||
277 | 1113 | build.updateStatus(BuildStatus.BUILDING) | ||
278 | 1114 | other_build = self.factory.makeCIBuild( | ||
279 | 1115 | git_repository=self.factory.makeGitRepository( | ||
280 | 1116 | information_type=InformationType.USERDATA)) | ||
281 | 1117 | other_build.updateStatus(BuildStatus.BUILDING) | ||
282 | 1118 | issuer = removeSecurityProxy(getUtility(IMacaroonIssuer, "ci-build")) | ||
283 | 1119 | macaroon = issuer.issueMacaroon(other_build) | ||
284 | 1120 | self.assertMacaroonDoesNotVerify( | ||
285 | 1121 | ["Caveat check for 'lp.ci-build %s' failed." % other_build.id], | ||
286 | 1122 | issuer, macaroon, build.git_repository) | ||
287 | 1123 | |||
288 | 1124 | def test_verifyMacaroon_wrong_repository(self): | ||
289 | 1125 | build = self.factory.makeCIBuild( | ||
290 | 1126 | git_repository=self.factory.makeGitRepository( | ||
291 | 1127 | information_type=InformationType.USERDATA)) | ||
292 | 1128 | other_repository = self.factory.makeGitRepository() | ||
293 | 1129 | build.updateStatus(BuildStatus.BUILDING) | ||
294 | 1130 | issuer = removeSecurityProxy(getUtility(IMacaroonIssuer, "ci-build")) | ||
295 | 1131 | macaroon = issuer.issueMacaroon(build) | ||
296 | 1132 | self.assertMacaroonDoesNotVerify( | ||
297 | 1133 | ["Caveat check for 'lp.ci-build %s' failed." % build.id], | ||
298 | 1134 | issuer, macaroon, other_repository) | ||
299 | diff --git a/lib/lp/code/xmlrpc/tests/test_git.py b/lib/lp/code/xmlrpc/tests/test_git.py | |||
300 | index 9dd57c1..f87abb7 100644 | |||
301 | --- a/lib/lp/code/xmlrpc/tests/test_git.py | |||
302 | +++ b/lib/lp/code/xmlrpc/tests/test_git.py | |||
303 | @@ -1803,6 +1803,47 @@ class TestGitAPI(TestGitAPIMixin, TestCaseWithFactory): | |||
304 | 1803 | repository.registrant, path, permission="read", | 1803 | repository.registrant, path, permission="read", |
305 | 1804 | macaroon_raw=macaroons[0].serialize()) | 1804 | macaroon_raw=macaroons[0].serialize()) |
306 | 1805 | 1805 | ||
307 | 1806 | def test_translatePath_private_ci_build(self): | ||
308 | 1807 | # A builder with a suitable macaroon can read from a repository | ||
309 | 1808 | # associated with a running private CI build. | ||
310 | 1809 | self.pushConfig( | ||
311 | 1810 | "launchpad", internal_macaroon_secret_key="some-secret") | ||
312 | 1811 | with person_logged_in(self.factory.makePerson()) as owner: | ||
313 | 1812 | repositories = [ | ||
314 | 1813 | self.factory.makeGitRepository( | ||
315 | 1814 | owner=owner, information_type=InformationType.USERDATA) | ||
316 | 1815 | for _ in range(2)] | ||
317 | 1816 | builds = [ | ||
318 | 1817 | self.factory.makeCIBuild(git_repository=repository) | ||
319 | 1818 | for repository in repositories] | ||
320 | 1819 | issuer = getUtility(IMacaroonIssuer, "ci-build") | ||
321 | 1820 | macaroons = [ | ||
322 | 1821 | removeSecurityProxy(issuer).issueMacaroon(build) | ||
323 | 1822 | for build in builds] | ||
324 | 1823 | repository = repositories[0] | ||
325 | 1824 | path = "/%s" % repository.unique_name | ||
326 | 1825 | self.assertUnauthorized( | ||
327 | 1826 | LAUNCHPAD_SERVICES, path, permission="write", | ||
328 | 1827 | macaroon_raw=macaroons[0].serialize()) | ||
329 | 1828 | removeSecurityProxy(builds[0]).updateStatus(BuildStatus.BUILDING) | ||
330 | 1829 | self.assertTranslates( | ||
331 | 1830 | LAUNCHPAD_SERVICES, path, repository, False, permission="read", | ||
332 | 1831 | macaroon_raw=macaroons[0].serialize(), private=True) | ||
333 | 1832 | self.assertUnauthorized( | ||
334 | 1833 | LAUNCHPAD_SERVICES, path, permission="read", | ||
335 | 1834 | macaroon_raw=macaroons[1].serialize()) | ||
336 | 1835 | self.assertUnauthorized( | ||
337 | 1836 | LAUNCHPAD_SERVICES, path, permission="read", | ||
338 | 1837 | macaroon_raw=Macaroon( | ||
339 | 1838 | location=config.vhost.mainsite.hostname, identifier="another", | ||
340 | 1839 | key="another-secret").serialize()) | ||
341 | 1840 | self.assertUnauthorized( | ||
342 | 1841 | LAUNCHPAD_SERVICES, path, permission="read", | ||
343 | 1842 | macaroon_raw="nonsense") | ||
344 | 1843 | self.assertUnauthorized( | ||
345 | 1844 | removeSecurityProxy(repository).registrant, path, | ||
346 | 1845 | permission="read", macaroon_raw=macaroons[0].serialize()) | ||
347 | 1846 | |||
348 | 1806 | def test_translatePath_user_macaroon(self): | 1847 | def test_translatePath_user_macaroon(self): |
349 | 1807 | # A user with a suitable macaroon can write to the corresponding | 1848 | # A user with a suitable macaroon can write to the corresponding |
350 | 1808 | # repository, but not others, even if they own them. | 1849 | # repository, but not others, even if they own them. |
351 | @@ -2345,6 +2386,32 @@ class TestGitAPI(TestGitAPIMixin, TestCaseWithFactory): | |||
352 | 2345 | faults.Unauthorized, None, | 2386 | faults.Unauthorized, None, |
353 | 2346 | "authenticateWithPassword", username, "nonsense") | 2387 | "authenticateWithPassword", username, "nonsense") |
354 | 2347 | 2388 | ||
355 | 2389 | def test_authenticateWithPassword_private_ci_build(self): | ||
356 | 2390 | self.pushConfig( | ||
357 | 2391 | "launchpad", internal_macaroon_secret_key="some-secret") | ||
358 | 2392 | with person_logged_in(self.factory.makePerson()) as owner: | ||
359 | 2393 | repository = self.factory.makeGitRepository( | ||
360 | 2394 | owner=owner, information_type=InformationType.USERDATA) | ||
361 | 2395 | build = self.factory.makeCIBuild(git_repository=repository) | ||
362 | 2396 | issuer = getUtility(IMacaroonIssuer, "ci-build") | ||
363 | 2397 | macaroon = removeSecurityProxy(issuer).issueMacaroon(build) | ||
364 | 2398 | for username in ("", "+launchpad-services"): | ||
365 | 2399 | self.assertEqual( | ||
366 | 2400 | {"macaroon": macaroon.serialize(), | ||
367 | 2401 | "user": "+launchpad-services"}, | ||
368 | 2402 | self.assertDoesNotFault( | ||
369 | 2403 | None, "authenticateWithPassword", | ||
370 | 2404 | username, macaroon.serialize())) | ||
371 | 2405 | other_macaroon = Macaroon( | ||
372 | 2406 | identifier="another", key="another-secret") | ||
373 | 2407 | self.assertFault( | ||
374 | 2408 | faults.Unauthorized, None, | ||
375 | 2409 | "authenticateWithPassword", | ||
376 | 2410 | username, other_macaroon.serialize()) | ||
377 | 2411 | self.assertFault( | ||
378 | 2412 | faults.Unauthorized, None, | ||
379 | 2413 | "authenticateWithPassword", username, "nonsense") | ||
380 | 2414 | |||
381 | 2348 | def test_authenticateWithPassword_user_macaroon(self): | 2415 | def test_authenticateWithPassword_user_macaroon(self): |
382 | 2349 | # A user with a suitable macaroon can authenticate using it, in | 2416 | # A user with a suitable macaroon can authenticate using it, in |
383 | 2350 | # which case we return both the macaroon and the uid for use by | 2417 | # which case we return both the macaroon and the uid for use by |
384 | @@ -2524,6 +2591,23 @@ class TestGitAPI(TestGitAPIMixin, TestCaseWithFactory): | |||
385 | 2524 | LAUNCHPAD_SERVICES, ref.repository, [path], {path: []}, | 2591 | LAUNCHPAD_SERVICES, ref.repository, [path], {path: []}, |
386 | 2525 | macaroon_raw=macaroon.serialize()) | 2592 | macaroon_raw=macaroon.serialize()) |
387 | 2526 | 2593 | ||
388 | 2594 | def test_checkRefPermissions_private_ci_build(self): | ||
389 | 2595 | # A builder with a suitable macaroon cannot write to a repository, | ||
390 | 2596 | # even if it is associated with a running private CI build. | ||
391 | 2597 | self.pushConfig( | ||
392 | 2598 | "launchpad", internal_macaroon_secret_key="some-secret") | ||
393 | 2599 | with person_logged_in(self.factory.makePerson()) as owner: | ||
394 | 2600 | [ref] = self.factory.makeGitRefs( | ||
395 | 2601 | owner=owner, information_type=InformationType.USERDATA) | ||
396 | 2602 | build = self.factory.makeCIBuild(git_repository=ref.repository) | ||
397 | 2603 | issuer = getUtility(IMacaroonIssuer, "ci-build") | ||
398 | 2604 | macaroon = removeSecurityProxy(issuer).issueMacaroon(build) | ||
399 | 2605 | build.updateStatus(BuildStatus.BUILDING) | ||
400 | 2606 | path = ref.path.encode("UTF-8") | ||
401 | 2607 | self.assertHasRefPermissions( | ||
402 | 2608 | LAUNCHPAD_SERVICES, ref.repository, [path], {path: []}, | ||
403 | 2609 | macaroon_raw=macaroon.serialize()) | ||
404 | 2610 | |||
405 | 2527 | def test_checkRefPermissions_user_macaroon(self): | 2611 | def test_checkRefPermissions_user_macaroon(self): |
406 | 2528 | # A user with a suitable macaroon has their ordinary privileges on | 2612 | # A user with a suitable macaroon has their ordinary privileges on |
407 | 2529 | # the corresponding repository, but not others, even if they own | 2613 | # the corresponding repository, but not others, even if they own |
408 | diff --git a/lib/lp/services/authserver/interfaces.py b/lib/lp/services/authserver/interfaces.py | |||
409 | index a1ec353..d94755f 100644 | |||
410 | --- a/lib/lp/services/authserver/interfaces.py | |||
411 | +++ b/lib/lp/services/authserver/interfaces.py | |||
412 | @@ -33,8 +33,8 @@ class IAuthServer(Interface): | |||
413 | 33 | `issuable_via_authserver` is True are permitted. | 33 | `issuable_via_authserver` is True are permitted. |
414 | 34 | :param context_type: A string identifying the type of context for | 34 | :param context_type: A string identifying the type of context for |
415 | 35 | which to issue the macaroon. Currently only 'LibraryFileAlias', | 35 | which to issue the macaroon. Currently only 'LibraryFileAlias', |
418 | 36 | 'BinaryPackageBuild', 'LiveFSBuild', 'SnapBuild', and | 36 | 'BinaryPackageBuild', 'LiveFSBuild', 'SnapBuild', |
419 | 37 | 'OCIRecipeBuild' are supported. | 37 | 'OCIRecipeBuild', and 'CIBuild' are supported. |
420 | 38 | :param context: The context for which to issue the macaroon. Note | 38 | :param context: The context for which to issue the macaroon. Note |
421 | 39 | that this is passed over XML-RPC, so it should be plain data | 39 | that this is passed over XML-RPC, so it should be plain data |
422 | 40 | (e.g. an ID) rather than a database object. | 40 | (e.g. an ID) rather than a database object. |
423 | @@ -47,7 +47,8 @@ class IAuthServer(Interface): | |||
424 | 47 | :param macaroon_raw: A serialised macaroon. | 47 | :param macaroon_raw: A serialised macaroon. |
425 | 48 | :param context_type: A string identifying the type of context to | 48 | :param context_type: A string identifying the type of context to |
426 | 49 | check. Currently only 'LibraryFileAlias', 'BinaryPackageBuild', | 49 | check. Currently only 'LibraryFileAlias', 'BinaryPackageBuild', |
428 | 50 | 'LiveFSBuild', 'SnapBuild', and 'OCIRecipeBuild' are supported. | 50 | 'LiveFSBuild', 'SnapBuild', 'OCIRecipeBuild', and 'CIBuild' are |
429 | 51 | supported. | ||
430 | 51 | :param context: The context to check. Note that this is passed over | 52 | :param context: The context to check. Note that this is passed over |
431 | 52 | XML-RPC, so it should be plain data (e.g. an ID) rather than a | 53 | XML-RPC, so it should be plain data (e.g. an ID) rather than a |
432 | 53 | database object. | 54 | database object. |
433 | diff --git a/lib/lp/services/authserver/xmlrpc.py b/lib/lp/services/authserver/xmlrpc.py | |||
434 | index dbee3e9..1c91bdf 100644 | |||
435 | --- a/lib/lp/services/authserver/xmlrpc.py | |||
436 | +++ b/lib/lp/services/authserver/xmlrpc.py | |||
437 | @@ -15,6 +15,7 @@ from zope.interface import implementer | |||
438 | 15 | from zope.interface.interfaces import ComponentLookupError | 15 | from zope.interface.interfaces import ComponentLookupError |
439 | 16 | from zope.security.proxy import removeSecurityProxy | 16 | from zope.security.proxy import removeSecurityProxy |
440 | 17 | 17 | ||
441 | 18 | from lp.code.interfaces.cibuild import ICIBuildSet | ||
442 | 18 | from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuildSet | 19 | from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuildSet |
443 | 19 | from lp.registry.interfaces.person import IPersonSet | 20 | from lp.registry.interfaces.person import IPersonSet |
444 | 20 | from lp.services.authserver.interfaces import ( | 21 | from lp.services.authserver.interfaces import ( |
445 | @@ -58,7 +59,8 @@ class AuthServerAPIView(LaunchpadXMLRPCView): | |||
446 | 58 | 59 | ||
447 | 59 | :param context_type: A string identifying the type of context. | 60 | :param context_type: A string identifying the type of context. |
448 | 60 | Currently only 'LibraryFileAlias', 'BinaryPackageBuild', | 61 | Currently only 'LibraryFileAlias', 'BinaryPackageBuild', |
450 | 61 | 'LiveFSBuild', 'SnapBuild', and 'OCIRecipeBuild' are supported. | 62 | 'LiveFSBuild', 'SnapBuild', 'OCIRecipeBuild', and 'CIBuild' are |
451 | 63 | supported. | ||
452 | 62 | :param context: The context as plain data (e.g. an ID). | 64 | :param context: The context as plain data (e.g. an ID). |
453 | 63 | :return: The resolved context, or None. | 65 | :return: The resolved context, or None. |
454 | 64 | """ | 66 | """ |
455 | @@ -78,8 +80,11 @@ class AuthServerAPIView(LaunchpadXMLRPCView): | |||
456 | 78 | # The context is a `SnapBuild` ID. | 80 | # The context is a `SnapBuild` ID. |
457 | 79 | return getUtility(ISnapBuildSet).getByID(context) | 81 | return getUtility(ISnapBuildSet).getByID(context) |
458 | 80 | elif context_type == 'OCIRecipeBuild': | 82 | elif context_type == 'OCIRecipeBuild': |
460 | 81 | # The context is an OCIRecipe ID. | 83 | # The context is an `OCIRecipeBuild` ID. |
461 | 82 | return getUtility(IOCIRecipeBuildSet).getByID(context) | 84 | return getUtility(IOCIRecipeBuildSet).getByID(context) |
462 | 85 | elif context_type == 'CIBuild': | ||
463 | 86 | # The context is a `CIBuild` ID. | ||
464 | 87 | return getUtility(ICIBuildSet).getByID(context) | ||
465 | 83 | else: | 88 | else: |
466 | 84 | return None | 89 | return None |
467 | 85 | 90 |
Looks good from comparing with the other build types (not that I fully understand every bit) :)