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, and when this option is not disabled then, it prepends a middleware that resolves the identity, from the provided credential provider and, it validates the account id based on the account_id_endpoint_mode option. When an identity is resolved by this middleware then, this identity is carry over in a command property bag called @context, which is then reused by the signer middleware, and any other middleware that needs to resolve identity. This is done for avoiding having to resolve identity multiple times in a single request.
The property is: $command['@context']['resolved_identity'];
  • Loading branch information
yenfryherrerafeliz committed Feb 22, 2024
1 parent 76d1165 commit 0876457
Show file tree
Hide file tree
Showing 34 changed files with 1,651 additions and 20 deletions.
79 changes: 79 additions & 0 deletions src/AccountIdEndpointMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php
namespace Aws;

use Aws\Exception\CredentialsException;

/**
* This middleware class resolves the identity from a credentials provider callable function
* and determine whether an account should have been resolved. When this middleware resolves
* identity then, the identity is included in the $command context bag property "$command['@context]"
* as resolved_identity. Example $command['@context]['resolved_identity'] = $resolvedIdentity, and
* when this property is set then, the signer middleware gives preference to use that resolved identity
* instead of resolving the provided credentials provider by the client. This is done to avoid having to
* resolve credentials more than once per request.
*/
class AccountIdEndpointMiddleware
{
/**
* @var callable $nextHandler
*/
private $nextHandler;
/**
* @var string $accountIdEndpointMode
*/
private $accountIdEndpointMode;
/**
* @var callable $credentialsProvider
*/
private $credentialsProvider;

/**
* @param callable $nextHandler
* @param string $accountIdEndpointMode
* @param callable $credentialsProvider
*/
public function __construct($nextHandler, $accountIdEndpointMode, $credentialsProvider)
{
$this->nextHandler = $nextHandler;
$this->accountIdEndpointMode = $accountIdEndpointMode;
$this->credentialsProvider = $credentialsProvider;
}

/**
* This method wraps a new instance of the AccountIdEndpointMiddleware.
*
* @param string $accountIddEndpointMode
* @param callable $credentialsProvider
* @return callable
*/
public static function wrap($accountIddEndpointMode, $credentialsProvider): callable
{
return function (callable $handler) use ($accountIddEndpointMode, $credentialsProvider) {
return new self($handler, $accountIddEndpointMode, $credentialsProvider);
};
}

public function __invoke($command)
{
$nextHandler = $this->nextHandler;
$fnCredentialsProvider = $this->credentialsProvider;
$resolvedIdentity = $fnCredentialsProvider()->wait();
if (empty($resolvedIdentity->getAccountId())) {
$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 'required':
throw new CredentialsException($message('required'));
case 'preferred':
error_log('Warning: ' . $message('preferred'), E_WARNING|E_NOTICE);
break;
}
}

$command['@context']['resolved_identity'] = $resolvedIdentity;

return $nextHandler($command);
}

}
18 changes: 18 additions & 0 deletions src/AwsClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ public function __construct(array $args)
$this->addEndpointV2Middleware();
}

$this->addAccountIdEndpointMiddleware($config, $args);

if (!is_null($this->api->getMetadata('awsQueryCompatible'))) {
$this->addQueryCompatibleInputMiddleware($this->api);
}
Expand Down Expand Up @@ -534,6 +536,22 @@ private function addEndpointV2Middleware()
);
}

private function addAccountIdEndpointMiddleware(array $config, array $args)
{
$accountIdEndpointMode = !empty($args['account_id_endpoint_mode'])
? $args['account_id_endpoint_mode']
: $config['account_id_endpoint_mode'];
if ($accountIdEndpointMode !== 'disabled') {
$this->handlerList->prependBuild(
AccountIdEndpointMiddleware::wrap(
$accountIdEndpointMode,
$this->credentialProvider
),
'account_id_endpoint-middleware'
);
}
}

/**
* Retrieves client context param definition from service model,
* creates mapping of client context param names with client-provided
Expand Down
33 changes: 32 additions & 1 deletion src/ClientResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,12 @@ 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' => [__CLASS__, '_default_account_id_endpoint_mode']
],
];

/**
Expand Down Expand Up @@ -1039,6 +1045,31 @@ public static function _apply_idempotency_auto_fill(
}
}

/**
* This function prepend a middleware that resolves credentials
* and that validates whether account_id should have been present
* as part of the resolved identity. We assume that at this point
* the default credentials has been applied to the configuration.
*
* @param array $args is the client arguments
* @return void
*/
public static function _default_account_id_endpoint_mode(
array $args
) {
$accountIdEndpointMode = $args['account_id_endpoint_mode'];
if (empty($accountIdEndpointMode)) {
$accountIdEndpointMode = ConfigurationResolver::resolve(
'account_id_endpoint_mode',
'preferred',
'string',
['use_aws_shared_config_files' => true]
);
}

return $accountIdEndpointMode;
}

public static function _default_endpoint_provider(array $args)
{
$service = isset($args['api']) ? $args['api'] : null;
Expand Down Expand Up @@ -1187,7 +1218,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
18 changes: 15 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,21 @@ 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->expires
];
}

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

/**
Expand Down
2 changes: 2 additions & 0 deletions src/Credentials/CredentialsInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,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
9 changes: 8 additions & 1 deletion src/EndpointV2/EndpointV2Middleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public function __invoke(CommandInterface $command)
$nextHandler = $this->nextHandler;
$operation = $this->api->getOperation($command->getName());
$commandArgs = $command->toArray();

$this->appendAccountIdParameter($commandArgs);
$providerArgs = $this->resolveArgs($commandArgs, $operation);
$endpoint = $this->endpointProvider->resolveEndpoint($providerArgs);

Expand Down Expand Up @@ -303,4 +303,11 @@ private function normalizeAuthScheme(array $authScheme) : array

return $normalizedAuthScheme;
}

private function appendAccountIdParameter(&$commandArgs)
{
if (isset($commandArgs['@context']['resolved_identity'])) {
$commandArgs['AWS::Auth::AccountId'] = $commandArgs['@context']['resolved_identity']->getAccountId();
}
}
}
Loading

0 comments on commit 0876457

Please sign in to comment.