diff --git a/Project.toml b/Project.toml index 9d152ec62f..28485f2c57 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "AWS" uuid = "fbe9abb3-538b-5e4e-ba9e-bc94f4f92ebc" license = "MIT" -version = "1.85.0" +version = "1.86.0" [deps] Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" diff --git a/src/AWSCredentials.jl b/src/AWSCredentials.jl index d6ca0588dd..aca874d16b 100644 --- a/src/AWSCredentials.jl +++ b/src/AWSCredentials.jl @@ -23,7 +23,8 @@ export AWSCredentials, external_process_credentials, localhost_is_ec2, localhost_is_lambda, - localhost_maybe_ec2 + localhost_maybe_ec2, + sso_credentials function localhost_maybe_ec2() return localhost_is_ec2() || isfile("/sys/devices/virtual/dmi/id/product_uuid") @@ -41,20 +42,22 @@ The fields `access_key_id` and `secret_key` hold the access keys used to authent [Temporary Security Credentials](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp.html) require the extra session `token` field. The `user_arn` and `account_number` fields are used to cache the result of the [`aws_user_arn`](@ref) and [`aws_account_number`](@ref) functions. -AWS.jl searches for credentials in a series of possible locations and stops as soon as it finds credentials. -The order of precedence for this search is as follows: +AWS.jl searches for credentials in multiple locations and stops once any credentials are found. +The credential preference order mostly [mirrors the AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-authentication.html#cli-chap-authentication-precedence) +and is as follows: -1. Passing credentials directly to the `AWSCredentials` constructor +1. Credentials or a profile passed directly to the `AWSCredentials` 2. [Environment variables](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html) -3. Shared credential file [(~/.aws/credentials)](http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html) -4. AWS config file [(~/.aws/config)](http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html). - This includes [Single Sign-On (SSO)](http://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sso.html) credentials. - SSO users should follow the configuration instructions at the above link, and use `aws sso login` to log in. -5. Assume Role provider via the aws config file -6. Instance metadata service on an Amazon EC2 instance that has an IAM role configured +3. [Web Identity](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-role.html#cli-configure-role-oidc) +4. [AWS Single Sign-On (SSO)](http://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sso.html) provided via the AWS configuration file +5. [AWS credentials file](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html) (e.g. "~/.aws/credentials") +6. [External process](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sourcing-external.html) set via `credential_process` in the AWS configuration file +7. [AWS configuration file](http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html) set via `aws_access_key_id` in the AWS configuration file +8. [Amazon ECS container credentials](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html) +9. [Amazon EC2 instance metadata](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html) Once the credentials are found, the method by which they were accessed is stored in the `renew` field -and the DateTime at which they will expire is stored in the `expiry` field. +and the `DateTime` at which they will expire is stored in the `expiry` field. This allows the credentials to be refreshed as needed using [`check_credentials`](@ref). If `renew` is set to `nothing`, no attempt will be made to refresh the credentials. Any renewal function is expected to return `nothing` on failure or a populated `AWSCredentials` object on success. @@ -110,15 +113,21 @@ Checks credential locations in the order: function AWSCredentials(; profile=nothing, throw_cred_error=true) creds = nothing credential_function = () -> nothing + explicit_profile = !isnothing(profile) profile = @something profile _aws_get_profile() - # Define our search options, expected to be callable with no arguments. - # Throw NoCredentials if none are found + # Define the credential preference order: + # https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-authentication.html#cli-chap-authentication-precedence + # + # Note that the AWS CLI documentation states that EC2 instance credentials are preferred + # over ECS container credentials. However, in practice when `AWS_CONTAINER_*` + # environmental variables are set the ECS container credentials are prefered instead. functions = [ - env_var_credentials, + () -> env_var_credentials(explicit_profile), + credentials_from_webtoken, + () -> sso_credentials(profile), () -> dot_aws_credentials(profile), () -> dot_aws_config(profile), - credentials_from_webtoken, ecs_instance_credentials, () -> ec2_instance_credentials(profile), ] @@ -314,13 +323,14 @@ function ec2_instance_credentials(profile::AbstractString) end """ - ecs_instance_credentials() -> Union{AWSCredential, Nothing} + ecs_instance_credentials() -> Union{AWSCredentials, Nothing} -Retrieve credentials from the local endpoint. Return `nothing` if not running on an ECS -instance. +Retrieve credentials from the ECS credential endpoint. If the ECS credential endpoint is +unavailable then `nothing` will be returned. More information can be found at: -https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html +- https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html +- https://docs.aws.amazon.com/sdkref/latest/guide/feature-container-credentials.html # Returns - `AWSCredentials`: AWSCredentials from `ECS` credentials URI, `nothing` if the Env Var is @@ -331,13 +341,23 @@ https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html - `ParsingError`: Invalid HTTP request target """ function ecs_instance_credentials() - if !haskey(ENV, "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") + # The Amazon ECS agent will automatically populate the environmental variable + # `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` when running inside of an ECS task. We're + # interpreting this to mean than ECS credential provider should only be used if the + # `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` variable is set. + # – https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html + if haskey(ENV, "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") + endpoint = "http://169.254.170.2" * ENV["AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"] + else return nothing end - uri = ENV["AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"] - - response = @mock HTTP.request("GET", "http://169.254.170.2$uri") + response = try + @mock HTTP.request("GET", endpoint; retry=false, connect_timeout=5) + catch e + e isa HTTP.Exceptions.ConnectError && return nothing + rethrow() + end new_creds = String(response.body) new_creds = JSON.parse(new_creds) @@ -355,12 +375,15 @@ function ecs_instance_credentials() end """ - env_var_credentials() -> Union{AWSCredential, Nothing} + env_var_credentials(explicit_profile::Bool=false) -> Union{AWSCredentials, Nothing} Use AWS environmental variables (e.g. AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, etc.) to create AWSCredentials. """ -function env_var_credentials() +function env_var_credentials(explicit_profile::Bool=false) + # Skip using environmental variables when a profile has been explicitly set + explicit_profile && return nothing + if haskey(ENV, "AWS_ACCESS_KEY_ID") && haskey(ENV, "AWS_SECRET_ACCESS_KEY") return AWSCredentials( ENV["AWS_ACCESS_KEY_ID"], @@ -375,9 +398,11 @@ function env_var_credentials() end """ - dot_aws_credentials(profile=nothing) -> Union{AWSCredential, Nothing} + dot_aws_credentials(profile=nothing) -> Union{AWSCredentials, Nothing} -Retrieve AWSCredentials from the `~/.aws/credentials` file +Retrieve `AWSCredentials` from the AWS CLI credentials file. The credential file defaults to +"~/.aws/credentials" but can be specified using the env variable +`AWS_SHARED_CREDENTIALS_FILE`. # Arguments - `profile`: Specific profile used to get AWSCredentials, default is `nothing` @@ -405,11 +430,45 @@ function dot_aws_credentials_file() end """ - dot_aws_config(profile=nothing) -> Union{AWSCredential, Nothing} + sso_credentials(profile=nothing) -> Union{AWSCredentials, Nothing} + +Retrieve credentials via AWS single sign-on (SSO) settings defined in the `profile` within +the AWS configuration file. If no SSO settings are found for the `profile` `nothing` is +returned. + +# Arguments +- `profile`: Specific profile used to get `AWSCredentials`, default is `nothing` +""" +function sso_credentials(profile=nothing) + config_file = @mock dot_aws_config_file() + + if isfile(config_file) + ini = read(Inifile(), config_file) + p = @something profile _aws_get_profile() + + # get all the fields for that profile + settings = _aws_profile_config(ini, p) + isempty(settings) && return nothing + + sso_start_url = get(settings, "sso_start_url", nothing) + + if !isnothing(sso_start_url) + access_key, secret_key, token, expiry = _aws_get_sso_credential_details(p, ini) + return AWSCredentials(access_key, secret_key, token; expiry=expiry) + end + end + + return nothing +end + +""" + dot_aws_config(profile=nothing) -> Union{AWSCredentials, Nothing} -Retrieve AWSCredentials for the default or specified profile from the `~/.aws/config` file. -Single sign-on profiles are also valid. If this fails, try to retrieve credentials from -`_aws_get_role()`, otherwise return `nothing` +Retrieve `AWSCredentials` from the AWS CLI configuration file. The configuration file +defaults to "~/.aws/config" but can be specified using the env variable `AWS_CONFIG_FILE`. +When no credentials are found for the given `profile` then the associated `source_profile` +will be used to recursively look up credentials of source profiles. If still no credentials +can be found then `nothing` will be returned. # Arguments - `profile`: Specific profile used to get AWSCredentials, default is `nothing` @@ -436,6 +495,11 @@ function dot_aws_config(profile=nothing) access_key, secret_key, token = _aws_get_credential_details(p, ini) return AWSCredentials(access_key, secret_key, token) elseif !isnothing(sso_start_url) + # Deprecation should only appear if `dot_aws_config` is called directly + Base.depwarn( + "SSO support in `dot_aws_config` is deprecated, use `sso_credentials` instead.", + :dot_aws_config, + ) access_key, secret_key, token, expiry = _aws_get_sso_credential_details(p, ini) return AWSCredentials(access_key, secret_key, token; expiry=expiry) else diff --git a/src/utilities/credentials.jl b/src/utilities/credentials.jl index 599489cf49..6e2bc52407 100644 --- a/src/utilities/credentials.jl +++ b/src/utilities/credentials.jl @@ -71,7 +71,7 @@ function _aws_get_role(role::AbstractString, ini::Inifile) duration_seconds = get(settings, "duration_seconds", nothing) credentials = nothing - for f in (dot_aws_credentials, dot_aws_config) + for f in (sso_credentials, dot_aws_credentials, dot_aws_config) credentials = f(source_profile) credentials === nothing || break end diff --git a/test/AWSCredentials.jl b/test/AWSCredentials.jl index 60c513b5d6..4f278cbe97 100644 --- a/test/AWSCredentials.jl +++ b/test/AWSCredentials.jl @@ -13,6 +13,8 @@ macro test_ecode(error_codes, expr) end end +const EXPIRATION_FMT = dateformat"yyyy-mm-dd\THH:MM:SS\Z" + @testset "Load Credentials" begin user = aws_user_arn(aws) @test occursin(r"^arn:aws:(iam|sts)::[0-9]+:[^:]+$", user) @@ -399,6 +401,297 @@ end end end end + + # Verify that the search order for credentials mirrors the behavior of the AWS CLI + # (version 2.11.13). Whenever support is added for new credential types new tests should + # be added to this test set. To determine the credential preference order used by AWS + # CLI it is recommended you use a set of valid credentials and a set of invalid + # credentials to determine the precedence. + # + # Documentation on credential precedence: + # - https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-authentication.html#cli-chap-authentication-precedence + # - https://docs.aws.amazon.com/sdk-for-net/v3/developer-guide/creds-assign.html + # - https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/credentials.html + @testset "Credential Precedence" begin + mktempdir() do dir + config_file = joinpath(dir, "config") + creds_file = joinpath(dir, "creds") + + basic_creds_content = """ + [profile1] + aws_access_key_id = AKI1 + aws_secret_access_key = SAK1 + + [profile2] + aws_access_key_id = AKI2 + aws_secret_access_key = SAK2 + """ + + ec2_json = Dict( + "AccessKeyId" => "AKI_EC2", + "SecretAccessKey" => "SAK_EC2", + "Token" => "TOK_EC2", + "Expiration" => Dates.format(now(UTC), EXPIRATION_FMT), + ) + + function ec2_metadata(url::AbstractString) + name = "local-credentials" + metadata_uri = "http://169.254.169.254/latest/meta-data" + if url == "$metadata_uri/iam/info" + return HTTP.Response(200, JSON.json("InstanceProfileArn" => "ARN0")) + elseif url == "$metadata_uri/iam/security-credentials/" + return HTTP.Response(200, name) + elseif url == "$metadata_uri/iam/security-credentials/$name" + return HTTP.Response(200, JSON.json(ec2_json)) + else + return HTTP.Response(404) + end + end + + ecs_json = Dict( + "AccessKeyId" => "AKI_ECS", + "SecretAccessKey" => "SAK_ECS", + "Token" => "TOK_ECS", + "Expiration" => Dates.format(now(UTC), EXPIRATION_FMT), + ) + + function ecs_metadata(url::AbstractString) + if startswith(url, "http://169.254.170.2/") + return HTTP.Response(200, JSON.json(ecs_json)) + else + return HTTP.Response(404) + end + end + + function http_request_patcher(funcs) + @patch function HTTP.request(method, url, args...; kwargs...) + local r + for f in funcs + r = f(string(url)) + r.status != 404 && break + end + return r + end + end + + withenv( + [k => nothing for k in filter(startswith("AWS_"), keys(ENV))]..., + "AWS_SHARED_CREDENTIALS_FILE" => creds_file, + "AWS_CONFIG_FILE" => config_file, + ) do + @testset "explicit profile preferred" begin + isfile(config_file) && rm(config_file) + write(creds_file, basic_creds_content) + + withenv("AWS_PROFILE" => "profile1") do + creds = AWSCredentials(; profile="profile2") + @test creds.access_key_id == "AKI2" + end + + withenv( + "AWS_ACCESS_KEY_ID" => "AKI0", + "AWS_SECRET_ACCESS_KEY" => "SAK0", + # format trick: using this comment to force use of multiple lines + ) do + creds = AWSCredentials(; profile="profile2") + @test creds.access_key_id == "AKI2" + end + end + + @testset "AWS_ACCESS_KEY_ID preferred over AWS_PROFILE" begin + isfile(config_file) && rm(config_file) + write(creds_file, basic_creds_content) + + withenv( + "AWS_PROFILE" => "profile1", + "AWS_ACCESS_KEY_ID" => "AKI0", + "AWS_SECRET_ACCESS_KEY" => "SAK0", + ) do + creds = AWSCredentials() + @test creds.access_key_id == "AKI0" + end + end + + # The AWS CLI used to use `AWS_DEFAULT_PROFILE` to set the AWS profile via the + # command line but this was deprecated in favor of `AWS_PROFILE`. We'll probably + # keeps support for this as long as AWS CLI continues to support it. + # https://github.com/aws/aws-cli/issues/2597 + @testset "AWS_PROFILE preferred over AWS_DEFAULT_PROFILE" begin + isfile(config_file) && rm(config_file) + write(creds_file, basic_creds_content) + + withenv( + "AWS_DEFAULT_PROFILE" => "profile1", + "AWS_PROFILE" => "profile2", + # format trick: using this comment to force use of multiple lines + ) do + creds = AWSCredentials() + @test creds.access_key_id == "AKI2" + end + end + + @testset "Web identity preferred over SSO" begin + write( + config_file, + """ + [default] + sso_start_url = https://my-sso-portal.awsapps.com/start + sso_role_name = role1 + """, + ) + isfile(creds_file) && rm(creds_file) + + web_identity_file = joinpath(dir, "web_identity") + write(web_identity_file, "webid") + + patches = [ + Patches._assume_role_patch( + "AssumeRoleWithWebIdentity"; + access_key="AKI_WEB", + secret_key="SAK_WEB", + session_token="TOK_WEB", + ), + Patches.sso_service_patches("AKI_SSO", "SAK_SSO"), + ] + + withenv( + "AWS_WEB_IDENTITY_TOKEN_FILE" => web_identity_file, + "AWS_ROLE_ARN" => "webid", + ) do + apply(patches) do + creds = AWSCredentials() + @test creds.access_key_id == "AKI_WEB" + end + end + end + + @testset "SSO preferred over credentials file" begin + write( + config_file, + """ + [profile profile1] + sso_start_url = https://my-sso-portal.awsapps.com/start + sso_role_name = role1 + """, + ) + write(creds_file, basic_creds_content) + + apply(Patches.sso_service_patches("AKI_SSO", "SAK_SSO")) do + creds = AWSCredentials(; profile="profile1") + @test creds.access_key_id == "AKI_SSO" + end + end + + @testset "Credential file over credential_process" begin + json = Dict( + "Version" => 1, + "AccessKeyId" => "AKI0", + "SecretAccessKey" => "SAK0", + # format trick: using this comment to force use of multiple lines + ) + write( + config_file, + """ + [profile profile1] + credential_process = echo '$(JSON.json(json))' + """, + ) + write(creds_file, basic_creds_content) + + creds = AWSCredentials(; profile="profile1") + @test creds.access_key_id == "AKI1" + end + + @testset "credential_process over config credentials" begin + json = Dict( + "Version" => 1, + "AccessKeyId" => "AKI0", + "SecretAccessKey" => "SAK0", + # format trick: using this comment to force use of multiple lines + ) + write( + config_file, + """ + [profile profile1] + aws_access_key_id = AKI1 + aws_secret_access_key = SAK1 + credential_process = echo '$(JSON.json(json))' + """, + ) + isfile(creds_file) && rm(creds_file) + + creds = AWSCredentials(; profile="profile1") + @test creds.access_key_id == "AKI0" + end + + @testset "default config credentials over ECS container credentials ENV variables" begin + write( + config_file, + """ + [default] + aws_access_key_id = AKI1 + aws_secret_access_key = SAK1 + """, + ) + isfile(creds_file) && rm(creds_file) + + withenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI" => "/get-creds") do + apply(http_request_patcher([ecs_metadata])) do + @test isnothing(AWS._aws_get_profile(; default=nothing)) + + creds = AWSCredentials() + @test creds.access_key_id == "AKI1" + end + end + end + + @testset "default config credentials over EC2 instance credentials" begin + write( + config_file, + """ + [default] + aws_access_key_id = AKI1 + aws_secret_access_key = SAK1 + """, + ) + isfile(creds_file) && rm(creds_file) + + apply(http_request_patcher([ec2_metadata])) do + @test isnothing(AWS._aws_get_profile(; default=nothing)) + + creds = AWSCredentials() + @test creds.access_key_id == "AKI1" + end + end + + @testset "ECS container credentials ENV variables over EC2 instance credentials" begin + isfile(config_file) && rm(config_file) + isfile(creds_file) && rm(creds_file) + + withenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI" => "/get-creds") do + apply(http_request_patcher([ec2_metadata, ecs_metadata])) do + creds = AWSCredentials() + @test creds.access_key_id == "AKI_ECS" + end + end + end + + # Note: It appears that the ECS container credentials are only used when + # a `AWS_CONTAINER_*` environmental variable is set. However, this test + # ensures that if we do add implicit support that the documented precedence + # order is not violated. + @testset "EC2 instance credentials over ECS container credentials" begin + isfile(config_file) && rm(config_file) + isfile(creds_file) && rm(creds_file) + + apply(http_request_patcher([ec2_metadata, ecs_metadata])) do + creds = AWSCredentials() + @test creds.access_key_id == "AKI_EC2" + end + end + end + end + end end @testset "Retrieving AWS Credentials" begin @@ -426,13 +719,13 @@ end uri = test_values["URI"] url = string(url) - if url == "http://169.254.169.254/latest/meta-data/iam/info" + metadata_uri = "http://169.254.169.254/latest/meta-data" + if url == "$metadata_uri/iam/info" instance_profile_arn = test_values["InstanceProfileArn"] return HTTP.Response("{\"InstanceProfileArn\": \"$instance_profile_arn\"}") - elseif url == "http://169.254.169.254/latest/meta-data/iam/security-credentials/" + elseif url == "$metadata_uri/iam/security-credentials/" return HTTP.Response(test_values["Security-Credentials"]) - elseif url == - "http://169.254.169.254/latest/meta-data/iam/security-credentials/$security_credentials" || + elseif url == "$metadata_uri/iam/security-credentials/$security_credentials" || url == "http://169.254.170.2$uri" my_dict = JSON.json(test_values) response = HTTP.Response(my_dict) @@ -502,7 +795,7 @@ end test_values["AccessKeyId"], test_values["SecretAccessKey"] ), ) do - specified_result = dot_aws_config(test_values["Test-SSO-Profile"]) + specified_result = sso_credentials(test_values["Test-SSO-Profile"]) @test specified_result.access_key_id == test_values["AccessKeyId"] @test specified_result.secret_key == test_values["SecretAccessKey"] @@ -529,47 +822,22 @@ end chmod(credential_process_file, 0o700) withenv("AWS_CONFIG_FILE" => config_file) do - @testset "support" begin - open(config_file, "w") do io - write( - io, - """ - [profile $(test_values["Test-Config-Profile"])] - credential_process = $(abspath(credential_process_file)) - """, - ) - end - - result = dot_aws_config(test_values["Test-Config-Profile"]) - - @test result.access_key_id == test_values["Test-AccessKeyId"] - @test result.secret_key == test_values["Test-SecretAccessKey"] - @test isempty(result.token) - @test result.expiry == typemax(DateTime) + open(config_file, "w") do io + write( + io, + """ + [profile $(test_values["Test-Config-Profile"])] + credential_process = $(abspath(credential_process_file)) + """, + ) end - # The AWS CLI uses the config file `credential_process` setting over - # specifying the config file `aws_access_key_id`/`aws_secret_access_key`. - @testset "precedence" begin - open(config_file, "w") do io - write( - io, - """ - [profile $(test_values["Test-Config-Profile"])] - aws_access_key_id = invalid - aws_secret_access_key = invalid - credential_process = $(abspath(credential_process_file)) - """, - ) - end - - result = dot_aws_config(test_values["Test-Config-Profile"]) + result = dot_aws_config(test_values["Test-Config-Profile"]) - @test result.access_key_id == test_values["Test-AccessKeyId"] - @test result.secret_key == test_values["Test-SecretAccessKey"] - @test isempty(result.token) - @test result.expiry == typemax(DateTime) - end + @test result.access_key_id == test_values["Test-AccessKeyId"] + @test result.secret_key == test_values["Test-SecretAccessKey"] + @test isempty(result.token) + @test result.expiry == typemax(DateTime) end end end @@ -690,6 +958,19 @@ end @test result.renew == ecs_instance_credentials end end + + # When the environmental variable isn't set then the ECS credential provider is + # unavailable. + withenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI" => nothing) do + @test ecs_instance_credentials() === nothing + end + + # Specifying the environmental variable results in us attempting to connect to the + # ECS credential provider. + withenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI" => "/invalid") do + # Internally throws a `ConnectError` exception + @test ecs_instance_credentials() === nothing + end end @testset "Web Identity File" begin @@ -780,7 +1061,7 @@ end "AccessKeyId" => "access-key", "SecretAccessKey" => "secret-key", "SessionToken" => "session-token", - "Expiration" => Dates.format(expiration, dateformat"yyyy-mm-dd\THH:MM:SS\Z"), + "Expiration" => Dates.format(expiration, EXPIRATION_FMT), ) creds = external_process_credentials(gen_process(temporary_resp)) @test creds.access_key_id == temporary_resp["AccessKeyId"] @@ -797,7 +1078,7 @@ end "Version" => 1, "AccessKeyId" => "access-key", "SecretAccessKey" => "secret-key", - "Expiration" => Dates.format(expiration, dateformat"yyyy-mm-dd\THH:MM:SS\Z"), + "Expiration" => Dates.format(expiration, EXPIRATION_FMT), ) ex = KeyError("SessionToken") @test_throws ex external_process_credentials(gen_process(missing_token_resp)) @@ -814,7 +1095,9 @@ end @testset "Credentials Not Found" begin patches = [ - @patch HTTP.request(method::String, url; kwargs...) = nothing + @patch function HTTP.request(method::String, url, args...; kwargs...) + throw(HTTP.Exceptions.ConnectError(string(url), "host is unreachable")) + end Patches._cred_file_patch Patches._config_file_patch ]