1
0
Fork 0

Rename basic-auth to http-basic, add docs/schema/config support, add local auth file support, add storage to auth.json, add store-auths config option, refs #1862

pull/2353/merge
Jordi Boggiano 2014-05-27 13:50:47 +02:00
parent 1d15910fa6
commit 90d1b6e08a
11 changed files with 202 additions and 50 deletions

View File

@ -743,6 +743,9 @@ The following options are supported:
* **preferred-install:** Defaults to `auto` and can be any of `source`, `dist` or * **preferred-install:** Defaults to `auto` and can be any of `source`, `dist` or
`auto`. This option allows you to set the install method Composer will prefer to `auto`. This option allows you to set the install method Composer will prefer to
use. use.
* **store-auths:** What to do after prompting for authentication, one of:
`true` (always store), `false` (do not store) and `"prompt"` (ask every
time), defaults to `"prompt"`.
* **github-protocols:** Defaults to `["git", "https", "ssh"]`. A list of protocols to * **github-protocols:** Defaults to `["git", "https", "ssh"]`. A list of protocols to
use when cloning from github.com, in priority order. You can reconfigure it to use when cloning from github.com, in priority order. You can reconfigure it to
for example prioritize the https protocol if you are behind a proxy or have somehow for example prioritize the https protocol if you are behind a proxy or have somehow
@ -753,6 +756,9 @@ The following options are supported:
rate limiting of their API. rate limiting of their API.
[Read more](articles/troubleshooting.md#api-rate-limit-and-oauth-tokens) [Read more](articles/troubleshooting.md#api-rate-limit-and-oauth-tokens)
on how to get an OAuth token for GitHub. on how to get an OAuth token for GitHub.
* **http-basic:** A list of domain names and username/passwords to authenticate
against them. For example using
`{"example.org": {"username": "alice", "password": "foo"}` as the value of this option will let composer authenticate against example.org.
* **vendor-dir:** Defaults to `vendor`. You can install dependencies into a * **vendor-dir:** Defaults to `vendor`. You can install dependencies into a
different directory if you want to. different directory if you want to.
* **bin-dir:** Defaults to `vendor/bin`. If a project includes binaries, they * **bin-dir:** Defaults to `vendor/bin`. If a project includes binaries, they
@ -802,6 +808,11 @@ Example:
} }
``` ```
> **Note:** Authentication-related config options like `http-basic` and
> `github-oauth` can also be specified inside a `auth.json` file that goes
> besides your `composer.json`. That way you can gitignore it and every
> developer can place their own credentials in there.
### scripts <span>(root-only)</span> ### scripts <span>(root-only)</span>
Composer allows you to hook into various parts of the installation process Composer allows you to hook into various parts of the installation process

View File

