UNPKG

9.58 kBJavaScriptView Raw
1"use strict";
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.
15Object.defineProperty(exports, "__esModule", { value: true });
16exports.AwsClient = void 0;
17const awsrequestsigner_1 = require("./awsrequestsigner");
18const 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 */
24class 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}
202exports.AwsClient = AwsClient;
203//# sourceMappingURL=awsclient.js.map
\No newline at end of file