Skip to content

Commit

Permalink
Rename basic-auth to http-basic, add docs/schema/config support, add …
Browse files Browse the repository at this point in the history
…local auth file support, add storage to auth.json, add store-auths config option, refs #1862
  • Loading branch information
Seldaek committed May 27, 2014
1 parent 1d15910 commit 90d1b6e
Show file tree
Hide file tree
Showing 11 changed files with 202 additions and 50 deletions.
11 changes: 11 additions & 0 deletions doc/04-schema.md
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion res/composer-schema.json
Expand Up @@ -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\"."
Expand Down Expand Up @@ -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",
Expand Down
50 changes: 44 additions & 6 deletions src/Composer/Command/ConfigCommand.php
Expand Up @@ -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'),
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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); };
Expand All @@ -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; }),
Expand Down
17 changes: 16 additions & 1 deletion src/Composer/Config.php
Expand Up @@ -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(
Expand All @@ -52,6 +56,7 @@ class Config
private $config;
private $repositories;
private $configSource;
private $authConfigSource;

public function __construct()
{
Expand All @@ -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)
*
Expand All @@ -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;
Expand Down
7 changes: 7 additions & 0 deletions src/Composer/Config/ConfigSourceInterface.php
Expand Up @@ -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();
}
52 changes: 49 additions & 3 deletions src/Composer/Config/JsonConfigSource.php
Expand Up @@ -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();
}

/**
Expand Down Expand Up @@ -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;
}
});
}

Expand All @@ -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]);
}
});
}

Expand Down Expand Up @@ -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());
Expand Down
51 changes: 17 additions & 34 deletions src/Composer/Factory.php
Expand Up @@ -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()));

This comment has been minimized.

Copy link
@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

This comment has been minimized.

Copy link
@Seldaek

Seldaek May 27, 2014

Author Member

Yup I'll try to add that later.

This comment has been minimized.

Copy link
@Seldaek

Seldaek May 31, 2014

Author Member

Done in 959cc4d

This comment has been minimized.

Copy link
@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' => '/*'),
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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()));

This comment has been minimized.

Copy link
@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');

Expand Down

2 comments on commit 90d1b6e

@renan
Copy link

@renan renan commented on 90d1b6e Jun 18, 2014

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@Seldaek
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.