[Launchpad-reviewers] [Merge] lp:~cjwatson/launchpad/codeimport-git-auth into lp:launchpad
The proposal to merge lp:~cjwatson/launchpad/codeimport-git-auth into lp:launchpad has been updated. Status: Needs review => Merged For more details, see: https://code.launchpad.net/~cjwatson/launchpad/codeimport-git-auth/+merge/307968 -- Your team Launchpad code reviewers is subscribed to branch lp:launchpad. ___ Mailing list: https://launchpad.net/~launchpad-reviewers Post to : launchpad-reviewers@lists.launchpad.net Unsubscribe : https://launchpad.net/~launchpad-reviewers More help : https://help.launchpad.net/ListHelp
Re: [Launchpad-reviewers] [Merge] lp:~cjwatson/launchpad/codeimport-git-auth into lp:launchpad
Diff comments: > > === modified file 'lib/lp/code/model/codeimportjob.py' > --- lib/lp/code/model/codeimportjob.py2016-10-03 17:00:56 + > +++ lib/lp/code/model/codeimportjob.py2016-10-07 15:56:06 + > @@ -340,3 +348,57 @@ > # 4) > getUtility(ICodeImportEventSet).newReclaim( > code_import, machine, job_id) > + > + > +@implementer(IMacaroonIssuer) > +class CodeImportJobMacaroonIssuer: > + > +@property > +def _root_secret(self): > +secret = config.codeimport.macaroon_secret_key > +if not secret: > +raise RuntimeError( > +"codeimport.macaroon_secret_key not configured.") > +return secret > + > +def issueMacaroon(self, context): > +"""See `IMacaroonIssuer`.""" > +assert context.code_import.git_repository is not None > +macaroon = Macaroon( > +location=config.vhost.mainsite.hostname, > +identifier="code-import-job", key=self._root_secret) > +macaroon.add_first_party_caveat("code-import-job %s" % context.id) > +return macaroon > + > +def checkMacaroonIssuer(self, macaroon): > +"""See `IMacaroonIssuer`.""" > +try: > +verifier = Verifier() > +verifier.satisfy_general( > +lambda caveat: caveat.startswith("code-import-job ")) > +return verifier.verify(macaroon, self._root_secret) > +except Exception: > +return False > + > +def verifyMacaroon(self, macaroon, context): > +"""See `IMacaroonIssuer`.""" > +if IGitRepository.providedBy(context): > +if context.repository_type != GitRepositoryType.IMPORTED: > +return False > +code_import = getUtility(ICodeImportSet).getByGitRepository( > +context) > +if code_import is None: > +return False > +job = code_import.import_job > +if job is None: > +return False > +else: > +job = context I agree it's odd, but on balance I prefer the opposite resolution from the one I think you're suggesting: I've moved the GitRepository -> CodeImportJob lookup out to the caller in GitAPI instead. That way, CodeImportJobMacaroonIssuer operates on CodeImportJob objects and GitAPI handles the specifics of GitRepository objects, which makes a lot more sense to me. > +try: > +verifier = Verifier() > +verifier.satisfy_exact("code-import-job %s" % job.id) > +return ( > +verifier.verify(macaroon, self._root_secret) and > +job.state == CodeImportJobState.RUNNING) > +except Exception: > +return False -- https://code.launchpad.net/~cjwatson/launchpad/codeimport-git-auth/+merge/307968 Your team Launchpad code reviewers is subscribed to branch lp:launchpad. ___ Mailing list: https://launchpad.net/~launchpad-reviewers Post to : launchpad-reviewers@lists.launchpad.net Unsubscribe : https://launchpad.net/~launchpad-reviewers More help : https://help.launchpad.net/ListHelp
Re: [Launchpad-reviewers] [Merge] lp:~cjwatson/launchpad/codeimport-git-auth into lp:launchpad
Review: Approve code Diff comments: > > === modified file 'lib/lp/code/model/codeimportjob.py' > --- lib/lp/code/model/codeimportjob.py2016-10-03 17:00:56 + > +++ lib/lp/code/model/codeimportjob.py2016-10-07 15:56:06 + > @@ -340,3 +348,57 @@ > # 4) > getUtility(ICodeImportEventSet).newReclaim( > code_import, machine, job_id) > + > + > +@implementer(IMacaroonIssuer) > +class CodeImportJobMacaroonIssuer: > + > +@property > +def _root_secret(self): > +secret = config.codeimport.macaroon_secret_key > +if not secret: > +raise RuntimeError( > +"codeimport.macaroon_secret_key not configured.") > +return secret > + > +def issueMacaroon(self, context): > +"""See `IMacaroonIssuer`.""" > +assert context.code_import.git_repository is not None > +macaroon = Macaroon( > +location=config.vhost.mainsite.hostname, > +identifier="code-import-job", key=self._root_secret) > +macaroon.add_first_party_caveat("code-import-job %s" % context.id) > +return macaroon > + > +def checkMacaroonIssuer(self, macaroon): > +"""See `IMacaroonIssuer`.""" > +try: > +verifier = Verifier() > +verifier.satisfy_general( > +lambda caveat: caveat.startswith("code-import-job ")) > +return verifier.verify(macaroon, self._root_secret) We should probably also check that the location matches. It's also slightly confusing that verifyMacaroon doesn't call checkMacaroonIssuer -- seems like a recipe for mistakes later. > +except Exception: > +return False > + > +def verifyMacaroon(self, macaroon, context): > +"""See `IMacaroonIssuer`.""" > +if IGitRepository.providedBy(context): > +if context.repository_type != GitRepositoryType.IMPORTED: > +return False > +code_import = getUtility(ICodeImportSet).getByGitRepository( > +context) > +if code_import is None: > +return False > +job = code_import.import_job > +if job is None: > +return False > +else: > +job = context This case is only used in tests. Would it be better to refactor this slightly so we didn't have the weird unused alternate case and tests that don't test the normal path? > +try: > +verifier = Verifier() > +verifier.satisfy_exact("code-import-job %s" % job.id) > +return ( > +verifier.verify(macaroon, self._root_secret) and > +job.state == CodeImportJobState.RUNNING) > +except Exception: > +return False Cunning. Undebuggable and fragile, but it'll work for now and hopefully nobody will touch it in inappropriate ways before we have a more generic mechanism. > > === modified file 'lib/lp/code/xmlrpc/git.py' > --- lib/lp/code/xmlrpc/git.py 2016-10-06 20:37:39 + > +++ lib/lp/code/xmlrpc/git.py 2016-10-07 15:56:06 + > @@ -68,22 +73,47 @@ > super(GitAPI, self).__init__(*args, **kwargs) > self.repository_set = getUtility(IGitRepositorySet) > > -def _performLookup(self, path): > +def _verifyMacaroon(self, macaroon_raw, repository=None): > +try: > +macaroon = Macaroon.deserialize(macaroon_raw) > +except Exception: > +return False > +try: > +issuer = getUtility(IMacaroonIssuer, macaroon.identifier) > +except ComponentLookupError: > +return False > +if repository is not None: > +return issuer.verifyMacaroon(macaroon, repository) > +else: > +return issuer.checkMacaroonIssuer(macaroon) > + > +def _performLookup(self, requester, path, auth_params): The new "requester" parameter seems unused. > repository, extra_path = getUtility(IGitLookup).getByPath(path) > if repository is None: > return None > -try: > -hosting_path = repository.getInternalPath() > -except Unauthorized: > -return None > -writable = ( > -repository.repository_type == GitRepositoryType.HOSTED and > -check_permission("launchpad.Edit", repository)) > +macaroon_raw = auth_params.get("macaroon") > +naked_repository = removeSecurityProxy(repository) > +if (macaroon_raw is not None and > +self._verifyMacaroon(macaroon_raw, naked_repository)): > +# The authentication parameters specifically grant access to > +# this repository, so we can bypass other checks. > +hosting_path = naked_repository.getInternalPath() > +writable = True > +private = naked_repository.private I wouldn't half mind a quick assertion here that repository_type == IMPORTED. Makes the i
[Launchpad-reviewers] [Merge] lp:~cjwatson/launchpad/codeimport-git-auth into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/codeimport-git-auth into lp:launchpad. Commit message: Allow pushing to Git repositories associated with running code import jobs using macaroon credentials. Requested reviews: Launchpad code reviewers (launchpad-reviewers) Related bugs: Bug #1469459 in Launchpad itself: "import external code into a LP git repo (natively)" https://bugs.launchpad.net/launchpad/+bug/1469459 For more details, see: https://code.launchpad.net/~cjwatson/launchpad/codeimport-git-auth/+merge/307968 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. -- Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/codeimport-git-auth into lp:launchpad. === modified file 'lib/lp/code/configure.zcml' --- lib/lp/code/configure.zcml 2016-10-03 17:00:56 + +++ lib/lp/code/configure.zcml 2016-10-07 15:56:06 + @@ -729,6 +729,15 @@ reclaimJob"/> + + + + + + === modified file 'lib/lp/code/model/codeimportjob.py' --- lib/lp/code/model/codeimportjob.py 2016-10-03 17:00:56 + +++ lib/lp/code/model/codeimportjob.py 2016-10-07 15:56:06 + @@ -12,6 +12,10 @@ import datetime +from pymacaroons import ( +Macaroon, +Verifier, +) from sqlobject import ( ForeignKey, IntCol, @@ -27,7 +31,9 @@ CodeImportMachineState, CodeImportResultStatus, CodeImportReviewStatus, +GitRepositoryType, ) +from lp.code.interfaces.codeimport import ICodeImportSet from lp.code.interfaces.codeimportevent import ICodeImportEventSet from lp.code.interfaces.codeimportjob import ( ICodeImportJob, @@ -37,6 +43,7 @@ ) from lp.code.interfaces.codeimportmachine import ICodeImportMachineSet from lp.code.interfaces.codeimportresult import ICodeImportResultSet +from lp.code.interfaces.gitrepository import IGitRepository from lp.code.model.codeimportresult import CodeImportResult from lp.registry.interfaces.person import validate_public_person from lp.services.config import config @@ -48,6 +55,7 @@ SQLBase, sqlvalues, ) +from lp.services.macaroons.interfaces import IMacaroonIssuer @implementer(ICodeImportJob) @@ -340,3 +348,57 @@ # 4) getUtility(ICodeImportEventSet).newReclaim( code_import, machine, job_id) + + +@implementer(IMacaroonIssuer) +class CodeImportJobMacaroonIssuer: + +@property +def _root_secret(self): +secret = config.codeimport.macaroon_secret_key +if not secret: +raise RuntimeError( +"codeimport.macaroon_secret_key not configured.") +return secret + +def issueMacaroon(self, context): +"""See `IMacaroonIssuer`.""" +assert context.code_import.git_repository is not None +macaroon = Macaroon( +location=config.vhost.mainsite.hostname, +identifier="code-import-job", key=self._root_secret) +macaroon.add_first_party_caveat("code-import-job %s" % context.id) +return macaroon + +def checkMacaroonIssuer(self, macaroon): +"""See `IMacaroonIssuer`.""" +try: +verifier = Verifier() +verifier.satisfy_general( +lambda caveat: caveat.startswith("code-import-job ")) +return verifier.verify(macaroon, self._root_secret) +except Exception: +return False + +def verifyMacaroon(self, macaroon, context): +"""See `IMacaroonIssuer`.""" +if IGitRepository.providedBy(context): +if context.repository_type != GitRepositoryType.IMPORTED: +return False +code_import = getUtility(ICodeImportSet).getByGitRepository( +context) +if code_import is None: +return False +job = code_import.import_job +if job is None: +return False +else: +job = context +try: +verifier = Verifier() +verifier.satisfy_exact("code-import-job %s" % job.id) +return ( +verifier.verify(macaroon, self._root_secret) and +job.state == CodeImportJobState.RUNNING) +except Exception: +return False === modified file 'lib/lp/code/model/tests/test_codeimportjob.py' --- lib/lp/code/model/tests/test_codeimportjob.py 2015-10-19 10:56:16 + +++ lib/lp/code/model/tests/test_codeimportjob.py 2016-10-07 15:56:06 + @@ -1,4 +1,4 @@ -# Copyright 2009-2011 Cano