Skip to content

Commit

Permalink
feat: account id endpoint resolution support
Browse files Browse the repository at this point in the history
This change add account_id as part of the identity resolution, from the different credentials provider. It also validates whether an account should have been resolved based on the configure option account_id_endpoint_mode. The way this is done is by using lazy resolvers. We wrap the credentials provider into a custom lazy resolver that will avoid resolving credentials more than once, which means that in that credentials lazy resolver the value will be resolved once, and it will be returned everytime the credentials provider is consumed/invoked. For accountId builts-in, we also use a lazy resolver which holds the validation for wheter account_id value should have been resolved as part of the resolved identity. This accountId built-ins lazy resolver is resolved from endpoint resolution.
  • Loading branch information
yenfryherrerafeliz committed Feb 23, 2024
1 parent 76d1165 commit 89aec5e
Show file tree
Hide file tree
Showing 14 changed files with 331 additions and 24 deletions.
68 changes: 68 additions & 0 deletions src/AccountIdLazyResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

namespace Aws;

use Aws\Exception\AccountIdNotFoundException;
use GuzzleHttp\Promise\FulfilledPromise;
use GuzzleHttp\Promise\Promise;

/**
* @inheritDoc
*/
class AccountIdLazyResolver implements LazyResolver
{
const ACCOUNT_ID_ENDPOINT_MODE_DISABLED = 'disabled';
const ACCOUNT_ID_ENDPOINT_MODE_REQUIRED = 'required';
const ACCOUNT_ID_ENDPOINT_MODE_PREFERRED = 'preferred';

/**
* @var LazyResolver $credentialsProvider
*/
private $credentialsProvider;
/**
* @var string $accountIdEndpointMode
*/
private $accountIdEndpointMode;

public function __construct(LazyResolver $credentialsProvider, $accountIdEndpointMode)
{
$this->credentialsProvider = $credentialsProvider;
$this->accountIdEndpointMode = $accountIdEndpointMode;
}

/**
* @inheritDoc
*/
public function resolve(bool $force = false): mixed
{
$identity = $this->credentialsProvider->resolve();
$accountId = $identity->getAccountId();
if (empty($accountId)) {
$message = function ($mode) {
return "It is ${mode} to resolve an account id based on the 'account_id_endpoint_mode' configuration. \n- If you are using credentials from a shared ini file, please make sure you have configured the property aws_account_id. \n- If you are using credentials defined in environment variables please make sure you have set AWS_ACCOUNT_ID. \n- Otherwise, if you are supplying credentials as part of client constructor parameters, please make sure you have set the property account_id.\n If you prefer to not use account id endpoint resolution then, please make account_id_endpoint_mode to be disabled by either providing it explicitly in the client, defining a config property in your shared config file account_id_endpoint_mode, or by setting an environment variable called AWS_ACCOUNT_ID_ENDPOINT_MODE, and the value for any of those source should be 'disabled' if the desire is to disable this behavior.";
};

switch ($this->accountIdEndpointMode) {
case self::ACCOUNT_ID_ENDPOINT_MODE_REQUIRED:
throw new AccountIdNotFoundException($message('required'));
case self::ACCOUNT_ID_ENDPOINT_MODE_PREFERRED:
trigger_error($message('preferred'), E_USER_WARNING);
return null;
case self::ACCOUNT_ID_ENDPOINT_MODE_DISABLED:
return null;
default:
throw new \RuntimeException("Unrecognized account_id_endpoint_mode value " . $this->accountIdEndpointMode."\n Valid Values are: [" . implode(', ', [self::ACCOUNT_ID_ENDPOINT_MODE_DISABLED, self::ACCOUNT_ID_ENDPOINT_MODE_PREFERRED, self::ACCOUNT_ID_ENDPOINT_MODE_REQUIRED]) . "]");
}
}

return $accountId;
}

/**
* @inheritDoc
*/
public function isResolved(): bool
{
return true;
}
}
8 changes: 5 additions & 3 deletions src/AwsClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -224,11 +224,11 @@ public function __construct(array $args)
$this->api = $config['api'];
$this->signatureProvider = $config['signature_provider'];
$this->endpoint = new Uri($config['endpoint']);
$this->credentialProvider = $config['credentials'];
$this->credentialProvider = new CredentialsLazyResolver($config['credentials']);
$this->tokenProvider = $config['token'];
$this->region = isset($config['region']) ? $config['region'] : null;
$this->config = $config['config'];
$this->setClientBuiltIns($args);
$this->setClientBuiltIns($args, $config);
$this->clientContextParams = $this->setClientContextParams($args);
$this->defaultRequestOptions = $config['http'];
$this->endpointProvider = $config['endpoint_provider'];
Expand Down Expand Up @@ -558,7 +558,7 @@ private function setClientContextParams($args)
/**
* Retrieves and sets default values used for endpoint resolution.
*/
private function setClientBuiltIns($args)
private function setClientBuiltIns($args, $clientConfig)
{
$builtIns = [];
$config = $this->getConfig();
Expand All @@ -582,6 +582,8 @@ private function setClientBuiltIns($args)
$builtIns['AWS::S3::ForcePathStyle'] = $config['use_path_style_endpoint'];
$builtIns['AWS::S3::DisableMultiRegionAccessPoints'] = $config['disable_multiregion_access_points'];
}
$builtIns['AWS::Auth::AccountId'] = new AccountIdLazyResolver($this->credentialProvider, $clientConfig['account_id_endpoint_mode']);

