Skip to content

Commit f25c87f

Browse files
slackersoftry4n1m3
authored andcommitted
Implement a provider for web identity tokens
1 parent cd6dd9b commit f25c87f

File tree

6 files changed

+150
-5
lines changed

6 files changed

+150
-5
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ sources in the following default order:
1111
3. An AWS [credentials file][1]
1212
4. ECS task credentials
1313
5. EC2 metadata
14+
6. EKS Pod Identity
15+
7. Web Identity
1416

1517
Usage
1618
-----

rebar.config

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,6 @@
4040
, deprecated_functions
4141
]}.
4242

43+
{dialyzer, [{plt_extra_apps, [xmerl]}]}.
44+
4345
{plugins, [ {rebar3_lint, "3.0.1"} ]}.

src/aws_credentials_provider.erl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
| aws_credentials_ecs
3838
| aws_credentials_ec2
3939
| aws_credentials_eks
40+
| aws_credentials_web_identity
4041
| module().
4142
-type error_log() :: [{provider(), term()}].
4243
-export_type([ options/0, expiration/0, provider/0, error_log/0 ]).
@@ -50,7 +51,8 @@
5051
aws_credentials_file,
5152
aws_credentials_ecs,
5253
aws_credentials_ec2,
53-
aws_credentials_eks]).
54+
aws_credentials_eks,
55+
aws_credentials_web_identity]).
5456

