blob: b362dd3152991769a27f728c67e7617d4f199a36 [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
15"""AWS Credentials and AWS Signature V4 Request Signer.
16
17This module provides credentials to access Google Cloud resources from Amazon
18Web Services (AWS) workloads. These credentials are recommended over the
19use of service account credentials in AWS as they do not involve the management
20of long-live service account private keys.
21
22AWS Credentials are initialized using external_account arguments which are
23typically loaded from the external credentials JSON file.
24Unlike other Credentials that can be initialized with a list of explicit
25arguments, secrets or credentials, external account clients use the
26environment and hints/guidelines provided by the external_account JSON
27file to retrieve credentials and exchange them for Google access tokens.
28
29This module also provides a basic implementation of the
30`AWS Signature Version 4`_ request signing algorithm.
31
32AWS Credentials use serialized signed requests to the
33`AWS STS GetCallerIdentity`_ API that can be exchanged for Google access tokens
34via the GCP STS endpoint.
35
36.. _AWS Signature Version 4: https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
37.. _AWS STS GetCallerIdentity: https://docs.aws.amazon.com/STS/latest/APIReference/API_GetCallerIdentity.html
38"""
39
40import hashlib
41import hmac
42import io
43import json
44import os
45import re
46
47from six.moves import http_client
48from six.moves import urllib
49
50from google.auth import _helpers
51from google.auth import environment_vars
52from google.auth import exceptions
53from google.auth import external_account
54
55# AWS Signature Version 4 signing algorithm identifier.
56_AWS_ALGORITHM = "AWS4-HMAC-SHA256"
57# The termination string for the AWS credential scope value as defined in
58# https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
59_AWS_REQUEST_TYPE = "aws4_request"
60# The AWS authorization header name for the security session token if available.
61_AWS_SECURITY_TOKEN_HEADER = "x-amz-security-token"
62# The AWS authorization header name for the auto-generated date.
63_AWS_DATE_HEADER = "x-amz-date"
64
65
66class RequestSigner(object):
67 """Implements an AWS request signer based on the AWS Signature Version 4 signing
68 process.
69 https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
70 """
71
72 def __init__(self, region_name):
73 """Instantiates an AWS request signer used to compute authenticated signed
74 requests to AWS APIs based on the AWS Signature Version 4 signing process.
75
76 Args:
77 region_name (str): The AWS region to use.
78 """
79
80 self._region_name = region_name
81
82 def get_request_options(
83 self,
84 aws_security_credentials,
85 url,
86 method,
87 request_payload="",
88 additional_headers={},
89 ):
90 """Generates the signed request for the provided HTTP request for calling
91 an AWS API. This follows the steps described at:
92 https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html
93
94 Args:
95 aws_security_credentials (Mapping[str, str]): A dictionary containing
96 the AWS security credentials.
97 url (str): The AWS service URL containing the canonical URI and
98 query string.
99 method (str): The HTTP method used to call this API.
100 request_payload (Optional[str]): The optional request payload if
101 available.
102 additional_headers (Optional[Mapping[str, str]]): The optional
103 additional headers needed for the requested AWS API.
104
105 Returns:
106 Mapping[str, str]: The AWS signed request dictionary object.
107 """
108 # Get AWS credentials.
109 access_key = aws_security_credentials.get("access_key_id")
110 secret_key = aws_security_credentials.get("secret_access_key")
111 security_token = aws_security_credentials.get("security_token")
112
113 additional_headers = additional_headers or {}
114
115 uri = urllib.parse.urlparse(url)
116 # Validate provided URL.
117 if not uri.hostname or uri.scheme != "https":
118 raise ValueError("Invalid AWS service URL")
119
120 header_map = _generate_authentication_header_map(
121 host=uri.hostname,
122 canonical_uri=os.path.normpath(uri.path or "/"),
123 canonical_querystring=_get_canonical_querystring(uri.query),
124 method=method,
125 region=self._region_name,
126 access_key=access_key,
127 secret_key=secret_key,
128 security_token=security_token,
129 request_payload=request_payload,
130 additional_headers=additional_headers,
131 )
132 headers = {
133 "Authorization": header_map.get("authorization_header"),
134 "host": uri.hostname,
135 }
136 # Add x-amz-date if available.
137 if "amz_date" in header_map:
138 headers[_AWS_DATE_HEADER] = header_map.get("amz_date")
139 # Append additional optional headers, eg. X-Amz-Target, Content-Type, etc.
140 for key in additional_headers:
141 headers[key] = additional_headers[key]
142
143 # Add session token if available.
144 if security_token is not None:
145 headers[_AWS_SECURITY_TOKEN_HEADER] = security_token
146
147 signed_request = {"url": url, "method": method, "headers": headers}
148 if request_payload:
149 signed_request["data"] = request_payload
150 return signed_request
151
152
153def _get_canonical_querystring(query):
154 """Generates the canonical query string given a raw query string.
155 Logic is based on
156 https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
157
158 Args:
159 query (str): The raw query string.
160
161 Returns:
162 str: The canonical query string.
163 """
164 # Parse raw query string.
165 querystring = urllib.parse.parse_qs(query)
166 querystring_encoded_map = {}
167 for key in querystring:
168 quote_key = urllib.parse.quote(key, safe="-_.~")
169 # URI encode key.
170 querystring_encoded_map[quote_key] = []
171 for item in querystring[key]:
172 # For each key, URI encode all values for that key.
173 querystring_encoded_map[quote_key].append(
174 urllib.parse.quote(item, safe="-_.~")
175 )
176 # Sort values for each key.
177 querystring_encoded_map[quote_key].sort()
178 # Sort keys.
179 sorted_keys = list(querystring_encoded_map.keys())
180 sorted_keys.sort()
181 # Reconstruct the query string. Preserve keys with multiple values.
182 querystring_encoded_pairs = []
183 for key in sorted_keys:
184 for item in querystring_encoded_map[key]:
185 querystring_encoded_pairs.append("{}={}".format(key, item))
186 return "&".join(querystring_encoded_pairs)
187
188
189def _sign(key, msg):
190 """Creates the HMAC-SHA256 hash of the provided message using the provided
191 key.
192
193 Args:
194 key (str): The HMAC-SHA256 key to use.
195 msg (str): The message to hash.
196
197 Returns:
198 str: The computed hash bytes.
199 """
200 return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
201
202
203def _get_signing_key(key, date_stamp, region_name, service_name):
204 """Calculates the signing key used to calculate the signature for
205 AWS Signature Version 4 based on:
206 https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
207
208 Args:
209 key (str): The AWS secret access key.
210 date_stamp (str): The '%Y%m%d' date format.
211 region_name (str): The AWS region.
212 service_name (str): The AWS service name, eg. sts.
213
214 Returns:
215 str: The signing key bytes.
216 """
217 k_date = _sign(("AWS4" + key).encode("utf-8"), date_stamp)
218 k_region = _sign(k_date, region_name)
219 k_service = _sign(k_region, service_name)
220 k_signing = _sign(k_service, "aws4_request")
221 return k_signing
222
223
224def _generate_authentication_header_map(
225 host,
226 canonical_uri,
227 canonical_querystring,
228 method,
229 region,
230 access_key,
231 secret_key,
232 security_token,
233 request_payload="",
234 additional_headers={},
235):
236 """Generates the authentication header map needed for generating the AWS
237 Signature Version 4 signed request.
238
239 Args:
240 host (str): The AWS service URL hostname.
241 canonical_uri (str): The AWS service URL path name.
242 canonical_querystring (str): The AWS service URL query string.
243 method (str): The HTTP method used to call this API.
244 region (str): The AWS region.
245 access_key (str): The AWS access key ID.
246 secret_key (str): The AWS secret access key.
247 security_token (Optional[str]): The AWS security session token. This is
248 available for temporary sessions.
249 request_payload (Optional[str]): The optional request payload if
250 available.
251 additional_headers (Optional[Mapping[str, str]]): The optional
252 additional headers needed for the requested AWS API.
253
254 Returns:
255 Mapping[str, str]: The AWS authentication header dictionary object.
256 This contains the x-amz-date and authorization header information.
257 """
258 # iam.amazonaws.com host => iam service.
259 # sts.us-east-2.amazonaws.com host => sts service.
260 service_name = host.split(".")[0]
261
262 current_time = _helpers.utcnow()
263 amz_date = current_time.strftime("%Y%m%dT%H%M%SZ")
264 date_stamp = current_time.strftime("%Y%m%d")
265
266 # Change all additional headers to be lower case.
267 full_headers = {}
268 for key in additional_headers:
269 full_headers[key.lower()] = additional_headers[key]
270 # Add AWS session token if available.
271 if security_token is not None:
272 full_headers[_AWS_SECURITY_TOKEN_HEADER] = security_token
273
274 # Required headers
275 full_headers["host"] = host
276 # Do not use generated x-amz-date if the date header is provided.
277 # Previously the date was not fixed with x-amz- and could be provided
278 # manually.
279 # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-header-value-trim.req
280 if "date" not in full_headers:
281 full_headers[_AWS_DATE_HEADER] = amz_date
282
283 # Header keys need to be sorted alphabetically.
284 canonical_headers = ""
285 header_keys = list(full_headers.keys())
286 header_keys.sort()
287 for key in header_keys:
288 canonical_headers = "{}{}:{}\n".format(
289 canonical_headers, key, full_headers[key]
290 )
291 signed_headers = ";".join(header_keys)
292
293 payload_hash = hashlib.sha256((request_payload or "").encode("utf-8")).hexdigest()
294
295 # https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
296 canonical_request = "{}\n{}\n{}\n{}\n{}\n{}".format(
297 method,
298 canonical_uri,
299 canonical_querystring,
300 canonical_headers,
301 signed_headers,
302 payload_hash,
303 )
304
305 credential_scope = "{}/{}/{}/{}".format(
306 date_stamp, region, service_name, _AWS_REQUEST_TYPE
307 )
308
309 # https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
310 string_to_sign = "{}\n{}\n{}\n{}".format(
311 _AWS_ALGORITHM,
312 amz_date,
313 credential_scope,
314 hashlib.sha256(canonical_request.encode("utf-8")).hexdigest(),
315 )
316
317 # https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
318 signing_key = _get_signing_key(secret_key, date_stamp, region, service_name)
319 signature = hmac.new(
320 signing_key, string_to_sign.encode("utf-8"), hashlib.sha256
321 ).hexdigest()
322
323 # https://docs.aws.amazon.com/general/latest/gr/sigv4-add-signature-to-request.html
324 authorization_header = "{} Credential={}/{}, SignedHeaders={}, Signature={}".format(
325 _AWS_ALGORITHM, access_key, credential_scope, signed_headers, signature
326 )
327
328 authentication_header = {"authorization_header": authorization_header}
329 # Do not use generated x-amz-date if the date header is provided.
330 if "date" not in full_headers:
331 authentication_header["amz_date"] = amz_date
332 return authentication_header
333
334
335class Credentials(external_account.Credentials):
336 """AWS external account credentials.
337 This is used to exchange serialized AWS signature v4 signed requests to
338 AWS STS GetCallerIdentity service for Google access tokens.
339 """
340
341 def __init__(
342 self,
343 audience,
344 subject_token_type,
345 token_url,
346 credential_source=None,
347 service_account_impersonation_url=None,
348 client_id=None,
349 client_secret=None,
350 quota_project_id=None,
351 scopes=None,
352 default_scopes=None,
353 ):
354 """Instantiates an AWS workload external account credentials object.
355
356 Args:
357 audience (str): The STS audience field.
358 subject_token_type (str): The subject token type.
359 token_url (str): The STS endpoint URL.
360 credential_source (Mapping): The credential source dictionary used
361 to provide instructions on how to retrieve external credential
362 to be exchanged for Google access tokens.
363 service_account_impersonation_url (Optional[str]): The optional
364 service account impersonation getAccessToken URL.
365 client_id (Optional[str]): The optional client ID.
366 client_secret (Optional[str]): The optional client secret.
367 quota_project_id (Optional[str]): The optional quota project ID.
368 scopes (Optional[Sequence[str]]): Optional scopes to request during
369 the authorization grant.
370 default_scopes (Optional[Sequence[str]]): Default scopes passed by a
371 Google client library. Use 'scopes' for user-defined scopes.
372
373 Raises:
374 google.auth.exceptions.RefreshError: If an error is encountered during
375 access token retrieval logic.
376 ValueError: For invalid parameters.
377
378 .. note:: Typically one of the helper constructors
379 :meth:`from_file` or
380 :meth:`from_info` are used instead of calling the constructor directly.
381 """
382 super(Credentials, self).__init__(
383 audience=audience,
384 subject_token_type=subject_token_type,
385 token_url=token_url,
386 credential_source=credential_source,
387 service_account_impersonation_url=service_account_impersonation_url,
388 client_id=client_id,
389 client_secret=client_secret,
390 quota_project_id=quota_project_id,
391 scopes=scopes,
392 default_scopes=default_scopes,
393 )
394 credential_source = credential_source or {}
395 self._environment_id = credential_source.get("environment_id") or ""
396 self._region_url = credential_source.get("region_url")
397 self._security_credentials_url = credential_source.get("url")
398 self._cred_verification_url = credential_source.get(
399 "regional_cred_verification_url"
400 )
401 self._region = None
402 self._request_signer = None
403 self._target_resource = audience
404
405 # Get the environment ID. Currently, only one version supported (v1).
406 matches = re.match(r"^(aws)([\d]+)$", self._environment_id)
407 if matches:
408 env_id, env_version = matches.groups()
409 else:
410 env_id, env_version = (None, None)
411
412 if env_id != "aws" or self._cred_verification_url is None:
413 raise ValueError("No valid AWS 'credential_source' provided")
414 elif int(env_version or "") != 1:
415 raise ValueError(
416 "aws version '{}' is not supported in the current build.".format(
417 env_version
418 )
419 )
420
421 def retrieve_subject_token(self, request):
422 """Retrieves the subject token using the credential_source object.
423 The subject token is a serialized `AWS GetCallerIdentity signed request`_.
424
425 The logic is summarized as:
426
427 Retrieve the AWS region from the AWS_REGION environment variable or from
428 the AWS metadata server availability-zone if not found in the
429 environment variable.
430
431 Check AWS credentials in environment variables. If not found, retrieve
432 from the AWS metadata server security-credentials endpoint.
433
434 When retrieving AWS credentials from the metadata server
435 security-credentials endpoint, the AWS role needs to be determined by
436 calling the security-credentials endpoint without any argument. Then the
437 credentials can be retrieved via: security-credentials/role_name
438
439 Generate the signed request to AWS STS GetCallerIdentity action.
440
441 Inject x-goog-cloud-target-resource into header and serialize the
442 signed request. This will be the subject-token to pass to GCP STS.
443
444 .. _AWS GetCallerIdentity signed request:
445 https://cloud.google.com/iam/docs/access-resources-aws#exchange-token
446
447 Args:
448 request (google.auth.transport.Request): A callable used to make
449 HTTP requests.
450 Returns:
451 str: The retrieved subject token.
452 """
453 # Initialize the request signer if not yet initialized after determining
454 # the current AWS region.
455 if self._request_signer is None:
456 self._region = self._get_region(request, self._region_url)
457 self._request_signer = RequestSigner(self._region)
458
459 # Retrieve the AWS security credentials needed to generate the signed
460 # request.
461 aws_security_credentials = self._get_security_credentials(request)
462 # Generate the signed request to AWS STS GetCallerIdentity API.
463 # Use the required regional endpoint. Otherwise, the request will fail.
464 request_options = self._request_signer.get_request_options(
465 aws_security_credentials,
466 self._cred_verification_url.replace("{region}", self._region),
467 "POST",
468 )
469 # The GCP STS endpoint expects the headers to be formatted as:
470 # [
471 # {key: 'x-amz-date', value: '...'},
472 # {key: 'Authorization', value: '...'},
473 # ...
474 # ]
475 # And then serialized as:
476 # quote(json.dumps({
477 # url: '...',
478 # method: 'POST',
479 # headers: [{key: 'x-amz-date', value: '...'}, ...]
480 # }))
481 request_headers = request_options.get("headers")
482 # The full, canonical resource name of the workload identity pool
483 # provider, with or without the HTTPS prefix.
484 # Including this header as part of the signature is recommended to
485 # ensure data integrity.
486 request_headers["x-goog-cloud-target-resource"] = self._target_resource
487
488 # Serialize AWS signed request.
489 # Keeping inner keys in sorted order makes testing easier for Python
490 # versions <=3.5 as the stringified JSON string would have a predictable
491 # key order.
492 aws_signed_req = {}
493 aws_signed_req["url"] = request_options.get("url")
494 aws_signed_req["method"] = request_options.get("method")
495 aws_signed_req["headers"] = []
496 # Reformat header to GCP STS expected format.
497 for key in sorted(request_headers.keys()):
498 aws_signed_req["headers"].append(
499 {"key": key, "value": request_headers[key]}
500 )
501
502 return urllib.parse.quote(
503 json.dumps(aws_signed_req, separators=(",", ":"), sort_keys=True)
504 )
505
506 def _get_region(self, request, url):
507 """Retrieves the current AWS region from either the AWS_REGION
508 environment variable or from the AWS metadata server.
509
510 Args:
511 request (google.auth.transport.Request): A callable used to make
512 HTTP requests.
513 url (str): The AWS metadata server region URL.
514
515 Returns:
516 str: The current AWS region.
517
518 Raises:
519 google.auth.exceptions.RefreshError: If an error occurs while
520 retrieving the AWS region.
521 """
522 # The AWS metadata server is not available in some AWS environments
523 # such as AWS lambda. Instead, it is available via environment
524 # variable.
525 env_aws_region = os.environ.get(environment_vars.AWS_REGION)
526 if env_aws_region is not None:
527 return env_aws_region
528
529 if not self._region_url:
530 raise exceptions.RefreshError("Unable to determine AWS region")
531 response = request(url=self._region_url, method="GET")
532
533 # Support both string and bytes type response.data.
534 response_body = (
535 response.data.decode("utf-8")
536 if hasattr(response.data, "decode")
537 else response.data
538 )
539
540 if response.status != 200:
541 raise exceptions.RefreshError(
542 "Unable to retrieve AWS region", response_body
543 )
544
545 # This endpoint will return the region in format: us-east-2b.
546 # Only the us-east-2 part should be used.
547 return response_body[:-1]
548
549 def _get_security_credentials(self, request):
550 """Retrieves the AWS security credentials required for signing AWS
551 requests from either the AWS security credentials environment variables
552 or from the AWS metadata server.
553
554 Args:
555 request (google.auth.transport.Request): A callable used to make
556 HTTP requests.
557
558 Returns:
559 Mapping[str, str]: The AWS security credentials dictionary object.
560
561 Raises:
562 google.auth.exceptions.RefreshError: If an error occurs while
563 retrieving the AWS security credentials.
564 """
565
566 # Check environment variables for permanent credentials first.
567 # https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html
568 env_aws_access_key_id = os.environ.get(environment_vars.AWS_ACCESS_KEY_ID)
569 env_aws_secret_access_key = os.environ.get(
570 environment_vars.AWS_SECRET_ACCESS_KEY
571 )
572 # This is normally not available for permanent credentials.
573 env_aws_session_token = os.environ.get(environment_vars.AWS_SESSION_TOKEN)
574 if env_aws_access_key_id and env_aws_secret_access_key:
575 return {
576 "access_key_id": env_aws_access_key_id,
577 "secret_access_key": env_aws_secret_access_key,
578 "security_token": env_aws_session_token,
579 }
580
581 # Get role name.
582 role_name = self._get_metadata_role_name(request)
583
584 # Get security credentials.
585 credentials = self._get_metadata_security_credentials(request, role_name)
586
587 return {
588 "access_key_id": credentials.get("AccessKeyId"),
589 "secret_access_key": credentials.get("SecretAccessKey"),
590 "security_token": credentials.get("Token"),
591 }
592
593 def _get_metadata_security_credentials(self, request, role_name):
594 """Retrieves the AWS security credentials required for signing AWS
595 requests from the AWS metadata server.
596
597 Args:
598 request (google.auth.transport.Request): A callable used to make
599 HTTP requests.
600 role_name (str): The AWS role name required by the AWS metadata
601 server security_credentials endpoint in order to return the
602 credentials.
603
604 Returns:
605 Mapping[str, str]: The AWS metadata server security credentials
606 response.
607
608 Raises:
609 google.auth.exceptions.RefreshError: If an error occurs while
610 retrieving the AWS security credentials.
611 """
612 headers = {"Content-Type": "application/json"}
613 response = request(
614 url="{}/{}".format(self._security_credentials_url, role_name),
615 method="GET",
616 headers=headers,
617 )
618
619 # support both string and bytes type response.data
620 response_body = (
621 response.data.decode("utf-8")
622 if hasattr(response.data, "decode")
623 else response.data
624 )
625
626 if response.status != http_client.OK:
627 raise exceptions.RefreshError(
628 "Unable to retrieve AWS security credentials", response_body
629 )
630
631 credentials_response = json.loads(response_body)
632
633 return credentials_response
634
635 def _get_metadata_role_name(self, request):
636 """Retrieves the AWS role currently attached to the current AWS
637 workload by querying the AWS metadata server. This is needed for the
638 AWS metadata server security credentials endpoint in order to retrieve
639 the AWS security credentials needed to sign requests to AWS APIs.
640
641 Args:
642 request (google.auth.transport.Request): A callable used to make
643 HTTP requests.
644
645 Returns:
646 str: The AWS role name.
647
648 Raises:
649 google.auth.exceptions.RefreshError: If an error occurs while
650 retrieving the AWS role name.
651 """
652 if self._security_credentials_url is None:
653 raise exceptions.RefreshError(
654 "Unable to determine the AWS metadata server security credentials endpoint"
655 )
656 response = request(url=self._security_credentials_url, method="GET")
657
658 # support both string and bytes type response.data
659 response_body = (
660 response.data.decode("utf-8")
661 if hasattr(response.data, "decode")
662 else response.data
663 )
664
665 if response.status != http_client.OK:
666 raise exceptions.RefreshError(
667 "Unable to retrieve AWS role name", response_body
668 )
669
670 return response_body
671
672 @classmethod
673 def from_info(cls, info, **kwargs):
674 """Creates an AWS Credentials instance from parsed external account info.
675
676 Args:
677 info (Mapping[str, str]): The AWS external account info in Google
678 format.
679 kwargs: Additional arguments to pass to the constructor.
680
681 Returns:
682 google.auth.aws.Credentials: The constructed credentials.
683
684 Raises:
685 ValueError: For invalid parameters.
686 """
687 return cls(
688 audience=info.get("audience"),
689 subject_token_type=info.get("subject_token_type"),
690 token_url=info.get("token_url"),
691 service_account_impersonation_url=info.get(
692 "service_account_impersonation_url"
693 ),
694 client_id=info.get("client_id"),
695 client_secret=info.get("client_secret"),
696 credential_source=info.get("credential_source"),
697 quota_project_id=info.get("quota_project_id"),
698 **kwargs
699 )
700
701 @classmethod
702 def from_file(cls, filename, **kwargs):
703 """Creates an AWS Credentials instance from an external account json file.
704
705 Args:
706 filename (str): The path to the AWS external account json file.
707 kwargs: Additional arguments to pass to the constructor.
708
709 Returns:
710 google.auth.aws.Credentials: The constructed credentials.
711 """
712 with io.open(filename, "r", encoding="utf-8") as json_file:
713 data = json.load(json_file)
714 return cls.from_info(data, **kwargs)