launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #24835
[Merge] ~pappacena/launchpad:xmlrpc-git-confirmRepoCreation into launchpad:master
Thiago F. Pappacena has proposed merging ~pappacena/launchpad:xmlrpc-git-confirmRepoCreation into launchpad:master with ~pappacena/launchpad:gitrepo-status as a prerequisite.
Commit message:
New XML-RPC to allow code hosting service to confirm a git repository creation on code hosting side.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/385301
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~pappacena/launchpad:xmlrpc-git-confirmRepoCreation into launchpad:master.
diff --git a/lib/lp/code/interfaces/gitapi.py b/lib/lp/code/interfaces/gitapi.py
index c289dee..f90de58 100644
--- a/lib/lp/code/interfaces/gitapi.py
+++ b/lib/lp/code/interfaces/gitapi.py
@@ -1,4 +1,4 @@
-# Copyright 2015 Canonical Ltd. This software is licensed under the
+# Copyright 2015-2020 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Interfaces for internal Git APIs."""
@@ -92,3 +92,15 @@ class IGitAPI(Interface):
if no repository can be found for 'translated_path',
or an `Unauthorized` fault for unauthorized push attempts.
"""
+
+ def confirmRepoCreation(translated_path):
+ """Confirm that repository creation.
+
+ When code hosting finishes creating the repository locally,
+ it should call back this method to confirm that the repository was
+ created, and Launchpad should make the repository available for end
+ users.
+
+ :param repository_id: The database ID of the repository, provided by
+ translatePath call when repo creation is necessary.
+ """
diff --git a/lib/lp/code/xmlrpc/git.py b/lib/lp/code/xmlrpc/git.py
index b34307a..a012695 100644
--- a/lib/lp/code/xmlrpc/git.py
+++ b/lib/lp/code/xmlrpc/git.py
@@ -1,4 +1,4 @@
-# Copyright 2015-2019 Canonical Ltd. This software is licensed under the
+# Copyright 2015-2020 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Implementations of the XML-RPC APIs for Git."""
@@ -31,6 +31,7 @@ from lp.app.validators import LaunchpadValidationError
from lp.code.enums import (
GitGranteeType,
GitPermissionType,
+ GitRepositoryStatus,
GitRepositoryType,
)
from lp.code.errors import (
@@ -69,6 +70,7 @@ from lp.registry.interfaces.product import (
NoSuchProduct,
)
from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
+from lp.services.database.interfaces import IStore
from lp.services.macaroons.interfaces import (
IMacaroonIssuer,
NO_USER,
@@ -584,3 +586,45 @@ class GitAPI(LaunchpadXMLRPCView):
[(ref_path.data, permissions)
for ref_path, permissions in result])
return result
+
+ def _confirmRepoCreation(self, requester, repository_id, auth_params):
+ if requester == LAUNCHPAD_ANONYMOUS:
+ requester = None
+ # We remove the security proxy here because we are in a controlled,
+ # internal environment, and "status" is a read-only attribute for
+ # users.
+ naked_repo = removeSecurityProxy(
+ getUtility(IGitLookup).get(repository_id))
+ if naked_repo is None:
+ raise faults.GitRepositoryNotFound(str(repository_id))
+
+ verified = self._verifyAuthParams(requester, naked_repo, auth_params)
+ if verified is not None and verified.user is NO_USER:
+ if not _can_internal_issuer_write(verified):
+ raise faults.Unauthorized()
+
+ # Only the repository owner can mark it as "available".
+ if requester != naked_repo.owner:
+ raise faults.GitRepositoryNotFound(str(repository_id))
+
+ naked_repo.status = GitRepositoryStatus.AVAILABLE
+ IStore(naked_repo).flush()
+ transaction.commit()
+
+ def confirmRepoCreation(self, repository_id, auth_params):
+ """See `IGitAPI`."""
+ logger = self._getLogger(auth_params.get("request-id"))
+ requester_id = _get_requester_id(auth_params)
+ logger.info(
+ "Request received: confirmRepoCreation('%s')", repository_id)
+ try:
+ result = run_with_login(
+ requester_id, self._confirmRepoCreation,
+ repository_id, auth_params)
+ except Exception as e:
+ result = e
+ if isinstance(result, xmlrpc_client.Fault):
+ logger.error("confirmRepoCreation failed: %r", result)
+ else:
+ logger.info("confirmRepoCreation succeeded: %s" % result)
+ return result
diff --git a/lib/lp/code/xmlrpc/tests/test_git.py b/lib/lp/code/xmlrpc/tests/test_git.py
index 6f12f1f..a57a809 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
import six
from six.moves import xmlrpc_client
from six.moves.urllib.parse import quote
+from storm.store import Store
from testtools.matchers import (
Equals,
IsInstance,
@@ -29,6 +30,7 @@ from lp.app.enums import InformationType
from lp.buildmaster.enums import BuildStatus
from lp.code.enums import (
GitGranteeType,
+ GitRepositoryStatus,
GitRepositoryType,
TargetRevisionControlSystems,
)
@@ -44,6 +46,7 @@ from lp.code.interfaces.gitrepository import (
)
from lp.code.tests.helpers import GitHostingFixture
from lp.registry.enums import TeamMembershipPolicy
+from lp.services.compat import mock
from lp.services.config import config
from lp.services.features.testing import FeatureFixture
from lp.services.macaroons.interfaces import (
@@ -280,6 +283,32 @@ class TestGitAPIMixin:
"writable": writable, "trailing": trailing, "private": private},
translation)
+ def assertConfirmsRepoCreation(self, requester, git_repository,
+ can_authenticate=True):
+ auth_params = _make_auth_params(
+ requester, can_authenticate=can_authenticate)
+ request_id = auth_params["request-id"]
+ result = self.assertDoesNotFault(
+ request_id, "confirmRepoCreation", git_repository.id, auth_params)
+ login(ANONYMOUS)
+ self.assertIsNone(result)
+ Store.of(git_repository).invalidate(git_repository)
+ self.assertEqual(git_repository.status, GitRepositoryStatus.AVAILABLE)
+
+ def assertConfirmRepoCreationFails(
+ self, failure, requester, git_repository, can_authenticate=True):
+ auth_params = _make_auth_params(
+ requester, can_authenticate=can_authenticate)
+ request_id = auth_params["request-id"]
+ original_status = git_repository.status
+ self.assertFault(
+ failure, request_id, "confirmRepoCreation", git_repository.id,
+ auth_params)
+ store = Store.of(git_repository)
+ if store:
+ store.invalidate(git_repository)
+ self.assertEqual(original_status, git_repository.status)
+
def assertCreates(self, requester, path, can_authenticate=False,
private=False):
auth_params = _make_auth_params(
@@ -660,6 +689,29 @@ class TestGitAPI(TestGitAPIMixin, TestCaseWithFactory):
layer = LaunchpadFunctionalLayer
+ def test_confirm_git_repository_creation(self):
+ owner = self.factory.makePerson()
+ repo = removeSecurityProxy(self.factory.makeGitRepository(owner=owner))
+ repo.status = GitRepositoryStatus.CREATING
+ self.assertConfirmsRepoCreation(owner, repo)
+
+ def test_only_owner_can_confirm_git_repository_creation(self):
+ requester = self.factory.makePerson()
+ owner = self.factory.makePerson()
+ repo = removeSecurityProxy(self.factory.makeGitRepository(owner=owner))
+ repo.status = GitRepositoryStatus.CREATING
+
+ expected_failure = faults.GitRepositoryNotFound(str(repo.id))
+ self.assertConfirmRepoCreationFails(expected_failure, requester, repo)
+
+ def test_confirm_git_repository_creation_of_non_existing_repository(self):
+ requester = self.factory.makePerson()
+ repo = mock.Mock()
+ repo.id = 99999
+
+ expected_failure = faults.GitRepositoryNotFound('99999')
+ self.assertConfirmRepoCreationFails(expected_failure, requester, repo)
+
def test_translatePath_cannot_translate(self):
# Sometimes translatePath will not know how to translate a path.
# When this happens, it returns a Fault saying so, including the
Follow ups