[Launchpad-reviewers] [Merge] lp:~cjwatson/launchpad/git-code-import-security-deletion into lp:launchpad

2016-10-12 Thread Colin Watson
Colin Watson has proposed merging 
lp:~cjwatson/launchpad/git-code-import-security-deletion into lp:launchpad with 
lp:~cjwatson/launchpad/codeimport-git-worker as a prerequisite.

Commit message:
Grant vcs-imports edit access to imported repositories, and handle deletion of 
imported repositories.

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/git-code-import-security-deletion/+merge/308319

This doesn't grant vcs-imports push access: IMPORTED repositories are handled 
specially in lp.code.xmlrpc.git.  However, it's still useful for the 
vcs-imports team to be able to edit import metadata.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of 
lp:~cjwatson/launchpad/git-code-import-security-deletion into lp:launchpad.
=== modified file 'lib/lp/_schema_circular_imports.py'
--- lib/lp/_schema_circular_imports.py	2016-06-25 08:05:06 +
+++ lib/lp/_schema_circular_imports.py	2016-10-12 23:49:44 +
@@ -578,6 +578,7 @@
 patch_collection_property(IGitRepository, 'subscriptions', IGitSubscription)
 patch_entry_return_type(IGitRepository, 'subscribe', IGitSubscription)
 patch_entry_return_type(IGitRepository, 'getSubscription', IGitSubscription)
+patch_reference_property(IGitRepository, 'code_import', ICodeImport)
 patch_collection_property(
 IGitRepository, 'landing_targets', IBranchMergeProposal)
 patch_collection_property(

=== modified file 'lib/lp/code/interfaces/gitnamespace.py'
--- lib/lp/code/interfaces/gitnamespace.py	2016-10-06 15:54:47 +
+++ lib/lp/code/interfaces/gitnamespace.py	2016-10-12 23:49:44 +
@@ -119,10 +119,12 @@
 :return: An `InformationType`.
 """
 
-def validateRegistrant(registrant):
+def validateRegistrant(registrant, repository=None):
 """Check that the registrant can create a repository in this namespace.
 
 :param registrant: An `IPerson`.
+:param repository: An optional `IGitRepository` to also check when
+working with imported repositories.
 :raises GitRepositoryCreatorNotMemberOfOwnerTeam: if the namespace
 owner is a team and the registrant is not in that team.
 :raises GitRepositoryCreatorNotOwner: if the namespace owner is an

=== modified file 'lib/lp/code/interfaces/gitrepository.py'
--- lib/lp/code/interfaces/gitrepository.py	2016-10-05 09:54:42 +
+++ lib/lp/code/interfaces/gitrepository.py	2016-10-12 23:49:44 +
@@ -260,6 +260,11 @@
 title=_("Persons subscribed to this repository."),
 readonly=True, value_type=Reference(IPerson)))
 
+code_import = exported(Reference(
+title=_("The associated CodeImport, if any."),
+# Really ICodeImport, patched in _schema_circular_imports.py.
+schema=Interface))
+
 def getRefByPath(path):
 """Look up a single reference in this repository by path.
 
@@ -946,14 +951,24 @@
 return identities
 
 
-def user_has_special_git_repository_access(user):
+def user_has_special_git_repository_access(user, repository=None):
 """Admins have special access.
 
 :param user: An `IPerson` or None.
+:param repository: An `IGitRepository` or None when checking collection
+access.
 """
 if user is None:
 return False
 roles = IPersonRoles(user)
 if roles.in_admin:
 return True
-return False
+if repository is None:
+return False
+code_import = repository.code_import
+if code_import is None:
+return False
+return (
+roles.in_vcs_imports
+or (IPersonRoles(repository.owner).in_vcs_imports
+and user.inTerm(code_import.registrant)))

=== modified file 'lib/lp/code/model/branch.py'
--- lib/lp/code/model/branch.py	2016-06-20 20:32:36 +
+++ lib/lp/code/model/branch.py	2016-10-12 23:49:44 +
@@ -117,6 +117,7 @@
 BRANCH_ID_ALIAS_PREFIX,
 compose_public_url,
 )
+from lp.code.interfaces.codeimport import ICodeImportSet
 from lp.code.interfaces.seriessourcepackagebranch import (
 IFindOfficialBranchLinks,
 )
@@ -813,8 +814,7 @@
 
 @cachedproperty
 def code_import(self):
-from lp.code.model.codeimport import CodeImportSet
-return CodeImportSet().getByBranch(self)
+return getUtility(ICodeImportSet).getByBranch(self)
 
 def _deletionRequirements(self):
 """Determine what operations must be performed to delete this branch.