@ -136,6 +136,15 @@
"description": "A hash of domain name => github API oauth tokens, typically {\"github.com\":\"<token>\"}.", "description": "A hash of domain name => github API oauth tokens, typically {\"github.com\":\"<token>\"}.",
"additionalProperties": true "additionalProperties": true
}, },
"http-basic": {
"type": "object",
"description": "A hash of domain name => {\"username\": \"...\", \"password\": \"...\"}.",
"additionalProperties": true
},
"store-auths": {
"type": ["string", "boolean"],
"description": "What to do after prompting for authentication, one of: true (store), false (do not store) or \"prompt\" (ask every time), defaults to prompt."
},
"vendor-dir": { "vendor-dir": {
"type": "string", "type": "string",
"description": "The location where all packages are installed, defaults to \"vendor\"." "description": "The location where all packages are installed, defaults to \"vendor\"."
@ -182,7 +191,7 @@
}, },
"optimize-autoloader": { "optimize-autoloader": {
"type": "boolean", "type": "boolean",
"description": "Always optimize when dumping the autoloader" "description": "Always optimize when dumping the autoloader."
}, },
"prepend-autoloader": { "prepend-autoloader": {
"type": "boolean", "type": "boolean",

View File

@ -53,6 +53,7 @@ class ConfigCommand extends Command
->setDefinition(array( ->setDefinition(array(
new InputOption('global', 'g', InputOption::VALUE_NONE, 'Apply command to the global config file'), new InputOption('global', 'g', InputOption::VALUE_NONE, 'Apply command to the global config file'),
new InputOption('editor', 'e', InputOption::VALUE_NONE, 'Open editor'), new InputOption('editor', 'e', InputOption::VALUE_NONE, 'Open editor'),
new InputOption('auth', 'a', InputOption::VALUE_NONE, 'Affect auth config file (only used for --editor)'),
new InputOption('unset', null, InputOption::VALUE_NONE, 'Unset the given setting-key'), new InputOption('unset', null, InputOption::VALUE_NONE, 'Unset the given setting-key'),
new InputOption('list', 'l', InputOption::VALUE_NONE, 'List configuration settings'), new InputOption('list', 'l', InputOption::VALUE_NONE, 'List configuration settings'),
new InputOption('file', 'f', InputOption::VALUE_REQUIRED, 'If you want to choose a different composer.json or config.json', 'composer.json'), new InputOption('file', 'f', InputOption::VALUE_REQUIRED, 'If you want to choose a different composer.json or config.json', 'composer.json'),
@ -113,12 +114,24 @@ EOT
$this->configFile = new JsonFile($configFile); $this->configFile = new JsonFile($configFile);
$this->configSource = new JsonConfigSource($this->configFile); $this->configSource = new JsonConfigSource($this->configFile);
$authConfigFile = $input->getOption('global')
? ($this->config->get('home') . '/auth.json')
: dirname(realpath($input->getOption('file'))) . '/auth.json';
$this->authConfigFile = new JsonFile($authConfigFile);
$this->authConfigSource = new JsonConfigSource($this->authConfigFile, true);
// initialize the global file if it's not there // initialize the global file if it's not there
if ($input->getOption('global') && !$this->configFile->exists()) { if ($input->getOption('global') && !$this->configFile->exists()) {
touch($this->configFile->getPath()); touch($this->configFile->getPath());
$this->configFile->write(array('config' => new \ArrayObject)); $this->configFile->write(array('config' => new \ArrayObject));
@chmod($this->configFile->getPath(), 0600); @chmod($this->configFile->getPath(), 0600);
} }
if ($input->getOption('global') && !$this->authConfigFile->exists()) {
touch($this->authConfigFile->getPath());
$this->authConfigFile->write(array('http-basic' => new \ArrayObject, 'github-oauth' => new \ArrayObject));
@chmod($this->authConfigFile->getPath(), 0600);
}
if (!$this->configFile->exists()) { if (!$this->configFile->exists()) {
throw new \RuntimeException('No composer.json found in the current directory'); throw new \RuntimeException('No composer.json found in the current directory');
@ -146,13 +159,15 @@ EOT
} }
} }
system($editor . ' ' . $this->configFile->getPath() . (defined('PHP_WINDOWS_VERSION_BUILD') ? '': ' > `tty`')); $file = $input->getOption('auth') ? $this->authConfigFile->getPath() : $this->configFile->getPath();
system($editor . ' ' . $file . (defined('PHP_WINDOWS_VERSION_BUILD') ? '': ' > `tty`'));
return 0; return 0;
} }
if (!$input->getOption('global')) { if (!$input->getOption('global')) {
$this->config->merge($this->configFile->read()); $this->config->merge($this->configFile->read());
$this->config->merge(array('config' => $this->authConfigFile->exists() ? $this->authConfigFile->read() : array()));
} }
// List the configuration of the file settings // List the configuration of the file settings
@ -236,16 +251,29 @@ EOT
} }
// handle github-oauth // handle github-oauth
if (preg_match('/^github-oauth\.(.+)/', $settingKey, $matches)) { if (preg_match('/^(github-oauth|http-basic)\.(.+)/', $settingKey, $matches)) {
if ($input->getOption('unset')) { if ($input->getOption('unset')) {
return $this->configSource->removeConfigSetting('github-oauth.'.$matches[1]); $this->authConfigSource->removeConfigSetting($matches[1].'.'.$matches[2]);
$this->configSource->removeConfigSetting($matches[1].'.'.$matches[2]);
return;
} }
if (1 !== count($values)) { if ($matches[1] === 'github-oauth') {
throw new \RuntimeException('Too many arguments, expected only one token'); if (1 !== count($values)) {
throw new \RuntimeException('Too many arguments, expected only one token');
}
$this->configSource->removeConfigSetting($matches[1].'.'.$matches[2]);
$this->authConfigSource->addConfigSetting($matches[1].'.'.$matches[2], $values[0]);
} elseif ($matches[1] === 'http-basic') {
if (2 !== count($values)) {
throw new \RuntimeException('Expected two arguments (username, password), got '.count($values));
}
$this->configSource->removeConfigSetting($matches[1].'.'.$matches[2]);
$this->authConfigSource->addConfigSetting($matches[1].'.'.$matches[2], array('username' => $values[0], 'password' => $values[1]));
} }
return $this->configSource->addConfigSetting('github-oauth.'.$matches[1], $values[0]); return;
} }
$booleanValidator = function ($val) { return in_array($val, array('true', 'false', '1', '0'), true); }; $booleanValidator = function ($val) { return in_array($val, array('true', 'false', '1', '0'), true); };
@ -259,6 +287,16 @@ EOT
function ($val) { return in_array($val, array('auto', 'source', 'dist'), true); }, function ($val) { return in_array($val, array('auto', 'source', 'dist'), true); },
function ($val) { return $val; } function ($val) { return $val; }
), ),
'store-auths' => array(
function ($val) { return in_array($val, array('true', 'false', 'prompt'), true); },
function ($val) {
if ('prompt' === $val) {
return 'prompt';
}
return $val !== 'false' && (bool) $val;
}
),
'notify-on-install' => array($booleanValidator, $booleanNormalizer), 'notify-on-install' => array($booleanValidator, $booleanNormalizer),
'vendor-dir' => array('is_string', function ($val) { return $val; }), 'vendor-dir' => array('is_string', function ($val) { return $val; }),
'bin-dir' => array('is_string', function ($val) { return $val; }), 'bin-dir' => array('is_string', function ($val) { return $val; }),

