%% @doc This provider reads the credentials and config files for the Amazon
%% CLI tools.
%% This format is documented here but an example might look
%% like:
%%
%% `~/.aws/credentials'
%%
%%
%% [default]
%% aws_access_key_id = AKIAIOSFODNN7EXAMPLE
%% aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
%% aws_session_token = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLETOKEN
%%
%%
%% `~/.aws/config'
%%
%%
%% [default]
%% region = us-east-1
%%
%%
%% Environment parameters:
%%
%% - <<"credentials_path">> - this is the base path to the both CLI configuration files.
%% And based on this path, credentials file should exist and config file is optional.
%% By default this is `~/.aws/'
%% - <<"profile">> - this is the desired profile to use in the credentials file.
%% By default this is <<"default">>
%%
%% @end
-module(aws_credentials_file).
-behaviour(aws_credentials_provider).
-export([fetch/1]).
-spec fetch(aws_credentials_provider:options()) ->
{ok, aws_credentials:credentials(), aws_credentials_provider:expiration()} |
{error, any()}.
fetch(Options) ->
FilePath = get_file_path(Options),
ConfigPath = does_credentials_file_exist(FilePath, config),
case does_credentials_file_exist(FilePath, credentials) of
{error, Error} ->
{error, {credentials_file_does_not_exist, Error}};
CredentialsPath ->
CredFile = parse_credentials_file(CredentialsPath, Options),
maybe_add_region(CredFile, ConfigPath, Options)
end.
-spec does_credentials_file_exist(string(), atom()) -> {error, any()} | string().
does_credentials_file_exist({error, _} = Error, _File) -> Error;
does_credentials_file_exist(Path, credentials) ->
maybe_path_from_env("AWS_SHARED_CREDENTIALS_FILE", check_path_exists(Path ++ "credentials"));
does_credentials_file_exist(Path, config) ->
maybe_path_from_env("AWS_CONFIG_FILE", check_path_exists(Path ++ "config")).
-spec maybe_path_from_env(string(), string()) -> {error, any()} | string().
maybe_path_from_env(EnvVar, FilePath) ->
case {os:getenv(EnvVar), FilePath} of
{false, {error, _} = Error} -> Error;
{false, Path} -> Path;
{EnvPath, {error, _}} -> check_path_exists(EnvPath);
{_, Path} -> Path
end.
-spec get_file_path(aws_credentials_provider:options()) -> {error, any()} | string().
get_file_path(Options) ->
case maps:get(credential_path, Options, undefined) of
undefined -> maybe_add_home("/.aws/");
Path -> Path
end.
-spec maybe_add_home(string()) -> string() | {error, any()}.
maybe_add_home(Path) ->
case os:getenv("HOME") of
false -> {error, could_not_get_home_directory_from_os_environment};
Home -> Home ++ Path
end.
-spec maybe_add_region(
{error, any()} | {ok, aws_credentials:credentials(), aws_credentials_provider:expiration()},
{error, any()} | string(),
aws_credentials_provider:options()
) -> {ok, aws_credentials:credentials(), aws_credentials_provider:expiration()}.
maybe_add_region({error, _} = Error, _Config, _Options) -> Error;
maybe_add_region(Result, {error, _Error}, _Options) -> Result;
maybe_add_region({ok, Credentials, Expiration}, ConfigPath, Options) ->
case parse_config_file(ConfigPath, Options) of
{ok, Config} ->
{ok, maps:put(region, maps:get(<<"region">>, Config), Credentials), Expiration};
{error, _} ->
{ok, Credentials, Expiration}
end.
-spec check_path_exists(string()) -> {error, 'file_not_found'} | string().
check_path_exists(Path) ->
case filelib:is_regular(Path) of
false -> {error, file_not_found};
true -> Path
end.
-spec parse_credentials_file(string(), aws_credentials_provider:options()) ->
{error, any()} | {ok, aws_credentials:credentials(), aws_credentials_provider:expiration()}.
parse_credentials_file(Path, Options) ->
{ok, F} = file:read_file(Path),
{ok, Profiles} = eini:parse(F),
Desired = desired_profile(Options),
case read_from_profile(Profiles, Desired) of
{error, _} = Error -> Error;
{ok, Profile} ->
case maps:is_key(<<"aws_session_token">>, Profile) of
true ->
{ok, aws_credentials:make_map(?MODULE,
maps:get(<<"aws_access_key_id">>, Profile),
maps:get(<<"aws_secret_access_key">>, Profile),
maps:get(<<"aws_session_token">>, Profile)),
maps:get(<<"aws_expiration">>, Profile, infinity)};
false ->
{ok, aws_credentials:make_map(?MODULE,
maps:get(<<"aws_access_key_id">>, Profile),
maps:get(<<"aws_secret_access_key">>, Profile)),
infinity}
end
end.
-spec parse_config_file(string(), aws_credentials_provider:options()) ->
{error, any()} | {ok, map()}.
parse_config_file(Path, Options) ->
{ok, F} = file:read_file(Path),
{ok, Profiles} = eini:parse(F),
Desired = desired_profile(Options),
read_from_profile(Profiles, Desired).
-spec read_from_profile(map(), binary()) -> any().
read_from_profile(File, Profile) ->
case maps:get(Profile, File, undefined) of
undefined -> {error, {desired_profile_not_found, Profile}};
Map ->
case maps:is_key(<<"credential_process">>, Map) of
true -> resolve_credential_process(Map);
false -> {ok, Map}
end
end.
%% Source credentials with an external process
%% https://docs.aws.amazon.com/cli/v1/userguide/cli-configure-sourcing-external.html
-spec resolve_credential_process(map()) -> {error, any()} | {ok, map()}.
resolve_credential_process(Map) ->
Process = maps:get(<<"credential_process">>, Map),
Stdout = os:cmd(binary_to_list(Process)),
case decode_credential_process_output(Stdout) of
{error, _} = Error ->
Error;
{ok, CredResult} ->
CredsMap = maps:from_list(lists:filtermap(
fun credential_process_field/1, maps:to_list(CredResult))),
{ok, maps:merge(Map, CredsMap)}
end.
-spec credential_process_field({binary(), any()}) -> {true, {binary(), any()}} | false.
credential_process_field({<<"AccessKeyId">>, AKI}) ->
{true, {<<"aws_access_key_id">>, AKI}};
credential_process_field({<<"SecretAccessKey">>, SAK}) ->
{true, {<<"aws_secret_access_key">>, SAK}};
credential_process_field({<<"SessionToken">>, ST}) ->
{true, {<<"aws_session_token">>, ST}};
credential_process_field({<<"Expiration">>, E}) ->
{true, {<<"aws_expiration">>, E}};
credential_process_field(_) ->
false.
-spec decode_credential_process_output(iodata()) -> {error, any()} | {ok, map()}.
decode_credential_process_output(Stdout) ->
try jsx:decode(iolist_to_binary(Stdout)) of
Decoded when is_map(Decoded) -> {ok, Decoded};
_NotAMap -> {error, {invalid_credential_process_output, Stdout}}
catch
error:_ -> {error, {invalid_credential_process_output, Stdout}}
end.
-spec desired_profile(aws_credentials_provider:options()) -> binary().
desired_profile(Options) ->
case {os:getenv("AWS_PROFILE"), maps:get(profile, Options, undefined)} of
{false, undefined} -> <<"default">>;
{false, Profile} -> Profile;
{AwsProfile, undefined} -> list_to_binary(AwsProfile);
{_, Profile} -> Profile
end.