aws-beam/aws-elixir

Need help with assuming roles with tags.

smashedtoatoms opened this issue · 11 comments

When I try to call AWS.STS.assume_role and tag the role, I can't figure out the data structure it wants for the tags. The Go docs say it should be a list of structs with Key and Value as the attributes of the struct. I have no idea what that translates to in elixir.

  1. I started with a map
out =
      AWS.STS.assume_role(client, %{
        "RoleArn" => "arn:aws:iam::000000000000:role/external/MyApp-MyAppRole-D390C2QK3BNF",
        "RoleSessionName" => "TheSessionName",
        "Tags" => %{"Key" => "AccountId", "Value" => "fakeAccountId"}
      })

resulted in

     ** (Protocol.UndefinedError) protocol String.Chars not implemented for %{"Key" => "AccountId", "Value" => "fakeAccountId"} of type Map. This protocol is implemented for the following type(s): Atom, BitString, Date, DateTime, Decimal, Float, Floki.Selector, Floki.Selector.AttributeSelector, Floki.Selector.Combinator, Floki.Selector.Functional, Floki.Selector.PseudoClass, Integer, List, NaiveDateTime, Phoenix.LiveComponent.CID, Postgrex.Copy, Postgrex.Query, Time, URI, Version, Version.Requirement
  1. So I put a map in a list to more closely match the Go SDK
    out =
          AWS.STS.assume_role(client, %{
            "RoleArn" => "arn:aws:iam::000000000000:role/external/MyApp-MyAppRole-D390C2QK3BNF",
            "RoleSessionName" => "TheSessionName",
            "Tags" => [%{Key: "AccountId", Value: "fakeAccountId"}]
          })
    resulted in
         ** (ArgumentError) cannot convert the given list to a string.
      
         To be converted to a string, a list must either be empty or only
         contain the following elements:
      
           * strings
           * integers representing Unicode code points
           * a list containing one of these three elements
      
         Please check the given list or call inspect/1 to get the list representation, got:
      
         [%{Key: "AccountId", Value: "fakeAccountId"}]
  2. So I did a list of keyword lists
    out =
          AWS.STS.assume_role(client, %{
            "RoleArn" => "arn:aws:iam::000000000000:role/external/MyApp-MyAppRole-D390C2QK3BNF",
            "RoleSessionName" => "TheSessionName",
            "Tags" => [[Key: "AccountId", Value: "fakeAccountId"]]
          })
    resulted in
         ** (ArgumentError) cannot convert the given list to a string.
      
         To be converted to a string, a list must either be empty or only
         contain the following elements:
      
           * strings
           * integers representing Unicode code points
           * a list containing one of these three elements
      
         Please check the given list or call inspect/1 to get the list representation, got:
      
         [[key: "AccountId", value: "fakeAccountId"]]
  3. Running out of ideas
    out =
          AWS.STS.assume_role(client, %{
            "RoleArn" => "arn:aws:iam::000000000000:role/external/MyApp-MyAppRole-D390C2QK3BNF",
            "RoleSessionName" => "TheSessionName",
            "Tags" => [AccountId: "fakeAccountId"]
          })
    resulted in
         ** (ArgumentError) cannot convert the given list to a string.
      
         To be converted to a string, a list must either be empty or only
         contain the following elements:
      
           * strings
           * integers representing Unicode code points
           * a list containing one of these three elements
      
         Please check the given list or call inspect/1 to get the list representation, got:
      
         [AccountId: "fakeAccountId"]

Tuples, you name it, nothing works. I even tried the string Key=AccountId,Value=fakeAccountId from the cli tool. Any help you can provide would be appreciated.

