-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(NODE-6040): extract aws temporary credential acquisition log…
…ic into a standalone module (#4050)
- Loading branch information
1 parent
d94439f
commit 918fe69
Showing
5 changed files
with
194 additions
and
130 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
import { type AWSCredentials, getAwsCredentialProvider } from '../../deps'; | ||
import { MongoAWSError } from '../../error'; | ||
import { request } from '../../utils'; | ||
|
||
const AWS_RELATIVE_URI = 'http://169.254.170.2'; | ||
const AWS_EC2_URI = 'http://169.254.169.254'; | ||
const AWS_EC2_PATH = '/latest/meta-data/iam/security-credentials'; | ||
|
||
/** | ||
* @internal | ||
* This interface matches the final result of fetching temporary credentials manually, outlined | ||
* in the spec [here](https://github.com/mongodb/specifications/blob/master/source/auth/auth.md#ec2-endpoint). | ||
* | ||
* When we use the AWS SDK, we map the response from the SDK to conform to this interface. | ||
*/ | ||
export interface AWSTempCredentials { | ||
AccessKeyId?: string; | ||
SecretAccessKey?: string; | ||
Token?: string; | ||
RoleArn?: string; | ||
Expiration?: Date; | ||
} | ||
|
||
/** | ||
* @internal | ||
* | ||
* Fetches temporary AWS credentials. | ||
*/ | ||
export abstract class AWSTemporaryCredentialProvider { | ||
abstract getCredentials(): Promise<AWSTempCredentials>; | ||
private static _awsSDK: ReturnType<typeof getAwsCredentialProvider>; | ||
protected static get awsSDK() { | ||
AWSTemporaryCredentialProvider._awsSDK ??= getAwsCredentialProvider(); | ||
return AWSTemporaryCredentialProvider._awsSDK; | ||
} | ||
|
||
static get isAWSSDKInstalled(): boolean { | ||
return !('kModuleError' in AWSTemporaryCredentialProvider.awsSDK); | ||
} | ||
} | ||
|
||
/** @internal */ | ||
export class AWSSDKCredentialProvider extends AWSTemporaryCredentialProvider { | ||
private _provider?: () => Promise<AWSCredentials>; | ||
/** | ||
* The AWS SDK caches credentials automatically and handles refresh when the credentials have expired. | ||
* To ensure this occurs, we need to cache the `provider` returned by the AWS sdk and re-use it when fetching credentials. | ||
*/ | ||
private get provider(): () => Promise<AWSCredentials> { | ||
if ('kModuleError' in AWSTemporaryCredentialProvider.awsSDK) { | ||
throw AWSTemporaryCredentialProvider.awsSDK.kModuleError; | ||
} | ||
if (this._provider) { | ||
return this._provider; | ||
} | ||
let { AWS_STS_REGIONAL_ENDPOINTS = '', AWS_REGION = '' } = process.env; | ||
AWS_STS_REGIONAL_ENDPOINTS = AWS_STS_REGIONAL_ENDPOINTS.toLowerCase(); | ||
AWS_REGION = AWS_REGION.toLowerCase(); | ||
|
||
/** The option setting should work only for users who have explicit settings in their environment, the driver should not encode "defaults" */ | ||
const awsRegionSettingsExist = | ||
AWS_REGION.length !== 0 && AWS_STS_REGIONAL_ENDPOINTS.length !== 0; | ||
|
||
/** | ||
* The following regions use the global AWS STS endpoint, sts.amazonaws.com, by default | ||
* https://docs.aws.amazon.com/sdkref/latest/guide/feature-sts-regionalized-endpoints.html | ||
*/ | ||
const LEGACY_REGIONS = new Set([ | ||
'ap-northeast-1', | ||
'ap-south-1', | ||
'ap-southeast-1', | ||
'ap-southeast-2', | ||
'aws-global', | ||
'ca-central-1', | ||
'eu-central-1', | ||
'eu-north-1', | ||
'eu-west-1', | ||
'eu-west-2', | ||
'eu-west-3', | ||
'sa-east-1', | ||
'us-east-1', | ||
'us-east-2', | ||
'us-west-1', | ||
'us-west-2' | ||
]); | ||
/** | ||
* If AWS_STS_REGIONAL_ENDPOINTS is set to regional, users are opting into the new behavior of respecting the region settings | ||
* | ||
* If AWS_STS_REGIONAL_ENDPOINTS is set to legacy, then "old" regions need to keep using the global setting. | ||
* Technically the SDK gets this wrong, it reaches out to 'sts.us-east-1.amazonaws.com' when it should be 'sts.amazonaws.com'. | ||
* That is not our bug to fix here. We leave that up to the SDK. | ||
*/ | ||
const useRegionalSts = | ||
AWS_STS_REGIONAL_ENDPOINTS === 'regional' || | ||
(AWS_STS_REGIONAL_ENDPOINTS === 'legacy' && !LEGACY_REGIONS.has(AWS_REGION)); | ||
|
||
this._provider = | ||
awsRegionSettingsExist && useRegionalSts | ||
? AWSTemporaryCredentialProvider.awsSDK.fromNodeProviderChain({ | ||
clientConfig: { region: AWS_REGION } | ||
}) | ||
: AWSTemporaryCredentialProvider.awsSDK.fromNodeProviderChain(); | ||
|
||
return this._provider; | ||
} | ||
|
||
override async getCredentials(): Promise<AWSTempCredentials> { | ||
/* | ||
* Creates a credential provider that will attempt to find credentials from the | ||
* following sources (listed in order of precedence): | ||
* | ||
* - Environment variables exposed via process.env | ||
* - SSO credentials from token cache | ||
* - Web identity token credentials | ||
* - Shared credentials and config ini files | ||
* - The EC2/ECS Instance Metadata Service | ||
*/ | ||
try { | ||
const creds = await this.provider(); | ||
return { | ||
AccessKeyId: creds.accessKeyId, | ||
SecretAccessKey: creds.secretAccessKey, | ||
Token: creds.sessionToken, | ||
Expiration: creds.expiration | ||
}; | ||
} catch (error) { | ||
throw new MongoAWSError(error.message, { cause: error }); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* @internal | ||
* Fetches credentials manually (without the AWS SDK), as outlined in the [Obtaining Credentials](https://github.com/mongodb/specifications/blob/master/source/auth/auth.md#obtaining-credentials) | ||
* section of the Auth spec. | ||
*/ | ||
export class LegacyAWSTemporaryCredentialProvider extends AWSTemporaryCredentialProvider { | ||
override async getCredentials(): Promise<AWSTempCredentials> { | ||
// If the environment variable AWS_CONTAINER_CREDENTIALS_RELATIVE_URI | ||
// is set then drivers MUST assume that it was set by an AWS ECS agent | ||
if (process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI) { | ||
return request(`${AWS_RELATIVE_URI}${process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI}`); | ||
} | ||
|
||
// Otherwise assume we are on an EC2 instance | ||
|
||
// get a token | ||
const token = await request(`${AWS_EC2_URI}/latest/api/token`, { | ||
method: 'PUT', | ||
json: false, | ||
headers: { 'X-aws-ec2-metadata-token-ttl-seconds': 30 } | ||
}); | ||
|
||
// get role name | ||
const roleName = await request(`${AWS_EC2_URI}/${AWS_EC2_PATH}`, { | ||
json: false, | ||
headers: { 'X-aws-ec2-metadata-token': token } | ||
}); | ||
|
||
// get temp credentials | ||
const creds = await request(`${AWS_EC2_URI}/${AWS_EC2_PATH}/${roleName}`, { | ||
headers: { 'X-aws-ec2-metadata-token': token } | ||
}); | ||
|
||
return creds; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.