View File

@ -39,6 +39,10 @@ class Config
'optimize-autoloader' => false, 'optimize-autoloader' => false,
'prepend-autoloader' => true, 'prepend-autoloader' => true,
'github-domains' => array('github.com'), 'github-domains' => array('github.com'),
'store-auths' => 'prompt',
// valid keys without defaults (auth config stuff):
// github-oauth
// http-basic
); );
public static $defaultRepositories = array( public static $defaultRepositories = array(
@ -52,6 +56,7 @@ class Config
private $config; private $config;
private $repositories; private $repositories;
private $configSource; private $configSource;
private $authConfigSource;
public function __construct() public function __construct()
{ {
@ -70,6 +75,16 @@ class Config
return $this->configSource; return $this->configSource;
} }
public function setAuthConfigSource(ConfigSourceInterface $source)
{
$this->authConfigSource = $source;
}
public function getAuthConfigSource()
{
return $this->authConfigSource;
}
/** /**
* Merges new config values with the existing ones (overriding) * Merges new config values with the existing ones (overriding)
* *
@ -80,7 +95,7 @@ class Config
// override defaults with given config // override defaults with given config
if (!empty($config['config']) && is_array($config['config'])) { if (!empty($config['config']) && is_array($config['config'])) {
foreach ($config['config'] as $key => $val) { foreach ($config['config'] as $key => $val) {
if (in_array($key, array('github-oauth')) && isset($this->config[$key])) { if (in_array($key, array('github-oauth', 'http-basic')) && isset($this->config[$key])) {
$this->config[$key] = array_merge($this->config[$key], $val); $this->config[$key] = array_merge($this->config[$key], $val);
} else { } else {
$this->config[$key] = $val; $this->config[$key] = $val;

View File

@ -66,4 +66,11 @@ interface ConfigSourceInterface
* @param string $name Name * @param string $name Name
*/ */
public function removeLink($type, $name); public function removeLink($type, $name);
/**
* Gives a user-friendly name to this source (file path or so)
*
* @return string
*/
public function getName();
} }