$this->clientBuiltIns += $builtIns;
}

Expand Down
27 changes: 25 additions & 2 deletions src/ClientResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,13 @@ class ClientResolver
'doc' => 'Set to false to disable checking for shared aws config files usually located in \'~/.aws/config\' and \'~/.aws/credentials\'. This will be ignored if you set the \'profile\' setting.',
'default' => true,
],
'account_id_endpoint_mode' => [
'type' => 'value',
'valid' => ['string'],
'doc' => 'To decide whether account_id must a be a required resolved credentials parameter. If this configuration is set to disabled, then account_id is not required. If set to preferred a warning will be logged when account_id is not resolved, and when set to required an exception will be thrown if account_id is not resolved.',
'default' => '',
'fn' => [__CLASS__, '_apply_account_id_endpoint_mode']
],
];

/**
Expand Down Expand Up @@ -600,7 +607,8 @@ public static function _apply_credentials($value, array &$args)
$value['key'],
$value['secret'],
isset($value['token']) ? $value['token'] : null,
isset($value['expires']) ? $value['expires'] : null
isset($value['expires']) ? $value['expires'] : null,
isset($value['accountId']) ? $value['accountId'] : null
)
);
} elseif ($value === false) {
Expand Down Expand Up @@ -1039,6 +1047,21 @@ public static function _apply_idempotency_auto_fill(
}
}

public static function _apply_account_id_endpoint_mode($value, array &$args)
{
$accountIdEndpointMode = $value;
if (empty($accountIdEndpointMode)) {
$accountIdEndpointMode = ConfigurationResolver::resolve(
'account_id_endpoint_mode',
'preferred',
'string',
['use_aws_shared_config_files' => true]
);
}

$args['account_id_endpoint_mode'] = $accountIdEndpointMode;
}

public static function _default_endpoint_provider(array $args)
{
$service = isset($args['api']) ? $args['api'] : null;
Expand Down Expand Up @@ -1187,7 +1210,7 @@ public static function _default_endpoint(array &$args)

return $value;
}

public static function _apply_region($value, array &$args)
{
if (empty($value)) {
Expand Down
41 changes: 35 additions & 6 deletions src/Credentials/CredentialProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class CredentialProvider
const ENV_PROFILE = 'AWS_PROFILE';
const ENV_ROLE_SESSION_NAME = 'AWS_ROLE_SESSION_NAME';
const ENV_SECRET = 'AWS_SECRET_ACCESS_KEY';
const ENV_ACCOUNT_ID = 'AWS_ACCOUNT_ID';
const ENV_SESSION = 'AWS_SESSION_TOKEN';
const ENV_TOKEN_FILE = 'AWS_WEB_IDENTITY_TOKEN_FILE';
const ENV_SHARED_CREDENTIALS_FILE = 'AWS_SHARED_CREDENTIALS_FILE';
Expand Down Expand Up @@ -291,9 +292,20 @@ public static function env()
// Use credentials from environment variables, if available
$key = getenv(self::ENV_KEY);
$secret = getenv(self::ENV_SECRET);
$accountId = getenv(self::ENV_ACCOUNT_ID);
if ($accountId === false) {
$accountId = null;
}

if ($key && $secret) {
return Promise\Create::promiseFor(
new Credentials($key, $secret, getenv(self::ENV_SESSION) ?: NULL)
new Credentials(
$key,
$secret,
getenv(self::ENV_SESSION) ?: NULL,
null,
$accountId
)
);
}

Expand Down Expand Up @@ -537,11 +549,18 @@ public static function ini($profile = null, $filename = null, array $config = []
: null;
}

$accountId = null;
if (!empty($data[$profile]['aws_account_id'])) {
$accountId = $data[$profile]['aws_account_id'];
}

return Promise\Create::promiseFor(
new Credentials(
$data[$profile]['aws_access_key_id'],
$data[$profile]['aws_secret_access_key'],
$data[$profile]['aws_session_token']
$data[$profile]['aws_session_token'],
null,
$accountId
)
);
};
Expand Down Expand Up @@ -616,12 +635,20 @@ public static function process($profile = null, $filename = null)
$processData['SessionToken'] = null;
}

$accountId = null;
if (!empty($processData['AccountId'])) {
$accountId = $processData['AccountId'];
} elseif (!empty($data[$profile]['aws_account_id'])) {
$accountId = $data[$profile]['aws_account_id'];
}

return Promise\Create::promiseFor(
new Credentials(
$processData['AccessKeyId'],
$processData['SecretAccessKey'],
$processData['SessionToken'],
$expires
$expires,
$accountId
)
);
};
Expand Down Expand Up @@ -704,8 +731,8 @@ private static function loadRoleProfile(
'RoleArn' => $roleArn,
'RoleSessionName' => $roleSessionName
]);

$credentials = $stsClient->createCredentials($result);

return Promise\Create::promiseFor($credentials);
}

Expand Down Expand Up @@ -897,7 +924,8 @@ private static function getSsoCredentials($profiles, $ssoProfileName, $filename,
$ssoCredentials['accessKeyId'],
$ssoCredentials['secretAccessKey'],
$ssoCredentials['sessionToken'],
$expiration
$expiration,
$ssoProfile['sso_account_id']
)
);
}
Expand Down Expand Up @@ -956,7 +984,8 @@ private static function getSsoCredentialsLegacy($profiles, $ssoProfileName, $fil
$ssoCredentials['accessKeyId'],
$ssoCredentials['secretAccessKey'],
$ssoCredentials['sessionToken'],
$expiration
$expiration,
$ssoProfile['sso_account_id']
)
);
}
Expand Down
16 changes: 13 additions & 3 deletions src/Credentials/Credentials.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class Credentials implements CredentialsInterface, \Serializable
private $secret;
private $token;
private $expires;
private $accountId;

/**
* Constructs a new BasicAWSCredentials object, with the specified AWS
Expand All @@ -21,12 +22,13 @@ class Credentials implements CredentialsInterface, \Serializable
* @param string $token Security token to use
* @param int $expires UNIX timestamp for when credentials expire
*/
public function __construct($key, $secret, $token = null, $expires = null)
public function __construct($key, $secret, $token = null, $expires = null, $accountId = null)
{
$this->key = trim($key);
$this->secret = trim($secret);
$this->token = $token;
$this->expires = $expires;
$this->accountId = $accountId;
}

