Browse Source

Merge pull request #8418 from glaubinix/f/github-authentication-behaviour

Git: fix authentication handling for private GitHub repositories
Jordi Boggiano 5 years ago
parent
commit
89f6b2c54c
2 changed files with 149 additions and 2 deletions
  1. 4 2
      src/Composer/Util/Git.php
  2. 145 0
      tests/Composer/Test/Util/GitTest.php

+ 4 - 2
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);

+ 145 - 0
tests/Composer/Test/Util/GitTest.php

@@ -0,0 +1,145 @@
+<?php
+
+namespace Composer\Test\Util;
+
+use Composer\Config;
+use Composer\IO\IOInterface;
+use Composer\Util\Filesystem;
+use Composer\Util\Git;
+use Composer\Util\ProcessExecutor;
+use PHPUnit\Framework\TestCase;
+
+class GitTest extends TestCase
+{
+    /** @var Git */
+    private $git;
+    /** @var IOInterface&\PHPUnit_Framework_MockObject_MockObject */
+    private $io;
+    /** @var Config&\PHPUnit_Framework_MockObject_MockObject */
+    private $config;
+    /** @var ProcessExecutor&\PHPUnit_Framework_MockObject_MockObject */
+    private $process;
+    /** @var Filesystem&\PHPUnit_Framework_MockObject_MockObject */
+    private $fs;
+
+    protected function setUp()
+    {
+        $this->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)),
+            ));
+    }
+}