Merge lp:~cjwatson/launchpad/restore-git-snap-build-macaroon into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18964
Proposed branch: lp:~cjwatson/launchpad/restore-git-snap-build-macaroon
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/refactor-git-code-import-authz-2
Diff against target: 274 lines (+145/-3)
5 files modified
lib/lp/code/model/tests/test_codeimportjob.py (+2/-0)
lib/lp/code/xmlrpc/git.py (+16/-2)
lib/lp/code/xmlrpc/tests/test_git.py (+89/-1)
lib/lp/snappy/model/snapbuild.py (+5/-0)
lib/lp/snappy/tests/test_snapbuild.py (+33/-0)
To merge this branch: bzr merge lp:~cjwatson/launchpad/restore-git-snap-build-macaroon
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+367204@code.launchpad.net

Commit message

Make GitAPI accept snap-build macaroons again.

Description of the change

I added support for snap-build macaroons recently, and then promptly forgot about it when refactoring code import authorisation, largely because I hadn't explicitly written any tests for it.

In the process of writing these tests, I noticed that I really should have forced snap-build macaroons to be read-only, so I've done that. The mechanism for doing this is pretty ad-hoc, but we can always improve it later.

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
=== modified file 'lib/lp/code/model/tests/test_codeimportjob.py'
--- lib/lp/code/model/tests/test_codeimportjob.py 2019-04-26 13:13:37 +0000
+++ lib/lp/code/model/tests/test_codeimportjob.py 2019-05-09 16:04:44 +0000
@@ -1336,6 +1336,8 @@
1336 getUtility(ICodeImportJobWorkflow).startJob(job, machine)1336 getUtility(ICodeImportJobWorkflow).startJob(job, machine)
1337 macaroon = removeSecurityProxy(issuer).issueMacaroon(job)1337 macaroon = removeSecurityProxy(issuer).issueMacaroon(job)
1338 self.assertMacaroonVerifies(1338 self.assertMacaroonVerifies(
1339 issuer, macaroon, None, require_context=False)
1340 self.assertMacaroonVerifies(
1339 issuer, macaroon, job, require_context=False)1341 issuer, macaroon, job, require_context=False)
1340 self.assertMacaroonVerifies(1342 self.assertMacaroonVerifies(
1341 issuer, macaroon, job.code_import.target, require_context=False)1343 issuer, macaroon, job.code_import.target, require_context=False)
13421344
=== modified file 'lib/lp/code/xmlrpc/git.py'
--- lib/lp/code/xmlrpc/git.py 2019-05-09 16:04:44 +0000
+++ lib/lp/code/xmlrpc/git.py 2019-05-09 16:04:44 +0000
@@ -103,6 +103,17 @@
103103
104 :param verified: An `IMacaroonVerificationResult`.104 :param verified: An `IMacaroonVerificationResult`.
105 """105 """
106 return verified.issuer_name in ("code-import-job", "snap-build")
107
108
109def _can_internal_issuer_write(verified):
110 """Does this internal-only issuer have write access?
111
112 Some macaroons used by internal services are intended for writing to the
113 repository; others only allow read access.
114
115 :param verified: An `IMacaroonVerificationResult`.
116 """
106 return verified.issuer_name == "code-import-job"117 return verified.issuer_name == "code-import-job"
107118
108119
@@ -162,7 +173,7 @@
162 # so we can bypass other checks. This is only permitted for173 # so we can bypass other checks. This is only permitted for
163 # selected macaroon issuers used by internal services.174 # selected macaroon issuers used by internal services.
164 hosting_path = naked_repository.getInternalPath()175 hosting_path = naked_repository.getInternalPath()
165 writable = True176 writable = _can_internal_issuer_write(verified)
166 private = naked_repository.private177 private = naked_repository.private
167178
168 # In any other case, the macaroon constrains the permissions of179 # In any other case, the macaroon constrains the permissions of
@@ -387,7 +398,7 @@
387 verified = self._verifyMacaroon(password)398 verified = self._verifyMacaroon(password)
388 if verified:399 if verified:
389 auth_params = {"macaroon": password}400 auth_params = {"macaroon": password}
390 if verified.issuer_name == "code-import-job":401 if _is_issuer_internal(verified):
391 auth_params["user"] = LAUNCHPAD_SERVICES402 auth_params["user"] = LAUNCHPAD_SERVICES
392 return auth_params403 return auth_params
393 # Only macaroons are supported for password authentication.404 # Only macaroons are supported for password authentication.
@@ -432,6 +443,9 @@
432 raise faults.Unauthorized()443 raise faults.Unauthorized()
433444
434 if internal:445 if internal:
446 if not _can_internal_issuer_write(verified):
447 raise faults.Unauthorized()
448
435 # We know that the authentication parameters449 # We know that the authentication parameters
436 # specifically grant access to this repository because450 # specifically grant access to this repository because
437 # we were able to verify the macaroon using the451 # we were able to verify the macaroon using the
438452
=== modified file 'lib/lp/code/xmlrpc/tests/test_git.py'
--- lib/lp/code/xmlrpc/tests/test_git.py 2019-05-09 16:04:44 +0000
+++ lib/lp/code/xmlrpc/tests/test_git.py 2019-05-09 16:04:44 +0000
@@ -20,6 +20,7 @@
20from zope.security.proxy import removeSecurityProxy20from zope.security.proxy import removeSecurityProxy
2121
22from lp.app.enums import InformationType22from lp.app.enums import InformationType
23from lp.buildmaster.enums import BuildStatus
23from lp.code.enums import (24from lp.code.enums import (
24 GitGranteeType,25 GitGranteeType,
25 GitRepositoryType,26 GitRepositoryType,
@@ -38,8 +39,10 @@
38from lp.code.xmlrpc.git import GitAPI39from lp.code.xmlrpc.git import GitAPI
39from lp.registry.enums import TeamMembershipPolicy40from lp.registry.enums import TeamMembershipPolicy
40from lp.services.config import config41from lp.services.config import config
42from lp.services.features.testing import FeatureFixture
41from lp.services.macaroons.interfaces import IMacaroonIssuer43from lp.services.macaroons.interfaces import IMacaroonIssuer
42from lp.services.webapp.escaping import html_escape44from lp.services.webapp.escaping import html_escape
45from lp.snappy.interfaces.snap import SNAP_TESTING_FLAGS
43from lp.testing import (46from lp.testing import (
44 admin_logged_in,47 admin_logged_in,
45 ANONYMOUS,48 ANONYMOUS,
@@ -1035,6 +1038,49 @@
1035 code_imports[0].registrant, path, permission="write",1038 code_imports[0].registrant, path, permission="write",
1036 macaroon_raw=macaroons[0].serialize())1039 macaroon_raw=macaroons[0].serialize())
10371040
1041 def test_translatePath_private_snap_build(self):
1042 # A builder with a suitable macaroon can read from a repository
1043 # associated with a running private snap build.
1044 self.useFixture(FeatureFixture(SNAP_TESTING_FLAGS))
1045 self.pushConfig(
1046 "launchpad", internal_macaroon_secret_key="some-secret")
1047 with person_logged_in(self.factory.makePerson()) as owner:
1048 refs = [
1049 self.factory.makeGitRefs(
1050 owner=owner, information_type=InformationType.USERDATA)[0]
1051 for _ in range(2)]
1052 builds = [
1053 self.factory.makeSnapBuild(
1054 requester=owner, owner=owner, git_ref=ref, private=True)
1055 for ref in refs]
1056 issuer = getUtility(IMacaroonIssuer, "snap-build")
1057 macaroons = [
1058 removeSecurityProxy(issuer).issueMacaroon(build)
1059 for build in builds]
1060 repository = refs[0].repository
1061 path = u"/%s" % repository.unique_name
1062 self.assertUnauthorized(
1063 LAUNCHPAD_SERVICES, path, permission="write",
1064 macaroon_raw=macaroons[0].serialize())
1065 removeSecurityProxy(builds[0]).updateStatus(BuildStatus.BUILDING)
1066 self.assertTranslates(
1067 LAUNCHPAD_SERVICES, path, repository, False, permission="read",
1068 macaroon_raw=macaroons[0].serialize(), private=True)
1069 self.assertUnauthorized(
1070 LAUNCHPAD_SERVICES, path, permission="read",
1071 macaroon_raw=macaroons[1].serialize())
1072 self.assertUnauthorized(
1073 LAUNCHPAD_SERVICES, path, permission="read",
1074 macaroon_raw=Macaroon(
1075 location=config.vhost.mainsite.hostname, identifier="another",
1076 key="another-secret").serialize())
1077 self.assertUnauthorized(
1078 LAUNCHPAD_SERVICES, path, permission="read",
1079 macaroon_raw="nonsense")
1080 self.assertUnauthorized(
1081 repository.registrant, path, permission="read",
1082 macaroon_raw=macaroons[0].serialize())
1083
1038 def test_notify(self):1084 def test_notify(self):
1039 # The notify call creates a GitRefScanJob.1085 # The notify call creates a GitRefScanJob.
1040 repository = self.factory.makeGitRepository()1086 repository = self.factory.makeGitRepository()
@@ -1088,6 +1134,29 @@
1088 self.git_api.authenticateWithPassword("", "nonsense"),1134 self.git_api.authenticateWithPassword("", "nonsense"),
1089 faults.Unauthorized)1135 faults.Unauthorized)
10901136
1137 def test_authenticateWithPassword_private_snap_build(self):
1138 self.useFixture(FeatureFixture(SNAP_TESTING_FLAGS))
1139 self.pushConfig(
1140 "launchpad", internal_macaroon_secret_key="some-secret")
1141 with person_logged_in(self.factory.makePerson()) as owner:
1142 [ref] = self.factory.makeGitRefs(
1143 owner=owner, information_type=InformationType.USERDATA)
1144 build = self.factory.makeSnapBuild(
1145 requester=owner, owner=owner, git_ref=ref, private=True)
1146 issuer = getUtility(IMacaroonIssuer, "snap-build")
1147 macaroon = removeSecurityProxy(issuer).issueMacaroon(build)
1148 self.assertEqual(
1149 {"macaroon": macaroon.serialize(), "user": "+launchpad-services"},
1150 self.git_api.authenticateWithPassword("", macaroon.serialize()))
1151 other_macaroon = Macaroon(identifier="another", key="another-secret")
1152 self.assertIsInstance(
1153 self.git_api.authenticateWithPassword(
1154 "", other_macaroon.serialize()),
1155 faults.Unauthorized)
1156 self.assertIsInstance(
1157 self.git_api.authenticateWithPassword("", "nonsense"),
1158 faults.Unauthorized)
1159
1091 def test_checkRefPermissions_code_import(self):1160 def test_checkRefPermissions_code_import(self):
1092 # A code import worker with a suitable macaroon has repository owner1161 # A code import worker with a suitable macaroon has repository owner
1093 # privileges on a repository associated with a running code import1162 # privileges on a repository associated with a running code import
@@ -1157,7 +1226,7 @@
1157 ref_path = "refs/heads/master"1226 ref_path = "refs/heads/master"
1158 self.assertHasRefPermissions(1227 self.assertHasRefPermissions(
1159 LAUNCHPAD_SERVICES, repository, [ref_path], {ref_path: []},1228 LAUNCHPAD_SERVICES, repository, [ref_path], {ref_path: []},
1160 macaroon_raw=macaroons[0])1229 macaroon_raw=macaroons[0].serialize())
1161 with celebrity_logged_in("vcs_imports"):1230 with celebrity_logged_in("vcs_imports"):
1162 getUtility(ICodeImportJobWorkflow).startJob(jobs[0], machine)1231 getUtility(ICodeImportJobWorkflow).startJob(jobs[0], machine)
1163 self.assertHasRefPermissions(1232 self.assertHasRefPermissions(
@@ -1179,6 +1248,25 @@
1179 code_imports[0].registrant, repository, [ref_path], {ref_path: []},1248 code_imports[0].registrant, repository, [ref_path], {ref_path: []},
1180 macaroon_raw=macaroons[0].serialize())1249 macaroon_raw=macaroons[0].serialize())
11811250
1251 def test_checkRefPermissions_private_snap_build(self):
1252 # A builder with a suitable macaroon cannot write to a repository,
1253 # even if it is associated with a running private snap build.
1254 self.useFixture(FeatureFixture(SNAP_TESTING_FLAGS))
1255 self.pushConfig(
1256 "launchpad", internal_macaroon_secret_key="some-secret")
1257 with person_logged_in(self.factory.makePerson()) as owner:
1258 [ref] = self.factory.makeGitRefs(
1259 owner=owner, information_type=InformationType.USERDATA)
1260 build = self.factory.makeSnapBuild(
1261 requester=owner, owner=owner, git_ref=ref, private=True)
1262 issuer = getUtility(IMacaroonIssuer, "snap-build")
1263 macaroon = removeSecurityProxy(issuer).issueMacaroon(build)
1264 build.updateStatus(BuildStatus.BUILDING)
1265 path = ref.path.encode("UTF-8")
1266 self.assertHasRefPermissions(
1267 LAUNCHPAD_SERVICES, ref.repository, [path], {path: []},
1268 macaroon_raw=macaroon.serialize())
1269
11821270
1183class TestGitAPISecurity(TestGitAPIMixin, TestCaseWithFactory):1271class TestGitAPISecurity(TestGitAPIMixin, TestCaseWithFactory):
1184 """Slow tests for `IGitAPI`.1272 """Slow tests for `IGitAPI`.
11851273
=== modified file 'lib/lp/snappy/model/snapbuild.py'
--- lib/lp/snappy/model/snapbuild.py 2019-05-01 15:47:37 +0000
+++ lib/lp/snappy/model/snapbuild.py 2019-05-09 16:04:44 +0000
@@ -628,6 +628,11 @@
628 # Circular import.628 # Circular import.
629 from lp.snappy.model.snap import Snap629 from lp.snappy.model.snap import Snap
630630
631 if context is None:
632 # We're only verifying that the macaroon could be valid for some
633 # context.
634 return True
635
631 try:636 try:
632 build_id = int(caveat_value)637 build_id = int(caveat_value)
633 except ValueError:638 except ValueError:
634639
=== modified file 'lib/lp/snappy/tests/test_snapbuild.py'
--- lib/lp/snappy/tests/test_snapbuild.py 2019-04-25 21:57:30 +0000
+++ lib/lp/snappy/tests/test_snapbuild.py 2019-05-09 16:04:44 +0000
@@ -843,6 +843,33 @@
843 macaroon = issuer.issueMacaroon(build)843 macaroon = issuer.issueMacaroon(build)
844 self.assertMacaroonVerifies(issuer, macaroon, ref.repository)844 self.assertMacaroonVerifies(issuer, macaroon, ref.repository)
845845
846 def test_verifyMacaroon_good_no_context(self):
847 [ref] = self.factory.makeGitRefs(
848 information_type=InformationType.USERDATA)
849 build = self.factory.makeSnapBuild(
850 snap=self.factory.makeSnap(git_ref=ref, private=True))
851 build.updateStatus(BuildStatus.BUILDING)
852 issuer = removeSecurityProxy(
853 getUtility(IMacaroonIssuer, "snap-build"))
854 macaroon = issuer.issueMacaroon(build)
855 self.assertMacaroonVerifies(
856 issuer, macaroon, None, require_context=False)
857 self.assertMacaroonVerifies(
858 issuer, macaroon, ref.repository, require_context=False)
859
860 def test_verifyMacaroon_no_context_but_require_context(self):
861 [ref] = self.factory.makeGitRefs(
862 information_type=InformationType.USERDATA)
863 build = self.factory.makeSnapBuild(
864 snap=self.factory.makeSnap(git_ref=ref, private=True))
865 build.updateStatus(BuildStatus.BUILDING)
866 issuer = removeSecurityProxy(
867 getUtility(IMacaroonIssuer, "snap-build"))
868 macaroon = issuer.issueMacaroon(build)
869 self.assertMacaroonDoesNotVerify(
870 ["Expected macaroon verification context but got None."],
871 issuer, macaroon, None)
872
846 def test_verifyMacaroon_wrong_location(self):873 def test_verifyMacaroon_wrong_location(self):
847 [ref] = self.factory.makeGitRefs(874 [ref] = self.factory.makeGitRefs(
848 information_type=InformationType.USERDATA)875 information_type=InformationType.USERDATA)
@@ -856,6 +883,9 @@
856 self.assertMacaroonDoesNotVerify(883 self.assertMacaroonDoesNotVerify(
857 ["Macaroon has unknown location 'another-location'."],884 ["Macaroon has unknown location 'another-location'."],
858 issuer, macaroon, ref.repository)885 issuer, macaroon, ref.repository)
886 self.assertMacaroonDoesNotVerify(
887 ["Macaroon has unknown location 'another-location'."],
888 issuer, macaroon, ref.repository, require_context=False)
859889
860 def test_verifyMacaroon_wrong_key(self):890 def test_verifyMacaroon_wrong_key(self):
861 [ref] = self.factory.makeGitRefs(891 [ref] = self.factory.makeGitRefs(
@@ -869,6 +899,9 @@
869 location=config.vhost.mainsite.hostname, key="another-secret")899 location=config.vhost.mainsite.hostname, key="another-secret")
870 self.assertMacaroonDoesNotVerify(900 self.assertMacaroonDoesNotVerify(
871 ["Signatures do not match"], issuer, macaroon, ref.repository)901 ["Signatures do not match"], issuer, macaroon, ref.repository)
902 self.assertMacaroonDoesNotVerify(
903 ["Signatures do not match"],
904 issuer, macaroon, ref.repository, require_context=False)
872905
873 def test_verifyMacaroon_refuses_branch(self):906 def test_verifyMacaroon_refuses_branch(self):
874 branch = self.factory.makeAnyBranch(907 branch = self.factory.makeAnyBranch(