Current section

Files

Jump to
ex_aws lib ex_aws credentials_ini file.ex
Raw

lib/ex_aws/credentials_ini/file.ex

if Code.ensure_loaded?(ConfigParser) do
defmodule ExAws.CredentialsIni.File do
@moduledoc false
# as per https://docs.aws.amazon.com/cli/latest/topic/config-vars.html
@valid_config_keys ~w(
aws_access_key_id aws_secret_access_key aws_session_token region
role_arn source_profile credential_source external_id mfa_serial role_session_name credential_process
sso_start_url sso_region sso_account_id sso_role_name sso_session
)
@special_merge_keys ~w(sso_session)
def security_credentials(profile_name) do
config_credentials = profile_from_config(profile_name)
shared_credentials = profile_from_shared_credentials(profile_name)
case config_credentials do
%{
sso_start_url: sso_start_url,
sso_account_id: sso_account_id,
sso_role_name: sso_role_name
} ->
sso_cache_key = Map.get(config_credentials, :sso_session, sso_start_url)
config = ExAws.Config.http_config(:sso)
case get_sso_role_credentials(sso_cache_key, sso_account_id, sso_role_name, config) do
{:ok, sso_creds} -> {:ok, Map.merge(sso_creds, shared_credentials)}
{:error, _} = err -> err
end
%{credential_process: credential_process} ->
config = ExAws.Config.http_config(:sso)
case get_credentials_from_process(credential_process, config) do
{:ok, credentials} -> {:ok, Map.merge(credentials, shared_credentials)}
{:error, _} = err -> err
end
_ ->
{:ok, Map.merge(config_credentials, shared_credentials)}
end
end
defp get_sso_role_credentials(sso_cache_key, sso_account_id, sso_role_name, config) do
with {_, {:ok, sso_cache_content}} <-
{:read, File.read(get_sso_cache_file(sso_cache_key))},
{_,
{:ok, %{"expiresAt" => expires_at, "accessToken" => access_token, "region" => region}}} <-
{:decode, config[:json_codec].decode(sso_cache_content)},
{_, :ok} <-
{:expiration, check_sso_expiration(expires_at)},
{_, {:ok, sso_creds}} <-
{:sso_creds,
request_sso_role_credentials(
access_token,
region,
sso_account_id,
sso_role_name,
config
)},
{_, {:ok, reformatted_creds}} <-
{:rename, rename_sso_credential_keys(sso_creds)} do
{:ok, reformatted_creds}
else
{:read, {:error, error}} -> {:error, "Could not read SSO cache file: #{error}"}
{:decode, _} -> {:error, "SSO cache file contains invalid json"}
{:expiration, error} -> error
{:sso_creds, error} -> error
{:rename, error} -> error
end
end
defp get_sso_cache_file(sso_cache_key) do
hash = :crypto.hash(:sha, sso_cache_key) |> Base.encode16() |> String.downcase()
System.user_home()
|> Path.join(".aws/sso/cache/#{hash}.json")
end
defp check_sso_expiration(expires_at_str) do
with {:ok, _} <- check_expiration(expires_at_str) do
:ok
else
{:timestamp, {:error, err}} ->
{:error, "SSO cache file has invalid expiration format: #{err}"}
{:expires, _} ->
{:error, "SSO access token is expired, refresh the token with `aws sso login`"}
end
end
defp request_sso_role_credentials(
access_token,
region,
account_id,
role_name,
config
) do
with {_, {:ok, %{status_code: 200, headers: _headers, body: body_raw}}} <-
{:request,
config[:http_client].request(
:get,
"https://portal.sso.#{region}.amazonaws.com/federation/credentials?account_id=#{account_id}&role_name=#{role_name}",
"",
[{"x-amz-sso_bearer_token", access_token}],
Map.get(config, :http_opts, [])
)
|> ExAws.Request.maybe_transform_response()},
{_, {:ok, body}} <- {:decode, config[:json_codec].decode(body_raw)} do
{:ok, body}
else
{:request, {_, %{status_code: status_code} = resp}} ->
{:error, "SSO role credentials request responded with #{status_code}: #{resp}"}
{:decode, err} ->
{:error, "Could not decode SSO role credentials response: #{err}"}
end
end
defp rename_sso_credential_keys(%{"roleCredentials" => role_credentials}) do
with {_, access_key} when not is_nil(access_key) <-
{:accessKey, Map.get(role_credentials, "accessKeyId")},
{_, expiration} when not is_nil(expiration) <-
{:expiration, Map.get(role_credentials, "expiration")},
{_, secret_access_key} when not is_nil(secret_access_key) <-
{:secretAccess, Map.get(role_credentials, "secretAccessKey")},
{_, session_token} when not is_nil(session_token) <-
{:sessionToken, Map.get(role_credentials, "sessionToken")} do
{:ok,
%{
access_key_id: access_key,
expiration: expiration,
secret_access_key: secret_access_key,
security_token: session_token
}}
else
{missing, _} -> {:error, "#{missing} is missing from SSO role credential response"}
end
end
defp get_credentials_from_process(credential_process, config) do
with {_, {:ok, process_result}} <-
{:process, execute_process(credential_process)},
{_, {:ok, %{"Version" => 1} = result}} <-
{:decode, config[:json_codec].decode(process_result)},
{_, {:ok, expiration}} <-
{:expiration, check_credentials_expiration(result)},
{_, {:ok, reformatted_creds}} <-
{:rename, format_result(result, expiration)} do
{:ok, reformatted_creds}
else
{:process, {:error, error}} -> {:error, "Could not execute process: #{error}"}
{:decode, _} -> {:error, "Credentials process results contains invalid json"}
{:expiration, error} -> error
{:rename, error} -> error
end
end
defp execute_process(credential_process) do
with [command | args] <- String.split(credential_process),
{result, 0} <- System.cmd(command, args, stderr_to_stdout: true) do
{:ok, result}
else
[] -> {:error, "Could not read command from config file : #{credential_process}"}
{error, exit_code} -> {:error, "Exit code : #{exit_code} - #{error}"}
end
end
defp format_result(result, nil) do
with {_, access_key} when not is_nil(access_key) <-
{:accessKey, Map.get(result, "AccessKeyId")},
{_, secret_access_key} when not is_nil(secret_access_key) <-
{:secretAccess, Map.get(result, "SecretAccessKey")} do
{:ok,
%{
access_key_id: access_key,
secret_access_key: secret_access_key
}}
else
{missing, _} -> {:error, "#{missing} is missing from credentials process response"}
end
end
defp format_result(result, expiration) do
with {_, access_key} when not is_nil(access_key) <-
{:accessKey, Map.get(result, "AccessKeyId")},
{_, secret_access_key} when not is_nil(secret_access_key) <-
{:secretAccess, Map.get(result, "SecretAccessKey")},
{_, session_token} when not is_nil(session_token) <-
{:sessionToken, Map.get(result, "SessionToken")} do
{:ok,
%{
access_key_id: access_key,
expiration: DateTime.to_unix(expiration),
secret_access_key: secret_access_key,
security_token: session_token
}}
else
{missing, _} -> {:error, "#{missing} is missing from credentials process response"}
end
end
defp check_credentials_expiration(%{"Expiration" => expiration_str}) do
with {:ok, expiration} <- check_expiration(expiration_str) do
{:ok, expiration}
else
{:timestamp, {:error, err}} ->
{:error, "Process returned invalid expiration format: #{err}"}
{:expires, _} ->
{:error, "Process returned expired credentials"}
end
end
defp check_credentials_expiration(_), do: {:ok, nil}
defp check_expiration(expiration_str) do
with {_, {:ok, expiration, _}} <- {:timestamp, DateTime.from_iso8601(expiration_str)},
{_, :gt} <- {:expires, DateTime.compare(expiration, DateTime.utc_now())} do
{:ok, expiration}
end
end
def parse_ini_file({:ok, contents}, :system) do
parse_ini_file({:ok, contents}, profile_name_from_env())
end
def parse_ini_file({:ok, contents}, profile_name) do
composite_key = "profile " <> profile_name
contents
|> ConfigParser.parse_string()
|> case do
{:ok, %{^profile_name => config} = full} ->
merge_special_keys(full, config)
|> strip_key_prefix()
{:ok, %{^composite_key => config} = full} ->
merge_special_keys(full, config)
|> strip_key_prefix()
{:ok, %{}} ->
%{}
_ ->
%{}
end
end
def parse_ini_file(_, _), do: %{}
def merge_special_keys(full_config, credentials) do
credentials
|> Map.take(@special_merge_keys)
|> Enum.reduce(credentials, fn {key, val}, acc ->
merge_section = "#{String.replace(key, "_", "-")} #{val}"
case full_config do
%{^merge_section => config} ->
Map.merge(config, acc)
_ ->
acc
end
end)
end
def strip_key_prefix(credentials) do
credentials
|> Map.take(@valid_config_keys)
|> Map.new(fn {key, val} ->
updated_key =
key
|> String.replace_leading("aws_", "")
|> String.to_atom()
{updated_key, val}
end)
end
def replace_token_key(credentials) do
case Map.pop(credentials, :session_token) do
{nil, credentials} ->
credentials
{token, credentials} ->
Map.put(credentials, :security_token, token)
end
end
defp profile_from_shared_credentials(profile_name) do
System.user_home()
|> Path.join(".aws/credentials")
|> File.read()
|> parse_ini_file(profile_name)
|> replace_token_key
end
defp profile_from_config(profile_name) do
section = profile_from_name(profile_name)
System.user_home()
|> Path.join(".aws/config")
|> File.read()
|> parse_ini_file(section)
end
defp profile_from_name(:system) do
profile_name_from_env()
|> profile_from_name()
end
defp profile_from_name("default"), do: "default"
defp profile_from_name(other), do: "profile #{other}"
defp profile_name_from_env() do
System.get_env("AWS_PROFILE") || "default"
end
end
else
defmodule ExAws.CredentialsIni.File do
@moduledoc false
def security_credentials(_), do: raise("ConfigParser required to use")
def parse_ini_file(_, _), do: raise("ConfigParser required to use")
def replace_token_key(_), do: raise("ConfigParser required to use")
end
end