CredentialProvider.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. <?php
  2. namespace Aws\Credentials;
  3. use Aws;
  4. use Aws\CacheInterface;
  5. use Aws\Exception\CredentialsException;
  6. use GuzzleHttp\Promise;
  7. /**
  8. * Credential providers are functions that accept no arguments and return a
  9. * promise that is fulfilled with an {@see \Aws\Credentials\CredentialsInterface}
  10. * or rejected with an {@see \Aws\Exception\CredentialsException}.
  11. *
  12. * <code>
  13. * use Aws\Credentials\CredentialProvider;
  14. * $provider = CredentialProvider::defaultProvider();
  15. * // Returns a CredentialsInterface or throws.
  16. * $creds = $provider()->wait();
  17. * </code>
  18. *
  19. * Credential providers can be composed to create credentials using conditional
  20. * logic that can create different credentials in different environments. You
  21. * can compose multiple providers into a single provider using
  22. * {@see Aws\Credentials\CredentialProvider::chain}. This function accepts
  23. * providers as variadic arguments and returns a new function that will invoke
  24. * each provider until a successful set of credentials is returned.
  25. *
  26. * <code>
  27. * // First try an INI file at this location.
  28. * $a = CredentialProvider::ini(null, '/path/to/file.ini');
  29. * // Then try an INI file at this location.
  30. * $b = CredentialProvider::ini(null, '/path/to/other-file.ini');
  31. * // Then try loading from environment variables.
  32. * $c = CredentialProvider::env();
  33. * // Combine the three providers together.
  34. * $composed = CredentialProvider::chain($a, $b, $c);
  35. * // Returns a promise that is fulfilled with credentials or throws.
  36. * $promise = $composed();
  37. * // Wait on the credentials to resolve.
  38. * $creds = $promise->wait();
  39. * </code>
  40. */
  41. class CredentialProvider
  42. {
  43. const ENV_KEY = 'AWS_ACCESS_KEY_ID';
  44. const ENV_SECRET = 'AWS_SECRET_ACCESS_KEY';
  45. const ENV_SESSION = 'AWS_SESSION_TOKEN';
  46. const ENV_PROFILE = 'AWS_PROFILE';
  47. /**
  48. * Create a default credential provider that first checks for environment
  49. * variables, then checks for the "default" profile in ~/.aws/credentials,
  50. * then checks for "profile default" profile in ~/.aws/config (which is
  51. * the default profile of AWS CLI), then tries to make a GET Request to
  52. * fetch credentials if Ecs environment variable is presented, and finally
  53. * checks for EC2 instance profile credentials.
  54. *
  55. * This provider is automatically wrapped in a memoize function that caches
  56. * previously provided credentials.
  57. *
  58. * @param array $config Optional array of ecs/instance profile credentials
  59. * provider options.
  60. *
  61. * @return callable
  62. */
  63. public static function defaultProvider(array $config = [])
  64. {
  65. $localCredentialProviders = self::localCredentialProviders();
  66. $remoteCredentialProviders = self::remoteCredentialProviders($config);
  67. return self::memoize(
  68. call_user_func_array(
  69. 'self::chain',
  70. array_merge($localCredentialProviders, $remoteCredentialProviders)
  71. )
  72. );
  73. }
  74. /**
  75. * Create a credential provider function from a set of static credentials.
  76. *
  77. * @param CredentialsInterface $creds
  78. *
  79. * @return callable
  80. */
  81. public static function fromCredentials(CredentialsInterface $creds)
  82. {
  83. $promise = Promise\promise_for($creds);
  84. return function () use ($promise) {
  85. return $promise;
  86. };
  87. }
  88. /**
  89. * Creates an aggregate credentials provider that invokes the provided
  90. * variadic providers one after the other until a provider returns
  91. * credentials.
  92. *
  93. * @return callable
  94. */
  95. public static function chain()
  96. {
  97. $links = func_get_args();
  98. if (empty($links)) {
  99. throw new \InvalidArgumentException('No providers in chain');
  100. }
  101. return function () use ($links) {
  102. /** @var callable $parent */
  103. $parent = array_shift($links);
  104. $promise = $parent();
  105. while ($next = array_shift($links)) {
  106. $promise = $promise->otherwise($next);
  107. }
  108. return $promise;
  109. };
  110. }
  111. /**
  112. * Wraps a credential provider and caches previously provided credentials.
  113. *
  114. * Ensures that cached credentials are refreshed when they expire.
  115. *
  116. * @param callable $provider Credentials provider function to wrap.
  117. *
  118. * @return callable
  119. */
  120. public static function memoize(callable $provider)
  121. {
  122. return function () use ($provider) {
  123. static $result;
  124. static $isConstant;
  125. // Constant credentials will be returned constantly.
  126. if ($isConstant) {
  127. return $result;
  128. }
  129. // Create the initial promise that will be used as the cached value
  130. // until it expires.
  131. if (null === $result) {
  132. $result = $provider();
  133. }
  134. // Return credentials that could expire and refresh when needed.
  135. return $result
  136. ->then(function (CredentialsInterface $creds) use ($provider, &$isConstant, &$result) {
  137. // Determine if these are constant credentials.
  138. if (!$creds->getExpiration()) {
  139. $isConstant = true;
  140. return $creds;
  141. }
  142. // Refresh expired credentials.
  143. if (!$creds->isExpired()) {
  144. return $creds;
  145. }
  146. // Refresh the result and forward the promise.
  147. return $result = $provider();
  148. });
  149. };
  150. }
  151. /**
  152. * Wraps a credential provider and saves provided credentials in an
  153. * instance of Aws\CacheInterface. Forwards calls when no credentials found
  154. * in cache and updates cache with the results.
  155. *
  156. * Defaults to using a simple file-based cache when none provided.
  157. *
  158. * @param callable $provider Credentials provider function to wrap
  159. * @param CacheInterface $cache Cache to store credentials
  160. * @param string|null $cacheKey (optional) Cache key to use
  161. *
  162. * @return callable
  163. */
  164. public static function cache(
  165. callable $provider,
  166. CacheInterface $cache,
  167. $cacheKey = null
  168. ) {
  169. $cacheKey = $cacheKey ?: 'aws_cached_credentials';
  170. return function () use ($provider, $cache, $cacheKey) {
  171. $found = $cache->get($cacheKey);
  172. if ($found instanceof CredentialsInterface && !$found->isExpired()) {
  173. return Promise\promise_for($found);
  174. }
  175. return $provider()
  176. ->then(function (CredentialsInterface $creds) use (
  177. $cache,
  178. $cacheKey
  179. ) {
  180. $cache->set(
  181. $cacheKey,
  182. $creds,
  183. null === $creds->getExpiration() ?
  184. 0 : $creds->getExpiration() - time()
  185. );
  186. return $creds;
  187. });
  188. };
  189. }
  190. /**
  191. * Provider that creates credentials from environment variables
  192. * AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN.
  193. *
  194. * @return callable
  195. */
  196. public static function env()
  197. {
  198. return function () {
  199. // Use credentials from environment variables, if available
  200. $key = getenv(self::ENV_KEY);
  201. $secret = getenv(self::ENV_SECRET);
  202. if ($key && $secret) {
  203. return Promise\promise_for(
  204. new Credentials($key, $secret, getenv(self::ENV_SESSION) ?: NULL)
  205. );
  206. }
  207. return self::reject('Could not find environment variable '
  208. . 'credentials in ' . self::ENV_KEY . '/' . self::ENV_SECRET);
  209. };
  210. }
  211. /**
  212. * Credential provider that creates credentials using instance profile
  213. * credentials.
  214. *
  215. * @param array $config Array of configuration data.
  216. *
  217. * @return InstanceProfileProvider
  218. * @see Aws\Credentials\InstanceProfileProvider for $config details.
  219. */
  220. public static function instanceProfile(array $config = [])
  221. {
  222. return new InstanceProfileProvider($config);
  223. }
  224. /**
  225. * Credential provider that creates credentials using
  226. * ecs credentials by a GET request, whose uri is specified
  227. * by environment variable
  228. *
  229. * @param array $config Array of configuration data.
  230. *
  231. * @return EcsCredentialProvider
  232. * @see Aws\Credentials\EcsCredentialProvider for $config details.
  233. */
  234. public static function ecsCredentials(array $config = [])
  235. {
  236. return new EcsCredentialProvider($config);
  237. }
  238. /**
  239. * Credential provider that creates credentials using assume role
  240. *
  241. * @param array $config Array of configuration data
  242. * @return callable
  243. * @see Aws\Credentials\AssumeRoleCredentialProvider for $config details.
  244. */
  245. public static function assumeRole(array $config=[])
  246. {
  247. return new AssumeRoleCredentialProvider($config);
  248. }
  249. /**
  250. * Credentials provider that creates credentials using an ini file stored
  251. * in the current user's home directory.
  252. *
  253. * @param string|null $profile Profile to use. If not specified will use
  254. * the "default" profile in "~/.aws/credentials".
  255. * @param string|null $filename If provided, uses a custom filename rather
  256. * than looking in the home directory.
  257. *
  258. * @return callable
  259. */
  260. public static function ini($profile = null, $filename = null)
  261. {
  262. $filename = $filename ?: (self::getHomeDir() . '/.aws/credentials');
  263. $profile = $profile ?: (getenv(self::ENV_PROFILE) ?: 'default');
  264. return function () use ($profile, $filename) {
  265. if (!is_readable($filename)) {
  266. return self::reject("Cannot read credentials from $filename");
  267. }
  268. $data = parse_ini_file($filename, true);
  269. if ($data === false) {
  270. return self::reject("Invalid credentials file: $filename");
  271. }
  272. if (!isset($data[$profile])) {
  273. return self::reject("'$profile' not found in credentials file");
  274. }
  275. if (!isset($data[$profile]['aws_access_key_id'])
  276. || !isset($data[$profile]['aws_secret_access_key'])
  277. ) {
  278. return self::reject("No credentials present in INI profile "
  279. . "'$profile' ($filename)");
  280. }
  281. if (empty($data[$profile]['aws_session_token'])) {
  282. $data[$profile]['aws_session_token']
  283. = isset($data[$profile]['aws_security_token'])
  284. ? $data[$profile]['aws_security_token']
  285. : null;
  286. }
  287. return Promise\promise_for(
  288. new Credentials(
  289. $data[$profile]['aws_access_key_id'],
  290. $data[$profile]['aws_secret_access_key'],
  291. $data[$profile]['aws_session_token']
  292. )
  293. );
  294. };
  295. }
  296. /**
  297. * Local credential providers returns a list of local credential providers
  298. * in following order:
  299. * - credentials from environment variables
  300. * - 'default' profile in '.aws/credentials' file
  301. * - 'profile default' profile in '.aws/config' file
  302. *
  303. * @return array
  304. */
  305. private static function localCredentialProviders()
  306. {
  307. return [
  308. self::env(),
  309. self::ini(),
  310. self::ini('profile default', self::getHomeDir() . '/.aws/config')
  311. ];
  312. }
  313. /**
  314. * Remote credential providers returns a list of credentials providers
  315. * for the remote endpoints such as EC2 or ECS Roles.
  316. *
  317. * @param array $config Array of configuration data.
  318. *
  319. * @return array
  320. * @see Aws\Credentials\InstanceProfileProvider for $config details.
  321. * @see Aws\Credentials\EcsCredentialProvider for $config details.
  322. */
  323. private static function remoteCredentialProviders(array $config = [])
  324. {
  325. if (!empty(getenv(EcsCredentialProvider::ENV_URI))) {
  326. $providers['ecs'] = self::ecsCredentials($config);
  327. }
  328. $providers['instance'] = self::instanceProfile($config);
  329. if (isset($config['credentials'])
  330. && $config['credentials'] instanceof CacheInterface
  331. ) {
  332. foreach ($providers as $key => $provider) {
  333. $providers[$key] = self::cache(
  334. $provider,
  335. $config['credentials'],
  336. 'aws_cached_' . $key . '_credentials'
  337. );
  338. }
  339. }
  340. return $providers;
  341. }
  342. /**
  343. * Gets the environment's HOME directory if available.
  344. *
  345. * @return null|string
  346. */
  347. private static function getHomeDir()
  348. {
  349. // On Linux/Unix-like systems, use the HOME environment variable
  350. if ($homeDir = getenv('HOME')) {
  351. return $homeDir;
  352. }
  353. // Get the HOMEDRIVE and HOMEPATH values for Windows hosts
  354. $homeDrive = getenv('HOMEDRIVE');
  355. $homePath = getenv('HOMEPATH');
  356. return ($homeDrive && $homePath) ? $homeDrive . $homePath : null;
  357. }
  358. private static function reject($msg)
  359. {
  360. return new Promise\RejectedPromise(new CredentialsException($msg));
  361. }
  362. }