@smashedtoatoms This is presumably fixed on master (by #142). Can you try to use that version?

I'm going to release a new version if that works for you.

I just tried it and got a 400 from STS. This is likely self-inflicted though. What should the data format for the tags be? Should it be a list of Key-Value maps since the underlying Go would be a slice of Key-Value structs?

{:error,
 {:unexpected_response,
  %{
    body: "<ErrorResponse xmlns=\"https://sts.amazonaws.com/doc/2011-06-15/\">\n  <Error>\n    <Type>Sender</Type>\n    <Code>MalformedInput</Code>\n    <Message>Unexpected complex element termination</Message>\n  </Error>\n  <RequestId>9e7d41f8-0b43-4b43-9769-c2171d0c44d6</RequestId>\n</ErrorResponse>\n",
    headers: [
      {"x-amzn-RequestId", "9e7d41f8-0b43-4b43-9769-c2171d0c44d6"},
      {"Content-Type", "text/xml"},
      {"Content-Length", "284"},
      {"Date", "Mon, 18 Jul 2022 13:29:03 GMT"},
      {"Connection", "close"}
    ],
    status_code: 400
  }}}

Here is the error if I use a list of structs:

{:error,
 {:unexpected_response,
  %{
    body: "<ErrorResponse xmlns=\"https://sts.amazonaws.com/doc/2011-06-15/\">\n  <Error>\n    <Type>Sender</Type>\n    <Code>MalformedInput</Code>\n    <Message>Top level element may not be treated as a list</Message>\n  </Error>\n  <RequestId>81d89cf2-f142-4a14-ac82-742a31bca1fb</RequestId>\n</ErrorResponse>\n",
    headers: [
      {"x-amzn-RequestId", "81d89cf2-f142-4a14-ac82-742a31bca1fb"},
      {"Content-Type", "text/xml"},
      {"Content-Length", "292"},
      {"Date", "Mon, 18 Jul 2022 13:31:54 GMT"},
      {"Connection", "close"}
    ],
    status_code: 400
  }}}

I tried list of keyword lists as well:

** (Protocol.UndefinedError) protocol String.Chars not implemented for {:Key, "AccountId"} of type Tuple. This protocol is implemented for the following type(s): Atom, BitString, Date, DateTime, Decimal, Float, Floki.Selector, Floki.Selector.AttributeSelector, Floki.Selector.Combinator, Floki.Selector.Functional, Floki.Selector.PseudoClass, Integer, List, NaiveDateTime, Phoenix.LiveComponent.CID, Postgrex.Copy, Postgrex.Query, Time, URI, Version, Version.Requirement

Disclaimer: I do not use aws-elixir and there may be some slight differences between aws-elixir and aws-erlang (which I do use) but could you try passing tags as follows:

<<"Tags.member.1.Key">> => <<"Team">>,
<<"Tags.member.1.Value">> => <<"smashed">>,
<<"Tags.member.2.Key">> => <<"Application">>,
<<"Tags.member.2.Value">> => <<"ToAtoms">>,
<<"Tags.member.3.Key">> => <<"Library">>,
<<"Tags.member.3.Value">> => <<"Aws-Elixir">>},

So forgive me for butchering potentially as I don't know if this even compiles but something along the lines of:

%{ "RoleArn" => "arn:aws:iam::000000000000:role/external/MyApp-MyAppRole-D390C2QK3BNF",
   "RoleSessionName" => "TheSessionName",
   "Tags.member.1.Key" => "Team",
   "Tags.member.1.Value" => "Smashed",
   "Tags.member.2.Key" => "Application",
   "Tags.member.2.Value" => "ToAtoms"
 }

@smashedtoatoms ^

@smashedtoatoms sorry, I'm not sure how this supposed to be. Maybe something like you mentioned that is accepted in the command line? Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html#id_session-tags_adding-assume-role

I would try:

AWS.STS.assume_role(client, %{
  "RoleArn" => "arn:aws:iam::000000000000:role/external/MyApp-MyAppRole-D390C2QK3BNF",
  "RoleSessionName" => "TheSessionName",
  "Tags" => ["Key=AccountId,Value=fakeAccountId"]
})

Hahahaha, you're totally on the right track. The STS API is absolutely bonkers. This works.

out =
      AWS.STS.assume_role(client, %{
        "RoleArn" => "arn:aws:iam::000000000000:role/external/MyApp-MyRole-D390C2QK3BNF",
        "RoleSessionName" => "nightmare",
        "Tags.member.1.Key" => "AccountId",
        "Tags.member.1.Value" => "fakeAccountId"
      })

It's the whole AWS API when it comes to tagging which is utter crazyness if you ask me... But glad to have helped! I'll document it in the README tonight as this is not the first time it came up and I know I struggled with it as well when getting these damn tags to work :-)

Are you ok with closing this @smashedtoatoms ?

Yep, let's close it. When I get assuming a tagged role done, I will do a PR for some example code.