-
Notifications
You must be signed in to change notification settings - Fork 62
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Create assume_role function * Better support for role chaining * Add tests * Formatting * Set project version to 1.89.0
- Loading branch information
Showing
6 changed files
with
332 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
""" | ||
assume_role(principal::AbstractAWSConfig, role; kwargs...) -> AbstractAWSConfig | ||
Assumes the IAM `role` via temporary credentials via the `principal` entity. The `principal` | ||
entity must be included in the trust policy of the `role`. | ||
[Role chaining](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_terms-and-concepts.html#iam-term-role-chaining) | ||
must be manually specified by multiple `assume_role` calls (e.g. "role-a" has permissions to | ||
assume "role-b": `assume_role(assume_role(AWSConfig(), "role-a"), "role-b")`). | ||
# Arguments | ||
- `principal::AbstractAWSConfig`: The AWS configuration and credentials of the principal | ||
entity (user or role) performing the `sts:AssumeRole` action. | ||
- `role::AbstractString`: The AWS IAM role to assume. Either a full role ARN or just the | ||
role name. If only the role name is specified the role will be assumed to reside in the | ||
same account used in the `principal` argument. | ||
# Keywords | ||
- `duration::Integer` (optional): Role session duration in seconds. | ||
- `mfa_serial::AbstractString` (optional): The identification number of the MFA device that | ||
is associated with the user making the `AssumeRole` API call. Either a serial number for a | ||
hardware device ("GAHT12345678") or an ARN for a virtual device | ||
("arn:aws:iam::123456789012:mfa/user"). When specified a MFA token must be provided via | ||
`token` or an interactive prompt. | ||
- `token::AbstractString` (optional): The value provided by the MFA device. Only can be | ||
specified when `mfa_serial` is set. | ||
- `session_name::AbstractString` (optional): The unique role session name associated with | ||
this API request. | ||
""" | ||
function assume_role(principal::AWSConfig, role; kwargs...) | ||
creds = assume_role_creds(principal, role; kwargs...) | ||
return AWSConfig(creds, principal.region, principal.output, principal.max_attempts) | ||
end | ||
|
||
""" | ||
assume_role(role; kwargs...) -> Function | ||
Create a function that assumes the IAM `role` via a deferred principal entity, i.e. a | ||
function equivalent to `principal -> assume_role(principal, role; kwargs...)`. Useful for | ||
[role chaining](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_terms-and-concepts.html#iam-term-role-chaining). | ||
# Examples | ||
Assume "role-a" which in turn assumes "role-b": | ||
```julia | ||
AWSConfig() |> assume_role("role-a") |> assume_role("role-b") | ||
``` | ||
""" | ||
assume_role(role; kwargs...) = principal -> assume_role(principal, role; kwargs...) | ||
|
||
""" | ||
assume_role_creds(principal, role; kwargs...) -> AWSCredentials | ||
Assumes the IAM `role` via temporary credentials via the `principal` entity and returns | ||
`AWSCredentials`. Typically, end-users should use [`assume_role`](@ref) instead. | ||
Details on the arguments and keywords for `assume_role_creds` can be found in the docstring | ||
for [`assume_role`](@ref). | ||
""" | ||
function assume_role_creds( | ||
principal::AbstractAWSConfig, | ||
role::AbstractString; | ||
duration::Union{Integer,Nothing}=nothing, | ||
mfa_serial::Union{AbstractString,Nothing}=nothing, | ||
token::Union{AbstractString,Nothing}=nothing, | ||
session_name::Union{AbstractString,Nothing}=nothing, | ||
) | ||
if startswith(role, "arn:aws:iam") | ||
# Avoiding unnecessary parsing the role ARN or performing an expensive API call | ||
account_id = "" | ||
role_arn = role | ||
else | ||
account_id = aws_account_number(principal) | ||
role_arn = "arn:aws:iam::$account_id:role/$role" | ||
end | ||
|
||
params = Dict{String,Any}("RoleArn" => role_arn) | ||
if session_name !== nothing | ||
params["RoleSessionName"] = session_name | ||
else | ||
params["RoleSessionName"] = _role_session_name( | ||
"AWS.jl-", | ||
ENV["USER"], | ||
"-" * Dates.format(now(UTC), dateformat"yyyymmdd\THHMMSS\Z"), | ||
) | ||
end | ||
|
||
if duration !== nothing | ||
params["DurationSeconds"] = duration | ||
end | ||
|
||
if mfa_serial !== nothing && token !== nothing | ||
params["SerialNumber"] = mfa_serial | ||
params["TokenCode"] = token | ||
elseif mfa_serial !== nothing && token === nothing | ||
params["SerialNumber"] = mfa_serial | ||
token = Base.getpass("Enter MFA code for $mfa_serial") | ||
params["TokenCode"] = Base.shred!(token) do t | ||
read(t, String) | ||
end | ||
elseif mfa_serial === nothing && token !== nothing | ||
msg = "Keyword `token` cannot be be specified when `mfa_serial` is not set" | ||
throw(ArgumentError(msg)) | ||
end | ||
|
||
response = AWSServices.sts( | ||
"AssumeRole", | ||
params; | ||
aws_config=principal, | ||
feature_set=AWS.FeatureSet(; use_response_type=true), | ||
) | ||
body = parse(response) | ||
role_creds = body["AssumeRoleResult"]["Credentials"] | ||
role_user = body["AssumeRoleResult"]["AssumedRoleUser"] | ||
renew = function () | ||
# Avoid passing the `token` into the credential renew function as it will be expired | ||
return assume_role_creds(principal, role_arn; duration, mfa_serial, session_name) | ||
end | ||
|
||
return AWSCredentials( | ||
role_creds["AccessKeyId"], | ||
role_creds["SecretAccessKey"], | ||
role_creds["SessionToken"], | ||
role_user["Arn"], | ||
account_id; # May as well populate "account_number" field when we have it | ||
expiry=DateTime(rstrip(role_creds["Expiration"], 'Z')), | ||
renew, | ||
) | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
function get_assumed_role(aws_config::AbstractAWSConfig=global_aws_config()) | ||
r = AWSServices.sts( | ||
"GetCallerIdentity"; | ||
aws_config, | ||
feature_set=AWS.FeatureSet(; use_response_type=true), | ||
) | ||
result = parse(r) | ||
arn = result["GetCallerIdentityResult"]["Arn"] | ||
m = match(r":assumed-role/(?<role>[^/]+)", arn) | ||
if m !== nothing | ||
return m["role"] | ||
else | ||
error("Caller Identity ARN is not an assumed role: $arn") | ||
end | ||
end | ||
|
||
get_assumed_role(creds::AWSCredentials) = get_assumed_role(AWSConfig(; creds)) | ||
|
||
@testset "assume_role / assume_role_creds" begin | ||
# In order to mitigate the effects of using `assume_role` in order to test itself we'll | ||
# use the lowest-level call with as many defaults as possible. | ||
base_config = aws | ||
creds = assume_role_creds(base_config, testset_role("AssumeRoleTestset")) | ||
config = AWSConfig(; creds) | ||
@test get_assumed_role(config) == testset_role("AssumeRoleTestset") | ||
|
||
role_a = testset_role("RoleA") | ||
role_b = testset_role("RoleB") | ||
|
||
@testset "basic" begin | ||
creds = assume_role_creds(config, role_a) | ||
@test creds isa AWSCredentials | ||
@test creds.token != "" # Temporary credentials | ||
@test creds.renew !== nothing | ||
|
||
cfg = assume_role(config, role_a) | ||
@test cfg isa AWSConfig | ||
@test cfg.credentials isa AWSCredentials | ||
@test cfg.region == config.region | ||
@test cfg.output == config.output | ||
@test cfg.max_attempts == config.max_attempts | ||
end | ||
|
||
@testset "role name/ARN" begin | ||
account_id = aws_account_number(config) | ||
|
||
creds = assume_role_creds(config, role_a) | ||
@test contains(creds.user_arn, r":assumed-role/" * (role_a * '/')) | ||
@test creds.account_number == account_id | ||
|
||
creds = assume_role_creds(config, "arn:aws:iam::$account_id:role/$role_a") | ||
@test contains(creds.user_arn, r":assumed-role/" * (role_a * '/')) | ||
@test creds.account_number == "" | ||
end | ||
|
||
@testset "duration" begin | ||
drift = Second(1) | ||
|
||
creds = assume_role_creds(config, role_a; duration=nothing) | ||
t = floor(now(UTC), Second) | ||
@test t <= creds.expiry <= t + Second(3600) + drift | ||
|
||
creds = assume_role_creds(config, role_a; duration=900) | ||
t = floor(now(UTC), Second) | ||
@test t <= creds.expiry <= t + Second(900) + drift | ||
end | ||
|
||
@testset "session_name" begin | ||
session_prefix = "AWS.jl-" * ENV["USER"] | ||
creds = assume_role_creds(config, role_a; session_name=nothing) | ||
regex = r":assumed-role/" * (role_a * '/' * session_prefix) * r"-\d{8}T\d{6}Z$" | ||
@test contains(creds.user_arn, regex) | ||
@test get_assumed_role(creds) == role_a | ||
|
||
session_name = "assume-role-session-name-testset-" * randstring(5) | ||
creds = assume_role_creds(config, role_a; session_name) | ||
regex = r":assumed-role/" * (role_a * '/' * session_name) * r"$" | ||
@test contains(creds.user_arn, regex) | ||
@test get_assumed_role(creds) == role_a | ||
end | ||
|
||
@testset "renew" begin | ||
creds = assume_role_creds(config, role_a; duration=nothing) | ||
@test creds.renew isa Function | ||
@test get_assumed_role(creds) == role_a | ||
|
||
new_creds = creds.renew() | ||
@test new_creds isa AWSCredentials | ||
@test get_assumed_role(new_creds) == role_a | ||
@test new_creds.access_key_id != creds.access_key_id | ||
@test new_creds.secret_key != creds.secret_key | ||
@test new_creds.expiry >= creds.expiry | ||
end | ||
|
||
@testset "role chaining" begin | ||
cfg = assume_role(assume_role(config, role_a), role_b) | ||
@test get_assumed_role(cfg) == role_b | ||
|
||
#! format: off | ||
cfg = config |> assume_role(role_a) |> assume_role(role_b) | ||
#! format: on | ||
@test get_assumed_role(cfg) == role_b | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
bd39b39
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@JuliaRegistrator register
bd39b39
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Registration pull request created: JuliaRegistries/General/86290
After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.
This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via: