Skip to content
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ sources in the following default order:
3. An AWS [credentials file][1]
4. ECS task credentials
5. EC2 metadata
6. EKS Pod Identity
7. Web Identity

Usage
-----
Expand Down
2 changes: 2 additions & 0 deletions rebar.config
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,6 @@
, deprecated_functions
]}.

{dialyzer, [{plt_extra_apps, [xmerl]}]}.

{plugins, [ {rebar3_lint, "3.0.1"} ]}.
4 changes: 3 additions & 1 deletion src/aws_credentials_provider.erl
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
| aws_credentials_ecs
| aws_credentials_ec2
| aws_credentials_eks
| aws_credentials_web_identity
| module().
-type error_log() :: [{provider(), term()}].
-export_type([ options/0, expiration/0, provider/0, error_log/0 ]).
Expand All @@ -50,7 +51,8 @@
aws_credentials_file,
aws_credentials_ecs,
aws_credentials_ec2,
aws_credentials_eks]).
aws_credentials_eks,
aws_credentials_web_identity]).

-spec fetch() ->
{ok, aws_credentials:credentials(), expiration()} |
Expand Down
66 changes: 66 additions & 0 deletions src/aws_credentials_web_identity.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
%% @doc This provider looks up credential information from web identity token
%% Environment parameters:
%% <ul>
%% <li> &lt;&lt;"role_session_name"&gt;&gt; - this is provided to the credential fetch endpoint,
%% and will label the provided session with that name, see:
%% https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html#API_AssumeRoleWithWebIdentity_RequestParameters
%% By default this is `erlang_aws_credentials'</li>
%% </ul>
%% @end
-module(aws_credentials_web_identity).
-behaviour(aws_credentials_provider).

-include_lib("xmerl/include/xmerl.hrl").

-define(ASSUME_ROLE_URL,
"https://sts.amazonaws.com/?Action=AssumeRoleWithWebIdentity&Version=2011-06-15" ++
"&RoleArn=~s&WebIdentityToken=~s&RoleSessionName=~s").
-define(DEFAULT_SESSION_NAME, "erlang_aws_credentials").

-export([fetch/1]).

-spec fetch(aws_credentials_provider:options()) ->
{error, _}
| {ok, aws_credentials:credentials(), aws_credentials_provider:expiration()}.
fetch(Options) ->
RoleArn = os:getenv("AWS_ROLE_ARN"),
TokenFile = os:getenv("AWS_WEB_IDENTITY_TOKEN_FILE"),
AuthToken = read_token(TokenFile),
SessionName = maps:get(role_session_name, Options, ?DEFAULT_SESSION_NAME),
Response = fetch_assume_role_token(RoleArn, AuthToken, SessionName),
make_map(Response).

-spec read_token(false | string()) -> {error, _} | {ok, binary()}.
read_token(false) -> {error, no_credentials};
read_token(Path) -> file:read_file(Path).

-spec fetch_assume_role_token(false | string(), {error, _} | {ok, binary()}, binary()) ->
{error, _}
| {ok, aws_credentials_httpc:status_code(),
aws_credentials_httpc:body(),
aws_credentials_httpc:headers()}.
fetch_assume_role_token(false, _AuthToken, _SessionName) -> {error, no_credentials};
fetch_assume_role_token(_RoleArn, {error, _Error} = Error, _SessionName) -> Error;
fetch_assume_role_token(RoleArn, {ok, AuthToken}, SessionName) ->
Url = lists:flatten(io_lib:format(?ASSUME_ROLE_URL, [RoleArn, AuthToken, SessionName])),
aws_credentials_httpc:request(get, Url).

-spec make_map({error, _}
| {ok, aws_credentials_httpc:status_code(),
aws_credentials_httpc:body(),
aws_credentials_httpc:headers()}) ->
{error, _}
| {ok, aws_credentials:credentials(), aws_credentials_provider:expiration()}.
make_map({error, _Error} = Error) -> Error;
make_map({ok, _Status, Body, _Headers}) ->
{Doc, []} = xmerl_scan:string(binary_to_list(Body)),
[#xmlText{value = AccessKeyId}] = xmerl_xpath:string("//Credentials/AccessKeyId/text()", Doc),
[#xmlText{value = SecretAccessKey}] =
xmerl_xpath:string("//Credentials/SecretAccessKey/text()", Doc),
[#xmlText{value = Token}] = xmerl_xpath:string("//Credentials/SessionToken/text()", Doc),
[#xmlText{value = Expiration}] = xmerl_xpath:string("//Credentials/Expiration/text()", Doc),
Creds = aws_credentials:make_map(?MODULE,
list_to_binary(AccessKeyId),
list_to_binary(SecretAccessKey),
list_to_binary(Token)),
{ok, Creds, list_to_binary(Expiration)}.
80 changes: 76 additions & 4 deletions test/aws_credentials_providers_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ all() ->
, {group, application_env}
, {group, ecs}
, {group, eks}
, {group, web_identity}
, {group, web_identity_default_session_name}
, {group, credential_process}
].

Expand All @@ -50,6 +52,8 @@ groups() ->
, {application_env, [], all_testcases()}
, {ecs, [], all_testcases()}
, {eks, [], all_testcases()}
, {web_identity, [], all_testcases()}
, {web_identity_default_session_name, [], all_testcases()}
, {credential_process, [], all_testcases()}
].

Expand All @@ -75,6 +79,8 @@ init_per_group(GroupName, Config) ->
application_env -> init_group(application_env, provider(env), application_env, Config);
credential_process ->
init_group(credential_process, provider(file), credential_process, Config);
web_identity_default_session_name = GroupName ->
init_group(GroupName, provider(web_identity), GroupName, Config);
GroupName -> init_group(GroupName, Config)
end.

Expand Down Expand Up @@ -123,6 +129,12 @@ assert_test(credential_process) ->
assert_test(eks) ->
Provider = provider(eks),
assert_values(?DUMMY_ACCESS_KEY, ?DUMMY_SECRET_ACCESS_KEY, Provider);
assert_test(WebIdentity) when WebIdentity =:= web_identity;
WebIdentity =:= web_identity_default_session_name ->
Provider = provider(web_identity),
assert_values(?DUMMY_ACCESS_KEY, ?DUMMY_SECRET_ACCESS_KEY, Provider),
#{token := Token} = aws_credentials:get_credentials(),
?assertEqual(<<"unused">>, Token);
assert_test(GroupName) ->
Provider = provider(GroupName),
assert_values(?DUMMY_ACCESS_KEY, ?DUMMY_SECRET_ACCESS_KEY, Provider).
Expand Down Expand Up @@ -159,6 +171,8 @@ provider_opts(credential_env, _Config) ->
#{credential_path => os:getenv("HOME")};
provider_opts(credential_process, Config) ->
#{credential_path => ?config(data_dir, Config) ++ "credential_process/"};
provider_opts(web_identity, _Config) ->
#{role_session_name => "overridden"};
provider_opts(_GroupName, _Config) ->
#{}.

Expand Down Expand Up @@ -213,6 +227,28 @@ setup_provider(eks, Config) ->
, {"AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE", OldTokenFile}
]
};
setup_provider(web_identity_default_session_name, Config) ->
OldRoleArn = os:getenv("AWS_ROLE_ARN"),
OldWebIdentityTokenFile = os:getenv("AWS_WEB_IDENTITY_TOKEN_FILE"),
os:putenv("AWS_ROLE_ARN", "arg:aws:iam::123123123"),
os:putenv("AWS_WEB_IDENTITY_TOKEN_FILE", ?config(data_dir, Config) ++ "web_identity/token"),
meck:new(httpc, [no_link, passthrough]),
meck:expect(httpc, request, fun mock_httpc_request_web_identity_default_session_name/5),
#{ mocks => [httpc]
, env => [ {"AWS_ROLE_ARN", OldRoleArn}
, {"AWS_WEB_IDENTITY_TOKEN_FILE", OldWebIdentityTokenFile}
]};
setup_provider(web_identity, Config) ->
OldRoleArn = os:getenv("AWS_ROLE_ARN"),
OldWebIdentityTokenFile = os:getenv("AWS_WEB_IDENTITY_TOKEN_FILE"),
os:putenv("AWS_ROLE_ARN", "arg:aws:iam::123123123"),
os:putenv("AWS_WEB_IDENTITY_TOKEN_FILE", ?config(data_dir, Config) ++ "web_identity/token"),
meck:new(httpc, [no_link, passthrough]),
meck:expect(httpc, request, fun mock_httpc_request_web_identity/5),
#{ mocks => [httpc]
, env => [ {"AWS_ROLE_ARN", OldRoleArn}
, {"AWS_WEB_IDENTITY_TOKEN_FILE", OldWebIdentityTokenFile}
]};
setup_provider(config_env, Config) ->
Old = os:getenv("AWS_CONFIG_FILE"),
os:putenv("AWS_CONFIG_FILE", ?config(data_dir, Config) ++ "env/config"),
Expand Down Expand Up @@ -283,6 +319,31 @@ mock_httpc_request_eks(Method, Request, HTTPOptions, Options, Profile) ->
meck:passthrough([Method, Request, HTTPOptions, Options, Profile])
end.

mock_httpc_request_web_identity_default_session_name(
Method, Request, HTTPOptions, Options, Profile) ->
case Request of
{"https://sts.amazonaws.com/" ++
"?Action=AssumeRoleWithWebIdentity&Version=2011-06-15" ++
"&RoleArn=arg:aws:iam::123123123" ++
"&WebIdentityToken=dummy-web-identity-token" ++
"&RoleSessionName=erlang_aws_credentials", []} ->
{ok, response('web-identity-credentials')};
_ ->
meck:passthrough([Method, Request, HTTPOptions, Options, Profile])
end.

mock_httpc_request_web_identity(Method, Request, HTTPOptions, Options, Profile) ->
case Request of
{"https://sts.amazonaws.com/" ++
"?Action=AssumeRoleWithWebIdentity&Version=2011-06-15" ++
"&RoleArn=arg:aws:iam::123123123" ++
"&WebIdentityToken=dummy-web-identity-token" ++
"&RoleSessionName=overridden", []} ->
{ok, response('web-identity-credentials')};
_ ->
meck:passthrough([Method, Request, HTTPOptions, Options, Profile])
end.

response(BodyTag) ->
StatusLine = {unused, 200, unused},
Headers = [],
Expand All @@ -296,23 +357,34 @@ body('security-credentials') ->
body('dummy-role') ->
jsx:encode(#{ 'AccessKeyId' => ?DUMMY_ACCESS_KEY
, 'SecretAccessKey' => ?DUMMY_SECRET_ACCESS_KEY
, 'Expiration' => <<"2025-09-25T23:43:56Z">>
, 'Expiration' => <<"2026-09-25T23:43:56Z">>
, 'Token' => unused
});
body('document') ->
jsx:encode(#{ 'region' => unused });
body('dummy-uri') ->
jsx:encode(#{ 'AccessKeyId' => ?DUMMY_ACCESS_KEY
, 'SecretAccessKey' => ?DUMMY_SECRET_ACCESS_KEY
, 'Expiration' => <<"2025-09-25T23:43:56Z">>
, 'Expiration' => <<"2026-09-25T23:43:56Z">>
, 'Token' => unused
});
body('eks-credentials') ->
jsx:encode(#{ 'AccessKeyId' => ?DUMMY_ACCESS_KEY
, 'SecretAccessKey' => ?DUMMY_SECRET_ACCESS_KEY
, 'Expiration' => <<"2025-09-25T23:43:56Z">>
, 'Expiration' => <<"2026-09-25T23:43:56Z">>
, 'Token' => unused
}).
});
body('web-identity-credentials') ->
<<"<AssumeRoleWithWebIdentityResponse>
<AssumeRoleWithWebIdentityResult>
<Credentials>
<AccessKeyId>", ?DUMMY_ACCESS_KEY/binary, "</AccessKeyId>
<SecretAccessKey>", ?DUMMY_SECRET_ACCESS_KEY/binary, "</SecretAccessKey>
<SessionToken>unused</SessionToken>
<Expiration>2026-09-25T23:43:56Z</Expiration>
</Credentials>
</AssumeRoleWithWebIdentityResult>
</AssumeRoleWithWebIdentityResponse>">>.

maybe_put_env(Key, false) ->
os:unsetenv(Key);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dummy-web-identity-token