Permalink
Browse files

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
  • Loading branch information...
1 parent 1d15910 commit 90d1b6e08a3135db3edef44e12478ee34f33f933 @Seldaek Seldaek committed May 27, 2014
View
@@ -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 @@ protected function configure()
->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 @@ protected function initialize(InputInterface $input, OutputInterface $output)
$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 @@ protected function execute(InputInterface $input, OutputInterface $output)
}
}
- 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 @@ protected function execute(InputInterface $input, OutputInterface $output)
}
// 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 (1 !== count($values)) {
- throw new \RuntimeException('Too many arguments, expected only one token');
+ 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 @@ protected function execute(InputInterface $input, OutputInterface $output)
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 @@ public function getConfigSource()
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 @@ public function merge(array $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 @@ public function addLink($type, $name, $value);
* @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();
}
@@ -29,13 +29,27 @@ 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 @@ public function removeRepository($name)
public function addConfigSetting($name, $value)
{
$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 @@ public function addConfigSetting($name, $value)
public function removeConfigSetting($name)
{
$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 @@ protected function manipulateJson($method, $args, $fallback)
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 @@ public static function createConfig()
// 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()));
@staabm
staabm May 27, 2014 Contributor

In verbose mode, it could be worth a line of debug-out that a global conf was read and from which location

@Seldaek
Seldaek May 27, 2014 Member

Yup I'll try to add that later.

@staabm
staabm May 31, 2014 Contributor

👍

+ }
+ $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 @@ public static function createConfig()
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,25 +236,20 @@ public function createComposer(IOInterface $io, $localConfig = null, $disablePlu
$localConfig = $file->read();
}
- // Configuration defaults
+ // Load config and override with local config/auth config
$config = static::createConfig();
$config->merge($localConfig);
- $io->loadConfiguration($config);
-
- // 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']);
+ if (isset($composerFile)) {
+ $localAuthFile = new JsonFile(dirname(realpath($composerFile)) . '/auth.json');
+ if ($localAuthFile->exists()) {
+ $config->merge(array('config' => $localAuthFile->read()));
@staabm
staabm May 27, 2014 Contributor

Same here: would be great to have a note on the cli about the auth file beeing used and from which location/path

+ $config->setAuthConfigSource(new JsonConfigSource($localAuthFile, true));
}
}
+ // load auth configs into the IO instance
+ $io->loadConfiguration($config);
+
$vendorDir = $config->get('vendor-dir');
$binDir = $config->get('bin-dir');
Oops, something went wrong.

2 comments on commit 90d1b6e

@renan
renan commented on 90d1b6e Jun 18, 2014

So I guess config.json won't be searched anymore for authentication? Only auth.json will be searched.

@Seldaek
Member

Technically auth.json is merely merged into the base config, so if you have stuff in config.json it should still work

Please sign in to comment.