Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

make AWS_ROLE_SESSION_NAME optional #265

Merged
merged 6 commits into from
Mar 31, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
49 changes: 36 additions & 13 deletions src/AWSCredentials.jl
Original file line number Diff line number Diff line change
Expand Up @@ -463,40 +463,44 @@ 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_role_session) &&
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]
role_session = ENV[token_role_session]
web_identity = read(ENV["AWS_WEB_IDENTITY_TOKEN_FILE"], String)
role_session = get(ENV, token_role_session) do
_role_session_name(
"AWS.jl-role-",
basename(role_arn),
"-" * Dates.format(@mock(now(UTC)), dateformat"yyyymmdd\THHMMSS\Z"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dates.format(@mock(now(UTC)), dateformat"yyyymmdd\THHMMSS\Z")

Maybe we pull this out as an internal function, a similar piece of code already exists in test/runtests.jl

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll look into that. Do you happen to know why that test function uses lowercase?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure this is ported over from AWSCore.jl, the only difference here is that the t becomes lowercase. I assume this is to increase readability. Removing the lowercase call wouldn't affect anything in terms of running on the AWS account, feel free to remove it.

julia> using Dates

julia> Dates.format(now(Dates.UTC), dateformat"yyyymmdd\THHMMSSsss\Z")
"20210331T145218874Z"

julia> lowercase(Dates.format(now(Dates.UTC), dateformat"yyyymmdd\THHMMSSsss\Z"))
"20210331t145225679z"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure it's worth it to refactor this into a separate function. The _now_formatted function includes milliseconds (which we could use here) and there are several different DateFormats used throughout the repo already:

src/AWS.jl:224:    query["Expires"] = Dates.format(time + Dates.Minute(2), dateformat"yyyy-mm-dd\THH:MM:SS\Z")
src/AWS.jl:246:    date = Dates.format(time, dateformat"yyyymmdd")
src/AWS.jl:247:    datetime = Dates.format(time, dateformat"yyyymmdd\THHMMSS\Z")
src/AWSCredentials.jl:482:            "-" * Dates.format(@mock(now(UTC)), dateformat"yyyymmdd\THHMMSS\Z"),
test/AWS.jl:49:    date = Dates.format(time, dateformat"yyyymmdd")
test/AWS.jl:88:        expected_x_amz_date = Dates.format(time, dateformat"yyyymmdd\THHMMSS\Z")
test/runtests.jl:33:    return lowercase(Dates.format(now(Dates.UTC), dateformat"yyyymmdd\THHMMSSsss\Z"))

As some of these formats are required to be specific to follow spec and others are just there to make identifiers unique I think there isn't a point in extracting this date formatting code. I could see some further refactoring of the _role_session_name to automatically add a timestamp suffix and possibly use it with other AWS services that support names

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense!

)
end

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)
)

role_creds = resp["AssumeRoleWithWebIdentityResult"]["Credentials"]
assumed_role_user = resp["AssumeRoleWithWebIdentityResult"]["AssumedRoleUser"]

return AWSCredentials(
role_creds["AccessKeyId"],
role_creds["SecretAccessKey"],
role_creds["SessionToken"];
role_creds["SessionToken"],
assumed_role_user["Arn"];
expiry=DateTime(rstrip(role_creds["Expiration"], 'Z')),
renew=credentials_from_webtoken
)
Expand Down Expand Up @@ -625,3 +629,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
53 changes: 45 additions & 8 deletions test/AWSCredentials.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -468,30 +473,62 @@ 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)
mattBrzezinski marked this conversation as resolved.
Show resolved Hide resolved
]

withenv(
"AWS_ROLE_ARN" => "foobar",
"AWS_WEB_IDENTITY_TOKEN_FILE" => web_identity_file,
"AWS_ROLE_SESSION_NAME" => nothing,
) do
apply(patches) do
result = credentials_from_webtoken()
@test result.user_arn == role_arn * "/" * session_name
end
end
end
end

Expand Down
37 changes: 24 additions & 13 deletions test/patch.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -69,17 +65,32 @@ _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)),
)

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))
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...)
Expand Down
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down