%% @doc This is the main interface to the library. It provides a function %% `get_credentials/0' which should return `Credentials :: map()' or `undefined'. %% If undefined, it will attempt to get credentials again after 5 seconds delay. %% @end -module(aws_credentials). -behaviour(gen_server). %% As per %% http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#instance-metadata-security-credentials %% %% We make new credentials available at least five minutes prior to the %% expiration of the old credentials. -define(ALERT_BEFORE_EXPIRY, 300). % 5 minutes -define(RETRY_DELAY, 5). % 5 seconds -define(GREGORIAN_TO_EPOCH_SECONDS, 62167219200). -include_lib("kernel/include/logger.hrl"). -export([ init/1 , terminate/2 , code_change/3 , handle_call/3 , handle_cast/2 , handle_info/2 , format_status/1 ]). -if(?OTP_RELEASE < 27). -export([format_status/2]). -endif. -export([ start_link/0 , stop/0 , get_credentials/0 , force_credentials_refresh/0 , force_credentials_refresh/1 , make_map/3 , make_map/4 , make_map/5 ]). -record(state, { credentials = undefined :: credentials() | undefined | information_redacted , tref = undefined :: reference() | undefined }). -opaque state() :: #state{}. -export_type([state/0]). % for linter only -type credentials() :: #{ credential_provider := aws_credentials_provider:provider() , access_key_id := access_key_id() , secret_access_key := secret_access_key() , token => token() , region => region() }. -type access_key_id() :: binary(). -type secret_access_key() :: binary(). -type token() :: binary(). -type region() :: binary(). -export_type([access_key_id/0, secret_access_key/0, token/0, region/0, credentials/0]). %%==================================================================== %% API %%==================================================================== -spec make_map(aws_credentials_provider:provider(), access_key_id(), secret_access_key()) -> credentials(). make_map(Provider, AccessId, SecretKey) -> #{ credential_provider => Provider, access_key_id => AccessId, secret_access_key => SecretKey }. -spec make_map(aws_credentials_provider:provider(), access_key_id(), secret_access_key(), token()) -> credentials(). make_map(Provider, AccessId, SecretKey, Token) -> M = make_map(Provider, AccessId, SecretKey), maps:put(token, Token, M). -spec make_map(aws_credentials_provider:provider(), access_key_id(), secret_access_key(), token(), region()) -> credentials(). make_map(Provider, AccessId, SecretKey, Token, Region) -> M = make_map(Provider, AccessId, SecretKey, Token), maps:put(region, Region, M). %% @doc Start the server that stores and automatically updates client %% credentials fetched from the instance metadata service. -spec start_link() -> {'ok', pid()}. start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). %% @doc Stop the server that holds the credentials. -spec stop() -> 'ok'. stop() -> gen_server:stop(?MODULE). %% @doc Get cached credential information. -spec get_credentials() -> credentials() | undefined. get_credentials() -> gen_server:call(?MODULE, get_credentials). %% @doc Force a credentials update (using the application environment %% options if any). -spec force_credentials_refresh() -> credentials() | undefined | {error, any()}. force_credentials_refresh() -> ProviderOptions = application:get_env(aws_credentials, provider_options, #{}), force_credentials_refresh(ProviderOptions). %% @doc Force a credentials update, passing options (which possibly override %% the options set in the erlang environment.) -spec force_credentials_refresh(aws_credentials_provider:options()) -> credentials() | undefined | {error, any()}. force_credentials_refresh(Options) -> gen_server:call(?MODULE, {force_refresh, Options}). %%==================================================================== %% Behaviour %%==================================================================== -spec init(_) -> {'ok', state()}. init(_Args) -> ProviderOptions = application:get_env(aws_credentials, provider_options, #{}), {ok, C, T} = fetch_credentials(ProviderOptions), {ok, #state{credentials=C, tref=T}}. -spec terminate(any(), state()) -> 'ok'. terminate(_Reason, _State) -> ok. -spec handle_call(any(), any(), state()) -> {'noreply', _} | {'reply', any(), state()}. handle_call(get_credentials, _From, #state{credentials=C}=State) -> {reply, C, State}; handle_call({force_refresh, Options}, _From, #state{tref=T}=State) -> {ok, C, NewT} = fetch_credentials(Options), case is_reference(T) of true -> erlang:cancel_timer(T); false -> ok end, {reply, C, State#state{credentials=C, tref=NewT}}; handle_call(Args, _From, State) -> ?LOG_WARNING("Unknown call: ~p~n", [Args], #{domain => [aws_credentials]}), {noreply, State}. -spec handle_cast(any(), state()) -> {'noreply', state()}. handle_cast(Message, State) -> ?LOG_WARNING("Unknown cast: ~p~n", [Message], #{domain => [aws_credentials]}), {noreply, State}. -spec handle_info(any(), state()) -> {'noreply', state()}. handle_info(refresh_credentials, State) -> ProviderOptions = application:get_env(aws_credentials, provider_options, #{}), case fetch_credentials(ProviderOptions) of {ok, undefined, T} -> {noreply, State#state{tref=T}}; {ok, C, T} -> {noreply, State#state{credentials=C, tref=T}} end; handle_info(Message, State) -> ?LOG_WARNING("Unknown message: ~p~n", [Message], #{domain => [aws_credentials]}), {noreply, State}. -spec code_change(any(), state(), any()) -> {'ok', state()}. code_change(_Prev, State, _Extra) -> {ok, State}. -spec format_status(gen_server:format_status()) -> gen_server:format_status(). format_status(#{reason := _Reason, state := State} = Status) -> Status#{state => State#state{credentials = information_redacted}}; format_status(Status) -> Status. -if(?OTP_RELEASE < 27). %% TODO: remove once OTP-25 is the oldest supported OTP version -spec format_status('normal' | 'terminate', any()) -> any(). format_status(_, [_PDict, State]) -> [{data, [{"State", State#state{credentials=information_redacted}}]}]. -endif. %%==================================================================== %% Internal functions %%==================================================================== -spec fetch_credentials(aws_credentials_provider:options()) -> {ok, credentials() | 'undefined', reference() | 'undefined'}. fetch_credentials(Options) -> ShouldCatch = not application:get_env(aws_credentials, fail_if_unavailable, false), try aws_credentials_provider:fetch(Options) of {ok, Credentials, ExpirationTime} -> Tref = setup_update_callback(ExpirationTime), {ok, Credentials, Tref}; {error, 'no_credentials'} -> ?LOG_INFO("No credentials available~n", [], #{domain => [aws_credentials]}), {ok, undefined_or_fail(ShouldCatch), setup_callback(?RETRY_DELAY)}; {error, ErrorLog} when is_list(ErrorLog) -> Metadata = #{domain => [aws_credentials]}, ?LOG_INFO("No credentials available~n", [], Metadata), [?LOG_ERROR("Provider ~p reports ~p", [Provider, Error], Metadata) || {Provider, Error} <- ErrorLog], {ok, undefined_or_fail(ShouldCatch), setup_callback(?RETRY_DELAY)} catch E:R:ST when ShouldCatch -> ?LOG_INFO("aws_credentials ignoring exception ~p:~p (~p)~n", [E, R, ST], #{domain => [aws_credentials]}), {ok, undefined, setup_callback(?RETRY_DELAY)} end. -spec undefined_or_fail(boolean()) -> undefined | no_return(). undefined_or_fail(true) -> undefined; undefined_or_fail(false) -> error(no_credentials). -spec setup_update_callback('infinity' | binary() | integer()) -> reference() | undefined. setup_update_callback(infinity) -> undefined; setup_update_callback(Expires) when is_binary(Expires) -> RefreshAfter = seconds_until_timestamp(Expires) - ?ALERT_BEFORE_EXPIRY, setup_callback(RefreshAfter); setup_update_callback(Expires) when is_integer(Expires) -> setup_callback(Expires - ?ALERT_BEFORE_EXPIRY). -spec setup_callback(pos_integer()) -> reference(). setup_callback(Seconds) -> erlang:send_after(Seconds * 1000, self(), refresh_credentials). -spec seconds_until_timestamp(binary()) -> integer(). seconds_until_timestamp(Timestamp) -> calendar:datetime_to_gregorian_seconds(iso8601:parse(Timestamp)) - (erlang:system_time(seconds) + ?GREGORIAN_TO_EPOCH_SECONDS).