← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/git-export-issue-access-token into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/git-export-issue-access-token into lp:launchpad with lp:~cjwatson/launchpad/git-honour-access-tokens as a prerequisite.

Commit message:
Add and export IGitRepository.issueAccessToken.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #1824399 in Launchpad itself: "Add Git HTTPS push tokens for snapcraft experiment"
  https://bugs.launchpad.net/launchpad/+bug/1824399

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/git-export-issue-access-token/+merge/366057

This is very basic and experimental, but I think it will fill the requirements for now.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/git-export-issue-access-token into lp:launchpad.
=== modified file 'lib/lp/code/interfaces/gitrepository.py'
--- lib/lp/code/interfaces/gitrepository.py	2019-04-01 10:09:31 +0000
+++ lib/lp/code/interfaces/gitrepository.py	2019-04-15 13:41:43 +0000
@@ -1,4 +1,4 @@
-# Copyright 2015-2018 Canonical Ltd.  This software is licensed under the
+# Copyright 2015-2019 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Git repository interfaces."""
@@ -651,6 +651,22 @@
         :return: A `ResultSet` of `IGitActivity`.
         """
 
+    @export_write_operation()
+    @operation_for_version("devel")
+    def issueAccessToken():
+        """Issue an access token for this repository.
+
+        Access tokens can be used to push to this repository over HTTPS.
+        They are only valid for a single repository, and have a short expiry
+        period (currently one week), so at the moment they are only suitable
+        in some limited situations.
+
+        This interface is experimental, and may be changed or removed
+        without notice.
+
+        :return: A serialised macaroon.
+        """
+
 
 class IGitRepositoryModerateAttributes(Interface):
     """IGitRepository attributes that can be edited by more than one community.

=== modified file 'lib/lp/code/model/gitrepository.py'
--- lib/lp/code/model/gitrepository.py	2019-04-15 13:41:42 +0000
+++ lib/lp/code/model/gitrepository.py	2019-04-15 13:41:43 +0000
@@ -1428,6 +1428,13 @@
         return DecoratedResultSet(
             results, pre_iter_hook=preloadDataForActivities)
 
+    def issueAccessToken(self):
+        """See `IGitRepository`."""
+        issuer = getUtility(IMacaroonIssuer, "git-repository")
+        # Our security adapter has already done the checks we need, apart
+        # from forbidding anonymous users which is done by the issuer.
+        return removeSecurityProxy(issuer).issueMacaroon(self).serialize()
+
     def canBeDeleted(self):
         """See `IGitRepository`."""
         # Can't delete if the repository is associated with anything.

=== modified file 'lib/lp/code/model/tests/test_gitrepository.py'
--- lib/lp/code/model/tests/test_gitrepository.py	2019-04-15 13:41:42 +0000
+++ lib/lp/code/model/tests/test_gitrepository.py	2019-04-15 13:41:43 +0000
@@ -34,6 +34,7 @@
     MatchesListwise,
     MatchesSetwise,
     MatchesStructure,
+    StartsWith,
     )
 import transaction
 from zope.component import getUtility
@@ -158,6 +159,7 @@
     api_url,
     celebrity_logged_in,
     login_person,
+    logout,
     person_logged_in,
     record_two_runs,
     TestCaseWithFactory,
@@ -174,7 +176,10 @@
     DoesNotSnapshot,
     HasQueryCount,
     )
-from lp.testing.pages import webservice_for_person
+from lp.testing.pages import (
+    LaunchpadWebServiceCaller,
+    webservice_for_person,
+    )
 from lp.xmlrpc import faults
 from lp.xmlrpc.interfaces import IPrivateApplication
 
@@ -3909,6 +3914,50 @@
             "refs/other": Equals([]),
             }))
 
+    def test_issueAccessToken(self):
+        # A user can request an access token via the webservice API.
+        self.pushConfig(
+            "launchpad", internal_macaroon_secret_key="some-secret")
+        repository = self.factory.makeGitRepository()
+        # Write access to the repository isn't checked at this stage
+        # (although the access token will only be useful if the user has
+        # some kind of write access).
+        requester = self.factory.makePerson()
+        with person_logged_in(requester):
+            repository_url = api_url(repository)
+        webservice = webservice_for_person(
+            requester, permission=OAuthPermission.WRITE_PUBLIC)
+        webservice.default_api_version = "devel"
+        response = webservice.named_post(repository_url, "issueAccessToken")
+        self.assertEqual(200, response.status)
+        macaroon = Macaroon.deserialize(json.loads(response.body))
+        with person_logged_in(ANONYMOUS):
+            self.assertThat(macaroon, MatchesStructure(
+                location=Equals(config.vhost.mainsite.hostname),
+                identifier=Equals("git-repository"),
+                caveats=MatchesListwise([
+                    MatchesStructure.byEquality(
+                        caveat_id="lp.git-repository %s" % repository.id),
+                    MatchesStructure(
+                        caveat_id=StartsWith("lp.openid-identifier ")),
+                    MatchesStructure(caveat_id=StartsWith("lp.expires ")),
+                    ])))
+
+    def test_issueAccessToken_anonymous(self):
+        # An anonymous user cannot request an access token via the
+        # webservice API.
+        repository = self.factory.makeGitRepository()
+        with person_logged_in(repository.owner):
+            repository_url = api_url(repository)
+        logout()
+        webservice = LaunchpadWebServiceCaller()
+        webservice.default_api_version = "devel"
+        response = webservice.named_post(repository_url, "issueAccessToken")
+        self.assertEqual(401, response.status)
+        self.assertEqual(
+            "git-repository macaroons may only be issued for a logged-in "
+            "user.", response.body)
+
 
 class TestGitRepositoryMacaroonIssuer(MacaroonTestMixin, TestCaseWithFactory):
     """Test GitRepository macaroon issuing and verification."""


Follow ups