@@ -1561,8 +1561,7 @@
 self, code_import, _('This is the import data for this branch.'))
 
 def __call__(self):
-from lp.code.model.codeimport import CodeImportSet
-CodeImportSet().delete(self.affected_object)
+getUtility(ICodeImportSet).delete(self.affected_object)
 
 
 

Re: [Launchpad-reviewers] [Merge] lp:~cjwatson/launchpad/codeimport-git-worker into lp:launchpad

2016-10-12 Thread William Grant
Review: Approve code



Diff comments:

> 
> === modified file 'lib/lp/codehosting/codeimport/worker.py'
> --- lib/lp/codehosting/codeimport/worker.py   2016-10-11 15:36:32 +
> +++ lib/lp/codehosting/codeimport/worker.py   2016-10-11 15:36:32 +
> @@ -918,3 +973,67 @@
>  """See `PullingImportWorker.probers`."""
>  from bzrlib.bzrdir import BzrProber, RemoteBzrProber
>  return [BzrProber, RemoteBzrProber]
> +
> +
> +class GitToGitImportWorker(ImportWorker):
> +"""An import worker for imports from Git to Git."""
> +
> +def _runGit(self, *args, **kwargs):
> +"""Run git with arguments, sending output to the logger."""
> +cmd = ["git"] + list(args)
> +git_process = subprocess.Popen(
> +cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, **kwargs)
> +for line in git_process.stdout:
> +line = line.decode("UTF-8", "replace").rstrip("\n")
> +# Remove any user/password data from URLs.
> +line = re.sub(r"://([^:]*:[^@]*@)(\S+)", r"://\2", line)
> +self._logger.info(line)
> +retcode = git_process.wait()
> +if retcode:
> +raise subprocess.CalledProcessError(retcode, cmd)
> +
> +def _doImport(self):
> +self._logger.info("Starting job.")
> +self._logger.info(config.codeimport.git_repository_store)
> +try:
> +self._opener_policy.checkOneURL(self.source_details.url)

When we move these to prodstack we should probably set up some more sensible 
outbound rules, but I guess it'll do for now. Hopefully git can't be confused 
into emitting random HTTP things to stderr.

