-module(aws@internal@providers@sts).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/aws/internal/providers/sts.gleam").
-export([default_options/2, fetch/4]).
-export_type([options/0, sts_credentials/0, error/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" STS AssumeRole provider.\n"
"\n"
" The plain `AssumeRole` flow — distinct from\n"
" `AssumeRoleWithWebIdentity` in `sts_web_identity.gleam` — needs the\n"
" caller to *already hold* credentials that have permission to assume\n"
" the target role. The caller's credentials sign the STS request via\n"
" SigV4; STS hands back temporary credentials for the assumed role.\n"
"\n"
" This is what the shared-config `role_arn` / `source_profile` chain\n"
" uses under the hood: resolve credentials for the source profile,\n"
" then call `AssumeRole` from those into the role declared on the\n"
" outer profile.\n"
"\n"
" Wire format is the same form-encoded `Action=AssumeRole&Version=\n"
" 2011-06-15&...` shape used by every Query-protocol STS API. We hand-\n"
" roll it here rather than going through the typed STS client because\n"
" the credential-chain bootstrap path has to be free of any\n"
" dependency on a signed Client (chicken-and-egg).\n"
).
-type options() :: {options,
binary(),
binary(),
binary(),
binary(),
integer(),
gleam@option:option(binary())}.
-type sts_credentials() :: {sts_credentials,
binary(),
binary(),
binary(),
integer()}.
-type error() :: {misconfigured, binary()} | {failed, binary()}.
-file("src/aws/internal/providers/sts.gleam", 85).
?DOC(
" Build options for a default AssumeRole call: global endpoint,\n"
" one-hour duration, no external id. Add overrides through\n"
" `Options(..opts, ...)`.\n"
).
-spec default_options(binary(), binary()) -> options().
default_options(Role_arn, Role_session_name) ->
{options,
<<"https://sts.amazonaws.com/"/utf8>>,
<<"us-east-1"/utf8>>,
Role_arn,
Role_session_name,
3600,
none}.
-file("src/aws/internal/providers/sts.gleam", 256).
-spec extract_required(binary(), binary()) -> {ok, binary()} | {error, error()}.
extract_required(Xml, Tag) ->
_pipe = aws@internal@text_scan:xml_tag_text(Xml, Tag),
gleam@result:replace_error(
_pipe,
{failed,
<<<<"STS response missing <"/utf8, Tag/binary>>/binary,
"> element"/utf8>>}
).
-file("src/aws/internal/providers/sts.gleam", 233).
-spec decode_xml(bitstring()) -> {ok, sts_credentials()} | {error, error()}.
decode_xml(Body) ->
gleam@result:'try'(
begin
_pipe = gleam@bit_array:to_string(Body),
gleam@result:replace_error(
_pipe,
{failed, <<"non-utf8 STS response body"/utf8>>}
)
end,
fun(Text) ->
gleam@result:'try'(
extract_required(Text, <<"AccessKeyId"/utf8>>),
fun(Access_key_id) ->
gleam@result:'try'(
extract_required(Text, <<"SecretAccessKey"/utf8>>),
fun(Secret_access_key) ->
gleam@result:'try'(
extract_required(Text, <<"SessionToken"/utf8>>),
fun(Session_token) ->
gleam@result:'try'(
extract_required(
Text,
<<"Expiration"/utf8>>
),
fun(Expiration) ->
gleam@result:'try'(
begin
_pipe@1 = aws_ffi:parse_iso8601(
Expiration
),
gleam@result:replace_error(
_pipe@1,
{failed,
<<<<"could not parse STS Expiration '"/utf8,
Expiration/binary>>/binary,
"'"/utf8>>}
)
end,
fun(Expires_at) ->
{ok,
{sts_credentials,
Access_key_id,
Secret_access_key,
Session_token,
Expires_at}}
end
)
end
)
end
)
end
)
end
)
end
).
-file("src/aws/internal/providers/sts.gleam", 217).
-spec describe_http(aws@internal@http_send:http_error()) -> binary().
describe_http(Error) ->
case Error of
{connect_failed, Reason} ->
<<"connect failed: "/utf8, Reason/binary>>;
timeout ->
<<"timeout"/utf8>>;
{invalid_body, Reason@1} ->
<<"invalid body: "/utf8, Reason@1/binary>>;
{other, Reason@2} ->
Reason@2
end.
-file("src/aws/internal/providers/sts.gleam", 206).
-spec host_from_endpoint(binary()) -> binary().
host_from_endpoint(Url) ->
After = case gleam@string:split_once(Url, <<"://"/utf8>>) of
{ok, {_, Rest}} ->
Rest;
{error, _} ->
Url
end,
case gleam@string:split_once(After, <<"/"/utf8>>) of
{ok, {Host, _}} ->
Host;
{error, _} ->
After
end.
-file("src/aws/internal/providers/sts.gleam", 156).
-spec build_signed_request(
fun((gleam@http@request:request(bitstring())) -> {ok,
gleam@http@response:response(bitstring())} |
{error, aws@internal@http_send:http_error()}),
aws@internal@sigv4:signing_credentials(),
options(),
bitstring(),
fun(() -> binary())
) -> {ok, gleam@http@request:request(bitstring())} | {error, binary()}.
build_signed_request(_, Source, Options, Body, Timestamp) ->
gleam@result:'try'(
begin
_pipe = gleam@http@request:to(erlang:element(2, Options)),
gleam@result:replace_error(
_pipe,
<<"invalid STS endpoint: "/utf8,
(erlang:element(2, Options))/binary>>
)
end,
fun(Base) ->
Host = host_from_endpoint(erlang:element(2, Options)),
Unsigned = {http_request,
<<"POST"/utf8>>,
<<"/"/utf8>>,
<<""/utf8>>,
[{header, <<"host"/utf8>>, Host},
{header,
<<"content-type"/utf8>>,
<<"application/x-www-form-urlencoded"/utf8>>}],
Body},
Signed = aws@internal@sigv4:sign(
Unsigned,
Source,
{signing_options,
Timestamp(),
erlang:element(3, Options),
<<"sts"/utf8>>,
true,
true,
false}
),
Req = begin
_pipe@1 = Base,
_pipe@2 = gleam@http@request:set_method(_pipe@1, post),
gleam@http@request:set_body(_pipe@2, Body)
end,
Req_with_headers = gleam@list:fold(
erlang:element(5, Signed),
Req,
fun(R, H) ->
gleam@http@request:set_header(
R,
erlang:element(2, H),
erlang:element(3, H)
)
end
),
{ok, Req_with_headers}
end
).
-file("src/aws/internal/providers/sts.gleam", 132).
-spec build_form_body(
binary(),
binary(),
integer(),
gleam@option:option(binary())
) -> binary().
build_form_body(Role_arn, Role_session_name, Duration_seconds, External_id) ->
Base = [{<<"Action"/utf8>>, <<"AssumeRole"/utf8>>},
{<<"Version"/utf8>>, <<"2011-06-15"/utf8>>},
{<<"RoleArn"/utf8>>, Role_arn},
{<<"RoleSessionName"/utf8>>, Role_session_name},
{<<"DurationSeconds"/utf8>>, erlang:integer_to_binary(Duration_seconds)}],
Pairs = case External_id of
{some, Eid} ->
lists:append(Base, [{<<"ExternalId"/utf8>>, Eid}]);
none ->
Base
end,
_pipe = Pairs,
_pipe@1 = gleam@list:map(
_pipe,
fun(P) ->
<<<<(aws@internal@uri:encode_component(erlang:element(1, P)))/binary,
"="/utf8>>/binary,
(aws@internal@uri:encode_component(erlang:element(2, P)))/binary>>
end
),
gleam@string:join(_pipe@1, <<"&"/utf8>>).
-file("src/aws/internal/providers/sts.gleam", 99).
-spec fetch(
fun((gleam@http@request:request(bitstring())) -> {ok,
gleam@http@response:response(bitstring())} |
{error, aws@internal@http_send:http_error()}),
aws@internal@sigv4:signing_credentials(),
options(),
fun(() -> binary())
) -> {ok, sts_credentials()} | {error, error()}.
fetch(Send, Source, Options, Timestamp) ->
Body_string = build_form_body(
erlang:element(4, Options),
erlang:element(5, Options),
erlang:element(6, Options),
erlang:element(7, Options)
),
Body = gleam_stdlib:identity(Body_string),
gleam@result:'try'(
begin
_pipe = build_signed_request(Send, Source, Options, Body, Timestamp),
gleam@result:map_error(_pipe, fun(Field@0) -> {failed, Field@0} end)
end,
fun(Req) ->
gleam@result:'try'(
begin
_pipe@1 = Send(Req),
gleam@result:map_error(
_pipe@1,
fun(E) ->
{failed,
<<"STS transport: "/utf8,
(describe_http(E))/binary>>}
end
)
end,
fun(Resp) -> case erlang:element(2, Resp) of
Code when (Code >= 200) andalso (Code < 300) ->
decode_xml(erlang:element(4, Resp));
Code@1 ->
{error,
{failed,
<<"STS AssumeRole returned status "/utf8,
(erlang:integer_to_binary(Code@1))/binary>>}}
end end
)
end
).