Merge ~pappacena/launchpad:xmlrpc-git-confirmRepoCreation into launchpad:master

Proposed by Thiago F. Pappacena
Status: Merged
Approved by: Thiago F. Pappacena
Approved revision: 367bfb34f2edaa7a7402d21724b4e2acb35d9014
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~pappacena/launchpad:xmlrpc-git-confirmRepoCreation
Merge into: launchpad:master
Prerequisite: ~pappacena/launchpad:gitrepo-status
Diff against target: 649 lines (+556/-5)
4 files modified
lib/lp/code/interfaces/gitapi.py (+24/-1)
lib/lp/code/model/gitrepository.py (+3/-2)
lib/lp/code/xmlrpc/git.py (+82/-1)
lib/lp/code/xmlrpc/tests/test_git.py (+447/-1)
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Ioana Lasc (community) Approve
Review via email: mp+385301@code.launchpad.net

Commit message

New Git XML-RPC methods: confirmRepoCreation/abortRepoCreation

The new flow on Turnip for repository creation will start on Turnip side itself, after Launchpad's translatePath call returning that a repository does not exist. After the local repository creation, Turnip will either confirm or abort the repository creation on LP using those new XML-RPC methods.

To post a comment you must log in.
bae4995... by Thiago F. Pappacena

Adding abortRepoCreation method

Revision history for this message
Colin Watson (cjwatson) wrote :

This looks like a really good start - thanks! There are some things that need sorting out related to the exact shape of the API and details of authorisation.

review: Needs Fixing
Revision history for this message
Colin Watson (cjwatson) wrote :

Also, could you make the MP commit message follow the recommendations in https://chris.beams.io/posts/git-commit/, so that the end result looks nicer in our history?

3e4eb7e... by Thiago F. Pappacena

Merge branch 'gitrepo-status' into xmlrpc-git-confirmRepoCreation

a93ffa7... by Thiago F. Pappacena

Automatically reclaim space if repo is AVAILABLE

5f1d7f1... by Thiago F. Pappacena

Adding more tests for operations authorization check

cfae7b8... by Thiago F. Pappacena

Using repository's path instead of id on abort/confirmRepoCreation calls

Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

I went through all the comments and fixed them.

Also, while writing some new tests I realized that we are not deleting code imports when we delete git repositories (and it breaks some "abort" scenarios). I've added the CodeImport removal.

Revision history for this message
Ioana Lasc (ilasc) wrote :

Nicely done!
Apart from one small comment inline, it looks good.

review: Needs Information
44fb2a8... by Thiago F. Pappacena

Merge branch 'master' into xmlrpc-git-confirmRepoCreation

367bfb3... by Thiago F. Pappacena

Break references when aborting repository creation

Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

Thanks for the comment, ilasc! I haven't noticed that. Pushing the changes now.

Revision history for this message
Ioana Lasc (ilasc) :
review: Approve
Revision history for this message
Colin Watson (cjwatson) :
review: Approve
Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

