1 | ;
|
2 | // Copyright 2021 Google LLC
|
3 | //
|
4 | // Licensed under the Apache License, Version 2.0 (the "License");
|
5 | // you may not use this file except in compliance with the License.
|
6 | // You may obtain a copy of the License at
|
7 | //
|
8 | // http://www.apache.org/licenses/LICENSE-2.0
|
9 | //
|
10 | // Unless required by applicable law or agreed to in writing, software
|
11 | // distributed under the License is distributed on an "AS IS" BASIS,
|
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13 | // See the License for the specific language governing permissions and
|
14 | // limitations under the License.
|
15 | Object.defineProperty(exports, "__esModule", { value: true });
|
16 | exports.AwsClient = void 0;
|
17 | const awsrequestsigner_1 = require("./awsrequestsigner");
|
18 | const baseexternalclient_1 = require("./baseexternalclient");
|
19 | /**
|
20 | * AWS external account client. This is used for AWS workloads, where
|
21 | * AWS STS GetCallerIdentity serialized signed requests are exchanged for
|
22 | * GCP access token.
|
23 | */
|
24 | class AwsClient extends baseexternalclient_1.BaseExternalAccountClient {
|
25 | /**
|
26 | * Instantiates an AwsClient instance using the provided JSON
|
27 | * object loaded from an external account credentials file.
|
28 | * An error is thrown if the credential is not a valid AWS credential.
|
29 | * @param options The external account options object typically loaded
|
30 | * from the external account JSON credential file.
|
31 | * @param additionalOptions Optional additional behavior customization
|
32 | * options. These currently customize expiration threshold time and
|
33 | * whether to retry on 401/403 API request errors.
|
34 | */
|
35 | constructor(options, additionalOptions) {
|
36 | var _a;
|
37 | super(options, additionalOptions);
|
38 | this.environmentId = options.credential_source.environment_id;
|
39 | // This is only required if the AWS region is not available in the
|
40 | // AWS_REGION or AWS_DEFAULT_REGION environment variables.
|
41 | this.regionUrl = options.credential_source.region_url;
|
42 | // This is only required if AWS security credentials are not available in
|
43 | // environment variables.
|
44 | this.securityCredentialsUrl = options.credential_source.url;
|
45 | this.regionalCredVerificationUrl =
|
46 | options.credential_source.regional_cred_verification_url;
|
47 | const match = (_a = this.environmentId) === null || _a === void 0 ? void 0 : _a.match(/^(aws)(\d+)$/);
|
48 | if (!match || !this.regionalCredVerificationUrl) {
|
49 | throw new Error('No valid AWS "credential_source" provided');
|
50 | }
|
51 | else if (parseInt(match[2], 10) !== 1) {
|
52 | throw new Error(`aws version "${match[2]}" is not supported in the current build.`);
|
53 | }
|
54 | this.awsRequestSigner = null;
|
55 | this.region = '';
|
56 | }
|
57 | /**
|
58 | * Triggered when an external subject token is needed to be exchanged for a
|
59 | * GCP access token via GCP STS endpoint.
|
60 | * This uses the `options.credential_source` object to figure out how
|
61 | * to retrieve the token using the current environment. In this case,
|
62 | * this uses a serialized AWS signed request to the STS GetCallerIdentity
|
63 | * endpoint.
|
64 | * The logic is summarized as:
|
65 | * 1. Retrieve AWS region from availability-zone.
|
66 | * 2a. Check AWS credentials in environment variables. If not found, get
|
67 | * from security-credentials endpoint.
|
68 | * 2b. Get AWS credentials from security-credentials endpoint. In order
|
69 | * to retrieve this, the AWS role needs to be determined by calling
|
70 | * security-credentials endpoint without any argument. Then the
|
71 | * credentials can be retrieved via: security-credentials/role_name
|
72 | * 3. Generate the signed request to AWS STS GetCallerIdentity action.
|
73 | * 4. Inject x-goog-cloud-target-resource into header and serialize the
|
74 | * signed request. This will be the subject-token to pass to GCP STS.
|
75 | * @return A promise that resolves with the external subject token.
|
76 | */
|
77 | async retrieveSubjectToken() {
|
78 | // Initialize AWS request signer if not already initialized.
|
79 | if (!this.awsRequestSigner) {
|
80 | this.region = await this.getAwsRegion();
|
81 | this.awsRequestSigner = new awsrequestsigner_1.AwsRequestSigner(async () => {
|
82 | // Check environment variables for permanent credentials first.
|
83 | // https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html
|
84 | if (process.env['AWS_ACCESS_KEY_ID'] &&
|
85 | process.env['AWS_SECRET_ACCESS_KEY']) {
|
86 | return {
|
87 | accessKeyId: process.env['AWS_ACCESS_KEY_ID'],
|
88 | secretAccessKey: process.env['AWS_SECRET_ACCESS_KEY'],
|
89 | // This is normally not available for permanent credentials.
|
90 | token: process.env['AWS_SESSION_TOKEN'],
|
91 | };
|
92 | }
|
93 | // Since the role on a VM can change, we don't need to cache it.
|
94 | const roleName = await this.getAwsRoleName();
|
95 | // Temporary credentials typically last for several hours.
|
96 | // Expiration is returned in response.
|
97 | // Consider future optimization of this logic to cache AWS tokens
|
98 | // until their natural expiration.
|
99 | const awsCreds = await this.getAwsSecurityCredentials(roleName);
|
100 | return {
|
101 | accessKeyId: awsCreds.AccessKeyId,
|
102 | secretAccessKey: awsCreds.SecretAccessKey,
|
103 | token: awsCreds.Token,
|
104 | };
|
105 | }, this.region);
|
106 | }
|
107 | // Generate signed request to AWS STS GetCallerIdentity API.
|
108 | // Use the required regional endpoint. Otherwise, the request will fail.
|
109 | const options = await this.awsRequestSigner.getRequestOptions({
|
110 | url: this.regionalCredVerificationUrl.replace('{region}', this.region),
|
111 | method: 'POST',
|
112 | });
|
113 | // The GCP STS endpoint expects the headers to be formatted as:
|
114 | // [
|
115 | // {key: 'x-amz-date', value: '...'},
|
116 | // {key: 'Authorization', value: '...'},
|
117 | // ...
|
118 | // ]
|
119 | // And then serialized as:
|
120 | // encodeURIComponent(JSON.stringify({
|
121 | // url: '...',
|
122 | // method: 'POST',
|
123 | // headers: [{key: 'x-amz-date', value: '...'}, ...]
|
124 | // }))
|
125 | const reformattedHeader = [];
|
126 | const extendedHeaders = Object.assign({
|
127 | // The full, canonical resource name of the workload identity pool
|
128 | // provider, with or without the HTTPS prefix.
|
129 | // Including this header as part of the signature is recommended to
|
130 | // ensure data integrity.
|
131 | 'x-goog-cloud-target-resource': this.audience,
|
132 | }, options.headers);
|
133 | // Reformat header to GCP STS expected format.
|
134 | for (const key in extendedHeaders) {
|
135 | reformattedHeader.push({
|
136 | key,
|
137 | value: extendedHeaders[key],
|
138 | });
|
139 | }
|
140 | // Serialize the reformatted signed request.
|
141 | return encodeURIComponent(JSON.stringify({
|
142 | url: options.url,
|
143 | method: options.method,
|
144 | headers: reformattedHeader,
|
145 | }));
|
146 | }
|
147 | /**
|
148 | * @return A promise that resolves with the current AWS region.
|
149 | */
|
150 | async getAwsRegion() {
|
151 | // Priority order for region determination:
|
152 | // AWS_REGION > AWS_DEFAULT_REGION > metadata server.
|
153 | if (process.env['AWS_REGION'] || process.env['AWS_DEFAULT_REGION']) {
|
154 | return (process.env['AWS_REGION'] || process.env['AWS_DEFAULT_REGION']);
|
155 | }
|
156 | if (!this.regionUrl) {
|
157 | throw new Error('Unable to determine AWS region due to missing ' +
|
158 | '"options.credential_source.region_url"');
|
159 | }
|
160 | const opts = {
|
161 | url: this.regionUrl,
|
162 | method: 'GET',
|
163 | responseType: 'text',
|
164 | };
|
165 | const response = await this.transporter.request(opts);
|
166 | // Remove last character. For example, if us-east-2b is returned,
|
167 | // the region would be us-east-2.
|
168 | return response.data.substr(0, response.data.length - 1);
|
169 | }
|
170 | /**
|
171 | * @return A promise that resolves with the assigned role to the current
|
172 | * AWS VM. This is needed for calling the security-credentials endpoint.
|
173 | */
|
174 | async getAwsRoleName() {
|
175 | if (!this.securityCredentialsUrl) {
|
176 | throw new Error('Unable to determine AWS role name due to missing ' +
|
177 | '"options.credential_source.url"');
|
178 | }
|
179 | const opts = {
|
180 | url: this.securityCredentialsUrl,
|
181 | method: 'GET',
|
182 | responseType: 'text',
|
183 | };
|
184 | const response = await this.transporter.request(opts);
|
185 | return response.data;
|
186 | }
|
187 | /**
|
188 | * Retrieves the temporary AWS credentials by calling the security-credentials
|
189 | * endpoint as specified in the `credential_source` object.
|
190 | * @param roleName The role attached to the current VM.
|
191 | * @return A promise that resolves with the temporary AWS credentials
|
192 | * needed for creating the GetCallerIdentity signed request.
|
193 | */
|
194 | async getAwsSecurityCredentials(roleName) {
|
195 | const response = await this.transporter.request({
|
196 | url: `${this.securityCredentialsUrl}/${roleName}`,
|
197 | responseType: 'json',
|
198 | });
|
199 | return response.data;
|
200 | }
|
201 | }
|
202 | exports.AwsClient = AwsClient;
|
203 | //# sourceMappingURL=awsclient.js.map |
\ | No newline at end of file |