From b8471156175c2133df61432b4455b709cbfc33f9 Mon Sep 17 00:00:00 2001 From: Stephan Vock Date: Thu, 7 Nov 2019 00:43:19 +0100 Subject: [PATCH] Git: fix authentication handling for private GitHub repositories --- src/Composer/Util/Git.php | 6 +- tests/Composer/Test/Util/GitTest.php | 145 +++++++++++++++++++++++++++ 2 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 tests/Composer/Test/Util/GitTest.php diff --git a/src/Composer/Util/Git.php b/src/Composer/Util/Git.php index 15c46d080..81c685011 100644 --- a/src/Composer/Util/Git.php +++ b/src/Composer/Util/Git.php @@ -85,7 +85,9 @@ class Git } // failed to checkout, first check git accessibility - $this->throwException('Failed to clone ' . $url . ' via ' . implode(', ', $protocols) . ' protocols, aborting.' . "\n\n" . implode("\n", $messages), $url); + if (!$this->io->hasAuthentication($match[1]) && !$this->io->isInteractive()) { + $this->throwException('Failed to clone ' . $url . ' via ' . implode(', ', $protocols) . ' protocols, aborting.' . "\n\n" . implode("\n", $messages), $url); + } } // if we have a private github url and the ssh protocol is disabled then we skip it and directly fallback to https @@ -97,7 +99,7 @@ class Git if ($bypassSshForGitHub || 0 !== $this->process->execute($command, $ignoredOutput, $cwd)) { // private github repository without ssh key access, try https with auth if (preg_match('{^git@' . self::getGitHubDomainsRegex($this->config) . ':(.+?)\.git$}i', $url, $match) - || preg_match('{^(https?)://' . self::getGitHubDomainsRegex($this->config) . '/(.*)}', $url, $match) + || preg_match('{^https?://' . self::getGitHubDomainsRegex($this->config) . '/(.*)}', $url, $match) ) { if (!$this->io->hasAuthentication($match[1])) { $gitHubUtil = new GitHub($this->io, $this->config, $this->process); diff --git a/tests/Composer/Test/Util/GitTest.php b/tests/Composer/Test/Util/GitTest.php new file mode 100644 index 000000000..7aa66be12 --- /dev/null +++ b/tests/Composer/Test/Util/GitTest.php @@ -0,0 +1,145 @@ +io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); + $this->config = $this->getMockBuilder('Composer\Config')->disableOriginalConstructor()->getMock(); + $this->process = $this->getMockBuilder('Composer\Util\ProcessExecutor')->disableOriginalConstructor()->getMock(); + $this->fs = $this->getMockBuilder('Composer\Util\Filesystem')->disableOriginalConstructor()->getMock(); + $this->git = new Git($this->io, $this->config, $this->process, $this->fs); + } + + /** + * @dataProvider publicGithubNoCredentialsProvider + */ + public function testRunCommandPublicGitHubRepositoryNotInitialClone($protocol, $expectedUrl) + { + $that = $this; + $commandCallable = function ($url) use ($that, $expectedUrl) { + $that->assertSame($expectedUrl, $url); + + return 'git command'; + }; + + $this->mockConfig($protocol); + + $this->process + ->expects($this->once()) + ->method('execute') + ->with($this->equalTo('git command')) + ->willReturn(0); + + $this->git->runCommand($commandCallable, 'https://github.com/acme/repo', null, true); + } + + public function publicGithubNoCredentialsProvider() + { + return array( + array('ssh', 'git@github.com:acme/repo'), + array('https', 'https://github.com/acme/repo'), + ); + } + + /** + * @expectedException \RuntimeException + */ + public function testRunCommandPrivateGitHubRepositoryNotInitialCloneNotInteractiveWithoutAuthentication() + { + $that = $this; + $commandCallable = function ($url) use ($that) { + $that->assertSame('https://github.com/acme/repo', $url); + + return 'git command'; + }; + + $this->mockConfig('https'); + + $this->process + ->method('execute') + ->willReturnMap(array( + array('git command', null, null, 1), + array('git --version', null, null, 0), + )); + + $this->git->runCommand($commandCallable, 'https://github.com/acme/repo', null, true); + } + + /** + * @dataProvider privateGithubWithCredentialsProvider + */ + public function testRunCommandPrivateGitHubRepositoryNotInitialCloneNotInteractiveWithAuthentication($gitUrl, $protocol, $gitHubToken, $expectedUrl) + { + $commandCallable = function ($url) use ($expectedUrl) { + if ($url !== $expectedUrl) { + return 'git command failing'; + } + + return 'git command ok'; + }; + + $this->mockConfig($protocol); + + $this->process + ->method('execute') + ->willReturnMap(array( + array('git command failing', null, null, 1), + array('git command ok', null, null, 0), + )); + + $this->io + ->method('isInteractive') + ->willReturn(false); + + $this->io + ->method('hasAuthentication') + ->with($this->equalTo('github.com')) + ->willReturn(true); + + $this->io + ->method('getAuthentication') + ->with($this->equalTo('github.com')) + ->willReturn(array('username' => 'token', 'password' => $gitHubToken)); + + $this->git->runCommand($commandCallable, $gitUrl, null, true); + } + + public function privateGithubWithCredentialsProvider() + { + return array( + array('git@github.com:acme/repo.git', 'ssh', 'MY_GITHUB_TOKEN', 'https://token:MY_GITHUB_TOKEN@github.com/acme/repo.git'), + array('https://github.com/acme/repo', 'https', 'MY_GITHUB_TOKEN', 'https://token:MY_GITHUB_TOKEN@github.com/acme/repo.git'), + ); + } + + private function mockConfig($protocol) + { + $this->config + ->method('get') + ->willReturnMap(array( + array('github-domains', 0, array('github.com')), + array('github-protocols', 0, array($protocol)), + )); + } +}