12
12
// See the License for the specific language governing permissions and
13
13
// limitations under the License.
14
14
15
- import { GaxiosOptions } from 'gaxios' ;
16
-
17
15
import { AwsRequestSigner , AwsSecurityCredentials } from './awsrequestsigner' ;
18
16
import {
19
17
BaseExternalAccountClient ,
20
18
BaseExternalAccountClientOptions ,
19
+ ExternalAccountSupplierContext ,
21
20
} from './baseexternalclient' ;
22
- import { Headers } from './oauth2client' ;
23
21
import { AuthClientOptions } from './authclient' ;
22
+ import { DefaultAwsSecurityCredentialsSupplier } from './defaultawssecuritycredentialssupplier' ;
24
23
25
24
/**
26
25
* AWS credentials JSON interface. This is used for AWS workloads.
@@ -47,16 +46,34 @@ export interface AwsClientOptions extends BaseExternalAccountClientOptions {
47
46
}
48
47
49
48
/**
50
- * Interface defining the AWS security-credentials endpoint response.
49
+ * Supplier interface for AWS security credentials. This can be implemented to
50
+ * return an AWS region and AWS security credentials. These credentials can
51
+ * then be exchanged for a GCP token by an {@link AwsClient}.
51
52
*/
52
- interface AwsSecurityCredentialsResponse {
53
- Code : string ;
54
- LastUpdated : string ;
55
- Type : string ;
56
- AccessKeyId : string ;
57
- SecretAccessKey : string ;
58
- Token : string ;
59
- Expiration : string ;
53
+ export interface AwsSecurityCredentialsSupplier {
54
+ /**
55
+ * Gets the active AWS region.
56
+ * @param context {@link ExternalAccountSupplierContext } from the calling
57
+ * {@link AwsClient}, contains the requested audience and subject token type
58
+ * for the external account identity as well as the transport from the
59
+ * calling client to use for requests.
60
+ * @return A promise that resolves with the AWS region string.
61
+ */
62
+ getAwsRegion : ( context : ExternalAccountSupplierContext ) => Promise < string > ;
63
+
64
+ /**
65
+ * Gets valid AWS security credentials for the requested external account
66
+ * identity. Note that these are not cached by the calling {@link AwsClient},
67
+ * so caching should be including in the implementation.
68
+ * @param context {@link ExternalAccountSupplierContext } from the calling
69
+ * {@link AwsClient}, contains the requested audience and subject token type
70
+ * for the external account identity as well as the transport from the
71
+ * calling client to use for requests.
72
+ * @return A promise that resolves with the requested {@link AwsSecurityCredentials}.
73
+ */
74
+ getAwsSecurityCredentials : (
75
+ context : ExternalAccountSupplierContext
76
+ ) => Promise < AwsSecurityCredentials > ;
60
77
}
61
78
62
79
/**
@@ -66,14 +83,18 @@ interface AwsSecurityCredentialsResponse {
66
83
*/
67
84
export class AwsClient extends BaseExternalAccountClient {
68
85
private readonly environmentId : string ;
69
- private readonly regionUrl ?: string ;
70
- private readonly securityCredentialsUrl ?: string ;
86
+ private readonly awsSecurityCredentialsSupplier : AwsSecurityCredentialsSupplier ;
71
87
private readonly regionalCredVerificationUrl : string ;
72
- private readonly imdsV2SessionTokenUrl ?: string ;
73
88
private awsRequestSigner : AwsRequestSigner | null ;
74
89
private region : string ;
75
90
91
+ /**
92
+ * @deprecated AWS client no validates the EC2 metadata address.
93
+ **/
76
94
static AWS_EC2_METADATA_IPV4_ADDRESS = '169.254.169.254' ;
95
+ /**
96
+ * @deprecated AWS client no validates the EC2 metadata address.
97
+ **/
77
98
static AWS_EC2_METADATA_IPV6_ADDRESS = 'fd00:ec2::254' ;
78
99
79
100
/**
@@ -95,14 +116,21 @@ export class AwsClient extends BaseExternalAccountClient {
95
116
this . environmentId = options . credential_source . environment_id ;
96
117
// This is only required if the AWS region is not available in the
97
118
// AWS_REGION or AWS_DEFAULT_REGION environment variables.
98
- this . regionUrl = options . credential_source . region_url ;
119
+ const regionUrl = options . credential_source . region_url ;
99
120
// This is only required if AWS security credentials are not available in
100
121
// environment variables.
101
- this . securityCredentialsUrl = options . credential_source . url ;
122
+ const securityCredentialsUrl = options . credential_source . url ;
123
+ const imdsV2SessionTokenUrl =
124
+ options . credential_source . imdsv2_session_token_url ;
125
+ this . awsSecurityCredentialsSupplier =
126
+ new DefaultAwsSecurityCredentialsSupplier ( {
127
+ regionUrl : regionUrl ,
128
+ securityCredentialsUrl : securityCredentialsUrl ,
129
+ imdsV2SessionTokenUrl : imdsV2SessionTokenUrl ,
130
+ } ) ;
131
+
102
132
this . regionalCredVerificationUrl =
103
133
options . credential_source . regional_cred_verification_url ;
104
- this . imdsV2SessionTokenUrl =
105
- options . credential_source . imdsv2_session_token_url ;
106
134
this . awsRequestSigner = null ;
107
135
this . region = '' ;
108
136
this . credentialSourceType = 'aws' ;
@@ -124,68 +152,22 @@ export class AwsClient extends BaseExternalAccountClient {
124
152
125
153
/**
126
154
* Triggered when an external subject token is needed to be exchanged for a
127
- * GCP access token via GCP STS endpoint.
128
- * This uses the `options.credential_source` object to figure out how
129
- * to retrieve the token using the current environment. In this case,
130
- * this uses a serialized AWS signed request to the STS GetCallerIdentity
131
- * endpoint.
132
- * The logic is summarized as:
133
- * 1. If imdsv2_session_token_url is provided in the credential source, then
134
- * fetch the aws session token and include it in the headers of the
135
- * metadata requests. This is a requirement for IDMSv2 but optional
136
- * for IDMSv1.
137
- * 2. Retrieve AWS region from availability-zone.
138
- * 3a. Check AWS credentials in environment variables. If not found, get
139
- * from security-credentials endpoint.
140
- * 3b. Get AWS credentials from security-credentials endpoint. In order
141
- * to retrieve this, the AWS role needs to be determined by calling
142
- * security-credentials endpoint without any argument. Then the
143
- * credentials can be retrieved via: security-credentials/role_name
144
- * 4. Generate the signed request to AWS STS GetCallerIdentity action.
145
- * 5. Inject x-goog-cloud-target-resource into header and serialize the
146
- * signed request. This will be the subject-token to pass to GCP STS.
155
+ * GCP access token via GCP STS endpoint. This will call the
156
+ * {@link AwsSecurityCredentialsSupplier} to retrieve an AWS region and AWS
157
+ * Security Credentials, then use them to create a signed AWS STS request that
158
+ * can be exchanged for a GCP access token.
147
159
* @return A promise that resolves with the external subject token.
148
160
*/
149
161
async retrieveSubjectToken ( ) : Promise < string > {
150
162
// Initialize AWS request signer if not already initialized.
151
163
if ( ! this . awsRequestSigner ) {
152
- const metadataHeaders : Headers = { } ;
153
- // Only retrieve the IMDSv2 session token if both the security credentials and region are
154
- // not retrievable through the environment.
155
- // The credential config contains all the URLs by default but clients may be running this
156
- // where the metadata server is not available and returning the credentials through the environment.
157
- // Removing this check may break them.
158
- if ( ! this . regionFromEnv && this . imdsV2SessionTokenUrl ) {
159
- metadataHeaders [ 'x-aws-ec2-metadata-token' ] =
160
- await this . getImdsV2SessionToken ( ) ;
161
- }
162
-
163
- this . region = await this . getAwsRegion ( metadataHeaders ) ;
164
+ this . region = await this . awsSecurityCredentialsSupplier . getAwsRegion (
165
+ this . supplierContext
166
+ ) ;
164
167
this . awsRequestSigner = new AwsRequestSigner ( async ( ) => {
165
- // Check environment variables for permanent credentials first.
166
- // https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html
167
- if ( this . securityCredentialsFromEnv ) {
168
- return this . securityCredentialsFromEnv ;
169
- }
170
- if ( this . imdsV2SessionTokenUrl ) {
171
- metadataHeaders [ 'x-aws-ec2-metadata-token' ] =
172
- await this . getImdsV2SessionToken ( ) ;
173
- }
174
- // Since the role on a VM can change, we don't need to cache it.
175
- const roleName = await this . getAwsRoleName ( metadataHeaders ) ;
176
- // Temporary credentials typically last for several hours.
177
- // Expiration is returned in response.
178
- // Consider future optimization of this logic to cache AWS tokens
179
- // until their natural expiration.
180
- const awsCreds = await this . getAwsSecurityCredentials (
181
- roleName ,
182
- metadataHeaders
168
+ return this . awsSecurityCredentialsSupplier . getAwsSecurityCredentials (
169
+ this . supplierContext
183
170
) ;
184
- return {
185
- accessKeyId : awsCreds . AccessKeyId ,
186
- secretAccessKey : awsCreds . SecretAccessKey ,
187
- token : awsCreds . Token ,
188
- } ;
189
171
} , this . region ) ;
190
172
}
191
173
@@ -234,112 +216,4 @@ export class AwsClient extends BaseExternalAccountClient {
234
216
} )
235
217
) ;
236
218
}
237
-
238
- /**
239
- * @return A promise that resolves with the IMDSv2 Session Token.
240
- */
241
- private async getImdsV2SessionToken ( ) : Promise < string > {
242
- const opts : GaxiosOptions = {
243
- url : this . imdsV2SessionTokenUrl ,
244
- method : 'PUT' ,
245
- responseType : 'text' ,
246
- headers : { 'x-aws-ec2-metadata-token-ttl-seconds' : '300' } ,
247
- } ;
248
- const response = await this . transporter . request < string > ( opts ) ;
249
- return response . data ;
250
- }
251
-
252
- /**
253
- * @param headers The headers to be used in the metadata request.
254
- * @return A promise that resolves with the current AWS region.
255
- */
256
- private async getAwsRegion ( headers : Headers ) : Promise < string > {
257
- // Priority order for region determination:
258
- // AWS_REGION > AWS_DEFAULT_REGION > metadata server.
259
- if ( this . regionFromEnv ) {
260
- return this . regionFromEnv ;
261
- }
262
- if ( ! this . regionUrl ) {
263
- throw new Error (
264
- 'Unable to determine AWS region due to missing ' +
265
- '"options.credential_source.region_url"'
266
- ) ;
267
- }
268
- const opts : GaxiosOptions = {
269
- url : this . regionUrl ,
270
- method : 'GET' ,
271
- responseType : 'text' ,
272
- headers : headers ,
273
- } ;
274
- const response = await this . transporter . request < string > ( opts ) ;
275
- // Remove last character. For example, if us-east-2b is returned,
276
- // the region would be us-east-2.
277
- return response . data . substr ( 0 , response . data . length - 1 ) ;
278
- }
279
-
280
- /**
281
- * @param headers The headers to be used in the metadata request.
282
- * @return A promise that resolves with the assigned role to the current
283
- * AWS VM. This is needed for calling the security-credentials endpoint.
284
- */
285
- private async getAwsRoleName ( headers : Headers ) : Promise < string > {
286
- if ( ! this . securityCredentialsUrl ) {
287
- throw new Error (
288
- 'Unable to determine AWS role name due to missing ' +
289
- '"options.credential_source.url"'
290
- ) ;
291
- }
292
- const opts : GaxiosOptions = {
293
- url : this . securityCredentialsUrl ,
294
- method : 'GET' ,
295
- responseType : 'text' ,
296
- headers : headers ,
297
- } ;
298
- const response = await this . transporter . request < string > ( opts ) ;
299
- return response . data ;
300
- }
301
-
302
- /**
303
- * Retrieves the temporary AWS credentials by calling the security-credentials
304
- * endpoint as specified in the `credential_source` object.
305
- * @param roleName The role attached to the current VM.
306
- * @param headers The headers to be used in the metadata request.
307
- * @return A promise that resolves with the temporary AWS credentials
308
- * needed for creating the GetCallerIdentity signed request.
309
- */
310
- private async getAwsSecurityCredentials (
311
- roleName : string ,
312
- headers : Headers
313
- ) : Promise < AwsSecurityCredentialsResponse > {
314
- const response =
315
- await this . transporter . request < AwsSecurityCredentialsResponse > ( {
316
- url : `${ this . securityCredentialsUrl } /${ roleName } ` ,
317
- responseType : 'json' ,
318
- headers : headers ,
319
- } ) ;
320
- return response . data ;
321
- }
322
-
323
- private get regionFromEnv ( ) : string | null {
324
- // The AWS region can be provided through AWS_REGION or AWS_DEFAULT_REGION.
325
- // Only one is required.
326
- return (
327
- process . env [ 'AWS_REGION' ] || process . env [ 'AWS_DEFAULT_REGION' ] || null
328
- ) ;
329
- }
330
-
331
- private get securityCredentialsFromEnv ( ) : AwsSecurityCredentials | null {
332
- // Both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are required.
333
- if (
334
- process . env [ 'AWS_ACCESS_KEY_ID' ] &&
335
- process . env [ 'AWS_SECRET_ACCESS_KEY' ]
336
- ) {
337
- return {
338
- accessKeyId : process . env [ 'AWS_ACCESS_KEY_ID' ] ,
339
- secretAccessKey : process . env [ 'AWS_SECRET_ACCESS_KEY' ] ,
340
- token : process . env [ 'AWS_SESSION_TOKEN' ] ,
341
- } ;
342
- }
343
- return null ;
344
- }
345
219
}
0 commit comments