From aef30a19be7115bb88248593f27c4093a4de0db1 Mon Sep 17 00:00:00 2001 From: Dimitri Tishchenko Date: Mon, 9 Dec 2019 14:37:21 -0800 Subject: [PATCH 1/2] assume role with web identity --- lib/ex_aws/sts.ex | 27 +++++++- .../assume_role_web_identity_adapter.ex | 62 +++++++++++++++++++ lib/ex_aws/sts/parsers.ex | 18 ++++++ test/lib/sts_test.exs | 17 +++++ 4 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 lib/ex_aws/sts/auth_cache/assume_role_web_identity_adapter.ex diff --git a/lib/ex_aws/sts.ex b/lib/ex_aws/sts.ex index 24dee3e..88b9807 100644 --- a/lib/ex_aws/sts.ex +++ b/lib/ex_aws/sts.ex @@ -19,15 +19,37 @@ defmodule ExAws.STS do @doc "Assume Role" @spec assume_role(role_arn :: String.t(), role_session_name :: String.t(), [assume_role_opt]) :: ExAws.Operation.Query.t() - def assume_role(role_arn, role_name, opts \\ []) do + def assume_role(role_arn, role_session_name, opts \\ []) do params = parse_opts(opts) |> Map.put("RoleArn", role_arn) - |> Map.put("RoleSessionName", role_name) + |> Map.put("RoleSessionName", role_session_name) request(:assume_role, params) end + @type assume_role_with_web_identity_opt :: + {:duration, pos_integer} + | {:provider_id, binary} + | {:policy, policy} + + @doc "Assume Role with Web Identity" + @spec assume_role_with_web_identity( + role_arn :: String.t(), + role_session_name :: String.t(), + web_identity_token :: String.t(), + [assume_role_with_web_identity_opt] + ) :: ExAws.Operation.Query.t() + def assume_role_with_web_identity(role_arn, role_session_name, web_identity_token, opts \\ []) do + params = + parse_opts(opts) + |> Map.put("RoleArn", role_arn) + |> Map.put("RoleSessionName", role_session_name) + |> Map.put("WebIdentityToken", web_identity_token) + + request(:assume_role_with_web_identity, params) + end + @doc "Decode Authorization Message" @spec decode_authorization_message(message :: String.t()) :: ExAws.Operation.Query.t() def decode_authorization_message(message) do @@ -96,5 +118,6 @@ defmodule ExAws.STS do defp parse_opt(opts, {:duration, val}), do: Map.put(opts, "DurationSeconds", val) defp parse_opt(opts, {:token_code, val}), do: Map.put(opts, "TokenCode", val) defp parse_opt(opts, {:serial_number, val}), do: Map.put(opts, "SerialNumber", val) + defp parse_opt(opts, {:provider_id, val}), do: Map.put(opts, "ProviderId", val) defp parse_opt(opts, {:policy, val}), do: Map.put(opts, "Policy", Poison.encode!(val)) end diff --git a/lib/ex_aws/sts/auth_cache/assume_role_web_identity_adapter.ex b/lib/ex_aws/sts/auth_cache/assume_role_web_identity_adapter.ex new file mode 100644 index 0000000..41f5a7e --- /dev/null +++ b/lib/ex_aws/sts/auth_cache/assume_role_web_identity_adapter.ex @@ -0,0 +1,62 @@ +defmodule ExAws.STS.AuthCache.AssumeRoleWebIdentityAdapter do + @moduledoc """ + Provides a custom Adapter which intercepts ExAWS configuration + which uses Role ARN + Source Profile for authentication. + """ + + @behaviour ExAws.Config.AuthCache.AuthConfigAdapter + + @impl true + def adapt_auth_config(auth, profile, expiration) + + def adapt_auth_config(%{source_profile: source_profile} = auth, _, expiration) do + source_profile_auth = ExAws.CredentialsIni.security_credentials(source_profile) + get_security_credentials(auth, source_profile_auth, expiration) + end + + def adapt_auth_config(auth, _, _), do: auth + + defp get_security_credentials(auth, source_profile_auth, expiration) do + duration = credential_duration_seconds(expiration) + role_session_name = Map.get(auth, :role_session_name, "default_session") + + assume_role_options = + case auth do + %{external_id: external_id} -> [duration: duration, external_id: external_id] + _ -> [duration: duration] + end + + assume_role_request = + ExAws.STS.assume_role_with_web_identity( + auth.role_arn, + role_session_name, + auth.web_identity_token, + assume_role_options + ) + + assume_role_config = ExAws.Config.new(:sts, source_profile_auth) + + with {:ok, result} <- ExAws.request(assume_role_request, assume_role_config) do + %{ + access_key_id: result.body.access_key_id, + secret_access_key: result.body.secret_access_key, + security_token: result.body.session_token, + role_arn: auth.role_arn, + role_session_name: role_session_name, + source_profile: auth.source_profile + } + else + {:error, reason} -> + {:error, reason} + end + end + + defp credential_duration_seconds(expiration_ms) do + # assume_role accepts a duration between 900 and 3600 seconds + # We're adding a buffer to make sure the credentials live longer than + # the refresh interval. + {min, max, buffer} = {900, 3600, 5} + seconds = div(expiration_ms, 1000) + buffer + Enum.max([Enum.min([max, seconds]), min]) + end +end diff --git a/lib/ex_aws/sts/parsers.ex b/lib/ex_aws/sts/parsers.ex index 2126380..16ef868 100644 --- a/lib/ex_aws/sts/parsers.ex +++ b/lib/ex_aws/sts/parsers.ex @@ -18,6 +18,24 @@ if Code.ensure_loaded?(SweetXml) do {:ok, Map.put(resp, :body, parsed_body)} end + def parse({:ok, %{body: xml} = resp}, :assume_role_with_web_identity) do + parsed_body = + xml + |> SweetXml.xpath(~x"//AssumeRoleWithWebIdentityResponse", + access_key_id: ~x"./AssumeRoleWithWebIdentityResult/Credentials/AccessKeyId/text()"s, + secret_access_key: + ~x"./AssumeRoleWithWebIdentityResult/Credentials/SecretAccessKey/text()"s, + session_token: ~x"./AssumeRoleWithWebIdentityResult/Credentials/SessionToken/text()"s, + expiration: ~x"./AssumeRoleWithWebIdentityResult/Credentials/Expiration/text()"s, + assumed_role_id: + ~x"./AssumeRoleWithWebIdentityResult/AssumedRoleUser/AssumedRoleId/text()"s, + assumed_role_arn: ~x"./AssumeRoleWithWebIdentityResult/AssumedRoleUser/Arn/text()"s, + request_id: request_id_xpath() + ) + + {:ok, Map.put(resp, :body, parsed_body)} + end + def parse({:ok, %{body: xml} = resp}, :get_caller_identity) do parsed_body = SweetXml.xpath(xml, ~x"//GetCallerIdentityResponse", diff --git a/test/lib/sts_test.exs b/test/lib/sts_test.exs index 35c3f0f..488cc19 100644 --- a/test/lib/sts_test.exs +++ b/test/lib/sts_test.exs @@ -23,6 +23,23 @@ defmodule ExAws.STSTest do assert expected == STS.assume_role(arn, name).params end + test "#assume_role_with_web_identity" do + version = "2011-06-15" + arn = "1111111/test_role" + name = "test role" + token = "atoken" + + expected = %{ + "Action" => "AssumeRoleWithWebIdentity", + "RoleSessionName" => name, + "RoleArn" => arn, + "WebIdentityToken" => token, + "Version" => version + } + + assert expected == STS.assume_role_with_web_identity(arn, name, token).params + end + test "#decode_authorization_message" do version = "2011-06-15" message = "msgcontent" From 97e45df637ca855722150323b5dbff683073cf0c Mon Sep 17 00:00:00 2001 From: Dimitri Tishchenko Date: Mon, 9 Dec 2019 16:28:35 -0800 Subject: [PATCH 2/2] get role arn and web identity token from env variables --- .../assume_role_web_identity_adapter.ex | 65 +++++++++++-------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/lib/ex_aws/sts/auth_cache/assume_role_web_identity_adapter.ex b/lib/ex_aws/sts/auth_cache/assume_role_web_identity_adapter.ex index 41f5a7e..f235452 100644 --- a/lib/ex_aws/sts/auth_cache/assume_role_web_identity_adapter.ex +++ b/lib/ex_aws/sts/auth_cache/assume_role_web_identity_adapter.ex @@ -20,34 +20,47 @@ defmodule ExAws.STS.AuthCache.AssumeRoleWebIdentityAdapter do duration = credential_duration_seconds(expiration) role_session_name = Map.get(auth, :role_session_name, "default_session") - assume_role_options = - case auth do - %{external_id: external_id} -> [duration: duration, external_id: external_id] - _ -> [duration: duration] - end + role_arn = System.get_env("AWS_ROLE_ARN") + web_identity_token_file = System.get_env("AWS_WEB_IDENTITY_TOKEN_FILE") + + if !is_nil(role_arn) && !is_nil(web_identity_token_file) do + assume_role_options = + case auth do + %{external_id: external_id} -> [duration: duration, external_id: external_id] + _ -> [duration: duration] + end + + with {:ok, web_identity_token} <- File.read(web_identity_token_file) do + assume_role_request = + ExAws.STS.assume_role_with_web_identity( + role_arn, + role_session_name, + web_identity_token, + assume_role_options + ) - assume_role_request = - ExAws.STS.assume_role_with_web_identity( - auth.role_arn, - role_session_name, - auth.web_identity_token, - assume_role_options - ) - - assume_role_config = ExAws.Config.new(:sts, source_profile_auth) - - with {:ok, result} <- ExAws.request(assume_role_request, assume_role_config) do - %{ - access_key_id: result.body.access_key_id, - secret_access_key: result.body.secret_access_key, - security_token: result.body.session_token, - role_arn: auth.role_arn, - role_session_name: role_session_name, - source_profile: auth.source_profile - } + assume_role_config = ExAws.Config.new(:sts, source_profile_auth) + + with {:ok, result} <- ExAws.request(assume_role_request, assume_role_config) do + %{ + access_key_id: result.body.access_key_id, + secret_access_key: result.body.secret_access_key, + security_token: result.body.session_token, + role_arn: auth.role_arn, + role_session_name: role_session_name, + source_profile: auth.source_profile + } + else + {:error, reason} -> + {:error, reason} + end + else + {:error, reason} -> + {:error, reason} + end else - {:error, reason} -> - {:error, reason} + {:error, + "AWS_ROLE_ARN and AWS_WEB_IDENTITY_TOKEN_FILE must be available in the environment"} end end