> +except BadUrl as e:
> +self._logger.info("Invalid URL: %s" % e)
> +return CodeImportWorkerExitCode.FAILURE_FORBIDDEN
> +unauth_target_url = urljoin(
> +config.codeimport.git_repository_store,
> +self.source_details.target_id)
> +split = urlsplit(unauth_target_url)
> +if split.hostname:
> +target_netloc = ":%s@%s" % (
> +self.source_details.macaroon.serialize(), split.hostname)
> +else:
> +target_netloc = ""
> +target_url = urlunsplit([
> +split.scheme, target_netloc, split.path, "", ""])
> +# XXX cjwatson 2016-10-11: Ideally we'd put credentials in a
> +# credentials store instead.  However, git only accepts credentials
> +# that have both a non-empty username and a non-empty password.
> +self._logger.info("Getting existing repository from hosting 
> service.")
> +try:
> +self._runGit("clone", "--bare", target_url, "repository")
> +except subprocess.CalledProcessError as e:
> +self._logger.info(
> +"Unable to get existing repository from hosting service: "
> +"%s" % e)
> +return CodeImportWorkerExitCode.FAILURE
> +self._logger.info("Fetching remote repository.")
> +try:
> +self._runGit(
> +"remote", "add", "-f", "--mirror=fetch",
> +"mirror", self.source_details.url, cwd="repository")

HEAD handling is always atrocious... but I hadn't realised git-remote fiddled 
around so manually in ways that aren't exposed elsewhere.

> +except subprocess.CalledProcessError as e:
> +self._logger.info("Unable to fetch remote repository: %s" % e)
> +return CodeImportWorkerExitCode.FAILURE_INVALID
> +self._logger.info("Pushing repository to hosting service.")
> +try:
> +self._runGit("push", "--mirror", target_url, cwd="repository")
> +except subprocess.CalledProcessError as e:
> +self._logger.info("Unable to push to hosting service: %s" % e)
> +return CodeImportWorkerExitCode.FAILURE
> +return CodeImportWorkerExitCode.SUCCESS
> 
> === modified file 'lib/lp/services/config/schema-lazr.conf'
> --- lib/lp/services/config/schema-lazr.conf   2016-10-11 15:36:32 +
> +++ lib/lp/services/config/schema-lazr.conf   2016-10-11 15:36:32 +
> @@ -417,7 +421,7 @@
>  svn_revisions_import_limit: 500
>  
>  # Secret key for macaroons used to grant git push permission to workers.
> -macaroon_secret_key:
> +macaroon_secret_key: none

I have no words, but carry on.

>  
>  [codeimportdispatcher]
>  # The directory where the code import worker should be directed to


-- 
https://code.launchpad.net/~cjwatson/launchpad/codeimport-git-worker/+merge/308142
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-worker into lp:launchpad

2016-10-12 Thread Colin Watson
I've sprinkled a bunch of extra comments around in various places.

Diff comments:

> 
> === modified file 'lib/lp/codehosting/codeimport/worker.py'
> --- lib/lp/codehosting/codeimport/worker.py   2016-10-11 15:36:32 +
> +++ lib/lp/codehosting/codeimport/worker.py   2016-10-11 15:36:32 +
> @@ -918,3 +973,67 @@
>  """See `PullingImportWorker.probers`."""
>  from bzrlib.bzrdir import BzrProber, RemoteBzrProber
>  return [BzrProber, RemoteBzrProber]
> +
> +
> +class GitToGitImportWorker(ImportWorker):
> +"""An import worker for imports from Git to Git."""
> +
> +def _runGit(self, *args, **kwargs):
> +"""Run git with arguments, sending output to the logger."""
> +cmd = ["git"] + list(args)
> +git_process = subprocess.Popen(
> +cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, **kwargs)
> +for line in git_process.stdout:
> +line = line.decode("UTF-8", "replace").rstrip("\n")
> +# Remove any user/password data from URLs.
> +line = re.sub(r"://([^:]*:[^@]*@)(\S+)", r"://\2", line)
> +self._logger.info(line)
> +retcode = git_process.wait()
> +if retcode:
> +raise subprocess.CalledProcessError(retcode, cmd)
> +
> +def _doImport(self):
> +self._logger.info("Starting job.")
> +self._logger.info(config.codeimport.git_repository_store)
> +try:
> +self._opener_policy.checkOneURL(self.source_details.url)

As far as I can see from http.c, git follows up to 20 redirects as long as 
they're to http/https/ftp/ftps schemes, and there's no way to configure this.

The only things that this would let through that checkOneURL tries to forbid 
would be redirects to git.launchpad.net and redirects to a blacklisted 
hostname, which in practice is always localhost or 127.0.0.1.  Perhaps we can 
get away with this state for the time being?  Redirects to git.launchpad.net 
would basically be a slightly annoying waste of resources but nothing more, 
while redirects to localhost should in practice be harmless given that code 
import workers don't run web or FTP servers.

The only way I can think of to improve isolation here would be to direct all 
git requests through a proxy, which would be a non-trivial chunk of work.

> +except BadUrl as e:
> +self._logger.info("Invalid URL: %s" % e)
> +return CodeImportWorkerExitCode.FAILURE_FORBIDDEN
> +unauth_target_url = urljoin(
> +config.codeimport.git_repository_store,
> +self.source_details.target_id)
> +split = urlsplit(unauth_target_url)
> +if split.hostname:
> +target_netloc = ":%s@%s" % (
> +self.source_details.macaroon.serialize(), split.hostname)
> +else:
> +target_netloc = ""
> +target_url = urlunsplit([
> +split.scheme, target_netloc, split.path, "", ""])
> +# XXX cjwatson 2016-10-11: Ideally we'd put credentials in a
> +# credentials store instead.  However, git only accepts credentials
> +# that have both a non-empty username and a non-empty password.
> +self._logger.info("Getting existing repository from hosting 
> service.")
> +try:
> +self._runGit("clone", "--bare", target_url, "repository")
> +except subprocess.CalledProcessError as e:
> +self._logger.info(
> +"Unable to get existing repository from hosting service: "
> +"%s" % e)
> +return CodeImportWorkerExitCode.FAILURE
> +self._logger.info("Fetching remote repository.")
> +try:
> +self._runGit(
> +"remote", "add", "-f", "--mirror=fetch",
> +"mirror", self.source_details.url, cwd="repository")

More or less, with the addition of --prune.  Though good grief HEAD handling is 
horrible, but I've just added a comment about that for now.

> +except subprocess.CalledProcessError as e:
> +self._logger.info("Unable to fetch remote repository: %s" % e)
> +return CodeImportWorkerExitCode.FAILURE_INVALID
> +self._logger.info("Pushing repository to hosting service.")
> +try:
> +self._runGit("push", "--mirror", target_url, cwd="repository")
> +except subprocess.CalledProcessError as e:
> +self._logger.info("Unable to push to hosting service: %s" % e)
> +return CodeImportWorkerExitCode.FAILURE
> +return CodeImportWorkerExitCode.SUCCESS
> 
> === modified file 'lib/lp/services/config/schema-lazr.conf'
> --- lib/lp/services/config/schema-lazr.conf   2016-10-11 15:36:32 +
> +++ lib/lp/services/config/schema-lazr.conf   2016-10-11 15:36:32 +
> @@ -417,7 +421,7 @@
>  svn_revisions_import_limit: 500
>  
>  # Secret key for macaroons used to grant git push permission to workers.
> -macaroon_secret_key:
> 

[Launchpad-reviewers] [Merge] lp:~cjwatson/launchpad/codeimport-create-hosting into lp:launchpad

2016-10-12 Thread noreply
The proposal to merge lp:~cjwatson/launchpad/codeimport-create-hosting into 
lp:launchpad has been updated.

Status: Needs review => Merged

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/codeimport-create-hosting/+merge/308132
-- 
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


[Launchpad-reviewers] [Merge] lp:~cjwatson/launchpad/codeimport-worker-refactor into lp:launchpad

2016-10-12 Thread noreply
The proposal to merge lp:~cjwatson/launchpad/codeimport-worker-refactor into 
lp:launchpad has been updated.

Status: Needs review => Merged

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/codeimport-worker-refactor/+merge/308122
-- 
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


[Launchpad-reviewers] [Merge] lp:~cjwatson/launchpad/codeimport-source-details-refactor into lp:launchpad

2016-10-12 Thread noreply
The proposal to merge lp:~cjwatson/launchpad/codeimport-source-details-refactor 
into lp:launchpad has been updated.

Status: Needs review => Merged

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/codeimport-source-details-refactor/+merge/308117
-- 
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


[Launchpad-reviewers] [Merge] lp:~cjwatson/launchpad/codeimport-git-auth into lp:launchpad

2016-10-12 Thread noreply
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

2016-10-12 Thread Colin Watson


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-worker-refactor into lp:launchpad

2016-10-12 Thread William Grant
Review: Approve code



Diff comments:

> === modified file 'lib/lp/code/xmlrpc/codeimportscheduler.py'
> --- lib/lp/code/xmlrpc/codeimportscheduler.py 2016-10-11 13:46:57 +
> +++ lib/lp/code/xmlrpc/codeimportscheduler.py 2016-10-11 13:46:57 +
> @@ -69,10 +69,10 @@
>  job = self._getJob(job_id)
>  arguments = CodeImportSourceDetails.fromCodeImportJob(
>  job).asArguments()
> -branch = job.code_import.branch
> -branch_url = canonical_url(branch)
> -log_file_name = '%s.log' % branch.unique_name[1:].replace('/', '-')
> -return (arguments, branch_url, log_file_name)
> +target = job.code_import.target
> +target_url = canonical_url(target)
> +log_file_name = '%s.log' % target.unique_name[1:].replace('/', '-')
> +return (arguments, target_url, log_file_name)

One of these days I think every concept will be named "target".

>  
>  @return_fault
>  def _updateHeartbeat(self, job_id, log_tail):


-- 
https://code.launchpad.net/~cjwatson/launchpad/codeimport-worker-refactor/+merge/308122
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-create-hosting into lp:launchpad

2016-10-12 Thread William Grant
Review: Approve code



Diff comments:

> 
> === modified file 'lib/lp/code/model/tests/test_codeimport.py'
> --- lib/lp/code/model/tests/test_codeimport.py2016-10-03 17:00:56 
> +
> +++ lib/lp/code/model/tests/test_codeimport.py2016-10-11 14:42:21 
> +
> @@ -66,15 +67,22 @@
>  "supports_source_cvs": True,
>  "supports_source_svn": True,
>  "supports_source_bzr": True,
> +"needs_hosting_fixture": False,

needs_git_hosting_fixture surely?

>  }),
>  ("GitRepository", {
>  "target_rcs_type": TargetRevisionControlSystems.GIT,
>  "supports_source_cvs": False,
>  "supports_source_svn": False,
>  "supports_source_bzr": False,
> +"needs_hosting_fixture": True,
>  }),
>  ]
>  
> +def setUp(self, *args, **kwargs):
> +super(TestCodeImportBase, self).setUp(*args, **kwargs)
> +if self.needs_hosting_fixture:
> +self.hosting_fixture = self.useFixture(GitHostingFixture())
> +
>  
>  class TestCodeImportCreation(TestCodeImportBase):
>  """Test the creation of CodeImports."""