View File

@ -28,14 +28,28 @@ class JsonConfigSource implements ConfigSourceInterface
*/ */
private $file; private $file;
/**
* @var bool
*/
private $authConfig;
/** /**
* Constructor * Constructor
* *
* @param JsonFile $file * @param JsonFile $file
*/ */
public function __construct(JsonFile $file) public function __construct(JsonFile $file, $authConfig = false)
{ {
$this->file = $file; $this->file = $file;
$this->authConfig = $authConfig;
}
/**
* {@inheritdoc}
*/
public function getName()
{
return $this->file->getPath();
} }
/** /**
@ -64,7 +78,16 @@ class JsonConfigSource implements ConfigSourceInterface
public function addConfigSetting($name, $value) public function addConfigSetting($name, $value)
{ {
$this->manipulateJson('addConfigSetting', $name, $value, function (&$config, $key, $val) { $this->manipulateJson('addConfigSetting', $name, $value, function (&$config, $key, $val) {
$config['config'][$key] = $val; if ($key === 'github-oauth' || $key === 'http-basic') {
list($key, $host) = explode('.', $key, 2);
if ($this->authConfig) {
$config[$key][$host] = $val;
} else {
$config['config'][$key][$host] = $val;
}
} else {
$config['config'][$key] = $val;
}
}); });
} }
@ -74,7 +97,16 @@ class JsonConfigSource implements ConfigSourceInterface
public function removeConfigSetting($name) public function removeConfigSetting($name)
{ {
$this->manipulateJson('removeConfigSetting', $name, function (&$config, $key) { $this->manipulateJson('removeConfigSetting', $name, function (&$config, $key) {
unset($config['config'][$key]); if ($key === 'github-oauth' || $key === 'http-basic') {
list($key, $host) = explode('.', $key, 2);
if ($this->authConfig) {
unset($config[$key][$host]);
} else {
unset($config['config'][$key][$host]);
}
} else {
unset($config['config'][$key]);
}
}); });
} }
@ -107,13 +139,27 @@ class JsonConfigSource implements ConfigSourceInterface
if ($this->file->exists()) { if ($this->file->exists()) {
$contents = file_get_contents($this->file->getPath()); $contents = file_get_contents($this->file->getPath());
} elseif ($this->authConfig) {
$contents = "{\n}\n";
} else { } else {
$contents = "{\n \"config\": {\n }\n}\n"; $contents = "{\n \"config\": {\n }\n}\n";
} }
$manipulator = new JsonManipulator($contents); $manipulator = new JsonManipulator($contents);
$newFile = !$this->file->exists(); $newFile = !$this->file->exists();
// override manipulator method for auth config files
if ($this->authConfig && $method === 'addConfigSetting') {
$method = 'addSubNode';
list($mainNode, $name) = explode('.', $args[0], 2);
$args = array($mainNode, $name, $args[1]);
} elseif ($this->authConfig && $method === 'removeConfigSetting') {
$method = 'removeSubNode';
list($mainNode, $name) = explode('.', $args[0], 2);
$args = array($mainNode, $name);
}
// try to update cleanly // try to update cleanly
if (call_user_func_array(array($manipulator, $method), $args)) { if (call_user_func_array(array($manipulator, $method), $args)) {
file_put_contents($this->file->getPath(), $manipulator->getContents()); file_put_contents($this->file->getPath(), $manipulator->getContents());

View File

@ -106,12 +106,20 @@ class Factory
// add dirs to the config // add dirs to the config
$config->merge(array('config' => array('home' => $home, 'cache-dir' => $cacheDir))); $config->merge(array('config' => array('home' => $home, 'cache-dir' => $cacheDir)));
// load global config
$file = new JsonFile($home.'/config.json'); $file = new JsonFile($home.'/config.json');
if ($file->exists()) { if ($file->exists()) {
$config->merge($file->read()); $config->merge($file->read());
} }
$config->setConfigSource(new JsonConfigSource($file)); $config->setConfigSource(new JsonConfigSource($file));
// load global auth file
$file = new JsonFile($config->get('home').'/auth.json');
if ($file->exists()) {
$config->merge(array('config' => $file->read()));
}
$config->setAuthConfigSource(new JsonConfigSource($file, true));
// move old cache dirs to the new locations // move old cache dirs to the new locations
$legacyPaths = array( $legacyPaths = array(
'cache-repo-dir' => array('/cache' => '/http*', '/cache.svn' => '/*', '/cache.github' => '/*'), 'cache-repo-dir' => array('/cache' => '/http*', '/cache.svn' => '/*', '/cache.github' => '/*'),
@ -147,26 +155,6 @@ class Factory
return $config; return $config;
} }
/**
* @return Config
*/
protected static function createAuthConfig()
{
$home = self::getHomeDir();
$config = new Config();
// add dirs to the config
$config->merge(array('config' => array('home' => $home)));
$file = new JsonFile($home.'/auth.json');
if ($file->exists()) {
$config->merge($file->read());
}
$config->setConfigSource(new JsonConfigSource($file));
return $config;
}
public static function getComposerFile() public static function getComposerFile()
{ {
return trim(getenv('COMPOSER')) ?: './composer.json'; return trim(getenv('COMPOSER')) ?: './composer.json';
@ -248,25 +236,20 @@ class Factory
$localConfig = $file->read(); $localConfig = $file->read();
} }
// Configuration defaults // Load config and override with local config/auth config
$config = static::createConfig(); $config = static::createConfig();
$config->merge($localConfig); $config->merge($localConfig);
$io->loadConfiguration($config); if (isset($composerFile)) {
$localAuthFile = new JsonFile(dirname(realpath($composerFile)) . '/auth.json');
// load separate auth config if ($localAuthFile->exists()) {
$authConfig = static::createAuthConfig(); $config->merge(array('config' => $localAuthFile->read()));
if ($basicauth = $authConfig->get('basic-auth')) { $config->setAuthConfigSource(new JsonConfigSource($localAuthFile, true));
foreach ($basicauth as $domain => $credentials) {
if(!isset($credentials['username'])) {
continue;
}
if(!isset($credentials['password'])) {
$credentials['password'] = null;
}
$io->setAuthentication($domain, $credentials['username'], $credentials['password']);
} }
} }
// load auth configs into the IO instance
$io->loadConfiguration($config);
$vendorDir = $config->get('vendor-dir'); $vendorDir = $config->get('vendor-dir');
$binDir = $config->get('bin-dir'); $binDir = $config->get('bin-dir');

