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

Proposed by Colin Watson
Status: Merged
Merged at revision: 19050
Proposed branch: lp:~cjwatson/launchpad/git-export-issue-access-token
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/git-honour-access-tokens
Diff against target: 140 lines (+81/-2)
3 files modified
lib/lp/code/interfaces/gitrepository.py (+17/-1)
lib/lp/code/model/gitrepository.py (+14/-0)
lib/lp/code/model/tests/test_gitrepository.py (+50/-1)
To merge this branch: bzr merge lp:~cjwatson/launchpad/git-export-issue-access-token
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+366057@code.launchpad.net

Commit message

Add and export IGitRepository.issueAccessToken.

Description of the change

This is very basic and experimental, but I think it will fill the requirements for now.

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
1=== modified file 'lib/lp/code/interfaces/gitrepository.py'
2--- lib/lp/code/interfaces/gitrepository.py 2019-04-01 10:09:31 +0000
3+++ lib/lp/code/interfaces/gitrepository.py 2019-05-11 11:18:34 +0000
4@@ -1,4 +1,4 @@
5-# Copyright 2015-2018 Canonical Ltd. This software is licensed under the
6+# Copyright 2015-2019 Canonical Ltd. This software is licensed under the
7 # GNU Affero General Public License version 3 (see the file LICENSE).
8
9 """Git repository interfaces."""
10@@ -651,6 +651,22 @@
11 :return: A `ResultSet` of `IGitActivity`.
12 """
13
14+ @export_write_operation()
15+ @operation_for_version("devel")
16+ def issueAccessToken():
17+ """Issue an access token for this repository.
18+
19+ Access tokens can be used to push to this repository over HTTPS.
20+ They are only valid for a single repository, and have a short expiry
21+ period (currently one week), so at the moment they are only suitable
22+ in some limited situations.
23+
24+ This interface is experimental, and may be changed or removed
25+ without notice.
26+
27+ :return: A serialised macaroon.
28+ """
29+
30
31 class IGitRepositoryModerateAttributes(Interface):
32 """IGitRepository attributes that can be edited by more than one community.
33
34=== modified file 'lib/lp/code/model/gitrepository.py'
35--- lib/lp/code/model/gitrepository.py 2019-05-03 13:18:52 +0000
36+++ lib/lp/code/model/gitrepository.py 2019-05-11 11:18:34 +0000
37@@ -1428,6 +1428,20 @@
38 return DecoratedResultSet(
39 results, pre_iter_hook=preloadDataForActivities)
40
41+ def issueAccessToken(self):
42+ """See `IGitRepository`."""
43+ issuer = getUtility(IMacaroonIssuer, "git-repository")
44+ # It's more usual in model code to pass the user as an argument,
45+ # e.g. using @call_with(user=REQUEST_USER) in the webservice
46+ # interface. However, in this case that would allow anyone who
47+ # constructs a way to call this method not via the webservice to
48+ # issue a token for any user, which seems like a bad idea.
49+ user = getUtility(ILaunchBag).user
50+ # Our security adapter has already done the checks we need, apart
51+ # from forbidding anonymous users which is done by the issuer.
52+ return removeSecurityProxy(issuer).issueMacaroon(
53+ self, user=user).serialize()
54+
55 def canBeDeleted(self):
56 """See `IGitRepository`."""
57 # Can't delete if the repository is associated with anything.
58
59=== modified file 'lib/lp/code/model/tests/test_gitrepository.py'
60--- lib/lp/code/model/tests/test_gitrepository.py 2019-05-03 13:18:52 +0000
61+++ lib/lp/code/model/tests/test_gitrepository.py 2019-05-11 11:18:34 +0000
62@@ -34,6 +34,7 @@
63 MatchesListwise,
64 MatchesSetwise,
65 MatchesStructure,
66+ StartsWith,
67 )
68 import transaction
69 from zope.component import getUtility
70@@ -158,6 +159,7 @@
71 api_url,
72 celebrity_logged_in,
73 login_person,
74+ logout,
75 person_logged_in,
76 record_two_runs,
77 TestCaseWithFactory,
78@@ -174,7 +176,10 @@
79 DoesNotSnapshot,
80 HasQueryCount,
81 )
82-from lp.testing.pages import webservice_for_person
83+from lp.testing.pages import (
84+ LaunchpadWebServiceCaller,
85+ webservice_for_person,
86+ )
87 from lp.xmlrpc import faults
88 from lp.xmlrpc.interfaces import IPrivateApplication
89
90@@ -3909,6 +3914,50 @@
91 "refs/other": Equals([]),
92 }))
93
94+ def test_issueAccessToken(self):
95+ # A user can request an access token via the webservice API.
96+ self.pushConfig("codehosting", git_macaroon_secret_key="some-secret")
97+ repository = self.factory.makeGitRepository()
98+ # Write access to the repository isn't checked at this stage
99+ # (although the access token will only be useful if the user has
100+ # some kind of write access).
101+ requester = self.factory.makePerson()
102+ with person_logged_in(requester):
103+ repository_url = api_url(repository)
104+ webservice = webservice_for_person(
105+ requester, permission=OAuthPermission.WRITE_PUBLIC)
106+ webservice.default_api_version = "devel"
107+ response = webservice.named_post(repository_url, "issueAccessToken")
108+ self.assertEqual(200, response.status)
109+ macaroon = Macaroon.deserialize(json.loads(response.body))
110+ with person_logged_in(ANONYMOUS):
111+ self.assertThat(macaroon, MatchesStructure(
112+ location=Equals(config.vhost.mainsite.hostname),
113+ identifier=Equals("git-repository"),
114+ caveats=MatchesListwise([
115+ MatchesStructure.byEquality(
116+ caveat_id="lp.git-repository %s" % repository.id),
117+ MatchesStructure(
118+ caveat_id=StartsWith(
119+ "lp.principal.openid-identifier ")),
120+ MatchesStructure(caveat_id=StartsWith("lp.expires ")),
121+ ])))
122+
123+ def test_issueAccessToken_anonymous(self):
124+ # An anonymous user cannot request an access token via the
125+ # webservice API.
126+ repository = self.factory.makeGitRepository()
127+ with person_logged_in(repository.owner):
128+ repository_url = api_url(repository)
129+ logout()
130+ webservice = LaunchpadWebServiceCaller()
131+ webservice.default_api_version = "devel"
132+ response = webservice.named_post(repository_url, "issueAccessToken")
133+ self.assertEqual(401, response.status)
134+ self.assertEqual(
135+ "git-repository macaroons may only be issued for a logged-in "
136+ "user.", response.body)
137+
138
139 class TestGitRepositoryMacaroonIssuer(MacaroonTestMixin, TestCaseWithFactory):
140 """Test GitRepository macaroon issuing and verification."""