Skip to main content

src/aws@internal@providers@sts_web_identity.erl

-module(aws@internal@providers@sts_web_identity).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/aws/internal/providers/sts_web_identity.gleam").
-export([fetch/2]).
-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 AssumeRoleWithWebIdentity provider — the IRSA (IAM Roles for\n"
    " Service Accounts) flow used inside EKS pods and any other environment\n"
    " that hands you a signed identity token plus an IAM role to assume.\n"
    "\n"
    " Flow:\n"
    "   1. Read the web identity token from `AWS_WEB_IDENTITY_TOKEN_FILE`\n"
    "      at fetch time (IRSA rotates the token periodically; we must not\n"
    "      pin it at provider construction).\n"
    "   2. POST form-encoded `Action=AssumeRoleWithWebIdentity` to STS with\n"
    "      `RoleArn`, `RoleSessionName`, `WebIdentityToken`, and a duration.\n"
    "   3. Pull the credentials out of the XML response.\n"
    "\n"
    " XML is parsed with simple `<Tag>value</Tag>` string scans — the STS\n"
    " response shape is fixed and well-known, so a real XML parser would be\n"
    " over-investment.\n"
).

-type options() :: {options, binary(), binary(), binary(), binary(), integer()}.

-type sts_credentials() :: {sts_credentials,
        binary(),
        binary(),
        binary(),
        integer()}.

-type error() :: {misconfigured, binary()} | {failed, binary()}.

-file("src/aws/internal/providers/sts_web_identity.gleam", 164).
-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_web_identity.gleam", 141).
-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_web_identity.gleam", 130).
-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_web_identity.gleam", 114).
-spec build_request(binary(), bitstring()) -> {ok,
        gleam@http@request:request(bitstring())} |
    {error, binary()}.
build_request(Endpoint, Body) ->
    gleam@result:'try'(
        begin
            _pipe = gleam@http@request:to(Endpoint),
            gleam@result:replace_error(
                _pipe,
                <<"invalid STS endpoint: "/utf8, Endpoint/binary>>
            )
        end,
        fun(Base) ->
            {ok,
                begin
                    _pipe@1 = Base,
                    _pipe@2 = gleam@http@request:set_method(_pipe@1, post),
                    _pipe@3 = gleam@http@request:set_body(_pipe@2, Body),
                    gleam@http@request:set_header(
                        _pipe@3,
                        <<"content-type"/utf8>>,
                        <<"application/x-www-form-urlencoded"/utf8>>
                    )
                end}
        end
    ).

-file("src/aws/internal/providers/sts_web_identity.gleam", 107).
-spec do_reverse(list(IHW), list(IHW)) -> list(IHW).
do_reverse(Xs, Acc) ->
    case Xs of
        [] ->
            Acc;

        [H | T] ->
            do_reverse(T, [H | Acc])
    end.

-file("src/aws/internal/providers/sts_web_identity.gleam", 103).
-spec list_reverse(list(IHT)) -> list(IHT).
list_reverse(Xs) ->
    do_reverse(Xs, []).

-file("src/aws/internal/providers/sts_web_identity.gleam", 87).
-spec do_encode_pairs(list({binary(), binary()}), list(binary())) -> list(binary()).
do_encode_pairs(Pairs, Acc) ->
    case Pairs of
        [] ->
            list_reverse(Acc);

        [{K, V} | Rest] ->
            do_encode_pairs(
                Rest,
                [<<<<(aws@internal@uri:encode_component(K))/binary, "="/utf8>>/binary,
                        (aws@internal@uri:encode_component(V))/binary>> |
                    Acc]
            )
    end.

-file("src/aws/internal/providers/sts_web_identity.gleam", 81).
-spec form_encode(list({binary(), binary()})) -> binary().
form_encode(Pairs) ->
    _pipe = Pairs,
    _pipe@1 = do_encode_pairs(_pipe, []),
    gleam@string:join(_pipe@1, <<"&"/utf8>>).

-file("src/aws/internal/providers/sts_web_identity.gleam", 54).
-spec fetch(
    fun((gleam@http@request:request(bitstring())) -> {ok,
            gleam@http@response:response(bitstring())} |
        {error, aws@internal@http_send:http_error()}),
    options()
) -> {ok, sts_credentials()} | {error, error()}.
fetch(Send, Options) ->
    Body = begin
        _pipe = [{<<"Action"/utf8>>, <<"AssumeRoleWithWebIdentity"/utf8>>},
            {<<"Version"/utf8>>, <<"2011-06-15"/utf8>>},
            {<<"RoleArn"/utf8>>, erlang:element(3, Options)},
            {<<"RoleSessionName"/utf8>>, erlang:element(4, Options)},
            {<<"WebIdentityToken"/utf8>>, erlang:element(5, Options)},
            {<<"DurationSeconds"/utf8>>,
                erlang:integer_to_binary(erlang:element(6, Options))}],
        form_encode(_pipe)
    end,
    gleam@result:'try'(
        begin
            _pipe@1 = build_request(
                erlang:element(2, Options),
                gleam_stdlib:identity(Body)
            ),
            gleam@result:map_error(_pipe@1, fun(Reason) -> {failed, Reason} end)
        end,
        fun(Req) ->
            gleam@result:'try'(
                begin
                    _pipe@2 = Send(Req),
                    gleam@result:map_error(
                        _pipe@2,
                        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 returned status "/utf8,
                                        (erlang:integer_to_binary(Code@1))/binary>>}}
                    end end
            )
        end
    ).