Skip to content

Commit

Permalink
Add credentials cache and wrap it around instance profiles in the pro…
Browse files Browse the repository at this point in the history
…vider chain
  • Loading branch information
jeskew committed Sep 1, 2015
1 parent 7584caf commit eb78e92
Show file tree
Hide file tree
Showing 8 changed files with 288 additions and 6 deletions.
6 changes: 4 additions & 2 deletions composer.json
Expand Up @@ -31,11 +31,13 @@
"ext-simplexml": "*",
"phpunit/phpunit": "~4.0",
"behat/behat": "~3.0",
"aws/aws-php-sns-message-validator": "^1.0"
"doctrine/cache": "~1.4",
"aws/aws-php-sns-message-validator": "~1.0"
},
"suggest": {
"ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages",
"ext-curl": "To send requests using cURL"
"ext-curl": "To send requests using cURL",
"doctrine/cache": "To use the DoctrineCacheAdapter"
},
"autoload": {
"psr-4": {
Expand Down
17 changes: 16 additions & 1 deletion docs/guide/configuration.rst
Expand Up @@ -46,7 +46,7 @@ that loads API files from the ``src/data`` folder of the SDK.
credentials
~~~~~~~~~~~

:Type: ``array|Aws\Credentials\CredentialsInterface|bool|callable``
:Type: ``array|Aws\Credentials\CredentialsInterface|bool|callable|Aws\CacheInterface``

If you do not provide a ``credentials`` option, the SDK will attempt to load
credentials from your environment in the following order:
Expand Down Expand Up @@ -109,6 +109,21 @@ create credentials using a function.
'credentials' => $provider
]);
Pass an instance of ``Aws\CacheInterface`` to cache the values returned by the
default provider chain across multiple processes.

.. code-block:: php
use Aws\DoctrineCacheAdapter;
use Aws\S3\S3Client;
use Doctrine\Common\Cache\ApcCache;
$s3 = new S3Client([
'version' => 'latest',
'region' => 'us-west-2',
'credentials' => new DoctrineCacheAdapter(new ApcCache),
]);
You can find more information about providing credentials to a client in the
:doc:`credentials` guide.

Expand Down
2 changes: 1 addition & 1 deletion src/ClientResolver.php
Expand Up @@ -338,7 +338,7 @@ public static function _apply_retries($value, array &$args, HandlerList $list)

public static function _apply_credentials($value, array &$args)
{
if (is_callable($value)) {
if (is_callable($value) || $value instanceof CacheInterface) {
return;
} elseif ($value instanceof CredentialsInterface) {
$args['credentials'] = CredentialProvider::fromCredentials($value);
Expand Down
51 changes: 50 additions & 1 deletion src/Credentials/CredentialProvider.php
Expand Up @@ -2,6 +2,7 @@
namespace Aws\Credentials;

use Aws;
use Aws\CacheInterface;
use Aws\Exception\CredentialsException;
use GuzzleHttp\Promise;

Expand Down Expand Up @@ -61,11 +62,16 @@ class CredentialProvider
*/
public static function defaultProvider(array $config = [])
{
$cache = isset($config['credentials'])
&& $config['credentials'] instanceof CacheInterface ?
$config['credentials']
: null;
return self::memoize(
self::chain(
self::env(),
self::ini(),
self::instanceProfile($config)
$cache ? self::cache(self::instanceProfile($config), $cache)
: self::instanceProfile($config)
)
);
}
Expand Down Expand Up @@ -156,6 +162,49 @@ public static function memoize(callable $provider)
};
}

/**
* Wraps a credential provider and saves provided credentials in an
* instance of Aws\CacheInterface. Forwards calls when no credentials found
* in cache and updates cache with the results.
*
* Defaults to using a simple file-based cache when none provided.
*
* @param callable $provider Credentials provider function to wrap
* @param CacheInterface $cache (optional) Cache to store credentials
* @param string|null $cacheKey (optional) Cache key to use
*
* @return callable
*/
public static function cache(
callable $provider,
CacheInterface $cache,
$cacheKey = null
) {
$cacheKey = $cacheKey ?: 'aws_cached_credentials';

return function () use ($provider, $cache, $cacheKey) {
$found = $cache->get($cacheKey);
if ($found instanceof CredentialsInterface && !$found->isExpired()) {
return Promise\promise_for($found);
}

return $provider()
->then(function (CredentialsInterface $creds) use (
$cache,
$cacheKey
) {
$cache->set(
$cacheKey,
$creds,
null === $creds->getExpiration() ?
0 : $creds->getExpiration() - time()
);

return $creds;
});
};
}

/**
* Provider that creates credentials from environment variables
* AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN.
Expand Down
27 changes: 26 additions & 1 deletion src/Credentials/Credentials.php
Expand Up @@ -5,7 +5,7 @@
* Basic implementation of the AWS Credentials interface that allows callers to
* pass in the AWS Access Key and AWS Secret Access Key in the constructor.
*/
class Credentials implements CredentialsInterface
class Credentials implements CredentialsInterface, \Serializable
{
private $key;
private $secret;
Expand All @@ -29,6 +29,16 @@ public function __construct($key, $secret, $token = null, $expires = null)
$this->expires = $expires;
}

public static function __set_state(array $state)
{
return new self(
$state['key'],
$state['secret'],
$state['token'],
$state['expires']
);
}

public function getAccessKeyId()
{
return $this->key;
Expand Down Expand Up @@ -63,4 +73,19 @@ public function toArray()
'expires' => $this->expires
];
}

