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
)
def security_credentials(profile_name) do
config_credentials = profile_from_config(profile_name)
shared_credentials = profile_from_shared_credentials(profile_name)
config = ExAws.Config.http_config(:sso)
case config_credentials do
%{
sso_start_url: sso_start_url,
sso_account_id: sso_account_id,
sso_role_name: sso_role_name
} ->
case get_sso_role_credentials(sso_start_url, sso_account_id, sso_role_name, config) do
{:ok, sso_creds} -> {:ok, Map.merge(sso_creds, shared_credentials)}
{:error, _} = err -> err
end
_ ->
{:ok, Map.merge(config_credentials, shared_credentials)}
end
end
defp get_sso_role_credentials(sso_start_url, sso_account_id, sso_role_name, config) do
with {_, {:ok, sso_cache_content}} <-
{:read, File.read(get_sso_cache_file(sso_start_url))},
{_,
{: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_start_url) do
hash = :crypto.hash(:sha, sso_start_url) |> 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, expires_at, _}} <- {:timestamp, DateTime.from_iso8601(expires_at_str)},
{_, :gt} <- {:expires, DateTime.compare(expires_at, DateTime.utc_now())} 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, [])
)},
{_, {: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
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
contents
|> ConfigParser.parse_string()
|> case do
{:ok, %{^profile_name => config}} ->
strip_key_prefix(config)
{:ok, %{}} ->
%{}
_ ->
%{}
end
end
def parse_ini_file(_, _), do: %{}
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 =
case profile_name do
:system -> "profile #{profile_name_from_env()}"
"default" -> "default"
other -> "profile #{other}"
end
System.user_home()
|> Path.join(".aws/config")
|> File.read()
|> parse_ini_file(section)
end
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