public static function __set_state(array $state)
Expand All @@ -35,7 +37,8 @@ public static function __set_state(array $state)
$state['key'],
$state['secret'],
$state['token'],
$state['expires']
$state['expires'],
$state['accountId']
);
}

Expand Down Expand Up @@ -64,13 +67,19 @@ public function isExpired()
return $this->expires !== null && time() >= $this->expires;
}

public function getAccountId()
{
return $this->accountId;
}

public function toArray()
{
return [
'key' => $this->key,
'secret' => $this->secret,
'token' => $this->token,
'expires' => $this->expires
'expires' => $this->expires,
'accountId' => $this->accountId
];
}

Expand All @@ -97,6 +106,7 @@ public function __unserialize($data)
$this->secret = $data['secret'];
$this->token = $data['token'];
$this->expires = $data['expires'];
$this->accountId = $data['accountId'];
}

/**
Expand Down
4 changes: 4 additions & 0 deletions src/Credentials/CredentialsInterface.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<?php
namespace Aws\Credentials;

use Symfony\Contracts\Translation\TranslatorTrait;

/**
* Provides access to the AWS credentials used for accessing AWS services: AWS
* access key ID, secret access key, and security token. These credentials are
Expand Down Expand Up @@ -49,4 +51,6 @@ public function isExpired();
* @return array
*/
public function toArray();

public function getAccountId();
}
3 changes: 2 additions & 1 deletion src/Credentials/EcsCredentialProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ public function __invoke()
$result['AccessKeyId'],
$result['SecretAccessKey'],
$result['Token'],
strtotime($result['Expiration'])
strtotime($result['Expiration']),
$result['AccountId']
);
})->otherwise(function ($reason) {
$reason = is_array($reason) ? $reason['exception'] : $reason;
Expand Down
8 changes: 7 additions & 1 deletion src/Credentials/InstanceProfileProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -216,11 +216,17 @@ public function __invoke($previousCredentials = null)
if (!isset($result)) {
$credentials = $previousCredentials;
} else {
$accountId = null;
if (!empty($result['AccountId'])) {
$accountId = $result['AccountId'];
}

$credentials = new Credentials(
$result['AccessKeyId'],
$result['SecretAccessKey'],
$result['Token'],
strtotime($result['Expiration'])
strtotime($result['Expiration']),
$accountId
);
}

Expand Down
Loading

0 comments on commit 89aec5e

Please sign in to comment.