public function serialize()
{
return json_encode($this->toArray());
}

public function unserialize($serialized)
{
$data = json_decode($serialized, true);

$this->key = $data['key'];
$this->secret = $data['secret'];
$this->token = $data['token'];
$this->expires = $data['expires'];
}
}
55 changes: 55 additions & 0 deletions src/DoctrineCacheAdapter.php
@@ -0,0 +1,55 @@
<?php
namespace Aws;

use Doctrine\Common\Cache\Cache;

class DoctrineCacheAdapter implements CacheInterface, Cache
{
/** @var Cache */
private $cache;

public function __construct(Cache $cache)
{
$this->cache = $cache;
}

public function get($key)
{
return $this->cache->fetch($key);
}

public function fetch($key)
{
return $this->get($key);
}

public function set($key, $value, $ttl = 0)
{
return $this->cache->save($key, $value, $ttl);
}

public function save($key, $value, $ttl = 0)
{
return $this->set($key, $value, $ttl);
}

public function remove($key)
{
return $this->cache->delete($key);
}

public function delete($key)
{
return $this->remove($key);
}

public function contains($key)
{
return $this->cache->contains($key);
}

public function getStats()
{
return $this->cache->getStats();
}
}
95 changes: 95 additions & 0 deletions tests/Credentials/CredentialProviderTest.php
Expand Up @@ -3,6 +3,8 @@

use Aws\Credentials\CredentialProvider;
use Aws\Credentials\Credentials;
use Aws\LruArrayCache;
use GuzzleHttp\Promise;