-- 
https://code.launchpad.net/~cjwatson/launchpad/codeimport-create-hosting/+merge/308132
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-source-details-refactor into lp:launchpad

2016-10-12 Thread William Grant
Review: Approve code


-- 
https://code.launchpad.net/~cjwatson/launchpad/codeimport-source-details-refactor/+merge/308117
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-worker into lp:launchpad

2016-10-12 Thread William Grant
Review: Approve code



Diff comments:

> === modified file 'configs/development/launchpad-lazr.conf'
> --- configs/development/launchpad-lazr.conf   2016-06-30 16:05:11 +
> +++ configs/development/launchpad-lazr.conf   2016-10-11 15:36:32 +
> @@ -55,6 +55,7 @@
>  
>  [codeimport]
>  bazaar_branch_store: file:///tmp/bazaar-branches
> +git_repository_store: https://git.launchpad.dev/

It's entirely confusing that git_repository_store and bazaar_branch_store are 
pretty radically different things. bazaar_branch_store is the intermediate tree 
on maple (formerly escudero), whereas git_repository_store is the actual 
hosting tree. The lack of puller for git imports is certainly a relief, but the 
chance of this not causing someone to faceplant is tiny.

A comment might be sufficient, but could we also get away with using the 
existing git_browse_root (GitRepository.git_https_url would be ideal, but 
returns None for private repos which are technically a possibility for imports)?

