diff --git a/include/erliam.hrl b/include/erliam.hrl deleted file mode 100644 index 201ddaa..0000000 --- a/include/erliam.hrl +++ /dev/null @@ -1,8 +0,0 @@ --type aws_datetime() :: string(). % "YYYYMMDDTHHMMSSZ" --type iso_datetime() :: string(). % "YYYY-MM-DDTHH:MM:SSZ" - --record(credentials, - {expiration :: undefined | iso_datetime(), - security_token :: undefined | string(), % required when using temporary credentials - secret_access_key :: string(), - access_key_id :: string()}). diff --git a/rebar.config b/rebar.config index 01e8040..cdcce3c 100644 --- a/rebar.config +++ b/rebar.config @@ -34,6 +34,6 @@ {project_plugins, [{rebar3_hex, "~> 7.0.4"}, {rebar3_format, "~> 1.2.1"}, - {rebar3_lint, "~> 2.0.1"}, + {rebar3_lint, "~> 3.0.0"}, {rebar3_hank, "~> 1.3.0"}, {rebar3_sheldon, "~> 0.4.2"}]}. diff --git a/src/awsv4.erl b/src/awsv4.erl index 3d7bab7..cd9bd04 100644 --- a/src/awsv4.erl +++ b/src/awsv4.erl @@ -7,22 +7,46 @@ %%% Created : 1 Feb 2017 by Mike Watters -module(awsv4). --export([headers/2, headers/3, headers/11, canonical_query/1, long_term_credentials/2, +-export([headers/2, headers/3, canonical_query/1, long_term_credentials/2, credentials_from_plist/1, isonow/0, isonow/1]). +-export([credentials_from_tuple/1, is_aws_credentials/1, credentials_expiration/1]). --include("erliam.hrl"). +-record(credentials, + {expiration :: undefined | erliam:iso_datetime(), + security_token :: undefined | string(), % required when using temporary credentials + secret_access_key :: string(), + access_key_id :: string()}). --type credentials() :: #credentials{}. - --export_type([credentials/0]). +-opaque credentials() :: #credentials{}. +-type datetime() :: string(). % "YYYYMMDDTHHMMSSZ" -type pairs() :: #{string() => iodata()} | [{string(), iodata()}]. +-export_type([credentials/0, datetime/0, pairs/0]). + %%%% API +%% @doc Function used to "convert" a tuple returned from an ets:lookup/2 call +%% into a valid opaque term of type credentials/0. +-spec credentials_from_tuple(tuple()) -> credentials(). +credentials_from_tuple(#credentials{} = Credentials) -> + Credentials. + +-spec is_aws_credentials(any()) -> boolean(). +is_aws_credentials(#credentials{}) -> + true; +is_aws_credentials(_) -> + false. + +-spec credentials_expiration(credentials()) -> undefined | erliam:iso_datetime(). +credentials_expiration(#credentials{expiration = ExpTime}) -> + ExpTime. + +-spec headers(credentials(), map()) -> [{string(), iodata()}]. headers(Credentials, Parameters) -> headers(Credentials, Parameters, <<>>). +-spec headers(credentials(), map(), undefined | binary()) -> [{string(), iodata()}]. headers(Credentials, Parameters, undefined) -> headers(Credentials, Parameters, <<>>); headers(Credentials, @@ -38,31 +62,19 @@ headers(Credentials, host => join($., [Service, Region, "amazonaws.com"])}, headers_(Credentials, maps:merge(Defaults, Parameters), RequestPayload). --spec headers(Credentials :: credentials(), - Service :: string(), - Region :: string(), - Host :: string(), - AwsDate :: undefined | aws_datetime(), - TargetAPI :: string() | undefined, - Method :: string(), - Path :: string(), - QueryParams :: pairs(), - ExtraSignedHeaders :: pairs(), - RequestPayload :: binary()) -> - [{HeaderName :: string(), HeaderValue :: iodata()}]. -headers(#credentials{secret_access_key = SecretAccessKey, - access_key_id = AccessKeyId, - security_token = SecurityToken}, - Service, - Region, - Host, - AwsDate, - TargetAPI, - Method, - Path, - QueryParams, - ExtraSignedHeaders, - RequestPayload) -> +headers_(#credentials{secret_access_key = SecretAccessKey, + access_key_id = AccessKeyId, + security_token = SecurityToken}, + #{service := Service, + region := Region, + host := Host, + target_api := TargetAPI, + aws_date := AwsDate, + method := Method, + path := Path, + query_params := QueryParams, + signed_headers := ExtraSignedHeaders}, + RequestPayload) -> ActualAwsDate = case AwsDate of undefined -> @@ -133,7 +145,7 @@ headers(#credentials{secret_access_key = SecretAccessKey, {"x-amz-content-sha256", PayloadHash} | Headers]. --spec isonow(calendar:datetime()) -> aws_datetime(). +-spec isonow(calendar:datetime()) -> datetime(). isonow({{Year, Month, Day}, {Hour, Min, Sec}}) -> lists:flatten( io_lib:format("~4.10.0B~2.10.0B~2.10.0BT~2.10.0B~2.10.0B~2.10.0BZ", @@ -165,29 +177,6 @@ credentials_from_plist(Plist) -> %%%% INTERNAL FUNCTIONS -headers_(Credentials, - #{service := Service, - region := Region, - host := Host, - target_api := TargetAPI, - aws_date := AwsDate, - method := Method, - path := Path, - query_params := QueryParams, - signed_headers := ExtraSignedHeaders}, - RequestPayload) -> - headers(Credentials, - Service, - Region, - Host, - AwsDate, - TargetAPI, - Method, - Path, - QueryParams, - ExtraSignedHeaders, - RequestPayload). - canonical_path(Path) -> %% note: should remove redundant and relative path components, except leave empty path %% components for s3. @@ -255,24 +244,27 @@ join_test() -> flattened(KVs) -> [{K, lists:flatten(V)} || {K, V} <- KVs]. -flattened_headers(Args) -> - flattened(apply(?MODULE, headers, Args)). +flattened_headers(Credentials, Parameters) -> + flattened(headers(Credentials, Parameters)). + +flattened_headers(Credentials, Parameters, RequestPayload) -> + flattened(headers(Credentials, Parameters, RequestPayload)). basic_headers_test() -> Actual = - flattened_headers([#credentials{secret_access_key = "secretkey", - access_key_id = "accesskey", - security_token = "securitytoken"}, - "kinesis", - "us-east-1", - "kinesis.us-east-1.amazonaws.com", - "20140629T022822Z", - "Kinesis_20131202.ListStreams", - "POST", - "/", - [], - #{}, - "something"]), + flattened_headers(#credentials{secret_access_key = "secretkey", + access_key_id = "accesskey", + security_token = "securitytoken"}, + #{service => "kinesis", + region => "us-east-1", + host => "kinesis.us-east-1.amazonaws.com", + target_api => "Kinesis_20131202.ListStreams", + aws_date => "20140629T022822Z", + method => "POST", + path => "/", + query_params => [], + signed_headers => #{}}, + "something"), Expected = flattened([{"authorization", ["AWS4-HMAC-SHA256 Credential=accesskey/20140629/us-east-1/kinesis/aws" @@ -291,14 +283,14 @@ basic_headers_test() -> aws4_example1_test() -> %% get-vanilla-query-order-key-case from AWSv4 test suite Actual = - flattened_headers([#credentials{secret_access_key = - "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", - access_key_id = "AKIDEXAMPLE"}, - #{aws_date => "20150830T123600Z", - service => "service", - region => "us-east-1", - host => "example.amazonaws.com", - query_params => #{"Param2" => "value2", "Param1" => "value1"}}]), + flattened_headers(#credentials{secret_access_key = + "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", + access_key_id = "AKIDEXAMPLE"}, + #{aws_date => "20150830T123600Z", + service => "service", + region => "us-east-1", + host => "example.amazonaws.com", + query_params => #{"Param2" => "value2", "Param1" => "value1"}}), Expected = flattened([{"authorization", ["AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/a" @@ -315,15 +307,15 @@ aws4_example1_test() -> aws4_example2_test() -> %% post-vanilla-query from AWSv4 test suite Actual = - flattened_headers([#credentials{secret_access_key = - "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", - access_key_id = "AKIDEXAMPLE"}, - #{aws_date => "20150830T123600Z", - service => "service", - region => "us-east-1", - host => "example.amazonaws.com", - method => "POST", - query_params => #{"Param1" => "value1"}}]), + flattened_headers(#credentials{secret_access_key = + "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", + access_key_id = "AKIDEXAMPLE"}, + #{aws_date => "20150830T123600Z", + service => "service", + region => "us-east-1", + host => "example.amazonaws.com", + method => "POST", + query_params => #{"Param1" => "value1"}}), Expected = flattened([{"authorization", ["AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/a" @@ -340,16 +332,16 @@ aws4_example2_test() -> aws4_example3_test() -> %% get-unreserved from AWSv4 test suite Actual = - flattened_headers([#credentials{secret_access_key = - "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", - access_key_id = "AKIDEXAMPLE"}, - #{aws_date => "20150830T123600Z", - service => "service", - region => "us-east-1", - host => "example.amazonaws.com", - path => - "/-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" - ++ "abcdefghijklmnopqrstuvwxyz"}]), + flattened_headers(#credentials{secret_access_key = + "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", + access_key_id = "AKIDEXAMPLE"}, + #{aws_date => "20150830T123600Z", + service => "service", + region => "us-east-1", + host => "example.amazonaws.com", + path => + "/-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" + ++ "abcdefghijklmnopqrstuvwxyz"}), Expected = flattened([{"authorization", ["AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/a" diff --git a/src/erliam.erl b/src/erliam.erl index 3041331..050e83a 100644 --- a/src/erliam.erl +++ b/src/erliam.erl @@ -11,6 +11,10 @@ %%% Created : 7 Apr 2017 by Mike Watters -module(erliam). +-type iso_datetime() :: string(). % "YYYY-MM-DDTHH:MM:SSZ" + +-export_type([iso_datetime/0]). + -export([httpc_profile/0, get_session_token/0, credentials/0, invalidate/0]). %% Return the current cached credentials (crash if none are cached or credential refresher diff --git a/src/erliam_srv.erl b/src/erliam_srv.erl index c63ef09..610cd6f 100644 --- a/src/erliam_srv.erl +++ b/src/erliam_srv.erl @@ -13,6 +13,9 @@ -format #{inline_items => {when_over, 19}}. +%% @todo Remove once https://github.com/inaka/elvis_core/issues/308 is dealt with +-elvis([{elvis_style, export_used_types, disable}]). + %% API -export([start_link/0, current/0, invalidate/0]). %% gen_server callbacks @@ -22,8 +25,6 @@ -define(TAB, ?MODULE). -define(MIN_LIFETIME, erliam_config:g(credential_min_lifetime, 120)). --include("erliam.hrl"). - -record(state, {}). -type state() :: #state{}. @@ -33,9 +34,10 @@ start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, noargs, []). +-spec current() -> awsv4:credentials(). current() -> [Credentials] = ets:lookup(?TAB, credentials), - Credentials. + awsv4:credentials_from_tuple(Credentials). invalidate() -> gen_server:call(?SERVER, invalidate). @@ -81,7 +83,7 @@ maybe_update_credentials() -> case ets:lookup(?TAB, credentials) of [Credentials] -> MinLifetime = ?MIN_LIFETIME, - case remaining_lifetime(Credentials) of + case remaining_lifetime(awsv4:credentials_from_tuple(Credentials)) of N when N =< MinLifetime -> update_credentials(); _ -> @@ -92,19 +94,21 @@ maybe_update_credentials() -> end. update_credentials() -> - case erliam:get_session_token() of - #credentials{} = Credentials -> - ets:insert(?TAB, Credentials), + TokenOrError = erliam:get_session_token(), + case awsv4:is_aws_credentials(TokenOrError) of + true -> + ets:insert(?TAB, TokenOrError), ok; - Error -> - error_logger:error_msg("failed to obtain session token: ~p", [Error]), - Error + false -> + error_logger:error_msg("Failed to obtain session token: ~p", [TokenOrError]), + TokenOrError end. -remaining_lifetime(#credentials{expiration = ExpTime}) -> +remaining_lifetime(Credentials) -> Now = calendar:universal_time(), + ExpTime = parse_exptime(awsv4:credentials_expiration(Credentials)), max(0, - calendar:datetime_to_gregorian_seconds(parse_exptime(ExpTime)) + calendar:datetime_to_gregorian_seconds(ExpTime) - calendar:datetime_to_gregorian_seconds(Now)). parse_exptime([Y1, Y2, Y3, Y4, $-, Mon1, Mon2, $-, D1, D2, $T, H1, H2, $:, Min1, Min2, $:, diff --git a/src/erliam_sts.erl b/src/erliam_sts.erl index b20ceae..1118ebc 100644 --- a/src/erliam_sts.erl +++ b/src/erliam_sts.erl @@ -15,7 +15,7 @@ -define(STS_TIMEOUT, 30000). %%%% API - +-spec get_session_token(awsv4:credentials()) -> {error, term()} | awsv4:credentials(). get_session_token(Credentials) -> Query = #{"Action" => "GetSessionToken", "Version" => "2011-06-15"}, Host = ?STS_HOST,