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

Allows profiles to automatically assume a role #34

Merged
merged 5 commits into from
Apr 19, 2018
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
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,22 @@ Create an IAM user...
```julia
iam(aws, "CreateUser", {"UserName" => "me"})
```


Automatically assume a role([details](https://docs.aws.amazon.com/cli/latest/userguide/cli-roles.html))...

For a user with the IAM profile `valid-iam-profile` already in their credentials file
that has permissions to a role called `example-role-name`:

~/.aws/config:
```
[profile example-role-name]
role_arn = arn:aws:iam::[role number here]:role/example-role-name
source_profile = valid-iam-profile
```


```julia
ENV["AWS_PROFILE"] = "example-role-name"
AWSCore.aws_config()
```
119 changes: 103 additions & 16 deletions src/AWSCredentials.jl
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ The fields `access_key_id` and `secret_key` hold the access keys used to authent
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.

The `AWSCredentials()` constructor tries to load local Credentials from
environment variables, `~/.aws/credentials` or EC2 instance credentials.
environment variables, `~/.aws/credentials`, `~/.aws/config` or EC2 instance credentials.
"""

mutable struct AWSCredentials
Expand Down Expand Up @@ -67,7 +67,7 @@ function AWSCredentials()

creds = env_instance_credentials()

elseif isfile(dot_aws_credentials_file())
elseif isfile(dot_aws_credentials_file()) || isfile(dot_aws_config_file())

creds = dot_aws_credentials()

Expand Down Expand Up @@ -251,33 +251,120 @@ end

using IniFile

dot_aws_credentials_file() = get(ENV, "AWS_CONFIG_FILE",
dot_aws_credentials_file() = get(ENV, "AWS_SHARED_CREDENTIALS_FILE",
joinpath(homedir(), ".aws", "credentials"))

"""
Load Credentials from [AWS CLI ~/.aws/credentials file]
(http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html).
"""
dot_aws_config_file() = get(ENV, "AWS_CONFIG_FILE",
joinpath(homedir(), ".aws", "config"))

function dot_aws_credentials()

@assert isfile(dot_aws_credentials_file())
function aws_get_role_details(profile::AbstractString, ini::Inifile)
if debug_level > 0
println("Loading \"$profile\" Profile from " *
dot_aws_config_file() * "... ")
end

ini = read(Inifile(), dot_aws_credentials_file())
role_arn = get(ini, profile, "role_arn")
source_profile = get(ini, profile, "source_profile")

profile = get(ENV, "AWS_DEFAULT_PROFILE",
get(ENV, "AWS_PROFILE", "default"))
profile = "profile $profile"
role_arn = get(ini, profile, "role_arn", role_arn)
source_profile = get(ini, profile, "source_profile", source_profile)

(source_profile, role_arn)
end

function aws_get_credential_details(profile::AbstractString, ini::Inifile, config::Bool)
if debug_level > 0
print("Loading \"$profile\" AWSCredentials from " *
dot_aws_credentials_file() * "... ")
filename = config ? dot_aws_config_file() : dot_aws_credentials_file()
println("Loading \"$profile\" AWSCredentials from " * filename
* "... ")
end

key_id = get(ini, profile, "aws_access_key_id")
key = get(ini, profile, "aws_secret_access_key")

if config
profile = "profile $profile"
key_id = get(ini, profile, "aws_access_key_id", key_id)
key = get(ini, profile, "aws_secret_access_key", key)
end

(key, key_id)
end

function aws_get_region(profile::AbstractString, ini::Inifile)
region = get(ENV, "AWS_DEFAULT_REGION", "us-east-1")

region = get(ini, profile, "region", region)
region = get(ini, "profile $profile", "region", region)
end

function aws_get_role(role::AbstractString, ini::Inifile)
source_profile, role_arn = aws_get_role_details(role, ini)

if source_profile == :notfound
error("Can't find AWS credentials!")
end

AWSCredentials(get(ini, profile, "aws_access_key_id"),
get(ini, profile, "aws_secret_access_key"))
if debug_level > 0
println("Assuming \"$source_profile\"... ")
end
credentials = dot_aws_credentials(source_profile)

config = AWSConfig(:creds=>credentials, :region=>aws_get_region(source_profile, ini))

role = Services.sts(
config,
"AssumeRole",
RoleArn=role_arn,
RoleSessionName=role
)
role_creds = role["Credentials"]

credentials = AWSCredentials(role_creds["AccessKeyId"],
role_creds["SecretAccessKey"],
role_creds["SessionToken"]
)
end

"""
Load Credentials from [AWS CLI ~/.aws/credentials file] or [AWS CLI ~/.aws/config file]
(http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html).
"""
function dot_aws_credentials(profile = nothing)
@assert isfile(dot_aws_credentials_file()) || isfile(dot_aws_config_file())

if profile == nothing
profile = get(ENV, "AWS_DEFAULT_PROFILE",
get(ENV, "AWS_PROFILE", "default"))
end

# According to the docs the order of precedence is:
# 1. credentials in the credential file
# 2. credentials in the config file
# 3. roles in the config file
credential_file = dot_aws_credentials_file()
ini = nothing
if isfile(credential_file)
ini = read(Inifile(), credential_file)
key, key_id = aws_get_credential_details(profile, ini, false)
if key != :notfound
return AWSCredentials(key_id, key)
end
end

config_file = dot_aws_config_file()
if isfile(config_file)
ini = read(Inifile(), config_file)
key, key_id = aws_get_credential_details(profile, ini, true)
if key != :notfound
AWSCredentials(key_id, key)
else
aws_get_role(profile, ini)
end
end
end

#==============================================================================#
# End of file.
Expand Down
117 changes: 117 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,123 @@ aws = aws_config()
end
end

@testset "AssumeRole" begin
mktemp() do config_file, config_io
write(config_io, """[profile test]
output = json
region = us-east-1

[profile test:dev]
source_profile = test
role_arn = arn:aws:iam::123456789000:role/Dev

[profile test:sub-dev]
source_profile = test:dev
role_arn = arn:aws:iam::123456789000:role/SubDev

[profile test2]
aws_access_key_id = WRONG_ACCESS_ID
aws_secret_access_key = WRONG_ACCESS_KEY
output = json
region = us-east-1

[profile test3]
source_profile = test:dev
role_arn = arn:aws:iam::123456789000:role/test3

[profile test4]
source_profile = test:dev
role_arn = arn:aws:iam::123456789000:role/test3
aws_access_key_id = RIGHT_ACCESS_ID4
aws_secret_access_key = RIGHT_ACCESS_KEY4
""")
close(config_io)

mktemp() do creds_file, creds_io
write(creds_io, """[test]
aws_access_key_id = TEST_ACCESS_ID
aws_secret_access_key = TEST_ACCESS_KEY

[test2]
aws_access_key_id = RIGHT_ACCESS_ID2
aws_secret_access_key = RIGHT_ACCESS_KEY2

[test3]
aws_access_key_id = RIGHT_ACCESS_ID3
aws_secret_access_key = RIGHT_ACCESS_KEY3
""")
close(creds_io)

withenv(
"AWS_SHARED_CREDENTIALS_FILE" => creds_file,
"AWS_CONFIG_FILE" => config_file,
"AWS_DEFAULT_PROFILE" => "test"
) do

# Check credentials load
config = AWSCore.aws_config()
creds = config[:creds]

@test creds.access_key_id == "TEST_ACCESS_ID"
@test creds.secret_key == "TEST_ACCESS_KEY"

# Check credential file takes precedence over config
ENV["AWS_DEFAULT_PROFILE"] = "test2"
config = AWSCore.aws_config()
creds = config[:creds]

@test creds.access_key_id == "RIGHT_ACCESS_ID2"
@test creds.secret_key == "RIGHT_ACCESS_KEY2"

# Check credentials take precedence over role
ENV["AWS_DEFAULT_PROFILE"] = "test3"
config = AWSCore.aws_config()
creds = config[:creds]

@test creds.access_key_id == "RIGHT_ACCESS_ID3"
@test creds.secret_key == "RIGHT_ACCESS_KEY3"

ENV["AWS_DEFAULT_PROFILE"] = "test4"
config = AWSCore.aws_config()
creds = config[:creds]

@test creds.access_key_id == "RIGHT_ACCESS_ID4"
@test creds.secret_key == "RIGHT_ACCESS_KEY4"

# Check we try to assume a role
ENV["AWS_DEFAULT_PROFILE"] = "test:dev"

try
AWSCore.aws_config()
@test false
catch e
@test e isa AWSCore.AWSException
@test ecode(e) == "InvalidClientTokenId"
end

# Check we try to assume a role
ENV["AWS_DEFAULT_PROFILE"] = "test:sub-dev"
let oldout = STDOUT
r,w = redirect_stdout()
try
AWSCore.aws_config()
@test false
catch e
@test e isa AWSCore.AWSException
@test ecode(e) == "InvalidClientTokenId"
end
redirect_stdout(oldout)
close(w)
output = convert(String, read(r))
contains(output, "Assuming \"test:dev\"")
contains(output, "Assuming \"test\"")
close(r)
end
end
end
end
end

@testset "XML Parsing" begin
XML(x)=parse_xml(x)

Expand Down