Merge lp:~cjwatson/launchpad/git-checkRefPermissions-macaroon into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18810
Proposed branch: lp:~cjwatson/launchpad/git-checkRefPermissions-macaroon
Merge into: lp:launchpad
Diff against target: 208 lines (+129/-12)
2 files modified
lib/lp/code/xmlrpc/git.py (+27/-8)
lib/lp/code/xmlrpc/tests/test_git.py (+102/-4)
To merge this branch: bzr merge lp:~cjwatson/launchpad/git-checkRefPermissions-macaroon
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+357732@code.launchpad.net

Commit message

Make GitAPI.checkRefPermissions work for pushes from code import workers.

Description of the change

See e.g. https://oops.canonical.com/oops/?oopsid=OOPS-030d238eea8c79e6d52e0205deafd603.

I've tried to follow the structure of translatePath and similar here.

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) wrote :

<wgrant> I need to go now but the diff looks good
<wgrant> Feel free to self approve
<wgrant> Checked in loggerhead

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/code/xmlrpc/git.py'
--- lib/lp/code/xmlrpc/git.py 2018-10-23 16:36:25 +0000
+++ lib/lp/code/xmlrpc/git.py 2018-10-24 01:17:40 +0000
@@ -366,18 +366,35 @@
366 matching_rules.append(rule)366 matching_rules.append(rule)
367 return matching_rules367 return matching_rules
368368
369 def _checkRefPermissions(self, requester, translated_path, ref_paths):369 def _checkRefPermissions(self, requester, translated_path, ref_paths,
370 auth_params):
371 if requester == LAUNCHPAD_ANONYMOUS:
372 requester = None
370 repository = removeSecurityProxy(373 repository = removeSecurityProxy(
371 getUtility(IGitLookup).getByHostingPath(translated_path))374 getUtility(IGitLookup).getByHostingPath(translated_path))
372 is_owner = requester.inTeam(repository.owner)
373 result = {}375 result = {}
374376
375 grants_for_user = defaultdict(list)377 grants_for_user = defaultdict(list)
376 grants = repository.findRuleGrantsByGrantee(requester)378 macaroon_raw = auth_params.get("macaroon")
377 if is_owner:379 if (macaroon_raw is not None and
378 owner_grants = repository.findRuleGrantsByGrantee(380 self._verifyMacaroon(macaroon_raw, repository)):
381 # The authentication parameters grant access as an anonymous
382 # repository owner.
383 # For the time being, this only works for code imports.
384 assert repository.repository_type == GitRepositoryType.IMPORTED
385 is_owner = True
386 grants = repository.findRuleGrantsByGrantee(
379 GitGranteeType.REPOSITORY_OWNER)387 GitGranteeType.REPOSITORY_OWNER)
380 grants = grants.union(owner_grants)388 elif requester is None:
389 is_owner = False
390 grants = []
391 else:
392 is_owner = requester.inTeam(repository.owner)
393 grants = repository.findRuleGrantsByGrantee(requester)
394 if is_owner:
395 owner_grants = repository.findRuleGrantsByGrantee(
396 GitGranteeType.REPOSITORY_OWNER)
397 grants = grants.union(owner_grants)
381 for grant in grants:398 for grant in grants:
382 grants_for_user[grant.rule].append(grant)399 grants_for_user[grant.rule].append(grant)
383400
@@ -407,9 +424,11 @@
407 def checkRefPermissions(self, translated_path, ref_paths, auth_params):424 def checkRefPermissions(self, translated_path, ref_paths, auth_params):
408 """ See `IGitAPI`"""425 """ See `IGitAPI`"""
409 requester_id = auth_params.get("uid")426 requester_id = auth_params.get("uid")
427 if requester_id is None:
428 requester_id = LAUNCHPAD_ANONYMOUS
410 return run_with_login(429 return run_with_login(
411 requester_id,430 requester_id,
412 self._checkRefPermissions,431 self._checkRefPermissions,
413 translated_path,432 translated_path,
414 ref_paths433 ref_paths,
415 )434 auth_params)
416435
=== modified file 'lib/lp/code/xmlrpc/tests/test_git.py'
--- lib/lp/code/xmlrpc/tests/test_git.py 2018-10-23 16:36:45 +0000
+++ lib/lp/code/xmlrpc/tests/test_git.py 2018-10-24 01:17:40 +0000
@@ -1,4 +1,4 @@
1# Copyright 2015-2016 Canonical Ltd. This software is licensed under the1# Copyright 2015-2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Tests for the internal Git API."""4"""Tests for the internal Git API."""
@@ -8,7 +8,6 @@
8from pymacaroons import Macaroon8from pymacaroons import Macaroon
9from testtools.matchers import (9from testtools.matchers import (
10 Equals,10 Equals,
11 MatchesListwise,
12 MatchesDict,11 MatchesDict,
13 )12 )
14from zope.component import getUtility13from zope.component import getUtility
@@ -187,6 +186,20 @@
187 {"clone_from": cloned_from.getInternalPath()},186 {"clone_from": cloned_from.getInternalPath()},
188 self.hosting_fixture.create.extract_kwargs()[0])187 self.hosting_fixture.create.extract_kwargs()[0])
189188
189 def assertHasRefPermissions(self, requester, repository, ref_paths,
190 permissions, macaroon_raw=None):
191 if requester is not None:
192 requester = requester.id
193 auth_params = {"uid": requester}
194 if macaroon_raw is not None:
195 auth_params["macaroon"] = macaroon_raw
196 translated_path = removeSecurityProxy(repository).getInternalPath()
197 results = self.git_api.checkRefPermissions(
198 translated_path, ref_paths, auth_params)
199 self.assertThat(results, MatchesDict({
200 ref_path: Equals(ref_permissions)
201 for ref_path, ref_permissions in permissions.items()}))
202
190 def test_translatePath_private_repository(self):203 def test_translatePath_private_repository(self):
191 requester = self.factory.makePerson()204 requester = self.factory.makePerson()
192 repository = removeSecurityProxy(205 repository = removeSecurityProxy(
@@ -893,7 +906,8 @@
893 removeSecurityProxy(issuer).issueMacaroon(job) for job in jobs]906 removeSecurityProxy(issuer).issueMacaroon(job) for job in jobs]
894 path = u"/%s" % code_imports[0].git_repository.unique_name907 path = u"/%s" % code_imports[0].git_repository.unique_name
895 self.assertPermissionDenied(908 self.assertPermissionDenied(
896 None, path, permission="write", macaroon_raw=macaroons[0])909 None, path, permission="write",
910 macaroon_raw=macaroons[0].serialize())
897 with celebrity_logged_in("vcs_imports"):911 with celebrity_logged_in("vcs_imports"):
898 getUtility(ICodeImportJobWorkflow).startJob(jobs[0], machine)912 getUtility(ICodeImportJobWorkflow).startJob(jobs[0], machine)
899 self.assertTranslates(913 self.assertTranslates(
@@ -931,7 +945,8 @@
931 removeSecurityProxy(issuer).issueMacaroon(job) for job in jobs]945 removeSecurityProxy(issuer).issueMacaroon(job) for job in jobs]
932 path = u"/%s" % code_imports[0].git_repository.unique_name946 path = u"/%s" % code_imports[0].git_repository.unique_name
933 self.assertPermissionDenied(947 self.assertPermissionDenied(
934 None, path, permission="write", macaroon_raw=macaroons[0])948 None, path, permission="write",
949 macaroon_raw=macaroons[0].serialize())
935 with celebrity_logged_in("vcs_imports"):950 with celebrity_logged_in("vcs_imports"):
936 getUtility(ICodeImportJobWorkflow).startJob(jobs[0], machine)951 getUtility(ICodeImportJobWorkflow).startJob(jobs[0], machine)
937 self.assertTranslates(952 self.assertTranslates(
@@ -1003,6 +1018,89 @@
1003 self.git_api.authenticateWithPassword("", "nonsense"),1018 self.git_api.authenticateWithPassword("", "nonsense"),
1004 faults.Unauthorized)1019 faults.Unauthorized)
10051020
1021 def test_checkRefPermissions_code_import(self):
1022 # A code import worker with a suitable macaroon has repository owner
1023 # privileges on a repository associated with a running code import
1024 # job.
1025 self.pushConfig("codeimport", macaroon_secret_key="some-secret")
1026 machine = self.factory.makeCodeImportMachine(set_online=True)
1027 code_imports = [
1028 self.factory.makeCodeImport(
1029 target_rcs_type=TargetRevisionControlSystems.GIT)
1030 for _ in range(2)]
1031 with celebrity_logged_in("vcs_imports"):
1032 jobs = [
1033 self.factory.makeCodeImportJob(code_import=code_import)
1034 for code_import in code_imports]
1035 issuer = getUtility(IMacaroonIssuer, "code-import-job")
1036 macaroons = [
1037 removeSecurityProxy(issuer).issueMacaroon(job) for job in jobs]
1038 repository = code_imports[0].git_repository
1039 ref_path = "refs/heads/master"
1040 self.assertHasRefPermissions(
1041 None, repository, [ref_path], {ref_path: []},
1042 macaroon_raw=macaroons[0].serialize())
1043 with celebrity_logged_in("vcs_imports"):
1044 getUtility(ICodeImportJobWorkflow).startJob(jobs[0], machine)
1045 self.assertHasRefPermissions(
1046 None, repository, [ref_path],
1047 {ref_path: ["create", "push", "force_push"]},
1048 macaroon_raw=macaroons[0].serialize())
1049 self.assertHasRefPermissions(
1050 None, repository, [ref_path], {ref_path: []},
1051 macaroon_raw=macaroons[1].serialize())
1052 self.assertHasRefPermissions(
1053 None, repository, [ref_path], {ref_path: []},
1054 macaroon_raw=Macaroon(
1055 location=config.vhost.mainsite.hostname, identifier="another",
1056 key="another-secret").serialize())
1057 self.assertHasRefPermissions(
1058 None, repository, [ref_path], {ref_path: []},
1059 macaroon_raw="nonsense")
1060
1061 def test_checkRefPermissions_private_code_import(self):
1062 # A code import worker with a suitable macaroon has repository owner
1063 # privileges on a repository associated with a running private code
1064 # import job.
1065 self.pushConfig("codeimport", macaroon_secret_key="some-secret")
1066 machine = self.factory.makeCodeImportMachine(set_online=True)
1067 code_imports = [
1068 self.factory.makeCodeImport(
1069 target_rcs_type=TargetRevisionControlSystems.GIT)
1070 for _ in range(2)]
1071 private_repository = code_imports[0].git_repository
1072 removeSecurityProxy(private_repository).transitionToInformationType(
1073 InformationType.PRIVATESECURITY, private_repository.owner)
1074 with celebrity_logged_in("vcs_imports"):
1075 jobs = [
1076 self.factory.makeCodeImportJob(code_import=code_import)
1077 for code_import in code_imports]
1078 issuer = getUtility(IMacaroonIssuer, "code-import-job")
1079 macaroons = [
1080 removeSecurityProxy(issuer).issueMacaroon(job) for job in jobs]
1081 repository = code_imports[0].git_repository
1082 ref_path = "refs/heads/master"
1083 self.assertHasRefPermissions(
1084 None, repository, [ref_path], {ref_path: []},
1085 macaroon_raw=macaroons[0])
1086 with celebrity_logged_in("vcs_imports"):
1087 getUtility(ICodeImportJobWorkflow).startJob(jobs[0], machine)
1088 self.assertHasRefPermissions(
1089 None, repository, [ref_path],
1090 {ref_path: ["create", "push", "force_push"]},
1091 macaroon_raw=macaroons[0].serialize())
1092 self.assertHasRefPermissions(
1093 None, repository, [ref_path], {ref_path: []},
1094 macaroon_raw=macaroons[1].serialize())
1095 self.assertHasRefPermissions(
1096 None, repository, [ref_path], {ref_path: []},
1097 macaroon_raw=Macaroon(
1098 location=config.vhost.mainsite.hostname, identifier="another",
1099 key="another-secret").serialize())
1100 self.assertHasRefPermissions(
1101 None, repository, [ref_path], {ref_path: []},
1102 macaroon_raw="nonsense")
1103
10061104
1007class TestGitAPISecurity(TestGitAPIMixin, TestCaseWithFactory):1105class TestGitAPISecurity(TestGitAPIMixin, TestCaseWithFactory):
1008 """Slow tests for `IGitAPI`.1106 """Slow tests for `IGitAPI`.