Skip to main content

src/aws@internal@providers@sts.erl

-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
    ).