Pushed the requested changes.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/lib/lp/code/interfaces/gitapi.py b/lib/lp/code/interfaces/gitapi.py
index c289dee..e4a91db 100644
--- a/lib/lp/code/interfaces/gitapi.py
+++ b/lib/lp/code/interfaces/gitapi.py
@@ -1,4 +1,4 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the1# Copyright 2015-2020 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"""Interfaces for internal Git APIs."""4"""Interfaces for internal Git APIs."""
@@ -92,3 +92,26 @@ class IGitAPI(Interface):
92 if no repository can be found for 'translated_path',92 if no repository can be found for 'translated_path',
93 or an `Unauthorized` fault for unauthorized push attempts.93 or an `Unauthorized` fault for unauthorized push attempts.
94 """94 """
95
96 def confirmRepoCreation(repository_id):
97 """Confirm that repository creation.
98
99 When code hosting finishes creating the repository locally,
100 it should call back this method to confirm that the repository was
101 created, and Launchpad should make the repository available for end
102 users.
103
104 :param repository_id: The database ID of the repository, provided by
105 translatePath call when repo creation is necessary.
106 """
107
108 def abortRepoCreation(repository_id):
109 """Abort the creation of a repository, removing it from database.
110
111 When code hosting fails to create a repository locally, it should
112 call back this method to indicate that the operation failed and the
113 repository should be removed from Launchpad's database.
114
115 :param repository_id: The database ID of the repository, provided by
116 translatePath call when repo creation is necessary.
117 """
diff --git a/lib/lp/code/model/gitrepository.py b/lib/lp/code/model/gitrepository.py
index bd1f3e7..c05b43b 100644
--- a/lib/lp/code/model/gitrepository.py
+++ b/lib/lp/code/model/gitrepository.py
@@ -1639,8 +1639,9 @@ class GitRepository(StormBase, WebhookTargetMixin, GitIdentityMixin):
1639 Store.of(self).remove(self)1639 Store.of(self).remove(self)
1640 # And now create a job to remove the repository from storage when1640 # And now create a job to remove the repository from storage when
1641 # it's done.1641 # it's done.
1642 getUtility(IReclaimGitRepositorySpaceJobSource).create(1642 if self.status == GitRepositoryStatus.AVAILABLE:
1643 repository_name, repository_path)1643 getUtility(IReclaimGitRepositorySpaceJobSource).create(
1644 repository_name, repository_path)
16441645
16451646
1646class DeletionOperation:1647class DeletionOperation:
diff --git a/lib/lp/code/xmlrpc/git.py b/lib/lp/code/xmlrpc/git.py
index b34307a..1e00b78 100644
--- a/lib/lp/code/xmlrpc/git.py
+++ b/lib/lp/code/xmlrpc/git.py
@@ -1,4 +1,4 @@
1# Copyright 2015-2019 Canonical Ltd. This software is licensed under the1# Copyright 2015-2020 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"""Implementations of the XML-RPC APIs for Git."""4"""Implementations of the XML-RPC APIs for Git."""
@@ -31,6 +31,7 @@ from lp.app.validators import LaunchpadValidationError
31from lp.code.enums import (31from lp.code.enums import (
32 GitGranteeType,32 GitGranteeType,
33 GitPermissionType,33 GitPermissionType,
34 GitRepositoryStatus,
34 GitRepositoryType,35 GitRepositoryType,
35 )36 )
36from lp.code.errors import (37from lp.code.errors import (
@@ -584,3 +585,83 @@ class GitAPI(LaunchpadXMLRPCView):
584 [(ref_path.data, permissions)585 [(ref_path.data, permissions)
585 for ref_path, permissions in result])586 for ref_path, permissions in result])
586 return result587 return result
588
589 def _validateRequesterCanManageRepoCreation(
590 self, requester, repository, auth_params):
591 """Makes sure the requester has permission to change repository
592 creation status."""
593 naked_repo = removeSecurityProxy(repository)
594 if requester == LAUNCHPAD_ANONYMOUS:
595 requester = None
596
597 verified = self._verifyAuthParams(requester, repository, auth_params)
598 if verified is not None and verified.user is NO_USER:
599 # For internal-services authentication, we check if its using a
600 # suitable macaroon that specifically grants access to this
601 # repository. This is only permitted for macaroons not bound to
602 # a user.
603 if not _can_internal_issuer_write(verified):
604 raise faults.Unauthorized()
605 else:
606 # This checks `requester` against `repo.registrant` because the
607 # requester should be the only user able to confirm/abort
608 # repository creation while it's being created.
609 if requester != naked_repo.registrant:
610 raise faults.Unauthorized()
611
612 if naked_repo.status != GitRepositoryStatus.CREATING:
613 raise faults.Unauthorized()
614
615 def _confirmRepoCreation(self, requester, translated_path, auth_params):
616 naked_repo = removeSecurityProxy(
617 getUtility(IGitLookup).getByHostingPath(translated_path))
618 if naked_repo is None:
619 raise faults.GitRepositoryNotFound(translated_path)
620 self._validateRequesterCanManageRepoCreation(
621 requester, naked_repo, auth_params)
622 naked_repo.status = GitRepositoryStatus.AVAILABLE
623
624 def confirmRepoCreation(self, translated_path, auth_params):
625 """See `IGitAPI`."""
626 logger = self._getLogger(auth_params.get("request-id"))
627 requester_id = _get_requester_id(auth_params)
628 logger.info(
629 "Request received: confirmRepoCreation('%s')", translated_path)
630 try:
631 result = run_with_login(
632 requester_id, self._confirmRepoCreation,
633 translated_path, auth_params)
634 except Exception as e:
635 result = e
636 if isinstance(result, xmlrpc_client.Fault):
637 logger.error("confirmRepoCreation failed: %r", result)
638 else:
639 logger.info("confirmRepoCreation succeeded: %s" % result)
640 return result
641
642 def _abortRepoCreation(self, requester, translated_path, auth_params):
643 naked_repo = removeSecurityProxy(
644 getUtility(IGitLookup).getByHostingPath(translated_path))
645 if naked_repo is None:
646 raise faults.GitRepositoryNotFound(translated_path)
647 self._validateRequesterCanManageRepoCreation(
648 requester, naked_repo, auth_params)
649 naked_repo.destroySelf(break_references=True)
650
651 def abortRepoCreation(self, translated_path, auth_params):
652 """See `IGitAPI`."""
653 logger = self._getLogger(auth_params.get("request-id"))
654 requester_id = _get_requester_id(auth_params)
655 logger.info(
656 "Request received: abortRepoCreation('%s')", translated_path)
657 try:
658 result = run_with_login(
659 requester_id, self._abortRepoCreation,
660 translated_path, auth_params)
661 except Exception as e:
662 result = e
663 if isinstance(result, xmlrpc_client.Fault):
664 logger.error("abortRepoCreation failed: %r", result)
665 else:
666 logger.info("abortRepoCreation succeeded: %s" % result)
667 return result
diff --git a/lib/lp/code/xmlrpc/tests/test_git.py b/lib/lp/code/xmlrpc/tests/test_git.py
index 6f12f1f..e2983a3 100644
--- a/lib/lp/code/xmlrpc/tests/test_git.py
+++ b/lib/lp/code/xmlrpc/tests/test_git.py
@@ -12,6 +12,7 @@ from pymacaroons import Macaroon
12import six12import six
13from six.moves import xmlrpc_client13from six.moves import xmlrpc_client
14from six.moves.urllib.parse import quote14from six.moves.urllib.parse import quote
15from storm.store import Store
15from testtools.matchers import (16from testtools.matchers import (
16 Equals,17 Equals,
17 IsInstance,18 IsInstance,
@@ -29,6 +30,7 @@ from lp.app.enums import InformationType
29from lp.buildmaster.enums import BuildStatus30from lp.buildmaster.enums import BuildStatus
30from lp.code.enums import (31from lp.code.enums import (
31 GitGranteeType,32 GitGranteeType,
33 GitRepositoryStatus,
32 GitRepositoryType,34 GitRepositoryType,
33 TargetRevisionControlSystems,35 TargetRevisionControlSystems,
34 )36 )
@@ -37,6 +39,7 @@ from lp.code.interfaces.codehosting import LAUNCHPAD_SERVICES
37from lp.code.interfaces.codeimportjob import ICodeImportJobWorkflow39from lp.code.interfaces.codeimportjob import ICodeImportJobWorkflow
38from lp.code.interfaces.gitcollection import IAllGitRepositories40from lp.code.interfaces.gitcollection import IAllGitRepositories
39from lp.code.interfaces.gitjob import IGitRefScanJobSource41from lp.code.interfaces.gitjob import IGitRefScanJobSource
42from lp.code.interfaces.gitlookup import IGitLookup
40from lp.code.interfaces.gitrepository import (43from lp.code.interfaces.gitrepository import (
41 GIT_REPOSITORY_NAME_VALIDATION_ERROR_MESSAGE,44 GIT_REPOSITORY_NAME_VALIDATION_ERROR_MESSAGE,
42 IGitRepository,45 IGitRepository,
@@ -280,6 +283,88 @@ class TestGitAPIMixin:
280 "writable": writable, "trailing": trailing, "private": private},283 "writable": writable, "trailing": trailing, "private": private},
281 translation)284 translation)
282285
286 def assertConfirmsRepoCreation(self, requester, git_repository,
287 can_authenticate=True, macaroon_raw=None):
288 translated_path = git_repository.getInternalPath()
289 auth_params = _make_auth_params(
290 requester, can_authenticate=can_authenticate,
291 macaroon_raw=macaroon_raw)
292 request_id = auth_params["request-id"]
293 result = self.assertDoesNotFault(
294 request_id, "confirmRepoCreation", translated_path, auth_params)
295 login(ANONYMOUS)
296 self.assertIsNone(result)
297 Store.of(git_repository).invalidate(git_repository)
298 self.assertEqual(GitRepositoryStatus.AVAILABLE, git_repository.status)
299
300 def assertConfirmRepoCreationFails(
301 self, failure, requester, git_repository, can_authenticate=True,
302 macaroon_raw=None):
303 translated_path = git_repository.getInternalPath()
304 auth_params = _make_auth_params(
305 requester, can_authenticate=can_authenticate,
306 macaroon_raw=macaroon_raw)
307 request_id = auth_params["request-id"]
308 original_status = git_repository.status
309 self.assertFault(
310 failure, request_id, "confirmRepoCreation", translated_path,
311 auth_params)
312 store = Store.of(git_repository)
313 if store:
314 store.invalidate(git_repository)
315 self.assertEqual(original_status, git_repository.status)
316
317 def assertConfirmRepoCreationUnauthorized(
318 self, requester, git_repository, can_authenticate=True,
319 macaroon_raw=None):
320 failure = faults.Unauthorized
321 self.assertConfirmRepoCreationFails(
322 failure, requester, git_repository, can_authenticate,
323 macaroon_raw)
324
325 def assertAbortsRepoCreation(self, requester, git_repository,
326 can_authenticate=True, macaroon_raw=None):
327 translated_path = git_repository.getInternalPath()
328 auth_params = _make_auth_params(
329 requester, can_authenticate=can_authenticate,
330 macaroon_raw=macaroon_raw)
331 request_id = auth_params["request-id"]
332 result = self.assertDoesNotFault(
333 request_id, "abortRepoCreation", translated_path, auth_params)
334 login(ANONYMOUS)
335 self.assertIsNone(result)
336 self.assertIsNone(
337 getUtility(IGitLookup).getByHostingPath(translated_path))
338
339 def assertAbortRepoCreationFails(
340 self, failure, requester, git_repository, can_authenticate=True,
341 macaroon_raw=None):
342 translated_path = git_repository.getInternalPath()
343 auth_params = _make_auth_params(
344 requester, can_authenticate=can_authenticate,
345 macaroon_raw=macaroon_raw)
346 request_id = auth_params["request-id"]
347 original_status = git_repository.status
348 self.assertFault(
349 failure, request_id, "abortRepoCreation", translated_path,
350 auth_params)
351
352 # If it's not expected to fail because the repo isn't there,
353 # make sure the repository was not changed in any way.
354 if not isinstance(failure, faults.GitRepositoryNotFound):
355 repo = removeSecurityProxy(
356 getUtility(IGitLookup).getByHostingPath(translated_path))
357 self.assertEqual(GitRepositoryStatus.CREATING, repo.status)
358 self.assertEqual(original_status, git_repository.status)
359
360 def assertAbortRepoCreationUnauthorized(
361 self, requester, git_repository, can_authenticate=True,
362 macaroon_raw=None):
363 failure = faults.Unauthorized
364 self.assertAbortRepoCreationFails(
365 failure, requester, git_repository, can_authenticate,
366 macaroon_raw)
367
283 def assertCreates(self, requester, path, can_authenticate=False,368 def assertCreates(self, requester, path, can_authenticate=False,
284 private=False):369 private=False):
285 auth_params = _make_auth_params(370 auth_params = _make_auth_params(
@@ -660,6 +745,367 @@ class TestGitAPI(TestGitAPIMixin, TestCaseWithFactory):
660745
661 layer = LaunchpadFunctionalLayer746 layer = LaunchpadFunctionalLayer
662747
748 def test_confirm_git_repository_creation(self):
749 owner = self.factory.makePerson()
750 repo = removeSecurityProxy(self.factory.makeGitRepository(owner=owner))
751 repo.status = GitRepositoryStatus.CREATING
752 self.assertConfirmsRepoCreation(owner, repo)
753
754 def test_only_requester_can_confirm_git_repository_creation(self):
755 requester = self.factory.makePerson()
756 repo = removeSecurityProxy(self.factory.makeGitRepository())
757 repo.status = GitRepositoryStatus.CREATING
758
759 self.assertConfirmRepoCreationUnauthorized(requester, repo)
760
761 def test_confirm_git_repository_creation_of_non_existing_repository(self):
762 owner = self.factory.makePerson()
763 repo = removeSecurityProxy(self.factory.makeGitRepository(owner=owner))
764 repo.status = GitRepositoryStatus.CREATING
765 repo.destroySelf()
766
767 expected_failure = faults.GitRepositoryNotFound(str(repo.id))
768 self.assertConfirmRepoCreationFails(expected_failure, owner, repo)
769
770 def test_confirm_git_repository_creation_public_code_import(self):
771 # A code import worker with a suitable macaroon can write to a
772 # repository associated with a running code import job.
773 self.pushConfig(
774 "launchpad", internal_macaroon_secret_key="some-secret")
775 machine = self.factory.makeCodeImportMachine(set_online=True)
776 code_imports = [
777 self.factory.makeCodeImport(
778 target_rcs_type=TargetRevisionControlSystems.GIT)
779 for _ in range(2)]
780 with celebrity_logged_in("vcs_imports"):
781 jobs = [
782 self.factory.makeCodeImportJob(code_import=code_import)
783 for code_import in code_imports]
784 issuer = getUtility(IMacaroonIssuer, "code-import-job")
785 macaroons = [
786 removeSecurityProxy(issuer).issueMacaroon(job) for job in jobs]
787
788 repo = removeSecurityProxy(code_imports[0].git_repository)
789 repo.status = GitRepositoryStatus.CREATING
790
791 self.assertConfirmRepoCreationUnauthorized(
792 LAUNCHPAD_SERVICES, repo,
793 macaroon_raw=macaroons[0].serialize())
794 with celebrity_logged_in("vcs_imports"):
795 getUtility(ICodeImportJobWorkflow).startJob(jobs[0], machine)
796 self.assertConfirmRepoCreationUnauthorized(
797 LAUNCHPAD_SERVICES, repo,
798 macaroon_raw=macaroons[1].serialize())
799 self.assertConfirmRepoCreationUnauthorized(
800 LAUNCHPAD_SERVICES, repo,
801 macaroon_raw=Macaroon(
802 location=config.vhost.mainsite.hostname,
803 identifier="another",
804 key="another-secret").serialize())
805 self.assertConfirmRepoCreationUnauthorized(
806 LAUNCHPAD_SERVICES, repo, macaroon_raw="nonsense")
807 self.assertConfirmRepoCreationUnauthorized(
808 code_imports[0].registrant, repo,
809 macaroon_raw=macaroons[0].serialize())
810 self.assertConfirmsRepoCreation(
811 LAUNCHPAD_SERVICES, repo,
812 macaroon_raw=macaroons[0].serialize())
813
814 def test_confirm_git_repository_creation_private_code_import(self):
815 # A code import worker with a suitable macaroon can write to a
816 # repository associated with a running private code import job.
817 self.pushConfig(
818 "launchpad", internal_macaroon_secret_key="some-secret")
819 machine = self.factory.makeCodeImportMachine(set_online=True)
820 code_imports = [
821 self.factory.makeCodeImport(
822 target_rcs_type=TargetRevisionControlSystems.GIT)
823 for _ in range(2)]
824 private_repository = code_imports[0].git_repository
825 removeSecurityProxy(
826 private_repository).transitionToInformationType(
827 InformationType.PRIVATESECURITY, private_repository.owner)
828 with celebrity_logged_in("vcs_imports"):
829 jobs = [
830 self.factory.makeCodeImportJob(code_import=code_import)
831 for code_import in code_imports]
832 issuer = getUtility(IMacaroonIssuer, "code-import-job")
833 macaroons = [
834 removeSecurityProxy(issuer).issueMacaroon(job) for job in jobs]
835
836 repo = removeSecurityProxy(code_imports[0].git_repository)
837 repo.status = GitRepositoryStatus.CREATING
838
839 self.assertConfirmRepoCreationUnauthorized(
840 LAUNCHPAD_SERVICES, repo,
841 macaroon_raw=macaroons[0].serialize())
842 with celebrity_logged_in("vcs_imports"):
843 getUtility(ICodeImportJobWorkflow).startJob(jobs[0], machine)
844 self.assertConfirmRepoCreationUnauthorized(
845 LAUNCHPAD_SERVICES, repo,
846 macaroon_raw=macaroons[1].serialize())
847 self.assertConfirmRepoCreationUnauthorized(
848 LAUNCHPAD_SERVICES, repo,
849 macaroon_raw=Macaroon(
850 location=config.vhost.mainsite.hostname,
851 identifier="another",
852 key="another-secret").serialize())
853 self.assertConfirmRepoCreationUnauthorized(
854 LAUNCHPAD_SERVICES, repo,
855 macaroon_raw="nonsense")
856 self.assertConfirmRepoCreationUnauthorized(
857 code_imports[0].registrant, repo,
858 macaroon_raw=macaroons[0].serialize())
859 self.assertConfirmsRepoCreation(
860 LAUNCHPAD_SERVICES, repo,
861 macaroon_raw=macaroons[0].serialize())
862
863 def test_confirm_git_repository_creation_user_macaroon(self):
864 # A user with a suitable macaroon can write to the corresponding
865 # repository, but not others, even if they own them.
866 self.pushConfig("codehosting",
867 git_macaroon_secret_key="some-secret")
868 requester = self.factory.makePerson()
869 repositories = [
870 self.factory.makeGitRepository(owner=requester) for _ in
871 range(2)]
872 repositories.append(self.factory.makeGitRepository(
873 owner=requester,
874 information_type=InformationType.PRIVATESECURITY))
875 issuer = getUtility(IMacaroonIssuer, "git-repository")
876 with person_logged_in(requester):
877 macaroons = [
878 removeSecurityProxy(issuer).issueMacaroon(
879 repository, user=requester)
880 for repository in repositories]
881 for i, repository in enumerate(repositories):
882 login(ANONYMOUS)
883 repository = removeSecurityProxy(repository)
884 repository.status = GitRepositoryStatus.CREATING
885
886 correct_macaroon = macaroons[i]
887 wrong_macaroon = macaroons[(i + 1) % len(macaroons)]
888
889 self.assertConfirmRepoCreationUnauthorized(
890 requester, repository, macaroon_raw=wrong_macaroon.serialize())
891 self.assertConfirmRepoCreationUnauthorized(
892 requester, repository,
893 macaroon_raw=Macaroon(
894 location=config.vhost.mainsite.hostname,
895 identifier="another",
896 key="another-secret").serialize())
897 self.assertConfirmRepoCreationUnauthorized(
898 requester, repository, macaroon_raw="nonsense")
899 self.assertConfirmsRepoCreation(
900 requester, repository,
901 macaroon_raw=correct_macaroon.serialize())
902
903 def test_confirm_git_repository_creation_user_mismatch(self):
904 # confirmRepoCreation refuses macaroons in the case where the user
905 # doesn't match what the issuer claims was verified.
906 issuer = DummyMacaroonIssuer()
907
908 self.useFixture(ZopeUtilityFixture(
909 issuer, IMacaroonIssuer, name="test"))
910 repository = self.factory.makeGitRepository()
911
912 macaroon = issuer.issueMacaroon(repository)
913 requesters = [self.factory.makePerson() for _ in range(2)]
914 for verified_user, unauthorized in (
915 (NO_USER, requesters + [None]),
916 (requesters[0], [LAUNCHPAD_SERVICES, requesters[1], None]),
917 (None, [LAUNCHPAD_SERVICES] + requesters + [None]),
918 ):
919 repository = removeSecurityProxy(repository)
920 repository.status = GitRepositoryStatus.CREATING
921 issuer._verified_user = verified_user
922 for requester in unauthorized:
923 login(ANONYMOUS)
924 self.assertConfirmRepoCreationUnauthorized(
925 requester, repository,
926 macaroon_raw=macaroon.serialize())
927
928 def test_abort_repo_creation(self):
929 requester = self.factory.makePerson()
930 repo = self.factory.makeGitRepository(owner=requester)
931 repo = removeSecurityProxy(repo)
932 repo.status = GitRepositoryStatus.CREATING
933 self.assertAbortsRepoCreation(requester, repo)
934
935 def test_only_requester_can_abort_git_repository_creation(self):
936 requester = self.factory.makePerson()
937 repo = removeSecurityProxy(self.factory.makeGitRepository())
938 repo.status = GitRepositoryStatus.CREATING
939
940 self.assertAbortRepoCreationUnauthorized(requester, repo)
941
942 def test_abort_git_repository_creation_public_code_import(self):
943 # A code import worker with a suitable macaroon can write to a
944 # repository associated with a running code import job.
945 self.pushConfig(
946 "launchpad", internal_macaroon_secret_key="some-secret")
947 machine = self.factory.makeCodeImportMachine(set_online=True)
948 code_imports = [
949 self.factory.makeCodeImport(
950 target_rcs_type=TargetRevisionControlSystems.GIT)
951 for _ in range(2)]
952 with celebrity_logged_in("vcs_imports"):
953 jobs = [
954 self.factory.makeCodeImportJob(code_import=code_import)
955 for code_import in code_imports]
956 issuer = getUtility(IMacaroonIssuer, "code-import-job")
957 macaroons = [
958 removeSecurityProxy(issuer).issueMacaroon(job) for job in jobs]
959
960 repo = removeSecurityProxy(code_imports[0].git_repository)
961 repo.status = GitRepositoryStatus.CREATING
962
963 self.assertAbortRepoCreationUnauthorized(
964 LAUNCHPAD_SERVICES, repo,
965 macaroon_raw=macaroons[0].serialize())
966 with celebrity_logged_in("vcs_imports"):
967 getUtility(ICodeImportJobWorkflow).startJob(jobs[0], machine)
968 self.assertAbortRepoCreationUnauthorized(
969 LAUNCHPAD_SERVICES, repo,
970 macaroon_raw=macaroons[1].serialize())
971 self.assertAbortRepoCreationUnauthorized(
972 LAUNCHPAD_SERVICES, repo,
973 macaroon_raw=Macaroon(
974 location=config.vhost.mainsite.hostname,
975 identifier="another",
976 key="another-secret").serialize())
977 self.assertAbortRepoCreationUnauthorized(
978 LAUNCHPAD_SERVICES, repo, macaroon_raw="nonsense")
979 self.assertAbortRepoCreationUnauthorized(
980 code_imports[0].registrant, repo,
981 macaroon_raw=macaroons[0].serialize())
982 self.assertConfirmsRepoCreation(
983 LAUNCHPAD_SERVICES, repo,
984 macaroon_raw=macaroons[0].serialize())
985
986 def test_abort_git_repository_creation_private_code_import(self):
987 # A code import worker with a suitable macaroon can write to a
988 # repository associated with a running private code import job.
989 self.pushConfig(
990 "launchpad", internal_macaroon_secret_key="some-secret")
991 machine = self.factory.makeCodeImportMachine(set_online=True)
992 code_imports = [
993 self.factory.makeCodeImport(
994 target_rcs_type=TargetRevisionControlSystems.GIT)
995 for _ in range(2)]
996 private_repository = code_imports[0].git_repository
997 removeSecurityProxy(
998 private_repository).transitionToInformationType(
999 InformationType.PRIVATESECURITY, private_repository.owner)
1000 with celebrity_logged_in("vcs_imports"):
1001 jobs = [
1002 self.factory.makeCodeImportJob(code_import=code_import)
1003 for code_import in code_imports]
1004 issuer = getUtility(IMacaroonIssuer, "code-import-job")
1005 macaroons = [
1006 removeSecurityProxy(issuer).issueMacaroon(job) for job in jobs]
1007
1008 repo = removeSecurityProxy(code_imports[0].git_repository)
1009 repo.status = GitRepositoryStatus.CREATING
1010
1011 self.assertAbortRepoCreationUnauthorized(
1012 LAUNCHPAD_SERVICES, repo,
1013 macaroon_raw=macaroons[0].serialize())
1014 with celebrity_logged_in("vcs_imports"):
1015 getUtility(ICodeImportJobWorkflow).startJob(jobs[0], machine)
1016 self.assertAbortRepoCreationUnauthorized(
1017 LAUNCHPAD_SERVICES, repo,
1018 macaroon_raw=macaroons[1].serialize())
1019 self.assertAbortRepoCreationUnauthorized(
1020 LAUNCHPAD_SERVICES, repo,
1021 macaroon_raw=Macaroon(
1022 location=config.vhost.mainsite.hostname,
1023 identifier="another",
1024 key="another-secret").serialize())
1025 self.assertAbortRepoCreationUnauthorized(
1026 LAUNCHPAD_SERVICES, repo,
1027 macaroon_raw="nonsense")
1028 self.assertAbortRepoCreationUnauthorized(
1029 code_imports[0].registrant, repo,
1030 macaroon_raw=macaroons[0].serialize())
1031 self.assertAbortsRepoCreation(
1032 LAUNCHPAD_SERVICES, repo,
1033 macaroon_raw=macaroons[0].serialize())
1034
1035 def test_abort_git_repository_creation_user_macaroon(self):
1036 # A user with a suitable macaroon can write to the corresponding
1037 # repository, but not others, even if they own them.
1038 self.pushConfig("codehosting",
1039 git_macaroon_secret_key="some-secret")
1040 requester = self.factory.makePerson()
1041 repositories = [
1042 self.factory.makeGitRepository(owner=requester) for _ in
1043 range(2)]
1044 repositories.append(self.factory.makeGitRepository(
1045 owner=requester,
1046 information_type=InformationType.PRIVATESECURITY))
1047 issuer = getUtility(IMacaroonIssuer, "git-repository")
1048 with person_logged_in(requester):
1049 macaroons = [
1050 removeSecurityProxy(issuer).issueMacaroon(
1051 repository, user=requester)
1052 for repository in repositories]
1053 for i, repository in enumerate(repositories):
1054 login(ANONYMOUS)
1055 repository = removeSecurityProxy(repository)
1056 repository.status = GitRepositoryStatus.CREATING
1057
1058 correct_macaroon = macaroons[i]
1059 wrong_macaroon = macaroons[(i + 1) % len(macaroons)]
1060
1061 self.assertAbortRepoCreationUnauthorized(
1062 requester, repository, macaroon_raw=wrong_macaroon.serialize())
1063 self.assertAbortRepoCreationUnauthorized(
1064 requester, repository,
1065 macaroon_raw=Macaroon(
1066 location=config.vhost.mainsite.hostname,
1067 identifier="another",
1068 key="another-secret").serialize())
1069 self.assertAbortRepoCreationUnauthorized(
1070 requester, repository, macaroon_raw="nonsense")
1071 self.assertAbortsRepoCreation(
1072 requester, repository,
1073 macaroon_raw=correct_macaroon.serialize())
1074
1075 def test_abort_git_repository_creation_user_mismatch(self):
1076 # confirmRepoCreation refuses macaroons in the case where the user
1077 # doesn't match what the issuer claims was verified.
1078 issuer = DummyMacaroonIssuer()
1079
1080 self.useFixture(ZopeUtilityFixture(
1081 issuer, IMacaroonIssuer, name="test"))
1082 repository = self.factory.makeGitRepository()
1083
1084 macaroon = issuer.issueMacaroon(repository)
1085 requesters = [self.factory.makePerson() for _ in range(2)]
1086 for verified_user, unauthorized in (
1087 (NO_USER, requesters + [None]),
1088 (requesters[0], [LAUNCHPAD_SERVICES, requesters[1], None]),
1089 (None, [LAUNCHPAD_SERVICES] + requesters + [None]),
1090 ):
1091 repository = removeSecurityProxy(repository)
1092 repository.status = GitRepositoryStatus.CREATING
1093 issuer._verified_user = verified_user
1094 for requester in unauthorized:
1095 login(ANONYMOUS)
1096 self.assertAbortRepoCreationUnauthorized(
1097 requester, repository,
1098 macaroon_raw=macaroon.serialize())
1099
1100 def test_abort_git_repository_creation_of_non_existing_repository(self):
1101 owner = self.factory.makePerson()
1102 repo = removeSecurityProxy(self.factory.makeGitRepository(owner=owner))
1103 repo.status = GitRepositoryStatus.CREATING
1104 repo.destroySelf()
1105
1106 expected_failure = faults.GitRepositoryNotFound(str(repo.id))
1107 self.assertAbortRepoCreationFails(expected_failure, owner, repo)
1108
663 def test_translatePath_cannot_translate(self):1109 def test_translatePath_cannot_translate(self):
664 # Sometimes translatePath will not know how to translate a path.1110 # Sometimes translatePath will not know how to translate a path.
665 # When this happens, it returns a Fault saying so, including the1111 # When this happens, it returns a Fault saying so, including the
@@ -1270,7 +1716,7 @@ class TestGitAPI(TestGitAPIMixin, TestCaseWithFactory):
1270 (requesters[0], [requesters[0]],1716 (requesters[0], [requesters[0]],
1271 [LAUNCHPAD_SERVICES, requesters[1], None]),1717 [LAUNCHPAD_SERVICES, requesters[1], None]),
1272 (None, [], [LAUNCHPAD_SERVICES] + requesters + [None]),1718 (None, [], [LAUNCHPAD_SERVICES] + requesters + [None]),
1273 ):1719 ):
1274 issuer._verified_user = verified_user1720 issuer._verified_user = verified_user
1275 for requester in authorized:1721 for requester in authorized:
1276 login(ANONYMOUS)1722 login(ANONYMOUS)