5557
-spec fetch() ->
5658
{ok, aws_credentials:credentials(), expiration()} |
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
%% @doc This provider looks up credential information from web identity token
2+
%% Environment parameters:
3+
%% <ul>
4+
%% <li> &lt;&lt;"role_session_name"&gt;&gt; - this is provided to the credential fetch endpoint,
5+
%% and will label the provided session with that name, see:
6+
%% https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html#API_AssumeRoleWithWebIdentity_RequestParameters
7+
%% By default this is `erlang_aws_credentials'</li>
8+
%% </ul>
9+
%% @end
10+
-module(aws_credentials_web_identity).
11+
-behaviour(aws_credentials_provider).
12+
13+
-include_lib("xmerl/include/xmerl.hrl").
14+
15+
-define(ASSUME_ROLE_URL,
16+
"https://sts.amazonaws.com/?Action=AssumeRoleWithWebIdentity&Version=2011-06-15" ++
17+
"&RoleArn=~s&WebIdentityToken=~s&RoleSessionName=~s").
18+
-define(DEFAULT_SESSION_NAME, "erlang_aws_credentials").
19+
20+
-export([fetch/1]).
21+
22+
-spec fetch(aws_credentials_provider:options()) ->
23+
{error, _}
24+
| {ok, aws_credentials:credentials(), aws_credentials_provider:expiration()}.
25+
fetch(Options) ->
26+
RoleArn = os:getenv("AWS_ROLE_ARN"),
27+
TokenFile = os:getenv("AWS_WEB_IDENTITY_TOKEN_FILE"),
28+
AuthToken = read_token(TokenFile),
29+
SessionName = maps:get(role_session_name, Options, ?DEFAULT_SESSION_NAME),
30+
Response = fetch_assume_role_token(RoleArn, AuthToken, SessionName),
31+
make_map(Response).
32+
33+
-spec read_token(false | string()) -> {error, _} | {ok, binary()}.
34+
read_token(false) -> {error, no_credentials};
35+
read_token(Path) -> file:read_file(Path).
36+
37+
-spec fetch_assume_role_token(false | string(), {error, _} | {ok, binary()}, binary()) ->
38+
{error, _}
39+
| {ok, aws_credentials_httpc:status_code(),
40+
aws_credentials_httpc:body(),
41+
aws_credentials_httpc:headers()}.
42+
fetch_assume_role_token(false, _AuthToken, _SessionName) -> {error, no_credentials};
43+
fetch_assume_role_token(_RoleArn, {error, _Error} = Error, _SessionName) -> Error;
44+
fetch_assume_role_token(RoleArn, {ok, AuthToken}, SessionName) ->
45+
Url = lists:flatten(io_lib:format(?ASSUME_ROLE_URL, [RoleArn, AuthToken, SessionName])),
46+
aws_credentials_httpc:request(get, Url).
47+
48+
-spec make_map({error, _}
49+
| {ok, aws_credentials_httpc:status_code(),
50+
aws_credentials_httpc:body(),
51+
aws_credentials_httpc:headers()}) ->
52+
{error, _}
53+
| {ok, aws_credentials:credentials(), aws_credentials_provider:expiration()}.
54+
make_map({error, _Error} = Error) -> Error;
55+
make_map({ok, _Status, Body, _Headers}) ->
56+
{Doc, []} = xmerl_scan:string(binary_to_list(Body)),
57+
[#xmlText{value = AccessKeyId}] = xmerl_xpath:string("//Credentials/AccessKeyId/text()", Doc),
58+
[#xmlText{value = SecretAccessKey}] =
59+
xmerl_xpath:string("//Credentials/SecretAccessKey/text()", Doc),
60+
[#xmlText{value = Token}] = xmerl_xpath:string("//Credentials/SessionToken/text()", Doc),
61+
[#xmlText{value = Expiration}] = xmerl_xpath:string("//Credentials/Expiration/text()", Doc),
62+
Creds = aws_credentials:make_map(?MODULE,
63+
list_to_binary(AccessKeyId),
64+
list_to_binary(SecretAccessKey),
65+
list_to_binary(Token)),
66+
{ok, Creds, list_to_binary(Expiration)}.

test/aws_credentials_providers_SUITE.erl

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ all() ->
3636
, {group, application_env}
3737
, {group, ecs}
3838
, {group, eks}
39+
, {group, web_identity}
40+
, {group, web_identity_default_session_name}
3941
, {group, credential_process}
4042
].
4143

@@ -50,6 +52,8 @@ groups() ->
5052
, {application_env, [], all_testcases()}
5153
, {ecs, [], all_testcases()}
5254
, {eks, [], all_testcases()}
55+
, {web_identity, [], all_testcases()}
56+
, {web_identity_default_session_name, [], all_testcases()}
5357
, {credential_process, [], all_testcases()}
5458
].
5559

@@ -75,6 +79,8 @@ init_per_group(GroupName, Config) ->
7579
application_env -> init_group(application_env, provider(env), application_env, Config);
7680
credential_process ->
7781
init_group(credential_process, provider(file), credential_process, Config);
82+
web_identity_default_session_name = GroupName ->
83+
init_group(GroupName, provider(web_identity), GroupName, Config);
7884
GroupName -> init_group(GroupName, Config)
7985
end.
8086

@@ -123,6 +129,12 @@ assert_test(credential_process) ->
123129
assert_test(eks) ->
124130
Provider = provider(eks),
125131
assert_values(?DUMMY_ACCESS_KEY, ?DUMMY_SECRET_ACCESS_KEY, Provider);
132+
assert_test(WebIdentity) when WebIdentity =:= web_identity;
133+
WebIdentity =:= web_identity_default_session_name ->
134+
Provider = provider(web_identity),
135+
assert_values(?DUMMY_ACCESS_KEY, ?DUMMY_SECRET_ACCESS_KEY, Provider),
136+
#{token := Token} = aws_credentials:get_credentials(),
137+
?assertEqual(<<"unused">>, Token);
126138
assert_test(GroupName) ->
127139
Provider = provider(GroupName),
128140
assert_values(?DUMMY_ACCESS_KEY, ?DUMMY_SECRET_ACCESS_KEY, Provider).
@@ -159,6 +171,8 @@ provider_opts(credential_env, _Config) ->
159171
#{credential_path => os:getenv("HOME")};
160172
provider_opts(credential_process, Config) ->
161173
#{credential_path => ?config(data_dir, Config) ++ "credential_process/"};
174+
provider_opts(web_identity, _Config) ->
175+
#{role_session_name => "overridden"};
162176
provider_opts(_GroupName, _Config) ->
163177
#{}.
164178

@@ -213,6 +227,28 @@ setup_provider(eks, Config) ->
213227
, {"AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE", OldTokenFile}
214228
]
215229
};
230+
setup_provider(web_identity_default_session_name, Config) ->
231+
OldRoleArn = os:getenv("AWS_ROLE_ARN"),
232+
OldWebIdentityTokenFile = os:getenv("AWS_WEB_IDENTITY_TOKEN_FILE"),
233+
os:putenv("AWS_ROLE_ARN", "arg:aws:iam::123123123"),
234+
os:putenv("AWS_WEB_IDENTITY_TOKEN_FILE", ?config(data_dir, Config) ++ "web_identity/token"),
235+
meck:new(httpc, [no_link, passthrough]),
236+
meck:expect(httpc, request, fun mock_httpc_request_web_identity_default_session_name/5),
237+
#{ mocks => [httpc]
238+
, env => [ {"AWS_ROLE_ARN", OldRoleArn}
239+
, {"AWS_WEB_IDENTITY_TOKEN_FILE", OldWebIdentityTokenFile}
240+
]};
241+
setup_provider(web_identity, Config) ->
242+
OldRoleArn = os:getenv("AWS_ROLE_ARN"),
243+
OldWebIdentityTokenFile = os:getenv("AWS_WEB_IDENTITY_TOKEN_FILE"),
244+
os:putenv("AWS_ROLE_ARN", "arg:aws:iam::123123123"),
245+
os:putenv("AWS_WEB_IDENTITY_TOKEN_FILE", ?config(data_dir, Config) ++ "web_identity/token"),
246+
meck:new(httpc, [no_link, passthrough]),
247+
meck:expect(httpc, request, fun mock_httpc_request_web_identity/5),
248+
#{ mocks => [httpc]
249+
, env => [ {"AWS_ROLE_ARN", OldRoleArn}
250+
, {"AWS_WEB_IDENTITY_TOKEN_FILE", OldWebIdentityTokenFile}
251+
]};
216252
setup_provider(config_env, Config) ->
217253
Old = os:getenv("AWS_CONFIG_FILE"),
218254
os:putenv("AWS_CONFIG_FILE", ?config(data_dir, Config) ++ "env/config"),
@@ -283,6 +319,31 @@ mock_httpc_request_eks(Method, Request, HTTPOptions, Options, Profile) ->
283319
meck:passthrough([Method, Request, HTTPOptions, Options, Profile])
284320
end.
285321

322+
mock_httpc_request_web_identity_default_session_name(
323+
Method, Request, HTTPOptions, Options, Profile) ->
324+
case Request of
325+
{"https://sts.amazonaws.com/" ++
326+
"?Action=AssumeRoleWithWebIdentity&Version=2011-06-15" ++
327+
"&RoleArn=arg:aws:iam::123123123" ++
328+
"&WebIdentityToken=dummy-web-identity-token" ++
329+
"&RoleSessionName=erlang_aws_credentials", []} ->
330+
{ok, response('web-identity-credentials')};
331+
_ ->
332+
meck:passthrough([Method, Request, HTTPOptions, Options, Profile])
333+
end.
334+
335+
mock_httpc_request_web_identity(Method, Request, HTTPOptions, Options, Profile) ->
336+
case Request of
337+
{"https://sts.amazonaws.com/" ++
338+
"?Action=AssumeRoleWithWebIdentity&Version=2011-06-15" ++
339+
"&RoleArn=arg:aws:iam::123123123" ++
340+
"&WebIdentityToken=dummy-web-identity-token" ++
341+
"&RoleSessionName=overridden", []} ->
342+
{ok, response('web-identity-credentials')};
343+
_ ->
344+
meck:passthrough([Method, Request, HTTPOptions, Options, Profile])
345+
end.
346+
286347
response(BodyTag) ->
287348
StatusLine = {unused, 200, unused},
288349
Headers = [],
@@ -296,23 +357,34 @@ body('security-credentials') ->
296357
body('dummy-role') ->
297358
jsx:encode(#{ 'AccessKeyId' => ?DUMMY_ACCESS_KEY
298359
, 'SecretAccessKey' => ?DUMMY_SECRET_ACCESS_KEY
299-
, 'Expiration' => <<"2025-09-25T23:43:56Z">>
360+
, 'Expiration' => <<"2026-09-25T23:43:56Z">>
300361
, 'Token' => unused
301362
});
302363
body('document') ->
303364
jsx:encode(#{ 'region' => unused });
304365
body('dummy-uri') ->
305366
jsx:encode(#{ 'AccessKeyId' => ?DUMMY_ACCESS_KEY
306367
, 'SecretAccessKey' => ?DUMMY_SECRET_ACCESS_KEY
307-
, 'Expiration' => <<"2025-09-25T23:43:56Z">>
368+
, 'Expiration' => <<"2026-09-25T23:43:56Z">>
308369
, 'Token' => unused
309370
});
310371
body('eks-credentials') ->
311372
jsx:encode(#{ 'AccessKeyId' => ?DUMMY_ACCESS_KEY
312373
, 'SecretAccessKey' => ?DUMMY_SECRET_ACCESS_KEY
313-
, 'Expiration' => <<"2025-09-25T23:43:56Z">>
374+
, 'Expiration' => <<"2026-09-25T23:43:56Z">>
314375
, 'Token' => unused
315-
}).
376+
});
377+
body('web-identity-credentials') ->
378+
<<"<AssumeRoleWithWebIdentityResponse>
379+
<AssumeRoleWithWebIdentityResult>
380+
<Credentials>
381+
<AccessKeyId>", ?DUMMY_ACCESS_KEY/binary, "</AccessKeyId>
382+
<SecretAccessKey>", ?DUMMY_SECRET_ACCESS_KEY/binary, "</SecretAccessKey>
383+
<SessionToken>unused</SessionToken>
384+
<Expiration>2026-09-25T23:43:56Z</Expiration>
385+
</Credentials>
386+
</AssumeRoleWithWebIdentityResult>
387+
</AssumeRoleWithWebIdentityResponse>">>.
316388

317389
maybe_put_env(Key, false) ->
318390
os:unsetenv(Key);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dummy-web-identity-token

0 commit comments

Comments
 (0)