Skip to content

Commit

Permalink
Merge pull request #9 from xp-forge/feature/credentialprovider
Browse files Browse the repository at this point in the history
Implement credential providers
  • Loading branch information
thekid authored Jun 29, 2024
2 parents a02d342 + 7da226a commit d493a7c
Show file tree
Hide file tree
Showing 11 changed files with 664 additions and 4 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ AWS Core for the XP Framework
[![Supports PHP 8.0+](https://raw.githubusercontent.com/xp-framework/web/master/static/php-8_0plus.svg)](http://php.net/)
[![Latest Stable Version](https://poser.pugx.org/xp-forge/aws/version.png)](https://packagist.org/packages/xp-forge/aws)

Provides common AWS functionality in a low-level and therefore lightweight library (*less than 1% of the size of the official PHP SDK!*)
Provides common AWS functionality in a low-level and therefore lightweight library (*less than 3% of the size of the official PHP SDK!*)

Invoking a lambda
-----------------
Expand Down
52 changes: 52 additions & 0 deletions src/main/php/com/amazon/aws/CredentialProvider.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php namespace com\amazon\aws;

use com\amazon\aws\credentials\{FromEnvironment, FromConfig, FromEcs, Provider};
use util\NoSuchElementException;

/** @test com.amazon.aws.unittest.CredentialProviderTest */
final class CredentialProvider implements Provider {
private static $raise;
private $delegates;

static function __static() {
self::$raise= new class() implements Provider {
public function credentials() {
throw new NoSuchElementException('None of the credential providers returned credentials');
}
};
}

/** Creates a new provider which queries all the given delegates */
public function __construct(Provider... $delegates) {
$this->delegates= $delegates;
}

/** @return ?com.amazon.aws.Credentials */
public function credentials() {
foreach ($this->delegates as $delegate) {
if (null !== ($credentials= $delegate->credentials())) return $credentials;
}
return null;
}

/**
* Returns default credential provider chain, checking, in the following order:
*
* 1. Environment variables
* 2. Shared credentials and config files
* 3. Amazon ECS container credentials
*
* If none of the above provide credentials, an exception is raised when invoking
* the `credentials()` method.
*
* @see https://docs.aws.amazon.com/sdk-for-kotlin/latest/developer-guide/credential-providers.html
*/
public static function default(): Provider {
return new self(
new FromEnvironment(),
new FromConfig(),
new FromEcs(),
self::$raise
);
}
}
18 changes: 16 additions & 2 deletions src/main/php/com/amazon/aws/Credentials.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,21 @@

/** @test com.amazon.aws.unittest.CredentialsTest */
class Credentials implements Value {
private $accessKey, $secretKey, $sessionToken;
private $accessKey, $secretKey, $sessionToken, $expiration;

/**
* Creates a new instance
*
* @param string $accessKey
* @param string|util.Secret $secretKey
* @param ?string $sessionToken
* @param ?int|string $expiration
*/
public function __construct($accessKey, $secretKey, $sessionToken= null) {
public function __construct($accessKey, $secretKey, $sessionToken= null, $expiration= null) {
$this->accessKey= $accessKey;
$this->secretKey= $secretKey instanceof Secret ? $secretKey : new Secret($secretKey);
$this->sessionToken= $sessionToken;
$this->expiration= null === $expiration || is_int($expiration) ? $expiration : strtotime($expiration);
}

/** @return string */
Expand All @@ -29,11 +31,23 @@ public function secretKey() { return $this->secretKey; }
/** @return ?string */
public function sessionToken() { return $this->sessionToken; }

/** @return ?int */
public function expiration() { return $this->expiration; }

/** @return string */
public function hashCode() {
return 'C'.sha1($this->accessKey.$this->secretKey->reveal().$this->sessionToken);
}

/**
* Check whether these credentials have expired
*
* @return bool
*/
public function expired() {
return null !== $this->expiration && $this->expiration <= time();
}

/** @return string */
public function toString() {
return sprintf(
Expand Down
64 changes: 64 additions & 0 deletions src/main/php/com/amazon/aws/credentials/FromConfig.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php namespace com\amazon\aws\credentials;

use com\amazon\aws\Credentials;
use io\{File, Path};
use lang\Environment;
use util\Secret;

/**
* Reads credentials from AWS config files
*
* @see https://docs.aws.amazon.com/sdkref/latest/guide/file-format.html
* @see https://docs.aws.amazon.com/sdkref/latest/guide/file-location.html
* @test com.amazon.aws.unittest.CredentialProviderTest
*/
class FromConfig implements Provider {
private $file, $profile;
private $modified= null;
private $credentials;

/**
* Creates a new configuration source. Checks the `AWS_SHARED_CREDENTIALS_FILE`
* environment variables and known shared credentials file locations if file is
* omitted. Checks `AWS_PROFILE` environment variable if profile is omitted, using
* `default` otherwise.
*
* @param ?string|io.File $file
* @param ?string $profile
*/
public function __construct($file= null, $profile= null) {
if (null === $file) {
$this->file= new File(Environment::variable('AWS_SHARED_CREDENTIALS_FILE', null)
?? new Path(Environment::homeDir(), '.aws', 'credentials')
);
} else if ($file instanceof File) {
$this->file= $file;
} else {
$this->file= new File($file);
}

$this->profile= $profile ?? Environment::variable('AWS_PROFILE', 'default');
}

/** @return ?com.amazon.aws.Credentials */
public function credentials() {
if (!$this->file->exists()) return $this->credentials= null;

// Only read the underlying file if its modification time has changed
$modified= $this->file->lastModified();
if ($modified >= $this->modified) {
$this->modified= $modified;

// Either check "profile [...]" or the default section
$config= parse_ini_file($this->file->getURI(), true, INI_SCANNER_RAW);
$section= $config['default' === $this->profile ? 'default' : "profile {$this->profile}"] ?? null;

$this->credentials= null === $section ? null : new Credentials(
$section['aws_access_key_id'],
new Secret($section['aws_secret_access_key']),
$section['aws_session_token'] ?? null
);
}
return $this->credentials;
}
}
76 changes: 76 additions & 0 deletions src/main/php/com/amazon/aws/credentials/FromEcs.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php namespace com\amazon\aws\credentials;

use com\amazon\aws\Credentials;
use lang\{Environment, IllegalStateException, Throwable};
use peer\URL;
use peer\http\{HttpConnection, HttpRequest};
use text\json\{Json, StreamInput};
use util\Secret;

/**
* Reads credentials from container credential provider. This credential
* provider is useful for Amazon Elastic Container Service (Amazon ECS)
* and Amazon Elastic Kubernetes Service (Amazon EKS) customers. SDKs
* attempt to load credentials from the specified HTTP endpoint.
*
* @see https://docs.aws.amazon.com/sdkref/latest/guide/feature-container-credentials.html
* @test com.amazon.aws.unittest.CredentialProviderTest
*/
class FromEcs implements Provider {
const DEFAULT_HOST= 'http://169.254.170.2';

private $conn, $userAgent;
private $credentials= null;

/** @param ?peer.HttpConnection $conn */
public function __construct($conn= null) {
$this->conn= $conn ?? new HttpConnection(self::DEFAULT_HOST);
$this->userAgent= sprintf(
'xp-aws/1.0.0 OS/%s/%s lang/php/%s',
php_uname('s'),
php_uname('r'),
PHP_VERSION
);
}

/** @return ?com.amazon.aws.Credentials */
public function credentials() {
if (null !== $this->credentials && !$this->credentials->expired()) return $this->credentials;

// Check AWS_CONTAINER_CREDENTIALS_*
if (null !== ($relative= Environment::variable('AWS_CONTAINER_CREDENTIALS_RELATIVE_URI', null))) {
$req= $this->conn->create(new HttpRequest());
$req->setTarget($relative);
} else if (null !== ($uri= Environment::variable('AWS_CONTAINER_CREDENTIALS_FULL_URI', null))) {
$req= new HttpRequest(new URL($uri));
} else {
return $this->credentials= null;
}

// Append authorization from AWS_CONTAINER_AUTHORIZATION_TOKEN_*, if existant
if (null !== ($file= Environment::variable('AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE', null))) {
$req->setHeader('Authorization', rtrim(file_get_contents($file), "\r\n"));
} else if (null !== ($token= Environment::variable('AWS_CONTAINER_AUTHORIZATION_TOKEN', null))) {
$req->setHeader('Authorization', $token);
}

$req->setHeader('User-Agent', $this->userAgent);
try {
$res= $this->conn->send($req);
} catch (Throwable $t) {
throw new IllegalStateException("Container credential provider {$req->getUrl()->getURL()} failed", $t);
}

if (200 !== $res->statusCode()) {
throw new IllegalStateException("Container credential provider {$req->getUrl()->getURL()} returned unexpected {$res->toString()}");
}

$credentials= Json::read(new StreamInput($res->in()));
return $this->credentials= new Credentials(
$credentials['AccessKeyId'],
$credentials['SecretAccessKey'],
$credentials['Token'] ?? null,
$credentials['Expiration'] ?? null
);
}
}
26 changes: 26 additions & 0 deletions src/main/php/com/amazon/aws/credentials/FromEnvironment.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php namespace com\amazon\aws\credentials;

use com\amazon\aws\Credentials;
use lang\Environment;
use util\Secret;

/**
* Loads credentials from the `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`,
* and (if present) `AWS_SESSION_TOKEN` environment variables
*
* @see https://docs.aws.amazon.com/sdkref/latest/guide/environment-variables.html
* @test com.amazon.aws.unittest.CredentialProviderTest
*/
class FromEnvironment implements Provider {

/** @return ?com.amazon.aws.Credentials */
public function credentials() {
if (null === ($accessKey= Environment::variable('AWS_ACCESS_KEY_ID', null))) return null;

return new Credentials(
$accessKey,
new Secret(Environment::variable('AWS_SECRET_ACCESS_KEY')),
Environment::variable('AWS_SESSION_TOKEN', null)
);
}
}
16 changes: 16 additions & 0 deletions src/main/php/com/amazon/aws/credentials/FromGiven.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php namespace com\amazon\aws\credentials;

use com\amazon\aws\Credentials;

/** @test com.amazon.aws.unittest.CredentialProviderTest */
class FromGiven implements Provider {
private $credentials;

public function __construct(Credentials $credentials) {
$this->credentials= $credentials;
}

/** @return ?com.amazon.aws.Credentials */
public function credentials() { return $this->credentials; }

}
8 changes: 8 additions & 0 deletions src/main/php/com/amazon/aws/credentials/Provider.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php namespace com\amazon\aws\credentials;

interface Provider {

/** @return ?com.amazon.aws.Credentials */
public function credentials();

}
Loading

0 comments on commit d493a7c

Please sign in to comment.