/**
* @covers \Aws\Credentials\CredentialProvider
Expand Down Expand Up @@ -46,6 +48,81 @@ public function tearDown()
putenv(CredentialProvider::ENV_PROFILE . '=' . $this->profile);
}

public function testCreatesFromCache()
{
$cache = new LruArrayCache;
$key = __CLASS__ . 'credentialsCache';
$saved = new Credentials('foo', 'bar', 'baz', PHP_INT_MAX);
$cache->set($key, $saved, $saved->getExpiration() - time());

$explodingProvider = function () {
throw new \BadFunctionCallException('This should never be called');
};

$found = call_user_func(
CredentialProvider::cache($explodingProvider, $cache, $key)
)
->wait();

$this->assertEquals($saved->getAccessKeyId(), $found->getAccessKeyId());
$this->assertEquals($saved->getSecretKey(), $found->getSecretKey());
$this->assertEquals($saved->getSecurityToken(), $found->getSecurityToken());
$this->assertEquals($saved->getExpiration(), $found->getExpiration());
}

public function testRefreshesCacheWhenCredsExpired()
{
$cache = new LruArrayCache;
$key = __CLASS__ . 'credentialsCache';
$saved = new Credentials('foo', 'bar', 'baz', time() - 1);
$cache->set($key, $saved);

$timesCalled = 0;
$recordKeepingProvider = function () use (&$timesCalled) {
++$timesCalled;
return Promise\promise_for(new Credentials('foo', 'bar', 'baz', PHP_INT_MAX));
};

call_user_func(
CredentialProvider::cache($recordKeepingProvider, $cache, $key)
)
->wait();

$this->assertEquals(1, $timesCalled);
}

public function testPersistsToCache()
{
$cache = new LruArrayCache;
$key = __CLASS__ . 'credentialsCache';
$creds = new Credentials('foo', 'bar', 'baz', PHP_INT_MAX);

$timesCalled = 0;
$volatileProvider = function () use ($creds, &$timesCalled) {
if (0 === $timesCalled) {
++$timesCalled;

return Promise\promise_for($creds);
}

throw new \BadFunctionCallException('I was called too many times!');
};

for ($i = 0; $i < 10; $i++) {
$found = call_user_func(
CredentialProvider::cache($volatileProvider, $cache, $key)
)
->wait();
}

$this->assertEquals(1, $timesCalled);
$this->assertEquals(1, count($cache));
$this->assertEquals($creds->getAccessKeyId(), $found->getAccessKeyId());
$this->assertEquals($creds->getSecretKey(), $found->getSecretKey());
$this->assertEquals($creds->getSecurityToken(), $found->getSecurityToken());
$this->assertEquals($creds->getExpiration(), $found->getExpiration());
}

public function testCreatesFromEnvironmentVariables()
{
$this->clearEnv();
Expand Down Expand Up @@ -185,6 +262,24 @@ public function testCallsDefaultsCreds()
$this->assertEquals('123', $creds->getSecretKey());
}

public function testCachesAsPartOfDefaultChain()
{
$cache = new LruArrayCache;
$cache->set('aws_cached_credentials', new Credentials(
'foo',
'bar'
));
$this->clearEnv();
putenv('HOME=/does/not/exist');
$credentials = call_user_func(CredentialProvider::defaultProvider([
'credentials' => $cache,
]))
->wait();

$this->assertEquals('foo', $credentials->getAccessKeyId());
$this->assertEquals('bar', $credentials->getSecretKey());
}

public function testChainsCredentials()
{
$dir = $this->clearEnv();
Expand Down
41 changes: 41 additions & 0 deletions tests/DoctrineCacheAdapterTest.php
@@ -0,0 +1,41 @@
<?php
namespace Aws\Test;

use Aws\CacheInterface;
use Aws\DoctrineCacheAdapter;
use Doctrine\Common\Cache\Cache;

class DoctrineCacheAdapterTest extends \PHPUnit_Framework_TestCase
{
public function testProxiesCallsToDoctrine()
{
$wrappedCache = $this->getMock(Cache::class);

$wrappedCache->expects($this->once())
->method('fetch')
->with('foo')
->willReturn('bar');
$wrappedCache->expects($this->once())
->method('save')
->with('foo', 'bar', 0)
->willReturn(true);
$wrappedCache->expects($this->once())
->method('delete')
->with('foo')
->willReturn(true);

$cache = new DoctrineCacheAdapter($wrappedCache);
$cache->set('foo', 'bar', 0);
$cache->get('foo');
$cache->remove('foo');
}

public function testAdaptsCacheToAwsAndDoctrine()
{
$wrappedCache = $this->getMock(Cache::class);
$cache = new DoctrineCacheAdapter($wrappedCache);

$this->assertInstanceOf(Cache::class, $cache);
$this->assertInstanceOf(CacheInterface::class, $cache);
}
}

0 comments on commit eb78e92

Please sign in to comment.