Skip to content
This repository has been archived by the owner on Sep 22, 2022. It is now read-only.

Assume role with web identity #1

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions lib/ex_aws/sts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
75 changes: 75 additions & 0 deletions lib/ex_aws/sts/auth_cache/assume_role_web_identity_adapter.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
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")

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_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,
"AWS_ROLE_ARN and AWS_WEB_IDENTITY_TOKEN_FILE must be available in the environment"}
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
18 changes: 18 additions & 0 deletions lib/ex_aws/sts/parsers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
17 changes: 17 additions & 0 deletions test/lib/sts_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down