Merge lp:~cjwatson/launchpad/codeimport-git-auth into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18229
Proposed branch: lp:~cjwatson/launchpad/codeimport-git-auth
Merge into: lp:launchpad
Diff against target: 596 lines (+353/-24)
7 files modified
lib/lp/code/configure.zcml (+9/-0)
lib/lp/code/model/codeimportjob.py (+54/-0)
lib/lp/code/model/tests/test_codeimportjob.py (+100/-1)
lib/lp/code/xmlrpc/git.py (+61/-14)
lib/lp/code/xmlrpc/tests/test_git.py (+81/-9)
lib/lp/services/config/schema-lazr.conf (+3/-0)
lib/lp/services/macaroons/interfaces.py (+45/-0)
To merge this branch: bzr merge lp:~cjwatson/launchpad/codeimport-git-auth
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+307968@code.launchpad.net

Commit message

Allow pushing to Git repositories associated with running code import jobs using macaroon credentials.

Description of the change

Allow pushing to Git repositories associated with running code import jobs using macaroon credentials.

We don't actually issue suitable macaroons as yet outside the test suite; that will come in a later branch.

I've tried to design the IMacaroonIssuer interface so that it can be useful for some other similar applications (e.g. granting access tokens to builders), but it's provisional and I entirely expect it to need to change when it's actually battle-tested for that kind of thing.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)
Revision history for this message
Colin Watson (cjwatson) :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/code/configure.zcml'
2--- lib/lp/code/configure.zcml 2016-10-03 17:00:56 +0000
3+++ lib/lp/code/configure.zcml 2016-10-12 12:42:25 +0000
4@@ -729,6 +729,15 @@
5 reclaimJob"/>
6 </securedutility>
7
8+ <!-- CodeImportJobMacaroonIssuer -->
9+
10+ <securedutility
11+ class="lp.code.model.codeimportjob.CodeImportJobMacaroonIssuer"
12+ provides="lp.services.macaroons.interfaces.IMacaroonIssuer"
13+ name="code-import-job">
14+ <allow interface="lp.services.macaroons.interfaces.IMacaroonIssuerPublic"/>
15+ </securedutility>
16+
17 <!-- CodeImportEvent -->
18
19 <class class="lp.code.model.codeimportevent.CodeImportEvent">
20
21=== modified file 'lib/lp/code/model/codeimportjob.py'
22--- lib/lp/code/model/codeimportjob.py 2016-10-03 17:00:56 +0000
23+++ lib/lp/code/model/codeimportjob.py 2016-10-12 12:42:25 +0000
24@@ -12,6 +12,10 @@
25
26 import datetime
27
28+from pymacaroons import (
29+ Macaroon,
30+ Verifier,
31+ )
32 from sqlobject import (
33 ForeignKey,
34 IntCol,
35@@ -27,7 +31,9 @@
36 CodeImportMachineState,
37 CodeImportResultStatus,
38 CodeImportReviewStatus,
39+ GitRepositoryType,
40 )
41+from lp.code.interfaces.codeimport import ICodeImportSet
42 from lp.code.interfaces.codeimportevent import ICodeImportEventSet
43 from lp.code.interfaces.codeimportjob import (
44 ICodeImportJob,
45@@ -37,6 +43,7 @@
46 )
47 from lp.code.interfaces.codeimportmachine import ICodeImportMachineSet
48 from lp.code.interfaces.codeimportresult import ICodeImportResultSet
49+from lp.code.interfaces.gitrepository import IGitRepository
50 from lp.code.model.codeimportresult import CodeImportResult
51 from lp.registry.interfaces.person import validate_public_person
52 from lp.services.config import config
53@@ -48,6 +55,7 @@
54 SQLBase,
55 sqlvalues,
56 )
57+from lp.services.macaroons.interfaces import IMacaroonIssuer
58
59
60 @implementer(ICodeImportJob)
61@@ -340,3 +348,49 @@
62 # 4)
63 getUtility(ICodeImportEventSet).newReclaim(
64 code_import, machine, job_id)
65+
66+
67+@implementer(IMacaroonIssuer)
68+class CodeImportJobMacaroonIssuer:
69+
70+ @property
71+ def _root_secret(self):
72+ secret = config.codeimport.macaroon_secret_key
73+ if not secret:
74+ raise RuntimeError(
75+ "codeimport.macaroon_secret_key not configured.")
76+ return secret
77+
78+ def issueMacaroon(self, context):
79+ """See `IMacaroonIssuer`."""
80+ assert context.code_import.git_repository is not None
81+ macaroon = Macaroon(
82+ location=config.vhost.mainsite.hostname,
83+ identifier="code-import-job", key=self._root_secret)
84+ macaroon.add_first_party_caveat("code-import-job %s" % context.id)
85+ return macaroon
86+
87+ def checkMacaroonIssuer(self, macaroon):
88+ """See `IMacaroonIssuer`."""
89+ if macaroon.location != config.vhost.mainsite.hostname:
90+ return False
91+ try:
92+ verifier = Verifier()
93+ verifier.satisfy_general(
94+ lambda caveat: caveat.startswith("code-import-job "))
95+ return verifier.verify(macaroon, self._root_secret)
96+ except Exception:
97+ return False
98+
99+ def verifyMacaroon(self, macaroon, context):
100+ """See `IMacaroonIssuer`."""
101+ if not self.checkMacaroonIssuer(macaroon):
102+ return False
103+ try:
104+ verifier = Verifier()
105+ verifier.satisfy_exact("code-import-job %s" % context.id)
106+ return (
107+ verifier.verify(macaroon, self._root_secret) and
108+ context.state == CodeImportJobState.RUNNING)
109+ except Exception:
110+ return False
111
112=== modified file 'lib/lp/code/model/tests/test_codeimportjob.py'
113--- lib/lp/code/model/tests/test_codeimportjob.py 2015-10-19 10:56:16 +0000
114+++ lib/lp/code/model/tests/test_codeimportjob.py 2016-10-12 12:42:25 +0000
115@@ -1,4 +1,4 @@
116-# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
117+# Copyright 2009-2016 Canonical Ltd. This software is licensed under the
118 # GNU Affero General Public License version 3 (see the file LICENSE).
119
120 """Unit tests for CodeImportJob and CodeImportJobWorkflow."""
121@@ -13,7 +13,12 @@
122 import StringIO
123 import unittest
124
125+from pymacaroons import Macaroon
126 from pytz import UTC
127+from testtools.matchers import (
128+ MatchesListwise,
129+ MatchesStructure,
130+ )
131 import transaction
132 from zope.component import getUtility
133 from zope.security.proxy import removeSecurityProxy
134@@ -23,6 +28,7 @@
135 CodeImportJobState,
136 CodeImportResultStatus,
137 CodeImportReviewStatus,
138+ TargetRevisionControlSystems,
139 )
140 from lp.code.interfaces.codeimport import ICodeImportSet
141 from lp.code.interfaces.codeimportevent import ICodeImportEventSet
142@@ -41,6 +47,7 @@
143 from lp.services.database.constants import UTC_NOW
144 from lp.services.librarian.interfaces import ILibraryFileAliasSet
145 from lp.services.librarian.interfaces.client import ILibrarianClient
146+from lp.services.macaroons.interfaces import IMacaroonIssuer
147 from lp.services.webapp import canonical_url
148 from lp.testing import (
149 ANONYMOUS,
150@@ -1144,5 +1151,97 @@
151 get_feedback_messages(user_browser.contents))
152
153
154+class TestCodeImportJobMacaroonIssuer(TestCaseWithFactory):
155+ """Test CodeImportJob macaroon issuing and verification."""
156+
157+ layer = DatabaseFunctionalLayer
158+
159+ def setUp(self):
160+ super(TestCodeImportJobMacaroonIssuer, self).setUp()
161+ login_for_code_imports()
162+ self.pushConfig("codeimport", macaroon_secret_key="some-secret")
163+
164+ def makeJob(self, target_rcs_type=TargetRevisionControlSystems.GIT):
165+ code_import = self.factory.makeCodeImport(
166+ target_rcs_type=target_rcs_type)
167+ return self.factory.makeCodeImportJob(code_import=code_import)
168+
169+ def test_issueMacaroon_refuses_branch(self):
170+ job = self.makeJob(target_rcs_type=TargetRevisionControlSystems.BZR)
171+ issuer = getUtility(IMacaroonIssuer, "code-import-job")
172+ self.assertRaises(
173+ AssertionError, removeSecurityProxy(issuer).issueMacaroon, job)
174+
175+ def test_issueMacaroon_good(self):
176+ job = self.makeJob()
177+ issuer = getUtility(IMacaroonIssuer, "code-import-job")
178+ macaroon = removeSecurityProxy(issuer).issueMacaroon(job)
179+ self.assertEqual("launchpad.dev", macaroon.location)
180+ self.assertEqual("code-import-job", macaroon.identifier)
181+ self.assertThat(macaroon.caveats, MatchesListwise([
182+ MatchesStructure.byEquality(
183+ caveat_id="code-import-job %s" % job.id),
184+ ]))
185+
186+ def test_checkMacaroonIssuer_good(self):
187+ job = self.makeJob()
188+ issuer = getUtility(IMacaroonIssuer, "code-import-job")
189+ macaroon = removeSecurityProxy(issuer).issueMacaroon(job)
190+ self.assertTrue(issuer.checkMacaroonIssuer(macaroon))
191+
192+ def test_checkMacaroonIssuer_wrong_location(self):
193+ issuer = getUtility(IMacaroonIssuer, "code-import-job")
194+ macaroon = Macaroon(
195+ location="another-location",
196+ key=removeSecurityProxy(issuer)._root_secret)
197+ self.assertFalse(issuer.checkMacaroonIssuer(macaroon))
198+
199+ def test_checkMacaroonIssuer_wrong_key(self):
200+ issuer = getUtility(IMacaroonIssuer, "code-import-job")
201+ macaroon = Macaroon(
202+ location=config.vhost.mainsite.hostname, key="another-secret")
203+ self.assertFalse(issuer.checkMacaroonIssuer(macaroon))
204+
205+ def test_verifyMacaroon_good(self):
206+ machine = self.factory.makeCodeImportMachine(set_online=True)
207+ job = self.makeJob()
208+ issuer = getUtility(IMacaroonIssuer, "code-import-job")
209+ getUtility(ICodeImportJobWorkflow).startJob(job, machine)
210+ macaroon = removeSecurityProxy(issuer).issueMacaroon(job)
211+ self.assertTrue(issuer.verifyMacaroon(macaroon, job))
212+
213+ def test_verifyMacaroon_wrong_location(self):
214+ machine = self.factory.makeCodeImportMachine(set_online=True)
215+ job = self.makeJob()
216+ issuer = getUtility(IMacaroonIssuer, "code-import-job")
217+ getUtility(ICodeImportJobWorkflow).startJob(job, machine)
218+ macaroon = Macaroon(
219+ location="another-location",
220+ key=removeSecurityProxy(issuer)._root_secret)
221+ self.assertFalse(issuer.verifyMacaroon(macaroon, job))
222+
223+ def test_verifyMacaroon_wrong_key(self):
224+ machine = self.factory.makeCodeImportMachine(set_online=True)
225+ job = self.makeJob()
226+ issuer = getUtility(IMacaroonIssuer, "code-import-job")
227+ getUtility(ICodeImportJobWorkflow).startJob(job, machine)
228+ macaroon = Macaroon(
229+ location=config.vhost.mainsite.hostname, key="another-secret")
230+ self.assertFalse(issuer.verifyMacaroon(macaroon, job))
231+
232+ def test_verifyMacaroon_not_running(self):
233+ job = self.makeJob()
234+ issuer = getUtility(IMacaroonIssuer, "code-import-job")
235+ macaroon = removeSecurityProxy(issuer).issueMacaroon(job)
236+ self.assertFalse(issuer.verifyMacaroon(macaroon, job))
237+
238+ def test_verifyMacaroon_wrong_job(self):
239+ job = self.makeJob()
240+ other_job = self.factory.makeCodeImportJob(code_import=job.code_import)
241+ issuer = getUtility(IMacaroonIssuer, "code-import-job")
242+ macaroon = removeSecurityProxy(issuer).issueMacaroon(other_job)
243+ self.assertFalse(issuer.verifyMacaroon(macaroon, job))
244+
245+
246 def test_suite():
247 return unittest.TestLoader().loadTestsFromName(__name__)
248
249=== modified file 'lib/lp/code/xmlrpc/git.py'
250--- lib/lp/code/xmlrpc/git.py 2016-10-06 20:37:39 +0000
251+++ lib/lp/code/xmlrpc/git.py 2016-10-12 12:42:25 +0000
252@@ -10,9 +10,13 @@
253
254 import sys
255
256+from pymacaroons import Macaroon
257 from storm.store import Store
258 import transaction
259-from zope.component import getUtility
260+from zope.component import (
261+ ComponentLookupError,
262+ getUtility,
263+ )
264 from zope.error.interfaces import IErrorReportingUtility
265 from zope.interface import implementer
266 from zope.security.interfaces import Unauthorized
267@@ -30,6 +34,7 @@
268 InvalidNamespace,
269 )
270 from lp.code.interfaces.codehosting import LAUNCHPAD_ANONYMOUS
271+from lp.code.interfaces.codeimport import ICodeImportSet
272 from lp.code.interfaces.gitapi import IGitAPI
273 from lp.code.interfaces.githosting import IGitHostingClient
274 from lp.code.interfaces.gitjob import IGitRefScanJobSource
275@@ -53,6 +58,7 @@
276 NoSuchProduct,
277 )
278 from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
279+from lp.services.macaroons.interfaces import IMacaroonIssuer
280 from lp.services.webapp import LaunchpadXMLRPCView
281 from lp.services.webapp.authorization import check_permission
282 from lp.services.webapp.errorlog import ScriptRequest
283@@ -68,22 +74,58 @@
284 super(GitAPI, self).__init__(*args, **kwargs)
285 self.repository_set = getUtility(IGitRepositorySet)
286
287- def _performLookup(self, path):
288+ def _verifyMacaroon(self, macaroon_raw, repository=None):
289+ try:
290+ macaroon = Macaroon.deserialize(macaroon_raw)
291+ except Exception:
292+ return False
293+ try:
294+ issuer = getUtility(IMacaroonIssuer, macaroon.identifier)
295+ except ComponentLookupError:
296+ return False
297+ if repository is not None:
298+ if repository.repository_type != GitRepositoryType.IMPORTED:
299+ return False
300+ code_import = getUtility(ICodeImportSet).getByGitRepository(
301+ repository)
302+ if code_import is None:
303+ return False
304+ job = code_import.import_job
305+ if job is None:
306+ return False
307+ return issuer.verifyMacaroon(macaroon, job)
308+ else:
309+ return issuer.checkMacaroonIssuer(macaroon)
310+
311+ def _performLookup(self, path, auth_params):
312 repository, extra_path = getUtility(IGitLookup).getByPath(path)
313 if repository is None:
314 return None
315- try:
316- hosting_path = repository.getInternalPath()
317- except Unauthorized:
318- return None
319- writable = (
320- repository.repository_type == GitRepositoryType.HOSTED and
321- check_permission("launchpad.Edit", repository))
322+ macaroon_raw = auth_params.get("macaroon")
323+ naked_repository = removeSecurityProxy(repository)
324+ if (macaroon_raw is not None and
325+ self._verifyMacaroon(macaroon_raw, naked_repository)):
326+ # The authentication parameters specifically grant access to
327+ # this repository, so we can bypass other checks.
328+ # For the time being, this only works for code imports.
329+ assert repository.repository_type == GitRepositoryType.IMPORTED
330+ hosting_path = naked_repository.getInternalPath()
331+ writable = True
332+ private = naked_repository.private
333+ else:
334+ try:
335+ hosting_path = repository.getInternalPath()
336+ except Unauthorized:
337+ return None
338+ writable = (
339+ repository.repository_type == GitRepositoryType.HOSTED and
340+ check_permission("launchpad.Edit", repository))
341+ private = repository.private
342 return {
343 "path": hosting_path,
344 "writable": writable,
345 "trailing": extra_path,
346- "private": repository.private,
347+ "private": private,
348 }
349
350 def _getGitNamespaceExtras(self, path, requester):
351@@ -233,11 +275,11 @@
352 if requester == LAUNCHPAD_ANONYMOUS:
353 requester = None
354 try:
355- result = self._performLookup(path)
356+ result = self._performLookup(path, auth_params)
357 if (result is None and requester is not None and
358 permission == "write"):
359 self._createRepository(requester, path)
360- result = self._performLookup(path)
361+ result = self._performLookup(path, auth_params)
362 if result is None:
363 raise faults.GitRepositoryNotFound(path)
364 if permission != "read" and not result["writable"]:
365@@ -286,5 +328,10 @@
366
367 def authenticateWithPassword(self, username, password):
368 """See `IGitAPI`."""
369- # Password authentication isn't supported yet.
370- return faults.Unauthorized()
371+ # XXX cjwatson 2016-10-06: We only support free-floating macaroons
372+ # at the moment, not ones bound to a user.
373+ if not username and self._verifyMacaroon(password):
374+ return {"macaroon": password}
375+ else:
376+ # Only macaroons are supported for password authentication.
377+ return faults.Unauthorized()
378
379=== modified file 'lib/lp/code/xmlrpc/tests/test_git.py'
380--- lib/lp/code/xmlrpc/tests/test_git.py 2016-10-06 11:17:05 +0000
381+++ lib/lp/code/xmlrpc/tests/test_git.py 2016-10-12 12:42:25 +0000
382@@ -5,6 +5,7 @@
383
384 __metaclass__ = type
385
386+from pymacaroons import Macaroon
387 from testscenarios import (
388 load_tests_apply_scenarios,
389 WithScenarios,
390@@ -13,8 +14,12 @@
391 from zope.security.proxy import removeSecurityProxy
392
393 from lp.app.enums import InformationType
394-from lp.code.enums import GitRepositoryType
395+from lp.code.enums import (
396+ GitRepositoryType,
397+ TargetRevisionControlSystems,
398+ )
399 from lp.code.errors import GitRepositoryCreationFault
400+from lp.code.interfaces.codeimportjob import ICodeImportJobWorkflow
401 from lp.code.interfaces.gitcollection import IAllGitRepositories
402 from lp.code.interfaces.gitjob import IGitRefScanJobSource
403 from lp.code.interfaces.gitrepository import (
404@@ -24,10 +29,13 @@
405 from lp.code.tests.helpers import GitHostingFixture
406 from lp.code.xmlrpc.git import GitAPI
407 from lp.registry.enums import TeamMembershipPolicy
408+from lp.services.config import config
409+from lp.services.macaroons.interfaces import IMacaroonIssuer
410 from lp.services.webapp.escaping import html_escape
411 from lp.testing import (
412 admin_logged_in,
413 ANONYMOUS,
414+ celebrity_logged_in,
415 login,
416 person_logged_in,
417 TestCaseWithFactory,
418@@ -74,13 +82,15 @@
419
420 def assertPermissionDenied(self, requester, path,
421 message="Permission denied.",
422- permission="read", can_authenticate=False):
423+ permission="read", can_authenticate=False,
424+ macaroon_raw=None):
425 """Assert that looking at the given path returns PermissionDenied."""
426 if requester is not None:
427 requester = requester.id
428- fault = self._translatePath(
429- path, permission,
430- {"uid": requester, "can-authenticate": can_authenticate})
431+ auth_params = {"uid": requester, "can-authenticate": can_authenticate}
432+ if macaroon_raw is not None:
433+ auth_params["macaroon"] = macaroon_raw
434+ fault = self._translatePath(path, permission, auth_params)
435 self.assertEqual(faults.PermissionDenied(message), fault)
436
437 def assertUnauthorized(self, requester, path,
438@@ -143,12 +153,13 @@
439
440 def assertTranslates(self, requester, path, repository, writable,
441 permission="read", can_authenticate=False,
442- trailing="", private=False):
443+ macaroon_raw=None, trailing="", private=False):
444 if requester is not None:
445 requester = requester.id
446- translation = self._translatePath(
447- path, permission,
448- {"uid": requester, "can-authenticate": can_authenticate})
449+ auth_params = {"uid": requester, "can-authenticate": can_authenticate}
450+ if macaroon_raw is not None:
451+ auth_params["macaroon"] = macaroon_raw
452+ translation = self._translatePath(path, permission, auth_params)
453 login(ANONYMOUS)
454 self.assertEqual(
455 {"path": repository.getInternalPath(), "writable": writable,
456@@ -634,6 +645,47 @@
457 "GitRepositoryCreationFault: nothing here",
458 self.oopses[0]["tb_text"])
459
460+ def test_translatePath_code_import(self):
461+ # A code import worker with a suitable macaroon can write to a
462+ # repository associated with a running code import job.
463+ self.pushConfig("codeimport", macaroon_secret_key="some-secret")
464+ machine = self.factory.makeCodeImportMachine(set_online=True)
465+ code_imports = [
466+ self.factory.makeCodeImport(
467+ target_rcs_type=TargetRevisionControlSystems.GIT)
468+ for _ in range(2)]
469+ with celebrity_logged_in("vcs_imports"):
470+ jobs = [
471+ self.factory.makeCodeImportJob(code_import=code_import)
472+ for code_import in code_imports]
473+ issuer = getUtility(IMacaroonIssuer, "code-import-job")
474+ macaroons = [
475+ removeSecurityProxy(issuer).issueMacaroon(job) for job in jobs]
476+ path = u"/%s" % code_imports[0].git_repository.unique_name
477+ self.assertPermissionDenied(
478+ None, path, permission="write", macaroon_raw=macaroons[0])
479+ with celebrity_logged_in("vcs_imports"):
480+ getUtility(ICodeImportJobWorkflow).startJob(jobs[0], machine)
481+ # This only works with new-style passing of authentication parameters.
482+ if self.auth_params_dict:
483+ self.assertTranslates(
484+ None, path, code_imports[0].git_repository, True,
485+ permission="write", macaroon_raw=macaroons[0].serialize())
486+ else:
487+ self.assertPermissionDenied(
488+ None, path, permission="write",
489+ macaroon_raw=macaroons[0].serialize())
490+ self.assertPermissionDenied(
491+ None, path, permission="write",
492+ macaroon_raw=macaroons[1].serialize())
493+ self.assertPermissionDenied(
494+ None, path, permission="write",
495+ macaroon_raw=Macaroon(
496+ location=config.vhost.mainsite.hostname, identifier="another",
497+ key="another-secret").serialize())
498+ self.assertPermissionDenied(
499+ None, path, permission="write", macaroon_raw="nonsense")
500+
501 def test_notify(self):
502 # The notify call creates a GitRefScanJob.
503 repository = self.factory.makeGitRepository()
504@@ -666,6 +718,26 @@
505 self.git_api.authenticateWithPassword('foo', 'bar'),
506 faults.Unauthorized)
507
508+ def test_authenticateWithPassword_code_import(self):
509+ self.pushConfig("codeimport", macaroon_secret_key="some-secret")
510+ code_import = self.factory.makeCodeImport(
511+ target_rcs_type=TargetRevisionControlSystems.GIT)
512+ with celebrity_logged_in("vcs_imports"):
513+ job = self.factory.makeCodeImportJob(code_import=code_import)
514+ issuer = getUtility(IMacaroonIssuer, "code-import-job")
515+ macaroon = removeSecurityProxy(issuer).issueMacaroon(job)
516+ self.assertEqual(
517+ {"macaroon": macaroon.serialize()},
518+ self.git_api.authenticateWithPassword("", macaroon.serialize()))
519+ other_macaroon = Macaroon(identifier="another", key="another-secret")
520+ self.assertIsInstance(
521+ self.git_api.authenticateWithPassword(
522+ "", other_macaroon.serialize()),
523+ faults.Unauthorized)
524+ self.assertIsInstance(
525+ self.git_api.authenticateWithPassword("", "nonsense"),
526+ faults.Unauthorized)
527+
528
529 class TestGitAPISecurity(TestGitAPIMixin, TestCaseWithFactory):
530 """Slow tests for `IGitAPI`.
531
532=== modified file 'lib/lp/services/config/schema-lazr.conf'
533--- lib/lp/services/config/schema-lazr.conf 2016-10-04 11:19:07 +0000
534+++ lib/lp/services/config/schema-lazr.conf 2016-10-12 12:42:25 +0000
535@@ -416,6 +416,9 @@
536 # Import only this many revisions from svn (via bzr-svn) at once.
537 svn_revisions_import_limit: 500
538
539+# Secret key for macaroons used to grant git push permission to workers.
540+macaroon_secret_key:
541+
542 [codeimportdispatcher]
543 # The directory where the code import worker should be directed to
544 # store its logs.
545
546=== added directory 'lib/lp/services/macaroons'
547=== added file 'lib/lp/services/macaroons/__init__.py'
548=== added file 'lib/lp/services/macaroons/interfaces.py'
549--- lib/lp/services/macaroons/interfaces.py 1970-01-01 00:00:00 +0000
550+++ lib/lp/services/macaroons/interfaces.py 2016-10-12 12:42:25 +0000
551@@ -0,0 +1,45 @@
552+# Copyright 2016 Canonical Ltd. This software is licensed under the
553+# GNU Affero General Public License version 3 (see the file LICENSE).
554+
555+"""Interface to a policy for issuing and verifying macaroons."""
556+
557+from __future__ import absolute_import, print_function, unicode_literals
558+
559+__metaclass__ = type
560+__all__ = [
561+ 'IMacaroonIssuer',
562+ ]
563+
564+from zope.interface import Interface
565+
566+
567+class IMacaroonIssuerPublic(Interface):
568+ """Public interface to a policy for verifying macaroons."""
569+
570+ def checkMacaroonIssuer(macaroon):
571+ """Check that `macaroon` was issued by this issuer.
572+
573+ This does not verify that the macaroon is valid for a given context,
574+ only that it could be valid for some context. Use this in the
575+ authentication part of an authentication/authorisation API.
576+ """
577+
578+ def verifyMacaroon(macaroon, context):
579+ """Verify that `macaroon` is valid for `context`.
580+
581+ :param macaroon: A `Macaroon`.
582+ :param context: The context to check.
583+ :return: True if `macaroon` is valid for `context`, otherwise False.
584+ """
585+
586+
587+class IMacaroonIssuer(IMacaroonIssuerPublic):
588+ """Interface to a policy for issuing and verifying macaroons."""
589+
590+ def issueMacaroon(context):
591+ """Issue a macaroon for `context`.
592+
593+ :param context: The context that the returned macaroon should relate
594+ to.
595+ :return: A macaroon.
596+ """