>  foreign_tree_store: file:///tmp/foreign-branches
>  
>  [codeimportdispatcher]
> 
> === modified file 'lib/lp/codehosting/codeimport/tests/test_worker.py'
> --- lib/lp/codehosting/codeimport/tests/test_worker.py2016-10-11 
> 15:36:32 +
> +++ lib/lp/codehosting/codeimport/tests/test_worker.py2016-10-11 
> 15:36:32 +
> @@ -1416,6 +1453,19 @@
>  str(code_import.branch.id), 'git',
>  'git://git.example.com/project.git']))
>  
> +def test_git_to_git_arguments(self):
> +self.pushConfig('codeimport', macaroon_secret_key='some-secret')
> +self.useFixture(GitHostingFixture())
> +code_import = self.factory.makeCodeImport(
> +git_repo_url="git://git.example.com/project.git",
> +target_rcs_type=TargetRevisionControlSystems.GIT)
> +self.assertArgumentsMatch(
> +code_import, MatchesListwise([
> +Equals(code_import.git_repository.unique_name),
> +Equals('git:git'), 
> Equals('git://git.example.com/project.git'),
> +CodeImportJobMacaroonVerifies(code_import.git_repository)]),
> +start_job=True)

A long time in the future in an editor far, far away: "Why would I want to 
start the job? Silly test author, I'll drop that pointlessness while I'm 
refactoring. Why is the macaroon verification silently failing now?"

