1
0
Fork 0

Developed bitbucket-oauth functionality.

pull/5055/head
Paul Wenke 2016-03-06 22:05:00 -05:00
parent 3cea62b6ed
commit d5332a1b5c
8 changed files with 245 additions and 2 deletions

View File

@ -491,6 +491,17 @@ EOT
return; return;
} }
// handle bitbucket-oauth
if (preg_match('/^(bitbucket-oauth)\.(.+)/', $settingKey, $matches)) {
if (2 !== count($values)) {
throw new \RuntimeException('Excepted two arguments (consumer-key, consumer-secret), got '.count($values));
}
$this->configSource->removeConfigSetting($matches[1].'.'.$matches[2]);
$this->authConfigSource->addConfigSetting($matches[1].'.'.$matches[2], array('consumer-key' => $values[0], 'consumer-secret' => $values[1]));
return;
}
throw new \InvalidArgumentException('Setting '.$settingKey.' does not exist or is not supported by this command'); throw new \InvalidArgumentException('Setting '.$settingKey.' does not exist or is not supported by this command');
} }

View File

@ -27,6 +27,7 @@ class Config
'preferred-install' => 'auto', 'preferred-install' => 'auto',
'notify-on-install' => true, 'notify-on-install' => true,
'github-protocols' => array('https', 'ssh', 'git'), 'github-protocols' => array('https', 'ssh', 'git'),
'bitbucket-protocols' => array('https'),
'vendor-dir' => 'vendor', 'vendor-dir' => 'vendor',
'bin-dir' => '{$vendor-dir}/bin', 'bin-dir' => '{$vendor-dir}/bin',
'cache-dir' => '{$home}/cache', 'cache-dir' => '{$home}/cache',
@ -45,6 +46,8 @@ class Config
'classmap-authoritative' => false, 'classmap-authoritative' => false,
'prepend-autoloader' => true, 'prepend-autoloader' => true,
'github-domains' => array('github.com'), 'github-domains' => array('github.com'),
'bitbucket-domains' => array('bitbucket.org'),
'bitbucket-expose-hostname' => true,
'disable-tls' => false, 'disable-tls' => false,
'secure-http' => true, 'secure-http' => true,
'cafile' => null, 'cafile' => null,
@ -59,6 +62,7 @@ class Config
// github-oauth // github-oauth
// gitlab-oauth // gitlab-oauth
// http-basic // http-basic
// bitbucket-oauth
); );
public static $defaultRepositories = array( public static $defaultRepositories = array(

View File

@ -81,7 +81,7 @@ class JsonConfigSource implements ConfigSourceInterface
{ {
$authConfig = $this->authConfig; $authConfig = $this->authConfig;
$this->manipulateJson('addConfigSetting', $name, $value, function (&$config, $key, $val) use ($authConfig) { $this->manipulateJson('addConfigSetting', $name, $value, function (&$config, $key, $val) use ($authConfig) {
if (preg_match('{^(github-oauth|gitlab-oauth|http-basic|platform)\.}', $key)) { if (preg_match('{^(github-oauth|gitlab-oauth|http-basic|platform|bitbucket)\.}', $key)) {
list($key, $host) = explode('.', $key, 2); list($key, $host) = explode('.', $key, 2);
if ($authConfig) { if ($authConfig) {
$config[$key][$host] = $val; $config[$key][$host] = $val;

View File

@ -88,6 +88,7 @@ abstract class BaseIO implements IOInterface
$githubOauth = $config->get('github-oauth') ?: array(); $githubOauth = $config->get('github-oauth') ?: array();
$gitlabOauth = $config->get('gitlab-oauth') ?: array(); $gitlabOauth = $config->get('gitlab-oauth') ?: array();
$httpBasic = $config->get('http-basic') ?: array(); $httpBasic = $config->get('http-basic') ?: array();
$bitbucketOauth = $config->get('bitbucket-oauth') ?: array();
// reload oauth token from config if available // reload oauth token from config if available
foreach ($githubOauth as $domain => $token) { foreach ($githubOauth as $domain => $token) {
@ -106,6 +107,10 @@ abstract class BaseIO implements IOInterface
$this->checkAndSetAuthentication($domain, $cred['username'], $cred['password']); $this->checkAndSetAuthentication($domain, $cred['username'], $cred['password']);
} }
foreach ($bitbucketOauth as $domain => $cred) {
$this->checkAndSetAuthentication($domain, $cred['consumer-key'], $cred['consumer-secret']);
}
// setup process timeout // setup process timeout
ProcessExecutor::setTimeout((int) $config->get('process-timeout')); ProcessExecutor::setTimeout((int) $config->get('process-timeout'));
} }

View File

@ -0,0 +1,183 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Util;
use Composer\Factory;
use Composer\IO\IOInterface;
use Composer\Config;
use Composer\Downloader\TransportException;
/**
* @author Paul Wenke <wenke.paul@gmail.com>
*/
class Bitbucket
{
protected $io;
protected $config;
protected $process;
protected $remoteFilesystem;
protected $token = array();
/**
* Constructor.
*
* @param IOInterface $io The IO instance
* @param Config $config The composer configuration
* @param ProcessExecutor $process Process instance, injectable for mocking
* @param RemoteFilesystem $remoteFilesystem Remote Filesystem, injectable for mocking
*/
public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, RemoteFilesystem $remoteFilesystem = null)
{
$this->io = $io;
$this->config = $config;
$this->process = $process ?: new ProcessExecutor;
$this->remoteFilesystem = $remoteFilesystem ?: Factory::createRemoteFilesystem($this->io, $config);
}
/**
* @return array
*/
public function getToken()
{
return $this->token;
}
/**
* Attempts to authorize a Bitbucket domain via OAuth
*
* @param string $originUrl The host this Bitbucket instance is located at
* @return bool true on success
*/
public function authorizeOAuth($originUrl)
{
if (!in_array($originUrl, $this->config->get('bitbucket-domains'))) {
return false;
}
// if available use token from git config
if (0 === $this->process->execute('git config bitbucket.accesstoken', $output)) {
$this->io->setAuthentication($originUrl, trim($output), 'x-oauth-basic');
return true;
}
return false;
}
/**
* @param string $originUrl
* @return bool
*/
private function requestAccessToken($originUrl)
{
try {
$apiUrl = 'bitbucket.org/site/oauth2/access_token';
$json = $this->remoteFilesystem->getContents($originUrl, 'https://'.$apiUrl, false, array(
'retry-auth-failure' => false,
));
$this->token = json_decode($json, true);
} catch (TransportException $e) {
if (in_array($e->getCode(), array(403, 401))) {
$this->io->writeError('<error>Invalid consumer provided.</error>');
$this->io->writeError('You can also add it manually later by using "composer config bitbucket-oauth.bitbucket.org <consumerKey> <consumerSecret>"');
return false;
}
throw $e;
}
}
/**
* Authorizes a Bitbucket domain interactively via OAuth
*
* @param string $originUrl The host this Bitbucket instance is located at
* @param string $message The reason this authorization is required
* @throws \RuntimeException
* @throws TransportException|\Exception
* @return bool true on success
*/
public function authorizeOAuthInteractively($originUrl, $message = null)
{
if ($message) {
$this->io->writeError($message);
}
$note = 'Composer';
if ($this->config->get('bitbucket-expose-hostname') === true && 0 === $this->process->execute('hostname', $output)) {
$note .= ' on ' . trim($output);
}
$note .= ' ' . date('Y-m-d Hi');
$url = 'https://confluence.atlassian.com/bitbucket/oauth-on-bitbucket-cloud-238027431.html';
$this->io->writeError(sprintf('Follow the instructions on %s', $url));
$this->io->writeError(sprintf('to create a consumer. It will be stored in "%s" for future use by Composer.', $this->config->getAuthConfigSource()->getName()));
$consumerKey = trim($this->io->askAndHideAnswer('Consumer Key (hidden): '));
if (!$consumerKey) {
$this->io->writeError('<warning>No consumer key given, aborting.</warning>');
$this->io->writeError('You can also add it manually later by using "composer config bitbucket-oauth.bitbucket.org <consumer-key> <consumer-secret>"');
return false;
}
$consumerSecret = trim($this->io->askAndHideAnswer('Consumer Secret (hidden): '));
if (!$consumerSecret) {
$this->io->writeError('<warning>No consumer secret given, aborting.</warning>');
$this->io->writeError('You can also add it manually later by using "composer config bitbucket-oauth.bitbucket.org <consumer-key> <consumer-secret>"');
return false;
}
$this->io->setAuthentication($originUrl, $consumerKey, $consumerSecret);
$this->requestAccessToken($originUrl);
// store value in user config
$this->config->getConfigSource()->removeConfigSetting('bitbucket-oauth.'.$originUrl);
$consumer = array(
"consumer-key" => $consumerKey,
"consumer-secret" => $consumerSecret
);
$this->config->getAuthConfigSource()->addConfigSetting('bitbucket-oauth.'.$originUrl, $consumer);
$this->io->writeError('<info>Consumer stored successfully.</info>');
return true;
}
/**
* Retrieves an access token from Bitbucket.
*
* @param string $originUrl
* @param string $consumerKey
* @param string $consumerSecret
* @return array
*/
public function requestToken($originUrl, $consumerKey, $consumerSecret)
{
if (!empty($this->token)) {
return $this->token;
}
$this->io->setAuthentication($originUrl, $consumerKey, $consumerSecret);
$this->requestAccessToken($originUrl);
return $this->token;
}
}

View File

@ -108,7 +108,33 @@ class Git
if ($this->io->hasAuthentication($match[1])) { if ($this->io->hasAuthentication($match[1])) {
$auth = $this->io->getAuthentication($match[1]); $auth = $this->io->getAuthentication($match[1]);
$authUrl = 'https://'.rawurlencode($auth['username']) . ':' . rawurlencode($auth['password']) . '@'.$match[1].'/'.$match[2].'.git'; $authUrl = 'https://' . rawurlencode($auth['username']) . ':' . rawurlencode($auth['password']) . '@' . $match[1] . '/' . $match[2] . '.git';
$command = call_user_func($commandCallable, $authUrl);
if (0 === $this->process->execute($command, $ignoredOutput, $cwd)) {
return;
}
}
} elseif (preg_match('{^(?:https?|git)://'.self::getBitbucketDomainsRegex($this->config).'/(.*)\.git}', $url, $match)) { //bitbucket oauth
$bitbucketUtil = new Bitbucket($this->io, $this->config, $this->process);
if (!$this->io->hasAuthentication($match[1])) {
$message = 'Cloning failed using an ssh key for authentication, enter your Bitbucket credentials to access private repos';
if (!$bitbucketUtil->authorizeOAuth($match[1]) && $this->io->isInteractive()) {
$bitbucketUtil->authorizeOAuthInteractively($match[1], $message);
$token = $bitbucketUtil->getToken();
$this->io->setAuthentication($match[1], 'x-token-auth', $token['access_token']);
}
} else { //We're authenticating with a locally stored consumer.
$auth = $this->io->getAuthentication($match[1]);
$token = $bitbucketUtil->requestToken($match[1], $auth['username'], $auth['password']);
$this->io->setAuthentication($match[1], 'x-token-auth', $token['access_token']);
}
if ($this->io->hasAuthentication($match[1])) {
$auth = $this->io->getAuthentication($match[1]);
$authUrl = 'https://' . rawurlencode($auth['username']) . ':' . rawurlencode($auth['password']) . '@' . $match[1] . '/' . $match[2] . '.git';
$command = call_user_func($commandCallable, $authUrl); $command = call_user_func($commandCallable, $authUrl);
if (0 === $this->process->execute($command, $ignoredOutput, $cwd)) { if (0 === $this->process->execute($command, $ignoredOutput, $cwd)) {
return; return;
@ -214,6 +240,11 @@ class Git
return '('.implode('|', array_map('preg_quote', $config->get('github-domains'))).')'; return '('.implode('|', array_map('preg_quote', $config->get('github-domains'))).')';
} }
public static function getBitbucketDomainsRegex(Config $config)
{
return '('.implode('|', array_map('preg_quote', $config->get('bitbucket-domains'))).')';
}
public static function sanitizeUrl($message) public static function sanitizeUrl($message)
{ {
return preg_replace('{://([^@]+?):.+?@}', '://$1:***@', $message); return preg_replace('{://([^@]+?):.+?@}', '://$1:***@', $message);

View File

@ -672,6 +672,11 @@ class RemoteFilesystem
$authStr = base64_encode($auth['username'] . ':' . $auth['password']); $authStr = base64_encode($auth['username'] . ':' . $auth['password']);
$headers[] = 'Authorization: Basic '.$authStr; $headers[] = 'Authorization: Basic '.$authStr;
} }
if ('bitbucket.org' === $originUrl) {
$options['http']['method'] = 'POST';
$options['http']['content']['grant_type'] = 'client_credentials';
}
} }
if (isset($options['http']['header']) && !is_array($options['http']['header'])) { if (isset($options['http']['header']) && !is_array($options['http']['header'])) {

View File

@ -129,6 +129,10 @@ final class StreamContextFactory
$options['http']['header'] = self::fixHttpHeaderField($options['http']['header']); $options['http']['header'] = self::fixHttpHeaderField($options['http']['header']);
} }
if (isset($options['http']['content'])) {
$options['http']['content'] = http_build_query($options['http']['content']);
}
if (defined('HHVM_VERSION')) { if (defined('HHVM_VERSION')) {
$phpVersion = 'HHVM ' . HHVM_VERSION; $phpVersion = 'HHVM ' . HHVM_VERSION;
} else { } else {