diff --git a/src/AWSCredentials.jl b/src/AWSCredentials.jl index e139c97b72..bcf47344b6 100644 --- a/src/AWSCredentials.jl +++ b/src/AWSCredentials.jl @@ -21,7 +21,8 @@ export AWSCredentials, env_var_credentials, localhost_is_ec2, localhost_maybe_ec2, - localhost_is_lambda + localhost_is_lambda, + credentials_from_webtoken """ AWSCredentials @@ -77,7 +78,7 @@ end function Base.show(io::IO, c::AWSCredentials) - println(io, + print(io, c.user_arn, isempty(c.user_arn) ? "" : " ", "(", @@ -452,6 +453,48 @@ function dot_aws_config(profile=nothing) end +function credentials_from_webtoken(profile=nothing) + token_role_arn = "AWS_ROLE_ARN" + token_role_session = "AWS_ROLE_SESSION_NAME" + token_web_identity = "AWS_WEB_IDENTITY_TOKEN_FILE" + + has_all_keys = + haskey(ENV, token_role_arn) && + haskey(ENV, token_role_session) && + haskey(ENV, token_web_identity) + + if !has_all_keys + throw(WebIdentityVarsNotSet( + "You need to set $(token_role_arn), $(token_role_session), $(token_web_identity) environment variables" + )) + end + + role_arn = ENV[token_role_arn] + role_session = ENV[token_role_session] + web_identity = read(ENV["AWS_WEB_IDENTITY_TOKEN_FILE"], String) + + resp = AWSServices.sts( + "AssumeRoleWithWebIdentity", + Dict( + "RoleArn" => role_arn, + "RoleSessionName" => role_session, + "WebIdentityToken" => web_identity + ); + aws_config=AWSConfig(profile=profile) + ) + + role_creds = resp["AssumeRoleWithWebIdentityResult"]["Credentials"] + + return AWSCredentials( + role_creds["AccessKeyId"], + role_creds["SecretAccessKey"], + role_creds["SessionToken"]; + expiry=DateTime(rstrip(role_creds["Expiration"], 'Z')), + renew=credentials_from_webtoken + ) +end + + """ _aws_get_credential_details(profile::AbstractString, ini::Inifile) -> Tuple diff --git a/src/AWSExceptions.jl b/src/AWSExceptions.jl index cda40a0ca2..b90f2f1b69 100644 --- a/src/AWSExceptions.jl +++ b/src/AWSExceptions.jl @@ -5,7 +5,7 @@ using JSON using XMLDict using XMLDict: XMLDictElement -export AWSException, ProtocolNotDefined, InvalidFileName, NoCredentials +export AWSException, ProtocolNotDefined, InvalidFileName, NoCredentials, WebIdentityVarsNotSet struct ProtocolNotDefined <: Exception message::String @@ -22,6 +22,11 @@ struct NoCredentials <: Exception end Base.show(io::IO, e::NoCredentials) = println(io, e.message) +struct WebIdentityVarsNotSet <: Exception + message::String +end +Base.show(io::IO, e::WebIdentityVarsNotSet) = println(io, e.message) + struct AWSException <: Exception code::String message::String diff --git a/test/AWSCredentials.jl b/test/AWSCredentials.jl index c666c62df8..9b297c2917 100644 --- a/test/AWSCredentials.jl +++ b/test/AWSCredentials.jl @@ -462,17 +462,35 @@ end end end - @testset "Credentials Not Found" begin - _http_request_patch = @patch function HTTP.request(method::String, url::String) - return nothing - end + @testset "Web Identity File" begin + mktempdir() do dir + web_identity_file = joinpath(dir, "web_identity") + write(web_identity_file, "foobar") - _cred_file_patch = @patch function dot_aws_credentials_file() - return "" + withenv( + "AWS_ROLE_ARN" => "foobar", + "AWS_ROLE_SESSION_NAME" => Patches.web_sesh_token, + "AWS_WEB_IDENTITY_TOKEN_FILE" => web_identity_file, + ) do + apply(Patches._web_identity_patch) do + result = credentials_from_webtoken() + + @test result.access_key_id == Patches.web_access_key + @test result.secret_key == Patches.web_secret_key + @test result.token == Patches.web_sesh_token + @test result.renew == credentials_from_webtoken + end + end end + end + + @testset "Web Identity File -- Exception" begin + @test_throws WebIdentityVarsNotSet credentials_from_webtoken() + end - _config_file_patch = @patch function dot_aws_config_file() - return "" + @testset "Credentials Not Found" begin + _http_request_patch = @patch function HTTP.request(method::String, url::String) + return nothing end ACCESS_KEY = "AWS_ACCESS_KEY_ID" @@ -485,7 +503,7 @@ end delete!(ENV, "AWS_ACCESS_KEY_ID") delete!(ENV, "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") - apply([_http_request_patch, _cred_file_patch, _config_file_patch]) do + apply([_http_request_patch, Patches._cred_file_patch, Patches._config_file_patch]) do @test_throws ErrorException AWSConfig() end finally diff --git a/test/patch.jl b/test/patch.jl index 9c322cfb62..d673912f1a 100644 --- a/test/patch.jl +++ b/test/patch.jl @@ -1,7 +1,9 @@ module Patches using AWS +using Dates using HTTP +using JSON using GitHub using Mocking using OrderedCollections: LittleDict @@ -42,6 +44,10 @@ body = """ response = HTTP.Messages.Response() +web_access_key = "web_identity_access_key" +web_secret_key = "web_identity_secret_key" +web_sesh_token = "web_session_token" + function _response!(; version::VersionNumber=version, status::Int64=status, headers::Array=headers, body::String=body) response.version = version response.status = status @@ -55,6 +61,27 @@ _aws_http_request_patch = @patch function AWS._http_request(request::Request) return response end +_cred_file_patch = @patch function dot_aws_credentials_file() + return "" +end + +_config_file_patch = @patch function dot_aws_config_file() + return "" +end + +_web_identity_patch = @patch function AWS._http_request(request) + creds = Dict( + "AccessKeyId" => web_access_key, + "SecretAccessKey" => web_secret_key, + "SessionToken" => web_sesh_token, + "Expiration" => string(now(UTC)) + ) + + result = Dict("AssumeRoleWithWebIdentityResult" => Dict("Credentials" => creds)) + + return HTTP.Response(200, ["Content-Type" => "text/json", "charset" => "utf-8"], body=json(result)) +end + _github_tree_patch = @patch function tree(repo, tree_obj; kwargs...) if tree_obj == "master" return Tree("test-sha", HTTP.URI(), [Dict("path"=>"apis", "sha"=>"apis-sha")], false) diff --git a/test/runtests.jl b/test/runtests.jl index acb3478ac4..2456f6a118 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,7 +1,7 @@ using AWS using AWS: AWSCredentials using AWS: AWSServices -using AWS.AWSExceptions: AWSException, NoCredentials +using AWS.AWSExceptions: AWSException, NoCredentials, WebIdentityVarsNotSet using AWS.AWSMetadataUtilities: _clean_documentation, _filter_latest_service_version, _generate_low_level_definition, _generate_high_level_definition, _generate_high_level_definitions, _get_aws_sdk_js_files, _get_service_and_version, _get_function_parameters, _clean_uri, _format_function_name,