From 52b62698b83dbbf5056a4de5d110243f9771d0bb Mon Sep 17 00:00:00 2001 From: kolia Date: Tue, 15 Dec 2020 18:45:29 +0000 Subject: [PATCH 1/6] Make AWS_ROLE_SESSION_NAME optional AWS docs indicate that it should be optional. If unspecified, a default is set and warning logged. --- src/AWSCredentials.jl | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/AWSCredentials.jl b/src/AWSCredentials.jl index 3dc70a03c4..8df1e43b17 100644 --- a/src/AWSCredentials.jl +++ b/src/AWSCredentials.jl @@ -470,7 +470,6 @@ function credentials_from_webtoken() has_all_keys = haskey(ENV, token_role_arn) && - haskey(ENV, token_role_session) && haskey(ENV, token_web_identity) if !has_all_keys @@ -478,7 +477,11 @@ function credentials_from_webtoken() end role_arn = ENV[token_role_arn] - role_session = ENV[token_role_session] + role_session = get!(ENV, token_role_session) do + default_role_session = replace("default-AWS.jl-role-session-$(now())", ":" => "-") + @warn "Consider setting (optional) AWS_ROLE_SESSION_NAME environment variable, defaulting to $default_role_session" + return default_role_session + end web_identity = read(ENV["AWS_WEB_IDENTITY_TOKEN_FILE"], String) resp = @mock AWSServices.sts( From e902c9c5c5400f0c613e0259df4b097248537f3a Mon Sep 17 00:00:00 2001 From: kolia Date: Tue, 15 Dec 2020 19:00:02 +0000 Subject: [PATCH 2/6] Add test. --- test/AWSCredentials.jl | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/AWSCredentials.jl b/test/AWSCredentials.jl index 36e3164164..00f3bda772 100644 --- a/test/AWSCredentials.jl +++ b/test/AWSCredentials.jl @@ -492,6 +492,27 @@ end @test expiry != result.expiry end end + + withenv( + "AWS_ROLE_ARN" => "foobar", + "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.renew == credentials_from_webtoken + expiry = result.expiry + + result = check_credentials(result) + + @test result.access_key_id == Patches.web_access_key + @test result.secret_key == Patches.web_secret_key + @test result.renew == credentials_from_webtoken + @test expiry != result.expiry + end + end end end From e9c1147d2cb493b1ad9260265293e44cd0abd305 Mon Sep 17 00:00:00 2001 From: kolia Date: Tue, 22 Dec 2020 10:13:22 -0500 Subject: [PATCH 3/6] do not modify ENV Co-authored-by: Curtis Vogt --- src/AWSCredentials.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AWSCredentials.jl b/src/AWSCredentials.jl index 8df1e43b17..81a40c185f 100644 --- a/src/AWSCredentials.jl +++ b/src/AWSCredentials.jl @@ -477,7 +477,7 @@ function credentials_from_webtoken() end role_arn = ENV[token_role_arn] - role_session = get!(ENV, token_role_session) do + role_session = get(ENV, token_role_session) do default_role_session = replace("default-AWS.jl-role-session-$(now())", ":" => "-") @warn "Consider setting (optional) AWS_ROLE_SESSION_NAME environment variable, defaulting to $default_role_session" return default_role_session From 286d2ceaa0b7c34e73e5473b35658a82cefee91f Mon Sep 17 00:00:00 2001 From: kolia Date: Mon, 1 Mar 2021 10:26:34 -0500 Subject: [PATCH 4/6] use date format instead of char replace Co-authored-by: Curtis Vogt --- src/AWSCredentials.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/AWSCredentials.jl b/src/AWSCredentials.jl index 81a40c185f..29a9701fc3 100644 --- a/src/AWSCredentials.jl +++ b/src/AWSCredentials.jl @@ -478,7 +478,8 @@ function credentials_from_webtoken() role_arn = ENV[token_role_arn] role_session = get(ENV, token_role_session) do - default_role_session = replace("default-AWS.jl-role-session-$(now())", ":" => "-") + timestamp = Dates.format(now(UTC), dateformat"yyyymmdd\THHMMSS\Z") + default_role_session = "default-AWS.jl-role-session-$timestamp" @warn "Consider setting (optional) AWS_ROLE_SESSION_NAME environment variable, defaulting to $default_role_session" return default_role_session end From 651fd5556ed12d9321af96a04831eddde5506d5d Mon Sep 17 00:00:00 2001 From: Curtis Vogt Date: Mon, 29 Mar 2021 14:49:35 -0500 Subject: [PATCH 5/6] Improve role session name and tests --- Project.toml | 3 ++- src/AWSCredentials.jl | 50 ++++++++++++++++++++++++------------ test/AWSCredentials.jl | 58 +++++++++++++++++++++++++++--------------- test/patch.jl | 31 ++++++++++++---------- test/runtests.jl | 1 + 5 files changed, 92 insertions(+), 51 deletions(-) diff --git a/Project.toml b/Project.toml index 1f2ea02565..55e40f720a 100644 --- a/Project.toml +++ b/Project.toml @@ -36,9 +36,10 @@ julia = "1" [extras] Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Suppressor = "fd094767-a336-5f1f-9728-57cf17d0bbfb" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" [targets] -test = ["Pkg", "Suppressor", "Test", "UUIDs"] +test = ["Pkg", "Random", "Suppressor", "Test", "UUIDs"] diff --git a/src/AWSCredentials.jl b/src/AWSCredentials.jl index 29a9701fc3..6e8d01ed7d 100644 --- a/src/AWSCredentials.jl +++ b/src/AWSCredentials.jl @@ -463,34 +463,32 @@ function dot_aws_config(profile=nothing) end +# https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-role.html#cli-configure-role-oidc function credentials_from_webtoken() token_role_arn = "AWS_ROLE_ARN" - token_role_session = "AWS_ROLE_SESSION_NAME" - token_web_identity = "AWS_WEB_IDENTITY_TOKEN_FILE" + token_web_identity_file = "AWS_WEB_IDENTITY_TOKEN_FILE" + token_role_session = "AWS_ROLE_SESSION_NAME" # Optional session name - has_all_keys = - haskey(ENV, token_role_arn) && - haskey(ENV, token_web_identity) - - if !has_all_keys + if !(haskey(ENV, token_role_arn) && haskey(ENV, token_web_identity_file)) return nothing end role_arn = ENV[token_role_arn] + web_identity = read(ENV["AWS_WEB_IDENTITY_TOKEN_FILE"], String) role_session = get(ENV, token_role_session) do - timestamp = Dates.format(now(UTC), dateformat"yyyymmdd\THHMMSS\Z") - default_role_session = "default-AWS.jl-role-session-$timestamp" - @warn "Consider setting (optional) AWS_ROLE_SESSION_NAME environment variable, defaulting to $default_role_session" - return default_role_session + _role_session_name( + "AWS.jl-role-", + basename(role_arn), + "-" * Dates.format(@mock(now(UTC)), dateformat"yyyymmdd\THHMMSS\Z"), + ) end - web_identity = read(ENV["AWS_WEB_IDENTITY_TOKEN_FILE"], String) - resp = @mock AWSServices.sts( + resp = AWSServices.sts( "AssumeRoleWithWebIdentity", Dict( "RoleArn" => role_arn, - "RoleSessionName" => role_session, - "WebIdentityToken" => web_identity + "RoleSessionName" => role_session, # Required by AssumeRoleWithWebIdentity + "WebIdentityToken" => web_identity, ); aws_config=AWSConfig(creds=nothing) ) @@ -500,7 +498,8 @@ function credentials_from_webtoken() return AWSCredentials( role_creds["AccessKeyId"], role_creds["SecretAccessKey"], - role_creds["SessionToken"]; + role_creds["SessionToken"], + role_creds["AssumedRoleUser"]["Arn"]; expiry=DateTime(rstrip(role_creds["Expiration"], 'Z')), renew=credentials_from_webtoken ) @@ -629,3 +628,22 @@ function _get_ini_value( return value end + + +""" + _role_session_name( + prefix::AbstractString, + name::AbstractString, + suffix::AbstractString, + ) -> String + +Generate a valid role session name. Currently only ensures that the session name is +64-characters or less. +""" +function _role_session_name(prefix, name, suffix) + b = IOBuffer() + write(b, prefix, name) + truncate(b, min(64 - length(suffix), b.size)) # Assumes ASCII + write(b, suffix) + return String(take!(b)) +end diff --git a/test/AWSCredentials.jl b/test/AWSCredentials.jl index 00f3bda772..7e478535aa 100644 --- a/test/AWSCredentials.jl +++ b/test/AWSCredentials.jl @@ -39,6 +39,11 @@ end ) end +@testset "_role_session_name" begin + @test AWS._role_session_name("prefix-", "name", "-suffix") == "prefix-name-suffix" + @test AWS._role_session_name("a" ^ 22, "b" ^ 22, "c" ^ 22) == "a" ^ 22 * "b" ^ 20 * "c" ^ 22 +end + @testset "AWSCredentials" begin @testset "Defaults" begin creds = AWSCredentials("access_key_id" ,"secret_key") @@ -468,49 +473,60 @@ end mktempdir() do dir web_identity_file = joinpath(dir, "web_identity") write(web_identity_file, "foobar") + session_name = "foobar-session" + + access_key = "access-key-$(randstring(6))" + secret_key = "secret-key-$(randstring(6))" + session_token = "session-token-$(randstring(6))" + role_arn = "arn:aws:sts::1234:assumed-role/foobar" + + patch = Patches._web_identity_patch(; + access_key=access_key, + secret_key=secret_key, + session_token=session_token, + role_arn=role_arn, + ) withenv( "AWS_ROLE_ARN" => "foobar", - "AWS_ROLE_SESSION_NAME" => Patches.web_sesh_token, "AWS_WEB_IDENTITY_TOKEN_FILE" => web_identity_file, + "AWS_ROLE_SESSION_NAME" => session_name, ) do - apply(Patches._web_identity_patch) do + apply(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.access_key_id == access_key + @test result.secret_key == secret_key + @test result.token == session_token + @test result.user_arn == role_arn * "/" * session_name @test result.renew == credentials_from_webtoken expiry = result.expiry result = check_credentials(result) - @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.access_key_id == access_key + @test result.secret_key == secret_key + @test result.token == session_token + @test result.user_arn == role_arn * "/" * session_name @test result.renew == credentials_from_webtoken @test expiry != result.expiry end end + session_name = "AWS.jl-role-foobar-20210101T000000Z" + patches = [ + patch + @patch Dates.now(::Type{UTC}) = DateTime(2021) + ] + withenv( "AWS_ROLE_ARN" => "foobar", "AWS_WEB_IDENTITY_TOKEN_FILE" => web_identity_file, + "AWS_ROLE_SESSION_NAME" => nothing, ) do - apply(Patches._web_identity_patch) do + apply(patches) do result = credentials_from_webtoken() - - @test result.access_key_id == Patches.web_access_key - @test result.secret_key == Patches.web_secret_key - @test result.renew == credentials_from_webtoken - expiry = result.expiry - - result = check_credentials(result) - - @test result.access_key_id == Patches.web_access_key - @test result.secret_key == Patches.web_secret_key - @test result.renew == credentials_from_webtoken - @test expiry != result.expiry + @test result.user_arn == role_arn * "/" * session_name end end end diff --git a/test/patch.jl b/test/patch.jl index d673912f1a..b46b30214d 100644 --- a/test/patch.jl +++ b/test/patch.jl @@ -44,10 +44,6 @@ 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 @@ -69,17 +65,26 @@ _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)) - ) +_web_identity_patch = function (; + access_key="web_identity_access_key", + secret_key="web_identity_secret_key", + session_token="web_session_token", + role_arn="arn:aws:sts:::assumed-role/role-name", +) + @patch function AWS._http_request(request) + params = Dict(split.(split(request.content, '&'), '=')) + creds = Dict( + "AccessKeyId" => access_key, + "SecretAccessKey" => secret_key, + "SessionToken" => session_token, + "Expiration" => string(now(UTC)), + "AssumedRoleUser" => Dict("Arn" => role_arn * "/" * params["RoleSessionName"]), + ) - result = Dict("AssumeRoleWithWebIdentityResult" => Dict("Credentials" => creds)) + result = Dict("AssumeRoleWithWebIdentityResult" => Dict("Credentials" => creds)) - return HTTP.Response(200, ["Content-Type" => "text/json", "charset" => "utf-8"], body=json(result)) + return HTTP.Response(200, ["Content-Type" => "text/json", "charset" => "utf-8"], body=json(result)) + end end _github_tree_patch = @patch function tree(repo, tree_obj; kwargs...) diff --git a/test/runtests.jl b/test/runtests.jl index 316aea3273..44dd03b356 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -16,6 +16,7 @@ using OrderedCollections: LittleDict, OrderedDict using MbedTLS: digest, MD_SHA256, MD_MD5 using Mocking using Pkg +using Random using Retry using Suppressor using Test From f1558ea6a7f16ce2e879a3cc03f5e27f6b2977dd Mon Sep 17 00:00:00 2001 From: Curtis Vogt Date: Mon, 29 Mar 2021 17:24:34 -0500 Subject: [PATCH 6/6] Correct AssumedRoleUser location --- src/AWSCredentials.jl | 3 ++- test/patch.jl | 10 ++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/AWSCredentials.jl b/src/AWSCredentials.jl index 6e8d01ed7d..064f26a09c 100644 --- a/src/AWSCredentials.jl +++ b/src/AWSCredentials.jl @@ -494,12 +494,13 @@ function credentials_from_webtoken() ) role_creds = resp["AssumeRoleWithWebIdentityResult"]["Credentials"] + assumed_role_user = resp["AssumeRoleWithWebIdentityResult"]["AssumedRoleUser"] return AWSCredentials( role_creds["AccessKeyId"], role_creds["SecretAccessKey"], role_creds["SessionToken"], - role_creds["AssumedRoleUser"]["Arn"]; + assumed_role_user["Arn"]; expiry=DateTime(rstrip(role_creds["Expiration"], 'Z')), renew=credentials_from_webtoken ) diff --git a/test/patch.jl b/test/patch.jl index b46b30214d..6eb9f32f00 100644 --- a/test/patch.jl +++ b/test/patch.jl @@ -78,10 +78,16 @@ _web_identity_patch = function (; "SecretAccessKey" => secret_key, "SessionToken" => session_token, "Expiration" => string(now(UTC)), - "AssumedRoleUser" => Dict("Arn" => role_arn * "/" * params["RoleSessionName"]), ) - result = Dict("AssumeRoleWithWebIdentityResult" => Dict("Credentials" => creds)) + result = Dict( + "AssumeRoleWithWebIdentityResult" => Dict( + "Credentials" => creds, + "AssumedRoleUser" => Dict( + "Arn" => role_arn * "/" * params["RoleSessionName"], + ), + ), + ) return HTTP.Response(200, ["Content-Type" => "text/json", "charset" => "utf-8"], body=json(result)) end