diff --git a/src/Credentials/CredentialProvider.php b/src/Credentials/CredentialProvider.php index 1647fe0da9..752c7278fc 100644 --- a/src/Credentials/CredentialProvider.php +++ b/src/Credentials/CredentialProvider.php @@ -96,19 +96,13 @@ public static function defaultProvider(array $config = []) ) { $defaultChain['sso'] = self::sso( $profileName, - self::getHomeDir() . '/.aws/config', + null, $config ); $defaultChain['process_credentials'] = self::process(); $defaultChain['ini'] = self::ini(); - $defaultChain['process_config'] = self::process( - 'profile ' . $profileName, - self::getHomeDir() . '/.aws/config' - ); - $defaultChain['ini_config'] = self::ini( - 'profile '. $profileName, - self::getHomeDir() . '/.aws/config' - ); + $defaultChain['process_config'] = self::process($profileName); + $defaultChain['ini_config'] = self::ini($profileName); } if (self::shouldUseEcs()) { @@ -325,21 +319,14 @@ public static function sso($ssoProfileName = 'default', $filename = null, $config = [] ) { - $filename = $filename ?: (self::getHomeDir() . '/.aws/config'); return function () use ($ssoProfileName, $filename, $config) { - if (!@is_readable($filename)) { - return self::reject("Cannot read credentials from $filename"); - } $profiles = self::loadProfiles($filename); if (isset($profiles[$ssoProfileName])) { $ssoProfile = $profiles[$ssoProfileName]; - } elseif (isset($profiles['profile ' . $ssoProfileName])) { - $ssoProfileName = 'profile ' . $ssoProfileName; - $ssoProfile = $profiles[$ssoProfileName]; } else { - return self::reject("Profile {$ssoProfileName} does not exist in {$filename}."); + return self::reject("Profile {$ssoProfileName} does not exist in config or credentials files."); } if (!empty($ssoProfile['sso_session'])) { @@ -455,8 +442,7 @@ public static function assumeRoleWithWebIdentityCredentialProvider(array $config * * @param string|null $profile Profile to use. If not specified will use * the "default" profile in "~/.aws/credentials". - * @param string|null $filename If provided, uses a custom filename rather - * than looking in the home directory. + * @param string|null $filename If provided, also load from a custom config file. * @param array|null $config If provided, may contain the following: * preferStaticCredentials: If true, prefer static * credentials to role_arn if both are present @@ -469,7 +455,6 @@ public static function assumeRoleWithWebIdentityCredentialProvider(array $config */ public static function ini($profile = null, $filename = null, array $config = []) { - $filename = self::getFileName($filename); $profile = $profile ?: (getenv(self::ENV_PROFILE) ?: 'default'); return function () use ($profile, $filename, $config) { @@ -481,15 +466,9 @@ public static function ini($profile = null, $filename = null, array $config = [] : false; $stsClient = isset($config['stsClient']) ? $config['stsClient'] : null; - if (!@is_readable($filename)) { - return self::reject("Cannot read credentials from $filename"); - } $data = self::loadProfiles($filename); - if ($data === false) { - return self::reject("Invalid credentials file: $filename"); - } if (!isset($data[$profile])) { - return self::reject("'$profile' not found in credentials file"); + return self::reject("'$profile' not found in config or credentials files"); } /* @@ -526,8 +505,8 @@ public static function ini($profile = null, $filename = null, array $config = [] if (!isset($data[$profile]['aws_access_key_id']) || !isset($data[$profile]['aws_secret_access_key']) ) { - return self::reject("No credentials present in INI profile " - . "'$profile' ($filename)"); + return self::reject("No credentials present in config or credentials files with profile " + . "'$profile'"); } if (empty($data[$profile]['aws_session_token'])) { @@ -553,30 +532,22 @@ public static function ini($profile = null, $filename = null, array $config = [] * * @param string|null $profile Profile to use. If not specified will use * the "default" profile in "~/.aws/credentials". - * @param string|null $filename If provided, uses a custom filename rather - * than looking in the home directory. + * @param string|null $filename If provided, also load from a custom config file. * * @return callable */ public static function process($profile = null, $filename = null) { - $filename = self::getFileName($filename); $profile = $profile ?: (getenv(self::ENV_PROFILE) ?: 'default'); return function () use ($profile, $filename) { - if (!@is_readable($filename)) { - return self::reject("Cannot read process credentials from $filename"); - } - $data = \Aws\parse_ini_file($filename, true, INI_SCANNER_RAW); - if ($data === false) { - return self::reject("Invalid credentials file: $filename"); - } + $data = self::loadProfiles($filename); if (!isset($data[$profile])) { - return self::reject("'$profile' not found in credentials file"); + return self::reject("'$profile' not found in config or credentials files"); } if (!isset($data[$profile]['credential_process'])) { - return self::reject("No credential_process present in INI profile " - . "'$profile' ($filename)"); + return self::reject("No credential_process present in config or credentials files with profile " + . "'$profile'"); } $credentialProcess = $data[$profile]['credential_process']; @@ -672,7 +643,7 @@ private static function loadRoleProfile( if (empty($roleArn)) { return self::reject( "A role_arn must be provided with credential_source in " . - "file {$filename} under profile {$profileName} " + "config or credentials files under profile {$profileName} " ); } } @@ -733,33 +704,16 @@ private static function getHomeDir() */ private static function loadProfiles($filename) { - $profileData = \Aws\parse_ini_file($filename, true, INI_SCANNER_RAW); - - // If loading .aws/credentials, also load .aws/config when AWS_SDK_LOAD_NONDEFAULT_CONFIG is set - if ($filename === self::getHomeDir() . '/.aws/credentials' - && getenv('AWS_SDK_LOAD_NONDEFAULT_CONFIG') - ) { - $configFilename = self::getHomeDir() . '/.aws/config'; - $configProfileData = \Aws\parse_ini_file($configFilename, true, INI_SCANNER_RAW); - foreach ($configProfileData as $name => $profile) { - // standardize config profile names - $name = str_replace('profile ', '', $name); - if (!isset($profileData[$name])) { - $profileData[$name] = $profile; - } - } - } - - return $profileData; + return self::loadDefaultProfiles($filename); } /** * Gets profiles from ~/.aws/credentials and ~/.aws/config ini files */ - private static function loadDefaultProfiles() { + private static function loadDefaultProfiles($extraFilename = null) { $profiles = []; - $credFile = self::getHomeDir() . '/.aws/credentials'; - $configFile = self::getHomeDir() . '/.aws/config'; + $credFile = getenv('AWS_SHARED_CREDENTIALS_FILE') ?: (self::getHomeDir() . '/.aws/credentials'); + $configFile = getenv('AWS_CONFIG_FILE') ?: (self::getHomeDir() . '/.aws/config'); if (file_exists($credFile)) { $profiles = \Aws\parse_ini_file($credFile, true, INI_SCANNER_RAW); } @@ -769,18 +723,24 @@ private static function loadDefaultProfiles() { foreach ($configProfileData as $name => $profile) { // standardize config profile names $name = str_replace('profile ', '', $name); - if (!isset($profiles[$name])) { - $profiles[$name] = $profile; - } + $profiles[$name] = array_merge($profiles[$name] ?? [], $profile); } } + if (!is_null($extraFilename) && $extraFilename != $credFile && $extraFilename != $configFile) { + $extraFileData = \Aws\parse_ini_file($extraFilename, true, INI_SCANNER_RAW); + foreach ($extraFileData as $name => $profile) { + // standardize config profile names + $name = str_replace('profile ', '', $name); + $profiles[$name] = array_merge($profiles[$name] ?? [], $profile); + } + } return $profiles; } public static function getCredentialsFromSource( $profileName = '', - $filename = '', + $filename = null, $config = [] ) { $data = self::loadProfiles($filename); @@ -826,19 +786,6 @@ private static function reject($msg) return new Promise\RejectedPromise(new CredentialsException($msg)); } - /** - * @param $filename - * @return string - */ - private static function getFileName($filename) - { - if (!isset($filename)) { - $filename = getenv(self::ENV_SHARED_CREDENTIALS_FILE) ?: - (self::getHomeDir() . '/.aws/credentials'); - } - return $filename; - } - /** * @return boolean */ @@ -866,7 +813,7 @@ private static function getSsoCredentials($profiles, $ssoProfileName, $filename, $sessionName = $ssoProfile['sso_session']; if (empty($profiles['sso-session ' . $sessionName])) { return self::reject( - "Could not find sso-session {$sessionName} in {$filename}" + "Could not find sso-session {$sessionName} in config or credentials files" ); } $ssoSession = $profiles['sso-session ' . $ssoProfile['sso_session']]; @@ -918,7 +865,7 @@ private static function getSsoCredentialsLegacy($profiles, $ssoProfileName, $fil || empty($ssoProfile['sso_role_name']) ) { return self::reject( - "Profile {$ssoProfileName} in {$filename} must contain the following keys: " + "Profile {$ssoProfileName} in config or credential files must contain the following keys: " . "sso_start_url, sso_region, sso_account_id, and sso_role_name." ); } diff --git a/src/Token/ParsesIniTrait.php b/src/Token/ParsesIniTrait.php index b96a6d97bc..9db7922dff 100644 --- a/src/Token/ParsesIniTrait.php +++ b/src/Token/ParsesIniTrait.php @@ -4,23 +4,34 @@ trait ParsesIniTrait { /** - * Gets profiles from specified $filename, or default ini files. + * Gets profiles from specified $extraFilename, or default ini files. */ - private static function loadProfiles($filename) - { - $profileData = \Aws\parse_ini_file($filename, true, INI_SCANNER_RAW); - $configFilename = self::getHomeDir() . '/.aws/config'; - if (is_readable($configFilename)) { - $configProfiles = \Aws\parse_ini_file($configFilename, true, INI_SCANNER_RAW); - $profileData = array_merge($configProfiles, $profileData); + private static function loadProfiles($extraFilename = null) { + $profiles = []; + $credFile = getenv('AWS_SHARED_CREDENTIALS_FILE') ?: (self::getHomeDir() . '/.aws/credentials'); + $configFile = getenv('AWS_CONFIG_FILE') ?: (self::getHomeDir() . '/.aws/config'); + if (file_exists($credFile)) { + $profiles = \Aws\parse_ini_file($credFile, true, INI_SCANNER_RAW); } - foreach ($profileData as $name => $profile) { - // standardize config profile names - $name = str_replace('profile ', '', $name); - $profileData[$name] = $profile; + + if (file_exists($configFile)) { + $configProfileData = \Aws\parse_ini_file($configFile, true, INI_SCANNER_RAW); + foreach ($configProfileData as $name => $profile) { + // standardize config profile names + $name = str_replace('profile ', '', $name); + $profiles[$name] = array_merge($profiles[$name] ?? [], $profile); + } } - return $profileData; + if (!is_null($extraFilename) && $extraFilename != $credFile && $extraFilename != $configFile) { + $extraFileData = \Aws\parse_ini_file($extraFilename, true, INI_SCANNER_RAW); + foreach ($extraFileData as $name => $profile) { + // standardize config profile names + $name = str_replace('profile ', '', $name); + $profiles[$name] = array_merge($profiles[$name] ?? [], $profile); + } + } + return $profiles; } /** diff --git a/src/Token/SsoTokenProvider.php b/src/Token/SsoTokenProvider.php index 777fe25dcc..072c3e0cb3 100644 --- a/src/Token/SsoTokenProvider.php +++ b/src/Token/SsoTokenProvider.php @@ -31,7 +31,7 @@ class SsoTokenProvider implements RefreshableTokenProviderInterface /** * Constructs a new SsoTokenProvider object, which will fetch a token from an authenticated SSO profile * @param string $profileName The name of the profile that contains the sso_session key - * @param string|null $configFilePath Name of the config file to sso profile from + * @param string|null $configFilePath If provided, also load from a custom config file. * @param SSOOIDCClient|null $ssoOidcClient The sso client for generating a new token */ public function __construct( @@ -40,7 +40,7 @@ public function __construct( SSOOIDCClient $ssoOidcClient = null ) { $this->profileName = $this->resolveProfileName($profileName); - $this->configFilePath = $this->resolveConfigFile($configFilePath); + $this->configFilePath = $configFilePath; $this->ssoOidcClient = $ssoOidcClient; } @@ -63,24 +63,6 @@ private function resolveProfileName($argProfileName): string } } - /** - * This method resolves the config file from where the profiles - * are going to be loaded from. If $argFileName is not empty then, - * it takes precedence over the default config file location. - * - * @param string|null $argConfigFilePath The config path provided as argument. - * - * @return string - */ - private function resolveConfigFile($argConfigFilePath): string - { - if (empty($argConfigFilePath)) { - return self::getHomeDir() . '/.aws/config'; - } else{ - return $argConfigFilePath; - } - } - /** * Loads cached sso credentials. * @@ -89,19 +71,15 @@ private function resolveConfigFile($argConfigFilePath): string public function __invoke() { return Promise\Coroutine::of(function () { - if (empty($this->configFilePath) || !is_readable($this->configFilePath)) { - throw new TokenException("Cannot read profiles from {$this->configFilePath}"); - } - $profiles = self::loadProfiles($this->configFilePath); if (!isset($profiles[$this->profileName])) { - throw new TokenException("Profile `{$this->profileName}` does not exist in {$this->configFilePath}."); + throw new TokenException("Profile `{$this->profileName}` does not exist in config or credentials files."); } $profile = $profiles[$this->profileName]; if (empty($profile['sso_session'])) { throw new TokenException( - "Profile `{$this->profileName}` in {$this->configFilePath} must contain an sso_session." + "Profile `{$this->profileName}` in config or credentials files must contain an sso_session." ); } @@ -110,7 +88,7 @@ public function __invoke() $profileSsoSession = 'sso-session ' . $ssoSessionName; if (empty($profiles[$profileSsoSession])) { throw new TokenException( - "Sso session `{$ssoSessionName}` does not exist in {$this->configFilePath}" + "Sso session `{$ssoSessionName}` does not exist in config or credentials files" ); } @@ -118,7 +96,7 @@ public function __invoke() foreach (['sso_start_url', 'sso_region'] as $requiredProp) { if (empty($sessionProfileData[$requiredProp])) { throw new TokenException( - "Sso session `{$ssoSessionName}` in {$this->configFilePath} is missing the required property `{$requiredProp}`" + "Sso session `{$ssoSessionName}` in config or credentials files is missing the required property `{$requiredProp}`" ); } } diff --git a/src/Token/TokenProvider.php b/src/Token/TokenProvider.php index feb8129272..a488ef5078 100644 --- a/src/Token/TokenProvider.php +++ b/src/Token/TokenProvider.php @@ -254,13 +254,13 @@ private static function reject($msg) * Token provider that creates a token from cached sso credentials * * @param string $profileName the name of the ini profile name - * @param string $filename the location of the ini file + * @param string|null $filename If provided, also load from a custom config file. * @param array $config configuration options * * @return SsoTokenProvider * @see Aws\Token\SsoTokenProvider for $config details. */ - public static function sso($profileName, $filename, $config = []) + public static function sso($profileName, $filename = null, $config = []) { $ssoClient = isset($config['ssoClient']) ? $config['ssoClient'] : null; diff --git a/tests/Credentials/CredentialProviderTest.php b/tests/Credentials/CredentialProviderTest.php index 1d92e1f7f5..6239f69c7f 100644 --- a/tests/Credentials/CredentialProviderTest.php +++ b/tests/Credentials/CredentialProviderTest.php @@ -288,7 +288,7 @@ public function testIgnoresIniWithUseAwsConfigFileFalse() public function testEnsuresIniFileIsValid() { - $this->expectExceptionMessage("Invalid credentials file:"); + $this->expectExceptionMessage("'default' not found in config or credentials files"); $this->expectException(\Aws\Exception\CredentialsException::class); $dir = $this->clearEnv(); file_put_contents($dir . '/credentials', "wef \n=\nwef"); @@ -477,7 +477,7 @@ public function testCreatesTemporaryFromProcessCredential() public function testEnsuresProcessCredentialIsPresent() { - $this->expectExceptionMessage("No credential_process present in INI profile"); + $this->expectExceptionMessage("No credential_process present in config or credentials files with profile"); $this->expectException(\Aws\Exception\CredentialsException::class); $dir = $this->clearEnv(); $ini = <<expectExceptionMessage("No credentials present in INI profile 'default'"); + $this->expectExceptionMessage("No credentials present in config or credentials files with profile 'default'"); $this->expectException(\Aws\Exception\CredentialsException::class); $dir = $this->clearEnv(); $ini = <<expectExceptionMessage("Cannot read credentials from"); + $this->expectExceptionMessage("Profile default does not exist in config or credentials files."); $this->expectException(\Aws\Exception\CredentialsException::class); $dir = $this->clearEnv(); @@ -1499,6 +1499,56 @@ public function testAssumeRoleInCredentialsFromSourceInConfig() unlink($dir . '/config'); } } + public function testAssumeRoleInCredentialsFromSourceInConfigSeparateFiles() + { + $dir = $this->clearEnv(); + $ini = << [ + 'AccessKeyId' => 'foo', + 'SecretAccessKey' => 'credentialSecret', + 'SessionToken' => null, + 'Expiration' => DateTimeResult::fromEpoch(time() + 10) + ], + ]; + $sts = $this->getTestClient('Sts'); + $this->addMockResults($sts, [ + new Result($result) + ]); + try { + $config = [ + 'stsClient' => $sts + ]; + $creds = call_user_func(CredentialProvider::ini( + 'assume', + $dir . '/config', + $config + ))->wait(); + $this->assertSame('foo', $creds->getAccessKeyId()); + $this->assertSame('credentialSecret', $creds->getSecretKey()); + $this->assertNull($creds->getSecurityToken()); + $this->assertFalse($creds->isExpired()); + } catch (\Exception $e) { + throw $e; + } finally { + unlink($dir . '/credentials'); + unlink($dir . '/config'); + } + } public function testAssumeRoleInConfigFromSourceInCredentials() { $dir = $this->clearEnv(); @@ -1872,7 +1922,8 @@ public function testCachesCacheableInDefaultChain() $credsForCache = new Credentials('foo', 'bar', 'baz', PHP_INT_MAX); foreach ($cacheable as $provider) { - $this->clearEnv(); + $dir = $this->clearEnv(); + putenv('HOME=' . dirname($dir)); if ($provider == 'ecs') putenv('AWS_CONTAINER_CREDENTIALS_RELATIVE_URI=/latest'); $cache = new LruArrayCache; @@ -1961,7 +2012,7 @@ public function testProcessCredentialConfigDefaultChain() { $dir = $this->clearEnv(); $configIni = <<assertSame('configFoo', $creds->getAccessKeyId()); } + public function testProcessCredentialConfigDefaultChainProfile() + { + $dir = $this->clearEnv(); + $configIni = <<wait(); + unlink($dir . '/config'); + $this->assertSame('configFoo', $creds->getAccessKeyId()); + } /** * @dataProvider shouldUseEcsProvider *