View File

@ -68,5 +68,12 @@ abstract class BaseIO implements IOInterface
$this->setAuthentication($domain, $token, 'x-oauth-basic'); $this->setAuthentication($domain, $token, 'x-oauth-basic');
} }
} }
// reload http basic credentials from config if available
if ($creds = $config->get('http-basic')) {
foreach ($creds as $domain => $cred) {
$this->setAuthentication($domain, $cred['username'], $cred['password']);
}
}
} }
} }

View File

@ -83,7 +83,7 @@ class GitHub
if ($message) { if ($message) {
$this->io->write($message); $this->io->write($message);
} }
$this->io->write('The credentials will be swapped for an OAuth token stored in '.$this->config->get('home').'/config.json, your password will not be stored'); $this->io->write('The credentials will be swapped for an OAuth token stored in '.$this->config->getAuthConfigSource()->getName().', your password will not be stored');
$this->io->write('To revoke access to this token you can visit https://github.com/settings/applications'); $this->io->write('To revoke access to this token you can visit https://github.com/settings/applications');
while ($attemptCounter++ < 5) { while ($attemptCounter++ < 5) {
try { try {
@ -186,9 +186,8 @@ class GitHub
$this->io->setAuthentication($originUrl, $contents['token'], 'x-oauth-basic'); $this->io->setAuthentication($originUrl, $contents['token'], 'x-oauth-basic');
// store value in user config // store value in user config
$githubTokens = $this->config->get('github-oauth') ?: array(); $this->config->getConfigSource()->removeConfigSetting('github-oauth.'.$originUrl);
$githubTokens[$originUrl] = $contents['token']; $this->config->getAuthConfigSource()->addConfigSetting('github-oauth.'.$originUrl, $contents['token']);
$this->config->getConfigSource()->addConfigSetting('github-oauth', $githubTokens);
return true; return true;
} }

View File

@ -37,6 +37,7 @@ class RemoteFilesystem
private $options; private $options;
private $retryAuthFailure; private $retryAuthFailure;
private $lastHeaders; private $lastHeaders;
private $storeAuth;
/** /**
* Constructor. * Constructor.
@ -249,7 +250,40 @@ class RemoteFilesystem
if ($this->retry) { if ($this->retry) {
$this->retry = false; $this->retry = false;
return $this->get($this->originUrl, $this->fileUrl, $additionalOptions, $this->fileName, $this->progress); $result = $this->get($this->originUrl, $this->fileUrl, $additionalOptions, $this->fileName, $this->progress);
$store = false;
$configSource = $this->config->getAuthConfigSource();
if ($this->storeAuth === true) {
$store = $configSource;
} elseif ($this->storeAuth === 'prompt') {
$answer = $this->io->askAndValidate(
'Do you want to store credentials for '.$this->originUrl.' in '.$configSource->getName().' ? [Yn] ',
function ($value) {
$input = strtolower(substr(trim($value), 0, 1));
if (in_array($input, array('y','n'))) {
return $input;
}
throw new \RuntimeException('Please answer (y)es or (n)o');
},
false,
'y'
);
if ($answer === 'y') {
$store = $configSource;
}
}
if ($store) {
$store->addConfigSetting(
'http-basic.'.$this->originUrl,
$this->io->getAuthentication($this->originUrl)
);
}
$this->storeAuth = false;
return $result;
} }
if (false === $result) { if (false === $result) {
@ -364,6 +398,7 @@ class RemoteFilesystem
$username = $this->io->ask(' Username: '); $username = $this->io->ask(' Username: ');
$password = $this->io->askAndHideAnswer(' Password: '); $password = $this->io->askAndHideAnswer(' Password: ');
$this->io->setAuthentication($this->originUrl, $username, $password); $this->io->setAuthentication($this->originUrl, $username, $password);
$this->storeAuth = $this->config->get('store-auths');
} }
$this->retry = true; $this->retry = true;

View File

@ -94,7 +94,9 @@ class GitHubDriverTest extends \PHPUnit_Framework_TestCase
->will($this->returnValue('{"master_branch": "test_master", "private": true}')); ->will($this->returnValue('{"master_branch": "test_master", "private": true}'));
$configSource = $this->getMock('Composer\Config\ConfigSourceInterface'); $configSource = $this->getMock('Composer\Config\ConfigSourceInterface');
$authConfigSource = $this->getMock('Composer\Config\ConfigSourceInterface');
$this->config->setConfigSource($configSource); $this->config->setConfigSource($configSource);
$this->config->setAuthConfigSource($authConfigSource);
$repoConfig = array( $repoConfig = array(
'url' => $repoUrl, 'url' => $repoUrl,