blob: 9ca08d5b2cdc05d28ff1e9f89f049565b78985f9 [file] [log] [blame]
bojeil-googled4d7f382021-02-16 12:33:20 -08001# Copyright 2020 Google LLC
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import datetime
16import json
17
18import mock
19import pytest
20from six.moves import http_client
21from six.moves import urllib
22
23from google.auth import _helpers
24from google.auth import aws
25from google.auth import environment_vars
26from google.auth import exceptions
27from google.auth import transport
28
29
30CLIENT_ID = "username"
31CLIENT_SECRET = "password"
32# Base64 encoding of "username:password".
33BASIC_AUTH_ENCODING = "dXNlcm5hbWU6cGFzc3dvcmQ="
34SERVICE_ACCOUNT_EMAIL = "service-1234@service-name.iam.gserviceaccount.com"
35SERVICE_ACCOUNT_IMPERSONATION_URL = (
36 "https://us-east1-iamcredentials.googleapis.com/v1/projects/-"
37 + "/serviceAccounts/{}:generateAccessToken".format(SERVICE_ACCOUNT_EMAIL)
38)
39QUOTA_PROJECT_ID = "QUOTA_PROJECT_ID"
40SCOPES = ["scope1", "scope2"]
41TOKEN_URL = "https://sts.googleapis.com/v1/token"
42SUBJECT_TOKEN_TYPE = "urn:ietf:params:aws:token-type:aws4_request"
43AUDIENCE = "//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID"
44REGION_URL = "http://169.254.169.254/latest/meta-data/placement/availability-zone"
45SECURITY_CREDS_URL = "http://169.254.169.254/latest/meta-data/iam/security-credentials"
46CRED_VERIFICATION_URL = (
47 "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15"
48)
49# Sample AWS security credentials to be used with tests that require a session token.
50ACCESS_KEY_ID = "ASIARD4OQDT6A77FR3CL"
51SECRET_ACCESS_KEY = "Y8AfSaucF37G4PpvfguKZ3/l7Id4uocLXxX0+VTx"
52TOKEN = "IQoJb3JpZ2luX2VjEIz//////////wEaCXVzLWVhc3QtMiJGMEQCIH7MHX/Oy/OB8OlLQa9GrqU1B914+iMikqWQW7vPCKlgAiA/Lsv8Jcafn14owfxXn95FURZNKaaphj0ykpmS+Ki+CSq0AwhlEAAaDDA3NzA3MTM5MTk5NiIMx9sAeP1ovlMTMKLjKpEDwuJQg41/QUKx0laTZYjPlQvjwSqS3OB9P1KAXPWSLkliVMMqaHqelvMF/WO/glv3KwuTfQsavRNs3v5pcSEm4SPO3l7mCs7KrQUHwGP0neZhIKxEXy+Ls//1C/Bqt53NL+LSbaGv6RPHaX82laz2qElphg95aVLdYgIFY6JWV5fzyjgnhz0DQmy62/Vi8pNcM2/VnxeCQ8CC8dRDSt52ry2v+nc77vstuI9xV5k8mPtnaPoJDRANh0bjwY5Sdwkbp+mGRUJBAQRlNgHUJusefXQgVKBCiyJY4w3Csd8Bgj9IyDV+Azuy1jQqfFZWgP68LSz5bURyIjlWDQunO82stZ0BgplKKAa/KJHBPCp8Qi6i99uy7qh76FQAqgVTsnDuU6fGpHDcsDSGoCls2HgZjZFPeOj8mmRhFk1Xqvkbjuz8V1cJk54d3gIJvQt8gD2D6yJQZecnuGWd5K2e2HohvCc8Fc9kBl1300nUJPV+k4tr/A5R/0QfEKOZL1/k5lf1g9CREnrM8LVkGxCgdYMxLQow1uTL+QU67AHRRSp5PhhGX4Rek+01vdYSnJCMaPhSEgcLqDlQkhk6MPsyT91QMXcWmyO+cAZwUPwnRamFepuP4K8k2KVXs/LIJHLELwAZ0ekyaS7CptgOqS7uaSTFG3U+vzFZLEnGvWQ7y9IPNQZ+Dffgh4p3vF4J68y9049sI6Sr5d5wbKkcbm8hdCDHZcv4lnqohquPirLiFQ3q7B17V9krMPu3mz1cg4Ekgcrn/E09NTsxAqD8NcZ7C7ECom9r+X3zkDOxaajW6hu3Az8hGlyylDaMiFfRbBJpTIlxp7jfa7CxikNgNtEKLH9iCzvuSg2vhA=="
53# To avoid json.dumps() differing behavior from one version to other,
54# the JSON payload is hardcoded.
55REQUEST_PARAMS = '{"KeySchema":[{"KeyType":"HASH","AttributeName":"Id"}],"TableName":"TestTable","AttributeDefinitions":[{"AttributeName":"Id","AttributeType":"S"}],"ProvisionedThroughput":{"WriteCapacityUnits":5,"ReadCapacityUnits":5}}'
56# Each tuple contains the following entries:
57# region, time, credentials, original_request, signed_request
58TEST_FIXTURES = [
59 # GET request (AWS botocore tests).
60 # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla.req
61 # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla.sreq
62 (
63 "us-east-1",
64 "2011-09-09T23:36:00Z",
65 {
66 "access_key_id": "AKIDEXAMPLE",
67 "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
68 },
69 {
70 "method": "GET",
71 "url": "https://host.foo.com",
72 "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT"},
73 },
74 {
75 "url": "https://host.foo.com",
76 "method": "GET",
77 "headers": {
78 "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=b27ccfbfa7df52a200ff74193ca6e32d4b48b8856fab7ebf1c595d0670a7e470",
79 "host": "host.foo.com",
80 "date": "Mon, 09 Sep 2011 23:36:00 GMT",
81 },
82 },
83 ),
84 # GET request with relative path (AWS botocore tests).
85 # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-relative-relative.req
86 # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-relative-relative.sreq
87 (
88 "us-east-1",
89 "2011-09-09T23:36:00Z",
90 {
91 "access_key_id": "AKIDEXAMPLE",
92 "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
93 },
94 {
95 "method": "GET",
96 "url": "https://host.foo.com/foo/bar/../..",
97 "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT"},
98 },
99 {
100 "url": "https://host.foo.com/foo/bar/../..",
101 "method": "GET",
102 "headers": {
103 "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=b27ccfbfa7df52a200ff74193ca6e32d4b48b8856fab7ebf1c595d0670a7e470",
104 "host": "host.foo.com",
105 "date": "Mon, 09 Sep 2011 23:36:00 GMT",
106 },
107 },
108 ),
109 # GET request with /./ path (AWS botocore tests).
110 # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-slash-dot-slash.req
111 # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-slash-dot-slash.sreq
112 (
113 "us-east-1",
114 "2011-09-09T23:36:00Z",
115 {
116 "access_key_id": "AKIDEXAMPLE",
117 "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
118 },
119 {
120 "method": "GET",
121 "url": "https://host.foo.com/./",
122 "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT"},
123 },
124 {
125 "url": "https://host.foo.com/./",
126 "method": "GET",
127 "headers": {
128 "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=b27ccfbfa7df52a200ff74193ca6e32d4b48b8856fab7ebf1c595d0670a7e470",
129 "host": "host.foo.com",
130 "date": "Mon, 09 Sep 2011 23:36:00 GMT",
131 },
132 },
133 ),
134 # GET request with pointless dot path (AWS botocore tests).
135 # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-slash-pointless-dot.req
136 # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-slash-pointless-dot.sreq
137 (
138 "us-east-1",
139 "2011-09-09T23:36:00Z",
140 {
141 "access_key_id": "AKIDEXAMPLE",
142 "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
143 },
144 {
145 "method": "GET",
146 "url": "https://host.foo.com/./foo",
147 "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT"},
148 },
149 {
150 "url": "https://host.foo.com/./foo",
151 "method": "GET",
152 "headers": {
153 "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=910e4d6c9abafaf87898e1eb4c929135782ea25bb0279703146455745391e63a",
154 "host": "host.foo.com",
155 "date": "Mon, 09 Sep 2011 23:36:00 GMT",
156 },
157 },
158 ),
159 # GET request with utf8 path (AWS botocore tests).
160 # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-utf8.req
161 # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-utf8.sreq
162 (
163 "us-east-1",
164 "2011-09-09T23:36:00Z",
165 {
166 "access_key_id": "AKIDEXAMPLE",
167 "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
168 },
169 {
170 "method": "GET",
171 "url": "https://host.foo.com/%E1%88%B4",
172 "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT"},
173 },
174 {
175 "url": "https://host.foo.com/%E1%88%B4",
176 "method": "GET",
177 "headers": {
178 "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=8d6634c189aa8c75c2e51e106b6b5121bed103fdb351f7d7d4381c738823af74",
179 "host": "host.foo.com",
180 "date": "Mon, 09 Sep 2011 23:36:00 GMT",
181 },
182 },
183 ),
184 # GET request with duplicate query key (AWS botocore tests).
185 # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla-query-order-key-case.req
186 # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla-query-order-key-case.sreq
187 (
188 "us-east-1",
189 "2011-09-09T23:36:00Z",
190 {
191 "access_key_id": "AKIDEXAMPLE",
192 "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
193 },
194 {
195 "method": "GET",
196 "url": "https://host.foo.com/?foo=Zoo&foo=aha",
197 "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT"},
198 },
199 {
200 "url": "https://host.foo.com/?foo=Zoo&foo=aha",
201 "method": "GET",
202 "headers": {
203 "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=be7148d34ebccdc6423b19085378aa0bee970bdc61d144bd1a8c48c33079ab09",
204 "host": "host.foo.com",
205 "date": "Mon, 09 Sep 2011 23:36:00 GMT",
206 },
207 },
208 ),
209 # GET request with duplicate out of order query key (AWS botocore tests).
210 # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla-query-order-value.req
211 # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla-query-order-value.sreq
212 (
213 "us-east-1",
214 "2011-09-09T23:36:00Z",
215 {
216 "access_key_id": "AKIDEXAMPLE",
217 "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
218 },
219 {
220 "method": "GET",
221 "url": "https://host.foo.com/?foo=b&foo=a",
222 "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT"},
223 },
224 {
225 "url": "https://host.foo.com/?foo=b&foo=a",
226 "method": "GET",
227 "headers": {
228 "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=feb926e49e382bec75c9d7dcb2a1b6dc8aa50ca43c25d2bc51143768c0875acc",
229 "host": "host.foo.com",
230 "date": "Mon, 09 Sep 2011 23:36:00 GMT",
231 },
232 },
233 ),
234 # GET request with utf8 query (AWS botocore tests).
235 # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla-ut8-query.req
236 # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla-ut8-query.sreq
237 (
238 "us-east-1",
239 "2011-09-09T23:36:00Z",
240 {
241 "access_key_id": "AKIDEXAMPLE",
242 "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
243 },
244 {
245 "method": "GET",
246 "url": "https://host.foo.com/?{}=bar".format(
247 urllib.parse.unquote("%E1%88%B4")
248 ),
249 "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT"},
250 },
251 {
252 "url": "https://host.foo.com/?{}=bar".format(
253 urllib.parse.unquote("%E1%88%B4")
254 ),
255 "method": "GET",
256 "headers": {
257 "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=6fb359e9a05394cc7074e0feb42573a2601abc0c869a953e8c5c12e4e01f1a8c",
258 "host": "host.foo.com",
259 "date": "Mon, 09 Sep 2011 23:36:00 GMT",
260 },
261 },
262 ),
263 # POST request with sorted headers (AWS botocore tests).
264 # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-header-key-sort.req
265 # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-header-key-sort.sreq
266 (
267 "us-east-1",
268 "2011-09-09T23:36:00Z",
269 {
270 "access_key_id": "AKIDEXAMPLE",
271 "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
272 },
273 {
274 "method": "POST",
275 "url": "https://host.foo.com/",
276 "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT", "ZOO": "zoobar"},
277 },
278 {
279 "url": "https://host.foo.com/",
280 "method": "POST",
281 "headers": {
282 "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host;zoo, Signature=b7a95a52518abbca0964a999a880429ab734f35ebbf1235bd79a5de87756dc4a",
283 "host": "host.foo.com",
284 "date": "Mon, 09 Sep 2011 23:36:00 GMT",
285 "ZOO": "zoobar",
286 },
287 },
288 ),
289 # POST request with upper case header value from AWS Python test harness.
290 # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-header-value-case.req
291 # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-header-value-case.sreq
292 (
293 "us-east-1",
294 "2011-09-09T23:36:00Z",
295 {
296 "access_key_id": "AKIDEXAMPLE",
297 "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
298 },
299 {
300 "method": "POST",
301 "url": "https://host.foo.com/",
302 "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT", "zoo": "ZOOBAR"},
303 },
304 {
305 "url": "https://host.foo.com/",
306 "method": "POST",
307 "headers": {
308 "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host;zoo, Signature=273313af9d0c265c531e11db70bbd653f3ba074c1009239e8559d3987039cad7",
309 "host": "host.foo.com",
310 "date": "Mon, 09 Sep 2011 23:36:00 GMT",
311 "zoo": "ZOOBAR",
312 },
313 },
314 ),
315 # POST request with header and no body (AWS botocore tests).
316 # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-header-value-trim.req
317 # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-header-value-trim.sreq
318 (
319 "us-east-1",
320 "2011-09-09T23:36:00Z",
321 {
322 "access_key_id": "AKIDEXAMPLE",
323 "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
324 },
325 {
326 "method": "POST",
327 "url": "https://host.foo.com/",
328 "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT", "p": "phfft"},
329 },
330 {
331 "url": "https://host.foo.com/",
332 "method": "POST",
333 "headers": {
334 "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host;p, Signature=debf546796015d6f6ded8626f5ce98597c33b47b9164cf6b17b4642036fcb592",
335 "host": "host.foo.com",
336 "date": "Mon, 09 Sep 2011 23:36:00 GMT",
337 "p": "phfft",
338 },
339 },
340 ),
341 # POST request with body and no header (AWS botocore tests).
342 # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-x-www-form-urlencoded.req
343 # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-x-www-form-urlencoded.sreq
344 (
345 "us-east-1",
346 "2011-09-09T23:36:00Z",
347 {
348 "access_key_id": "AKIDEXAMPLE",
349 "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
350 },
351 {
352 "method": "POST",
353 "url": "https://host.foo.com/",
354 "headers": {
355 "Content-Type": "application/x-www-form-urlencoded",
356 "date": "Mon, 09 Sep 2011 23:36:00 GMT",
357 },
358 "data": "foo=bar",
359 },
360 {
361 "url": "https://host.foo.com/",
362 "method": "POST",
363 "headers": {
364 "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=content-type;date;host, Signature=5a15b22cf462f047318703b92e6f4f38884e4a7ab7b1d6426ca46a8bd1c26cbc",
365 "host": "host.foo.com",
366 "Content-Type": "application/x-www-form-urlencoded",
367 "date": "Mon, 09 Sep 2011 23:36:00 GMT",
368 },
369 "data": "foo=bar",
370 },
371 ),
372 # POST request with querystring (AWS botocore tests).
373 # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-vanilla-query.req
374 # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-vanilla-query.sreq
375 (
376 "us-east-1",
377 "2011-09-09T23:36:00Z",
378 {
379 "access_key_id": "AKIDEXAMPLE",
380 "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
381 },
382 {
383 "method": "POST",
384 "url": "https://host.foo.com/?foo=bar",
385 "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT"},
386 },
387 {
388 "url": "https://host.foo.com/?foo=bar",
389 "method": "POST",
390 "headers": {
391 "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=b6e3b79003ce0743a491606ba1035a804593b0efb1e20a11cba83f8c25a57a92",
392 "host": "host.foo.com",
393 "date": "Mon, 09 Sep 2011 23:36:00 GMT",
394 },
395 },
396 ),
397 # GET request with session token credentials.
398 (
399 "us-east-2",
400 "2020-08-11T06:55:22Z",
401 {
402 "access_key_id": ACCESS_KEY_ID,
403 "secret_access_key": SECRET_ACCESS_KEY,
404 "security_token": TOKEN,
405 },
406 {
407 "method": "GET",
408 "url": "https://ec2.us-east-2.amazonaws.com?Action=DescribeRegions&Version=2013-10-15",
409 },
410 {
411 "url": "https://ec2.us-east-2.amazonaws.com?Action=DescribeRegions&Version=2013-10-15",
412 "method": "GET",
413 "headers": {
414 "Authorization": "AWS4-HMAC-SHA256 Credential="
415 + ACCESS_KEY_ID
416 + "/20200811/us-east-2/ec2/aws4_request, SignedHeaders=host;x-amz-date;x-amz-security-token, Signature=631ea80cddfaa545fdadb120dc92c9f18166e38a5c47b50fab9fce476e022855",
417 "host": "ec2.us-east-2.amazonaws.com",
418 "x-amz-date": "20200811T065522Z",
419 "x-amz-security-token": TOKEN,
420 },
421 },
422 ),
423 # POST request with session token credentials.
424 (
425 "us-east-2",
426 "2020-08-11T06:55:22Z",
427 {
428 "access_key_id": ACCESS_KEY_ID,
429 "secret_access_key": SECRET_ACCESS_KEY,
430 "security_token": TOKEN,
431 },
432 {
433 "method": "POST",
434 "url": "https://sts.us-east-2.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
435 },
436 {
437 "url": "https://sts.us-east-2.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
438 "method": "POST",
439 "headers": {
440 "Authorization": "AWS4-HMAC-SHA256 Credential="
441 + ACCESS_KEY_ID
442 + "/20200811/us-east-2/sts/aws4_request, SignedHeaders=host;x-amz-date;x-amz-security-token, Signature=73452984e4a880ffdc5c392355733ec3f5ba310d5e0609a89244440cadfe7a7a",
443 "host": "sts.us-east-2.amazonaws.com",
444 "x-amz-date": "20200811T065522Z",
445 "x-amz-security-token": TOKEN,
446 },
447 },
448 ),
449 # POST request with computed x-amz-date and no data.
450 (
451 "us-east-2",
452 "2020-08-11T06:55:22Z",
453 {"access_key_id": ACCESS_KEY_ID, "secret_access_key": SECRET_ACCESS_KEY},
454 {
455 "method": "POST",
456 "url": "https://sts.us-east-2.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
457 },
458 {
459 "url": "https://sts.us-east-2.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
460 "method": "POST",
461 "headers": {
462 "Authorization": "AWS4-HMAC-SHA256 Credential="
463 + ACCESS_KEY_ID
464 + "/20200811/us-east-2/sts/aws4_request, SignedHeaders=host;x-amz-date, Signature=d095ba304919cd0d5570ba8a3787884ee78b860f268ed040ba23831d55536d56",
465 "host": "sts.us-east-2.amazonaws.com",
466 "x-amz-date": "20200811T065522Z",
467 },
468 },
469 ),
470 # POST request with session token and additional headers/data.
471 (
472 "us-east-2",
473 "2020-08-11T06:55:22Z",
474 {
475 "access_key_id": ACCESS_KEY_ID,
476 "secret_access_key": SECRET_ACCESS_KEY,
477 "security_token": TOKEN,
478 },
479 {
480 "method": "POST",
481 "url": "https://dynamodb.us-east-2.amazonaws.com/",
482 "headers": {
483 "Content-Type": "application/x-amz-json-1.0",
484 "x-amz-target": "DynamoDB_20120810.CreateTable",
485 },
486 "data": REQUEST_PARAMS,
487 },
488 {
489 "url": "https://dynamodb.us-east-2.amazonaws.com/",
490 "method": "POST",
491 "headers": {
492 "Authorization": "AWS4-HMAC-SHA256 Credential="
493 + ACCESS_KEY_ID
494 + "/20200811/us-east-2/dynamodb/aws4_request, SignedHeaders=content-type;host;x-amz-date;x-amz-security-token;x-amz-target, Signature=fdaa5b9cc9c86b80fe61eaf504141c0b3523780349120f2bd8145448456e0385",
495 "host": "dynamodb.us-east-2.amazonaws.com",
496 "x-amz-date": "20200811T065522Z",
497 "Content-Type": "application/x-amz-json-1.0",
498 "x-amz-target": "DynamoDB_20120810.CreateTable",
499 "x-amz-security-token": TOKEN,
500 },
501 "data": REQUEST_PARAMS,
502 },
503 ),
504]
505
506
507class TestRequestSigner(object):
508 @pytest.mark.parametrize(
509 "region, time, credentials, original_request, signed_request", TEST_FIXTURES
510 )
511 @mock.patch("google.auth._helpers.utcnow")
512 def test_get_request_options(
513 self, utcnow, region, time, credentials, original_request, signed_request
514 ):
515 utcnow.return_value = datetime.datetime.strptime(time, "%Y-%m-%dT%H:%M:%SZ")
516 request_signer = aws.RequestSigner(region)
517 actual_signed_request = request_signer.get_request_options(
518 credentials,
519 original_request.get("url"),
520 original_request.get("method"),
521 original_request.get("data"),
522 original_request.get("headers"),
523 )
524
525 assert actual_signed_request == signed_request
526
527 def test_get_request_options_with_missing_scheme_url(self):
528 request_signer = aws.RequestSigner("us-east-2")
529
530 with pytest.raises(ValueError) as excinfo:
531 request_signer.get_request_options(
532 {
533 "access_key_id": ACCESS_KEY_ID,
534 "secret_access_key": SECRET_ACCESS_KEY,
535 },
536 "invalid",
537 "POST",
538 )
539
540 assert excinfo.match(r"Invalid AWS service URL")
541
542 def test_get_request_options_with_invalid_scheme_url(self):
543 request_signer = aws.RequestSigner("us-east-2")
544
545 with pytest.raises(ValueError) as excinfo:
546 request_signer.get_request_options(
547 {
548 "access_key_id": ACCESS_KEY_ID,
549 "secret_access_key": SECRET_ACCESS_KEY,
550 },
551 "http://invalid",
552 "POST",
553 )
554
555 assert excinfo.match(r"Invalid AWS service URL")
556
557 def test_get_request_options_with_missing_hostname_url(self):
558 request_signer = aws.RequestSigner("us-east-2")
559
560 with pytest.raises(ValueError) as excinfo:
561 request_signer.get_request_options(
562 {
563 "access_key_id": ACCESS_KEY_ID,
564 "secret_access_key": SECRET_ACCESS_KEY,
565 },
566 "https://",
567 "POST",
568 )
569
570 assert excinfo.match(r"Invalid AWS service URL")
571
572
573class TestCredentials(object):
574 AWS_REGION = "us-east-2"
575 AWS_ROLE = "gcp-aws-role"
576 AWS_SECURITY_CREDENTIALS_RESPONSE = {
577 "AccessKeyId": ACCESS_KEY_ID,
578 "SecretAccessKey": SECRET_ACCESS_KEY,
579 "Token": TOKEN,
580 }
581 AWS_SIGNATURE_TIME = "2020-08-11T06:55:22Z"
582 CREDENTIAL_SOURCE = {
583 "environment_id": "aws1",
584 "region_url": REGION_URL,
585 "url": SECURITY_CREDS_URL,
586 "regional_cred_verification_url": CRED_VERIFICATION_URL,
587 }
588 SUCCESS_RESPONSE = {
589 "access_token": "ACCESS_TOKEN",
590 "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
591 "token_type": "Bearer",
592 "expires_in": 3600,
593 "scope": " ".join(SCOPES),
594 }
595
596 @classmethod
597 def make_serialized_aws_signed_request(
598 cls,
599 aws_security_credentials,
600 region_name="us-east-2",
601 url="https://sts.us-east-2.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
602 ):
603 """Utility to generate serialize AWS signed requests.
604 This makes it easy to assert generated subject tokens based on the
605 provided AWS security credentials, regions and AWS STS endpoint.
606 """
607 request_signer = aws.RequestSigner(region_name)
608 signed_request = request_signer.get_request_options(
609 aws_security_credentials, url, "POST"
610 )
611 reformatted_signed_request = {
612 "url": signed_request.get("url"),
613 "method": signed_request.get("method"),
614 "headers": [
615 {
616 "key": "Authorization",
617 "value": signed_request.get("headers").get("Authorization"),
618 },
619 {"key": "host", "value": signed_request.get("headers").get("host")},
620 {
621 "key": "x-amz-date",
622 "value": signed_request.get("headers").get("x-amz-date"),
623 },
624 ],
625 }
626 # Include security token if available.
627 if "security_token" in aws_security_credentials:
628 reformatted_signed_request.get("headers").append(
629 {
630 "key": "x-amz-security-token",
631 "value": signed_request.get("headers").get("x-amz-security-token"),
632 }
633 )
634 # Append x-goog-cloud-target-resource header.
635 reformatted_signed_request.get("headers").append(
636 {"key": "x-goog-cloud-target-resource", "value": AUDIENCE}
637 ),
638 return urllib.parse.quote(
639 json.dumps(
640 reformatted_signed_request, separators=(",", ":"), sort_keys=True
641 )
642 )
643
644 @classmethod
645 def make_mock_request(
646 cls,
647 region_status=None,
648 region_name=None,
649 role_status=None,
650 role_name=None,
651 security_credentials_status=None,
652 security_credentials_data=None,
653 token_status=None,
654 token_data=None,
655 impersonation_status=None,
656 impersonation_data=None,
657 ):
658 """Utility function to generate a mock HTTP request object.
659 This will facilitate testing various edge cases by specify how the
660 various endpoints will respond while generating a Google Access token
661 in an AWS environment.
662 """
663 responses = []
664 if region_status:
665 # AWS region request.
666 region_response = mock.create_autospec(transport.Response, instance=True)
667 region_response.status = region_status
668 if region_name:
669 region_response.data = "{}b".format(region_name).encode("utf-8")
670 responses.append(region_response)
671
672 if role_status:
673 # AWS role name request.
674 role_response = mock.create_autospec(transport.Response, instance=True)
675 role_response.status = role_status
676 if role_name:
677 role_response.data = role_name.encode("utf-8")
678 responses.append(role_response)
679
680 if security_credentials_status:
681 # AWS security credentials request.
682 security_credentials_response = mock.create_autospec(
683 transport.Response, instance=True
684 )
685 security_credentials_response.status = security_credentials_status
686 if security_credentials_data:
687 security_credentials_response.data = json.dumps(
688 security_credentials_data
689 ).encode("utf-8")
690 responses.append(security_credentials_response)
691
692 if token_status:
693 # GCP token exchange request.
694 token_response = mock.create_autospec(transport.Response, instance=True)
695 token_response.status = token_status
696 token_response.data = json.dumps(token_data).encode("utf-8")
697 responses.append(token_response)
698
699 if impersonation_status:
700 # Service account impersonation request.
701 impersonation_response = mock.create_autospec(
702 transport.Response, instance=True
703 )
704 impersonation_response.status = impersonation_status
705 impersonation_response.data = json.dumps(impersonation_data).encode("utf-8")
706 responses.append(impersonation_response)
707
708 request = mock.create_autospec(transport.Request)
709 request.side_effect = responses
710
711 return request
712
713 @classmethod
714 def make_credentials(
715 cls,
716 credential_source,
717 client_id=None,
718 client_secret=None,
719 quota_project_id=None,
720 scopes=None,
721 default_scopes=None,
722 service_account_impersonation_url=None,
723 ):
724 return aws.Credentials(
725 audience=AUDIENCE,
726 subject_token_type=SUBJECT_TOKEN_TYPE,
727 token_url=TOKEN_URL,
728 service_account_impersonation_url=service_account_impersonation_url,
729 credential_source=credential_source,
730 client_id=client_id,
731 client_secret=client_secret,
732 quota_project_id=quota_project_id,
733 scopes=scopes,
734 default_scopes=default_scopes,
735 )
736
737 @classmethod
738 def assert_aws_metadata_request_kwargs(cls, request_kwargs, url, headers=None):
739 assert request_kwargs["url"] == url
740 # All used AWS metadata server endpoints use GET HTTP method.
741 assert request_kwargs["method"] == "GET"
742 if headers:
743 assert request_kwargs["headers"] == headers
744 else:
745 assert "headers" not in request_kwargs
746 # None of the endpoints used require any data in request.
747 assert "body" not in request_kwargs
748
749 @classmethod
750 def assert_token_request_kwargs(
751 cls, request_kwargs, headers, request_data, token_url=TOKEN_URL
752 ):
753 assert request_kwargs["url"] == token_url
754 assert request_kwargs["method"] == "POST"
755 assert request_kwargs["headers"] == headers
756 assert request_kwargs["body"] is not None
757 body_tuples = urllib.parse.parse_qsl(request_kwargs["body"])
758 assert len(body_tuples) == len(request_data.keys())
759 for (k, v) in body_tuples:
760 assert v.decode("utf-8") == request_data[k.decode("utf-8")]
761
762 @classmethod
763 def assert_impersonation_request_kwargs(
764 cls,
765 request_kwargs,
766 headers,
767 request_data,
768 service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
769 ):
770 assert request_kwargs["url"] == service_account_impersonation_url
771 assert request_kwargs["method"] == "POST"
772 assert request_kwargs["headers"] == headers
773 assert request_kwargs["body"] is not None
774 body_json = json.loads(request_kwargs["body"].decode("utf-8"))
775 assert body_json == request_data
776
777 @mock.patch.object(aws.Credentials, "__init__", return_value=None)
778 def test_from_info_full_options(self, mock_init):
779 credentials = aws.Credentials.from_info(
780 {
781 "audience": AUDIENCE,
782 "subject_token_type": SUBJECT_TOKEN_TYPE,
783 "token_url": TOKEN_URL,
784 "service_account_impersonation_url": SERVICE_ACCOUNT_IMPERSONATION_URL,
785 "client_id": CLIENT_ID,
786 "client_secret": CLIENT_SECRET,
787 "quota_project_id": QUOTA_PROJECT_ID,
788 "credential_source": self.CREDENTIAL_SOURCE,
789 }
790 )
791
792 # Confirm aws.Credentials instance initialized with the expected parameters.
793 assert isinstance(credentials, aws.Credentials)
794 mock_init.assert_called_once_with(
795 audience=AUDIENCE,
796 subject_token_type=SUBJECT_TOKEN_TYPE,
797 token_url=TOKEN_URL,
798 service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
799 client_id=CLIENT_ID,
800 client_secret=CLIENT_SECRET,
801 credential_source=self.CREDENTIAL_SOURCE,
802 quota_project_id=QUOTA_PROJECT_ID,
803 )
804
805 @mock.patch.object(aws.Credentials, "__init__", return_value=None)
806 def test_from_info_required_options_only(self, mock_init):
807 credentials = aws.Credentials.from_info(
808 {
809 "audience": AUDIENCE,
810 "subject_token_type": SUBJECT_TOKEN_TYPE,
811 "token_url": TOKEN_URL,
812 "credential_source": self.CREDENTIAL_SOURCE,
813 }
814 )
815
816 # Confirm aws.Credentials instance initialized with the expected parameters.
817 assert isinstance(credentials, aws.Credentials)
818 mock_init.assert_called_once_with(
819 audience=AUDIENCE,
820 subject_token_type=SUBJECT_TOKEN_TYPE,
821 token_url=TOKEN_URL,
822 service_account_impersonation_url=None,
823 client_id=None,
824 client_secret=None,
825 credential_source=self.CREDENTIAL_SOURCE,
826 quota_project_id=None,
827 )
828
829 @mock.patch.object(aws.Credentials, "__init__", return_value=None)
830 def test_from_file_full_options(self, mock_init, tmpdir):
831 info = {
832 "audience": AUDIENCE,
833 "subject_token_type": SUBJECT_TOKEN_TYPE,
834 "token_url": TOKEN_URL,
835 "service_account_impersonation_url": SERVICE_ACCOUNT_IMPERSONATION_URL,
836 "client_id": CLIENT_ID,
837 "client_secret": CLIENT_SECRET,
838 "quota_project_id": QUOTA_PROJECT_ID,
839 "credential_source": self.CREDENTIAL_SOURCE,
840 }
841 config_file = tmpdir.join("config.json")
842 config_file.write(json.dumps(info))
843 credentials = aws.Credentials.from_file(str(config_file))
844
845 # Confirm aws.Credentials instance initialized with the expected parameters.
846 assert isinstance(credentials, aws.Credentials)
847 mock_init.assert_called_once_with(
848 audience=AUDIENCE,
849 subject_token_type=SUBJECT_TOKEN_TYPE,
850 token_url=TOKEN_URL,
851 service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
852 client_id=CLIENT_ID,
853 client_secret=CLIENT_SECRET,
854 credential_source=self.CREDENTIAL_SOURCE,
855 quota_project_id=QUOTA_PROJECT_ID,
856 )
857
858 @mock.patch.object(aws.Credentials, "__init__", return_value=None)
859 def test_from_file_required_options_only(self, mock_init, tmpdir):
860 info = {
861 "audience": AUDIENCE,
862 "subject_token_type": SUBJECT_TOKEN_TYPE,
863 "token_url": TOKEN_URL,
864 "credential_source": self.CREDENTIAL_SOURCE,
865 }
866 config_file = tmpdir.join("config.json")
867 config_file.write(json.dumps(info))
868 credentials = aws.Credentials.from_file(str(config_file))
869
870 # Confirm aws.Credentials instance initialized with the expected parameters.
871 assert isinstance(credentials, aws.Credentials)
872 mock_init.assert_called_once_with(
873 audience=AUDIENCE,
874 subject_token_type=SUBJECT_TOKEN_TYPE,
875 token_url=TOKEN_URL,
876 service_account_impersonation_url=None,
877 client_id=None,
878 client_secret=None,
879 credential_source=self.CREDENTIAL_SOURCE,
880 quota_project_id=None,
881 )
882
883 def test_constructor_invalid_credential_source(self):
884 # Provide invalid credential source.
885 credential_source = {"unsupported": "value"}
886
887 with pytest.raises(ValueError) as excinfo:
888 self.make_credentials(credential_source=credential_source)
889
890 assert excinfo.match(r"No valid AWS 'credential_source' provided")
891
892 def test_constructor_invalid_environment_id(self):
893 # Provide invalid environment_id.
894 credential_source = self.CREDENTIAL_SOURCE.copy()
895 credential_source["environment_id"] = "azure1"
896
897 with pytest.raises(ValueError) as excinfo:
898 self.make_credentials(credential_source=credential_source)
899
900 assert excinfo.match(r"No valid AWS 'credential_source' provided")
901
902 def test_constructor_missing_cred_verification_url(self):
903 # regional_cred_verification_url is a required field.
904 credential_source = self.CREDENTIAL_SOURCE.copy()
905 credential_source.pop("regional_cred_verification_url")
906
907 with pytest.raises(ValueError) as excinfo:
908 self.make_credentials(credential_source=credential_source)
909
910 assert excinfo.match(r"No valid AWS 'credential_source' provided")
911
912 def test_constructor_invalid_environment_id_version(self):
913 # Provide an unsupported version.
914 credential_source = self.CREDENTIAL_SOURCE.copy()
915 credential_source["environment_id"] = "aws3"
916
917 with pytest.raises(ValueError) as excinfo:
918 self.make_credentials(credential_source=credential_source)
919
920 assert excinfo.match(r"aws version '3' is not supported in the current build.")
921
bojeil-googlef97499c2021-06-09 07:58:25 -0700922 def test_info(self):
923 credentials = self.make_credentials(
924 credential_source=self.CREDENTIAL_SOURCE.copy()
925 )
926
927 assert credentials.info == {
928 "type": "external_account",
929 "audience": AUDIENCE,
930 "subject_token_type": SUBJECT_TOKEN_TYPE,
931 "token_url": TOKEN_URL,
932 "credential_source": self.CREDENTIAL_SOURCE,
933 }
934
bojeil-googled4d7f382021-02-16 12:33:20 -0800935 def test_retrieve_subject_token_missing_region_url(self):
936 # When AWS_REGION envvar is not available, region_url is required for
937 # determining the current AWS region.
938 credential_source = self.CREDENTIAL_SOURCE.copy()
939 credential_source.pop("region_url")
940 credentials = self.make_credentials(credential_source=credential_source)
941
942 with pytest.raises(exceptions.RefreshError) as excinfo:
943 credentials.retrieve_subject_token(None)
944
945 assert excinfo.match(r"Unable to determine AWS region")
946
947 @mock.patch("google.auth._helpers.utcnow")
948 def test_retrieve_subject_token_success_temp_creds_no_environment_vars(
949 self, utcnow
950 ):
951 utcnow.return_value = datetime.datetime.strptime(
952 self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
953 )
954 request = self.make_mock_request(
955 region_status=http_client.OK,
956 region_name=self.AWS_REGION,
957 role_status=http_client.OK,
958 role_name=self.AWS_ROLE,
959 security_credentials_status=http_client.OK,
960 security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE,
961 )
962 credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE)
963
964 subject_token = credentials.retrieve_subject_token(request)
965
966 assert subject_token == self.make_serialized_aws_signed_request(
967 {
968 "access_key_id": ACCESS_KEY_ID,
969 "secret_access_key": SECRET_ACCESS_KEY,
970 "security_token": TOKEN,
971 }
972 )
973 # Assert region request.
974 self.assert_aws_metadata_request_kwargs(
arithmetic1728d80c85f2021-03-08 13:35:44 -0800975 request.call_args_list[0][1], REGION_URL
bojeil-googled4d7f382021-02-16 12:33:20 -0800976 )
977 # Assert role request.
978 self.assert_aws_metadata_request_kwargs(
arithmetic1728d80c85f2021-03-08 13:35:44 -0800979 request.call_args_list[1][1], SECURITY_CREDS_URL
bojeil-googled4d7f382021-02-16 12:33:20 -0800980 )
981 # Assert security credentials request.
982 self.assert_aws_metadata_request_kwargs(
arithmetic1728d80c85f2021-03-08 13:35:44 -0800983 request.call_args_list[2][1],
bojeil-googled4d7f382021-02-16 12:33:20 -0800984 "{}/{}".format(SECURITY_CREDS_URL, self.AWS_ROLE),
985 {"Content-Type": "application/json"},
986 )
987
988 # Retrieve subject_token again. Region should not be queried again.
989 new_request = self.make_mock_request(
990 role_status=http_client.OK,
991 role_name=self.AWS_ROLE,
992 security_credentials_status=http_client.OK,
993 security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE,
994 )
995
996 credentials.retrieve_subject_token(new_request)
997
998 # Only 2 requests should be sent as the region is cached.
999 assert len(new_request.call_args_list) == 2
1000 # Assert role request.
1001 self.assert_aws_metadata_request_kwargs(
arithmetic1728d80c85f2021-03-08 13:35:44 -08001002 new_request.call_args_list[0][1], SECURITY_CREDS_URL
bojeil-googled4d7f382021-02-16 12:33:20 -08001003 )
1004 # Assert security credentials request.
1005 self.assert_aws_metadata_request_kwargs(
arithmetic1728d80c85f2021-03-08 13:35:44 -08001006 new_request.call_args_list[1][1],
bojeil-googled4d7f382021-02-16 12:33:20 -08001007 "{}/{}".format(SECURITY_CREDS_URL, self.AWS_ROLE),
1008 {"Content-Type": "application/json"},
1009 )
1010
1011 @mock.patch("google.auth._helpers.utcnow")
1012 def test_retrieve_subject_token_success_permanent_creds_no_environment_vars(
1013 self, utcnow
1014 ):
1015 # Simualte a permanent credential without a session token is
1016 # returned by the security-credentials endpoint.
1017 security_creds_response = self.AWS_SECURITY_CREDENTIALS_RESPONSE.copy()
1018 security_creds_response.pop("Token")
1019 utcnow.return_value = datetime.datetime.strptime(
1020 self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
1021 )
1022 request = self.make_mock_request(
1023 region_status=http_client.OK,
1024 region_name=self.AWS_REGION,
1025 role_status=http_client.OK,
1026 role_name=self.AWS_ROLE,
1027 security_credentials_status=http_client.OK,
1028 security_credentials_data=security_creds_response,
1029 )
1030 credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE)
1031
1032 subject_token = credentials.retrieve_subject_token(request)
1033
1034 assert subject_token == self.make_serialized_aws_signed_request(
1035 {"access_key_id": ACCESS_KEY_ID, "secret_access_key": SECRET_ACCESS_KEY}
1036 )
1037
1038 @mock.patch("google.auth._helpers.utcnow")
1039 def test_retrieve_subject_token_success_environment_vars(self, utcnow, monkeypatch):
1040 monkeypatch.setenv(environment_vars.AWS_ACCESS_KEY_ID, ACCESS_KEY_ID)
1041 monkeypatch.setenv(environment_vars.AWS_SECRET_ACCESS_KEY, SECRET_ACCESS_KEY)
1042 monkeypatch.setenv(environment_vars.AWS_SESSION_TOKEN, TOKEN)
1043 monkeypatch.setenv(environment_vars.AWS_REGION, self.AWS_REGION)
1044 utcnow.return_value = datetime.datetime.strptime(
1045 self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
1046 )
1047 credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE)
1048
1049 subject_token = credentials.retrieve_subject_token(None)
1050
1051 assert subject_token == self.make_serialized_aws_signed_request(
1052 {
1053 "access_key_id": ACCESS_KEY_ID,
1054 "secret_access_key": SECRET_ACCESS_KEY,
1055 "security_token": TOKEN,
1056 }
1057 )
1058
1059 @mock.patch("google.auth._helpers.utcnow")
Ryan Kohler199da472021-03-12 16:04:02 -08001060 def test_retrieve_subject_token_success_environment_vars_with_default_region(
1061 self, utcnow, monkeypatch
1062 ):
1063 monkeypatch.setenv(environment_vars.AWS_ACCESS_KEY_ID, ACCESS_KEY_ID)
1064 monkeypatch.setenv(environment_vars.AWS_SECRET_ACCESS_KEY, SECRET_ACCESS_KEY)
1065 monkeypatch.setenv(environment_vars.AWS_SESSION_TOKEN, TOKEN)
1066 monkeypatch.setenv(environment_vars.AWS_DEFAULT_REGION, self.AWS_REGION)
1067 utcnow.return_value = datetime.datetime.strptime(
1068 self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
1069 )
1070 credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE)
1071
1072 subject_token = credentials.retrieve_subject_token(None)
1073
1074 assert subject_token == self.make_serialized_aws_signed_request(
1075 {
1076 "access_key_id": ACCESS_KEY_ID,
1077 "secret_access_key": SECRET_ACCESS_KEY,
1078 "security_token": TOKEN,
1079 }
1080 )
1081
1082 @mock.patch("google.auth._helpers.utcnow")
1083 def test_retrieve_subject_token_success_environment_vars_with_both_regions_set(
1084 self, utcnow, monkeypatch
1085 ):
1086 monkeypatch.setenv(environment_vars.AWS_ACCESS_KEY_ID, ACCESS_KEY_ID)
1087 monkeypatch.setenv(environment_vars.AWS_SECRET_ACCESS_KEY, SECRET_ACCESS_KEY)
1088 monkeypatch.setenv(environment_vars.AWS_SESSION_TOKEN, TOKEN)
1089 monkeypatch.setenv(environment_vars.AWS_DEFAULT_REGION, "Malformed AWS Region")
1090 # This test makes sure that the AWS_REGION gets used over AWS_DEFAULT_REGION,
1091 # So, AWS_DEFAULT_REGION is set to something that would cause the test to fail,
1092 # And AWS_REGION is set to the a valid value, and it should succeed
1093 monkeypatch.setenv(environment_vars.AWS_REGION, self.AWS_REGION)
1094 utcnow.return_value = datetime.datetime.strptime(
1095 self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
1096 )
1097 credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE)
1098
1099 subject_token = credentials.retrieve_subject_token(None)
1100
1101 assert subject_token == self.make_serialized_aws_signed_request(
1102 {
1103 "access_key_id": ACCESS_KEY_ID,
1104 "secret_access_key": SECRET_ACCESS_KEY,
1105 "security_token": TOKEN,
1106 }
1107 )
1108
1109 @mock.patch("google.auth._helpers.utcnow")
bojeil-googled4d7f382021-02-16 12:33:20 -08001110 def test_retrieve_subject_token_success_environment_vars_no_session_token(
1111 self, utcnow, monkeypatch
1112 ):
1113 monkeypatch.setenv(environment_vars.AWS_ACCESS_KEY_ID, ACCESS_KEY_ID)
1114 monkeypatch.setenv(environment_vars.AWS_SECRET_ACCESS_KEY, SECRET_ACCESS_KEY)
1115 monkeypatch.setenv(environment_vars.AWS_REGION, self.AWS_REGION)
1116 utcnow.return_value = datetime.datetime.strptime(
1117 self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
1118 )
1119 credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE)
1120
1121 subject_token = credentials.retrieve_subject_token(None)
1122
1123 assert subject_token == self.make_serialized_aws_signed_request(
1124 {"access_key_id": ACCESS_KEY_ID, "secret_access_key": SECRET_ACCESS_KEY}
1125 )
1126
1127 @mock.patch("google.auth._helpers.utcnow")
1128 def test_retrieve_subject_token_success_environment_vars_except_region(
1129 self, utcnow, monkeypatch
1130 ):
1131 monkeypatch.setenv(environment_vars.AWS_ACCESS_KEY_ID, ACCESS_KEY_ID)
1132 monkeypatch.setenv(environment_vars.AWS_SECRET_ACCESS_KEY, SECRET_ACCESS_KEY)
1133 monkeypatch.setenv(environment_vars.AWS_SESSION_TOKEN, TOKEN)
1134 utcnow.return_value = datetime.datetime.strptime(
1135 self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
1136 )
1137 # Region will be queried since it is not found in envvars.
1138 request = self.make_mock_request(
1139 region_status=http_client.OK, region_name=self.AWS_REGION
1140 )
1141 credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE)
1142
1143 subject_token = credentials.retrieve_subject_token(request)
1144
1145 assert subject_token == self.make_serialized_aws_signed_request(
1146 {
1147 "access_key_id": ACCESS_KEY_ID,
1148 "secret_access_key": SECRET_ACCESS_KEY,
1149 "security_token": TOKEN,
1150 }
1151 )
1152
1153 def test_retrieve_subject_token_error_determining_aws_region(self):
1154 # Simulate error in retrieving the AWS region.
1155 request = self.make_mock_request(region_status=http_client.BAD_REQUEST)
1156 credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE)
1157
1158 with pytest.raises(exceptions.RefreshError) as excinfo:
1159 credentials.retrieve_subject_token(request)
1160
1161 assert excinfo.match(r"Unable to retrieve AWS region")
1162
1163 def test_retrieve_subject_token_error_determining_aws_role(self):
1164 # Simulate error in retrieving the AWS role name.
1165 request = self.make_mock_request(
1166 region_status=http_client.OK,
1167 region_name=self.AWS_REGION,
1168 role_status=http_client.BAD_REQUEST,
1169 )
1170 credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE)
1171
1172 with pytest.raises(exceptions.RefreshError) as excinfo:
1173 credentials.retrieve_subject_token(request)
1174
1175 assert excinfo.match(r"Unable to retrieve AWS role name")
1176
1177 def test_retrieve_subject_token_error_determining_security_creds_url(self):
1178 # Simulate the security-credentials url is missing. This is needed for
1179 # determining the AWS security credentials when not found in envvars.
1180 credential_source = self.CREDENTIAL_SOURCE.copy()
1181 credential_source.pop("url")
1182 request = self.make_mock_request(
1183 region_status=http_client.OK, region_name=self.AWS_REGION
1184 )
1185 credentials = self.make_credentials(credential_source=credential_source)
1186
1187 with pytest.raises(exceptions.RefreshError) as excinfo:
1188 credentials.retrieve_subject_token(request)
1189
1190 assert excinfo.match(
1191 r"Unable to determine the AWS metadata server security credentials endpoint"
1192 )
1193
1194 def test_retrieve_subject_token_error_determining_aws_security_creds(self):
1195 # Simulate error in retrieving the AWS security credentials.
1196 request = self.make_mock_request(
1197 region_status=http_client.OK,
1198 region_name=self.AWS_REGION,
1199 role_status=http_client.OK,
1200 role_name=self.AWS_ROLE,
1201 security_credentials_status=http_client.BAD_REQUEST,
1202 )
1203 credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE)
1204
1205 with pytest.raises(exceptions.RefreshError) as excinfo:
1206 credentials.retrieve_subject_token(request)
1207
1208 assert excinfo.match(r"Unable to retrieve AWS security credentials")
1209
1210 @mock.patch("google.auth._helpers.utcnow")
1211 def test_refresh_success_without_impersonation_ignore_default_scopes(self, utcnow):
1212 utcnow.return_value = datetime.datetime.strptime(
1213 self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
1214 )
1215 expected_subject_token = self.make_serialized_aws_signed_request(
1216 {
1217 "access_key_id": ACCESS_KEY_ID,
1218 "secret_access_key": SECRET_ACCESS_KEY,
1219 "security_token": TOKEN,
1220 }
1221 )
1222 token_headers = {
1223 "Content-Type": "application/x-www-form-urlencoded",
1224 "Authorization": "Basic " + BASIC_AUTH_ENCODING,
1225 }
1226 token_request_data = {
1227 "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
1228 "audience": AUDIENCE,
1229 "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
1230 "scope": " ".join(SCOPES),
1231 "subject_token": expected_subject_token,
1232 "subject_token_type": SUBJECT_TOKEN_TYPE,
1233 }
1234 request = self.make_mock_request(
1235 region_status=http_client.OK,
1236 region_name=self.AWS_REGION,
1237 role_status=http_client.OK,
1238 role_name=self.AWS_ROLE,
1239 security_credentials_status=http_client.OK,
1240 security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE,
1241 token_status=http_client.OK,
1242 token_data=self.SUCCESS_RESPONSE,
1243 )
1244 credentials = self.make_credentials(
1245 client_id=CLIENT_ID,
1246 client_secret=CLIENT_SECRET,
1247 credential_source=self.CREDENTIAL_SOURCE,
1248 quota_project_id=QUOTA_PROJECT_ID,
1249 scopes=SCOPES,
1250 # Default scopes should be ignored.
1251 default_scopes=["ignored"],
1252 )
1253
1254 credentials.refresh(request)
1255
1256 assert len(request.call_args_list) == 4
1257 # Fourth request should be sent to GCP STS endpoint.
1258 self.assert_token_request_kwargs(
arithmetic1728d80c85f2021-03-08 13:35:44 -08001259 request.call_args_list[3][1], token_headers, token_request_data
bojeil-googled4d7f382021-02-16 12:33:20 -08001260 )
1261 assert credentials.token == self.SUCCESS_RESPONSE["access_token"]
1262 assert credentials.quota_project_id == QUOTA_PROJECT_ID
1263 assert credentials.scopes == SCOPES
1264 assert credentials.default_scopes == ["ignored"]
1265
1266 @mock.patch("google.auth._helpers.utcnow")
1267 def test_refresh_success_without_impersonation_use_default_scopes(self, utcnow):
1268 utcnow.return_value = datetime.datetime.strptime(
1269 self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
1270 )
1271 expected_subject_token = self.make_serialized_aws_signed_request(
1272 {
1273 "access_key_id": ACCESS_KEY_ID,
1274 "secret_access_key": SECRET_ACCESS_KEY,
1275 "security_token": TOKEN,
1276 }
1277 )
1278 token_headers = {
1279 "Content-Type": "application/x-www-form-urlencoded",
1280 "Authorization": "Basic " + BASIC_AUTH_ENCODING,
1281 }
1282 token_request_data = {
1283 "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
1284 "audience": AUDIENCE,
1285 "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
1286 "scope": " ".join(SCOPES),
1287 "subject_token": expected_subject_token,
1288 "subject_token_type": SUBJECT_TOKEN_TYPE,
1289 }
1290 request = self.make_mock_request(
1291 region_status=http_client.OK,
1292 region_name=self.AWS_REGION,
1293 role_status=http_client.OK,
1294 role_name=self.AWS_ROLE,
1295 security_credentials_status=http_client.OK,
1296 security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE,
1297 token_status=http_client.OK,
1298 token_data=self.SUCCESS_RESPONSE,
1299 )
1300 credentials = self.make_credentials(
1301 client_id=CLIENT_ID,
1302 client_secret=CLIENT_SECRET,
1303 credential_source=self.CREDENTIAL_SOURCE,
1304 quota_project_id=QUOTA_PROJECT_ID,
1305 scopes=None,
1306 # Default scopes should be used since user specified scopes are none.
1307 default_scopes=SCOPES,
1308 )
1309
1310 credentials.refresh(request)
1311
1312 assert len(request.call_args_list) == 4
1313 # Fourth request should be sent to GCP STS endpoint.
1314 self.assert_token_request_kwargs(
arithmetic1728d80c85f2021-03-08 13:35:44 -08001315 request.call_args_list[3][1], token_headers, token_request_data
bojeil-googled4d7f382021-02-16 12:33:20 -08001316 )
1317 assert credentials.token == self.SUCCESS_RESPONSE["access_token"]
1318 assert credentials.quota_project_id == QUOTA_PROJECT_ID
1319 assert credentials.scopes is None
1320 assert credentials.default_scopes == SCOPES
1321
1322 @mock.patch("google.auth._helpers.utcnow")
1323 def test_refresh_success_with_impersonation_ignore_default_scopes(self, utcnow):
1324 utcnow.return_value = datetime.datetime.strptime(
1325 self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
1326 )
1327 expire_time = (
1328 _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=3600)
1329 ).isoformat("T") + "Z"
1330 expected_subject_token = self.make_serialized_aws_signed_request(
1331 {
1332 "access_key_id": ACCESS_KEY_ID,
1333 "secret_access_key": SECRET_ACCESS_KEY,
1334 "security_token": TOKEN,
1335 }
1336 )
1337 token_headers = {
1338 "Content-Type": "application/x-www-form-urlencoded",
1339 "Authorization": "Basic " + BASIC_AUTH_ENCODING,
1340 }
1341 token_request_data = {
1342 "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
1343 "audience": AUDIENCE,
1344 "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
1345 "scope": "https://www.googleapis.com/auth/iam",
1346 "subject_token": expected_subject_token,
1347 "subject_token_type": SUBJECT_TOKEN_TYPE,
1348 }
1349 # Service account impersonation request/response.
1350 impersonation_response = {
1351 "accessToken": "SA_ACCESS_TOKEN",
1352 "expireTime": expire_time,
1353 }
1354 impersonation_headers = {
1355 "Content-Type": "application/json",
1356 "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]),
1357 "x-goog-user-project": QUOTA_PROJECT_ID,
1358 }
1359 impersonation_request_data = {
1360 "delegates": None,
1361 "scope": SCOPES,
1362 "lifetime": "3600s",
1363 }
1364 request = self.make_mock_request(
1365 region_status=http_client.OK,
1366 region_name=self.AWS_REGION,
1367 role_status=http_client.OK,
1368 role_name=self.AWS_ROLE,
1369 security_credentials_status=http_client.OK,
1370 security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE,
1371 token_status=http_client.OK,
1372 token_data=self.SUCCESS_RESPONSE,
1373 impersonation_status=http_client.OK,
1374 impersonation_data=impersonation_response,
1375 )
1376 credentials = self.make_credentials(
1377 client_id=CLIENT_ID,
1378 client_secret=CLIENT_SECRET,
1379 credential_source=self.CREDENTIAL_SOURCE,
1380 service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
1381 quota_project_id=QUOTA_PROJECT_ID,
1382 scopes=SCOPES,
1383 # Default scopes should be ignored.
1384 default_scopes=["ignored"],
1385 )
1386
1387 credentials.refresh(request)
1388
1389 assert len(request.call_args_list) == 5
1390 # Fourth request should be sent to GCP STS endpoint.
1391 self.assert_token_request_kwargs(
arithmetic1728d80c85f2021-03-08 13:35:44 -08001392 request.call_args_list[3][1], token_headers, token_request_data
bojeil-googled4d7f382021-02-16 12:33:20 -08001393 )
1394 # Fifth request should be sent to iamcredentials endpoint for service
1395 # account impersonation.
1396 self.assert_impersonation_request_kwargs(
arithmetic1728d80c85f2021-03-08 13:35:44 -08001397 request.call_args_list[4][1],
bojeil-googled4d7f382021-02-16 12:33:20 -08001398 impersonation_headers,
1399 impersonation_request_data,
1400 )
1401 assert credentials.token == impersonation_response["accessToken"]
1402 assert credentials.quota_project_id == QUOTA_PROJECT_ID
1403 assert credentials.scopes == SCOPES
1404 assert credentials.default_scopes == ["ignored"]
1405
1406 @mock.patch("google.auth._helpers.utcnow")
1407 def test_refresh_success_with_impersonation_use_default_scopes(self, utcnow):
1408 utcnow.return_value = datetime.datetime.strptime(
1409 self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
1410 )
1411 expire_time = (
1412 _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=3600)
1413 ).isoformat("T") + "Z"
1414 expected_subject_token = self.make_serialized_aws_signed_request(
1415 {
1416 "access_key_id": ACCESS_KEY_ID,
1417 "secret_access_key": SECRET_ACCESS_KEY,
1418 "security_token": TOKEN,
1419 }
1420 )
1421 token_headers = {
1422 "Content-Type": "application/x-www-form-urlencoded",
1423 "Authorization": "Basic " + BASIC_AUTH_ENCODING,
1424 }
1425 token_request_data = {
1426 "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
1427 "audience": AUDIENCE,
1428 "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
1429 "scope": "https://www.googleapis.com/auth/iam",
1430 "subject_token": expected_subject_token,
1431 "subject_token_type": SUBJECT_TOKEN_TYPE,
1432 }
1433 # Service account impersonation request/response.
1434 impersonation_response = {
1435 "accessToken": "SA_ACCESS_TOKEN",
1436 "expireTime": expire_time,
1437 }
1438 impersonation_headers = {
1439 "Content-Type": "application/json",
1440 "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]),
1441 "x-goog-user-project": QUOTA_PROJECT_ID,
1442 }
1443 impersonation_request_data = {
1444 "delegates": None,
1445 "scope": SCOPES,
1446 "lifetime": "3600s",
1447 }
1448 request = self.make_mock_request(
1449 region_status=http_client.OK,
1450 region_name=self.AWS_REGION,
1451 role_status=http_client.OK,
1452 role_name=self.AWS_ROLE,
1453 security_credentials_status=http_client.OK,
1454 security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE,
1455 token_status=http_client.OK,
1456 token_data=self.SUCCESS_RESPONSE,
1457 impersonation_status=http_client.OK,
1458 impersonation_data=impersonation_response,
1459 )
1460 credentials = self.make_credentials(
1461 client_id=CLIENT_ID,
1462 client_secret=CLIENT_SECRET,
1463 credential_source=self.CREDENTIAL_SOURCE,
1464 service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
1465 quota_project_id=QUOTA_PROJECT_ID,
1466 scopes=None,
1467 # Default scopes should be used since user specified scopes are none.
1468 default_scopes=SCOPES,
1469 )
1470
1471 credentials.refresh(request)
1472
1473 assert len(request.call_args_list) == 5
1474 # Fourth request should be sent to GCP STS endpoint.
1475 self.assert_token_request_kwargs(
arithmetic1728d80c85f2021-03-08 13:35:44 -08001476 request.call_args_list[3][1], token_headers, token_request_data
bojeil-googled4d7f382021-02-16 12:33:20 -08001477 )
1478 # Fifth request should be sent to iamcredentials endpoint for service
1479 # account impersonation.
1480 self.assert_impersonation_request_kwargs(
arithmetic1728d80c85f2021-03-08 13:35:44 -08001481 request.call_args_list[4][1],
bojeil-googled4d7f382021-02-16 12:33:20 -08001482 impersonation_headers,
1483 impersonation_request_data,
1484 )
1485 assert credentials.token == impersonation_response["accessToken"]
1486 assert credentials.quota_project_id == QUOTA_PROJECT_ID
1487 assert credentials.scopes is None
1488 assert credentials.default_scopes == SCOPES
1489
1490 def test_refresh_with_retrieve_subject_token_error(self):
1491 request = self.make_mock_request(region_status=http_client.BAD_REQUEST)
1492 credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE)
1493
1494 with pytest.raises(exceptions.RefreshError) as excinfo:
1495 credentials.refresh(request)
1496
1497 assert excinfo.match(r"Unable to retrieve AWS region")