> +
>  def test_cvs_arguments(self):
>  code_import = self.factory.makeCodeImport(
>  cvs_root=':pserver:f...@example.com/bar', cvs_module='bar')
> 
> === modified file 'lib/lp/codehosting/codeimport/worker.py'
> --- lib/lp/codehosting/codeimport/worker.py   2016-10-11 15:36:32 +
> +++ lib/lp/codehosting/codeimport/worker.py   2016-10-11 15:36:32 +
> @@ -269,36 +282,50 @@
>  of the information suitable for passing around on executables' command
>  lines.
>  
> -:ivar target_id: The id of the Bazaar branch associated with this code
> -import, used for locating the existing import and the foreign tree.
> +:ivar target_id: The id of the Bazaar branch or the path of the Git
> +repository associated with this code import, used for locating the
> +existing import and the foreign tree.
>  :ivar rcstype: 'cvs', 'git', 'bzr-svn', 'bzr' as appropriate.
> +:ivar target_rcstype: 'bzr' or 'git' as appropriate.
>  :ivar url: The branch URL if rcstype in ['bzr-svn', 'git', 'bzr'], None
>  otherwise.
>  :ivar cvs_root: The $CVSROOT if rcstype == 'cvs', None otherwise.
>  :ivar cvs_module: The CVS module if rcstype == 'cvs', None otherwise.
>  :ivar stacked_on_url: The URL of the branch that the associated branch
>  is stacked on, if any.
> +:ivar macaroon: A macaroon granting authority to push to the target
> +repository if target_rcstype == 'git', None otherwise.
>  """
>  
> -def __init__(self, target_id, rcstype, url=None, cvs_root=None,
> - cvs_module=None, stacked_on_url=None):
> +def __init__(self, target_id, rcstype, target_rcstype, url=None,
> + cvs_root=None, cvs_module=None, stacked_on_url=None,
> + macaroon=None):
>  self.target_id = target_id
>  self.rcstype = rcstype
> +self.target_rcstype = target_rcstype
>  self.url = url
>  self.cvs_root = cvs_root
>  self.cvs_module = cvs_module
>  self.stacked_on_url = stacked_on_url
> +self.macaroon = macaroon
>  
>  @classmethod
>  def fromArguments(cls, arguments):
>  """Convert command line-style arguments to an instance."""
> -target_id = int(arguments.pop(0))
> +target_id = arguments.pop(0)
>  rcstype = arguments.pop(0)
> +if ':' in rcstype:
> +rcstype, target_rcstype = rcstype.split(':', 1)
> +else:
> +target_rcstype = 'bzr'

XXX this for cleanup post-deployment?

>  if 

Re: [Launchpad-reviewers] [Merge] lp:~cjwatson/launchpad/codeimport-git-auth into lp:launchpad

2016-10-12 Thread William Grant
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