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
parent
1d15910fa6
commit
90d1b6e08a
|
@ -743,6 +743,9 @@ The following options are supported:
|
|||
* **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
|
||||
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
|
||||
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
|
||||
|
@ -753,6 +756,9 @@ The following options are supported:
|
|||
rate limiting of their API.
|
||||
[Read more](articles/troubleshooting.md#api-rate-limit-and-oauth-tokens)
|
||||
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
|
||||
different directory if you want to.
|
||||
* **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>
|
||||
|
||||
Composer allows you to hook into various parts of the installation process
|
||||
|
|
|
@ -136,6 +136,15 @@
|
|||
"description": "A hash of domain name => github API oauth tokens, typically {\"github.com\":\"<token>\"}.",
|
||||
"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": {
|
||||
"type": "string",
|
||||
"description": "The location where all packages are installed, defaults to \"vendor\"."
|
||||
|
@ -182,7 +191,7 @@
|
|||
},
|
||||
"optimize-autoloader": {
|
||||
"type": "boolean",
|
||||
"description": "Always optimize when dumping the autoloader"
|
||||
"description": "Always optimize when dumping the autoloader."
|
||||
},
|
||||
"prepend-autoloader": {
|
||||
"type": "boolean",
|
||||
|
|
|
@ -53,6 +53,7 @@ class ConfigCommand extends Command
|
|||
->setDefinition(array(
|
||||
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('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('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'),
|
||||
|
@ -113,12 +114,24 @@ EOT
|
|||
$this->configFile = new JsonFile($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
|
||||
if ($input->getOption('global') && !$this->configFile->exists()) {
|
||||
touch($this->configFile->getPath());
|
||||
$this->configFile->write(array('config' => new \ArrayObject));
|
||||
@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()) {
|
||||
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;
|
||||
}
|
||||
|
||||
if (!$input->getOption('global')) {
|
||||
$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
|
||||
|
@ -236,16 +251,29 @@ EOT
|
|||
}
|
||||
|
||||
// handle github-oauth
|
||||
if (preg_match('/^github-oauth\.(.+)/', $settingKey, $matches)) {
|
||||
if (preg_match('/^(github-oauth|http-basic)\.(.+)/', $settingKey, $matches)) {
|
||||
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 ($matches[1] === 'github-oauth') {
|
||||
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); };
|
||||
|
@ -259,6 +287,16 @@ EOT
|
|||
function ($val) { return in_array($val, array('auto', 'source', 'dist'), true); },
|
||||
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),
|
||||
'vendor-dir' => array('is_string', function ($val) { return $val; }),
|
||||
'bin-dir' => array('is_string', function ($val) { return $val; }),
|
||||
|
|
|
@ -39,6 +39,10 @@ class Config
|
|||
'optimize-autoloader' => false,
|
||||
'prepend-autoloader' => true,
|
||||
'github-domains' => array('github.com'),
|
||||
'store-auths' => 'prompt',
|
||||
// valid keys without defaults (auth config stuff):
|
||||
// github-oauth
|
||||
// http-basic
|
||||
);
|
||||
|
||||
public static $defaultRepositories = array(
|
||||
|
@ -52,6 +56,7 @@ class Config
|
|||
private $config;
|
||||
private $repositories;
|
||||
private $configSource;
|
||||
private $authConfigSource;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
@ -70,6 +75,16 @@ class Config
|
|||
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)
|
||||
*
|
||||
|
@ -80,7 +95,7 @@ class Config
|
|||
// override defaults with given config
|
||||
if (!empty($config['config']) && is_array($config['config'])) {
|
||||
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);
|
||||
} else {
|
||||
$this->config[$key] = $val;
|
||||
|
|
|
@ -66,4 +66,11 @@ interface ConfigSourceInterface
|
|||
* @param string $name Name
|
||||
*/
|
||||
public function removeLink($type, $name);
|
||||
|
||||
/**
|
||||
* Gives a user-friendly name to this source (file path or so)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName();
|
||||
}
|
||||
|
|
|
@ -28,14 +28,28 @@ class JsonConfigSource implements ConfigSourceInterface
|
|||
*/
|
||||
private $file;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $authConfig;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param JsonFile $file
|
||||
*/
|
||||
public function __construct(JsonFile $file)
|
||||
public function __construct(JsonFile $file, $authConfig = false)
|
||||
{
|
||||
$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)
|
||||
{
|
||||
$this->manipulateJson('addConfigSetting', $name, $value, function (&$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)
|
||||
{
|
||||
$this->manipulateJson('removeConfigSetting', $name, function (&$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()) {
|
||||
$contents = file_get_contents($this->file->getPath());
|
||||
} elseif ($this->authConfig) {
|
||||
$contents = "{\n}\n";
|
||||
} else {
|
||||
$contents = "{\n \"config\": {\n }\n}\n";
|
||||
}
|
||||
|
||||
$manipulator = new JsonManipulator($contents);
|
||||
|
||||
$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
|
||||
if (call_user_func_array(array($manipulator, $method), $args)) {
|
||||
file_put_contents($this->file->getPath(), $manipulator->getContents());
|
||||
|
|
|
@ -106,12 +106,20 @@ class Factory
|
|||
// add dirs to the config
|
||||
$config->merge(array('config' => array('home' => $home, 'cache-dir' => $cacheDir)));
|
||||
|
||||
// load global config
|
||||
$file = new JsonFile($home.'/config.json');
|
||||
if ($file->exists()) {
|
||||
$config->merge($file->read());
|
||||
}
|
||||
$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
|
||||
$legacyPaths = array(
|
||||
'cache-repo-dir' => array('/cache' => '/http*', '/cache.svn' => '/*', '/cache.github' => '/*'),
|
||||
|
@ -147,26 +155,6 @@ class Factory
|
|||
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()
|
||||
{
|
||||
return trim(getenv('COMPOSER')) ?: './composer.json';
|
||||
|
@ -248,24 +236,19 @@ class Factory
|
|||
$localConfig = $file->read();
|
||||
}
|
||||
|
||||
// Configuration defaults
|
||||
// Load config and override with local config/auth config
|
||||
$config = static::createConfig();
|
||||
$config->merge($localConfig);
|
||||
$io->loadConfiguration($config);
|
||||
if (isset($composerFile)) {
|
||||
$localAuthFile = new JsonFile(dirname(realpath($composerFile)) . '/auth.json');
|
||||
if ($localAuthFile->exists()) {
|
||||
$config->merge(array('config' => $localAuthFile->read()));
|
||||
$config->setAuthConfigSource(new JsonConfigSource($localAuthFile, true));
|
||||
}
|
||||
}
|
||||
|
||||
// load separate auth config
|
||||
$authConfig = static::createAuthConfig();
|
||||
if ($basicauth = $authConfig->get('basic-auth')) {
|
||||
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');
|
||||
$binDir = $config->get('bin-dir');
|
||||
|
|
|
@ -68,5 +68,12 @@ abstract class BaseIO implements IOInterface
|
|||
$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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -83,7 +83,7 @@ class GitHub
|
|||
if ($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');
|
||||
while ($attemptCounter++ < 5) {
|
||||
try {
|
||||
|
@ -186,9 +186,8 @@ class GitHub
|
|||
$this->io->setAuthentication($originUrl, $contents['token'], 'x-oauth-basic');
|
||||
|
||||
// store value in user config
|
||||
$githubTokens = $this->config->get('github-oauth') ?: array();
|
||||
$githubTokens[$originUrl] = $contents['token'];
|
||||
$this->config->getConfigSource()->addConfigSetting('github-oauth', $githubTokens);
|
||||
$this->config->getConfigSource()->removeConfigSetting('github-oauth.'.$originUrl);
|
||||
$this->config->getAuthConfigSource()->addConfigSetting('github-oauth.'.$originUrl, $contents['token']);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ class RemoteFilesystem
|
|||
private $options;
|
||||
private $retryAuthFailure;
|
||||
private $lastHeaders;
|
||||
private $storeAuth;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
|
@ -249,7 +250,40 @@ class RemoteFilesystem
|
|||
if ($this->retry) {
|
||||
$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) {
|
||||
|
@ -364,6 +398,7 @@ class RemoteFilesystem
|
|||
$username = $this->io->ask(' Username: ');
|
||||
$password = $this->io->askAndHideAnswer(' Password: ');
|
||||
$this->io->setAuthentication($this->originUrl, $username, $password);
|
||||
$this->storeAuth = $this->config->get('store-auths');
|
||||
}
|
||||
|
||||
$this->retry = true;
|
||||
|
|
|
@ -94,7 +94,9 @@ class GitHubDriverTest extends \PHPUnit_Framework_TestCase
|
|||
->will($this->returnValue('{"master_branch": "test_master", "private": true}'));
|
||||
|
||||
$configSource = $this->getMock('Composer\Config\ConfigSourceInterface');
|
||||
$authConfigSource = $this->getMock('Composer\Config\ConfigSourceInterface');
|
||||
$this->config->setConfigSource($configSource);
|
||||
$this->config->setAuthConfigSource($authConfigSource);
|
||||
|
||||
$repoConfig = array(
|
||||
'url' => $repoUrl,
|
||||
|
|
Loading…
Reference in New Issue