Skip to content

Commit 0c8337c

Browse files
Merge pull request #14709 from rabbitmq/mergify/bp/v4.2.x/pr-14664
Comprehensive tests for the `rabbitmq_aws` Cuttlefish schema (backport #14664)
2 parents 056efd2 + 5f3878f commit 0c8337c

File tree

11 files changed

+157
-87
lines changed

11 files changed

+157
-87
lines changed

deps/rabbitmq_aws/.editorconfig

Lines changed: 0 additions & 15 deletions
This file was deleted.

deps/rabbitmq_aws/Makefile

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ define PROJECT_ENV
77
[]
88
endef
99

10+
BUILD_DEPS = rabbit
11+
TEST_DEPS = meck rabbitmq_ct_helpers rabbitmq_ct_client_helpers
1012
LOCAL_DEPS = crypto inets ssl xmerl public_key
11-
BUILD_DEPS = rabbit_common
12-
TEST_DEPS = meck rabbit rabbitmq_ct_helpers rabbitmq_ct_client_helpers
13+
14+
PLT_APPS = rabbit
1315

1416
DEP_EARLY_PLUGINS = rabbit_common/mk/rabbitmq-early-plugin.mk
1517
DEP_PLUGINS = rabbit_common/mk/rabbitmq-plugin.mk

deps/rabbitmq_aws/README.md

Lines changed: 29 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ A fork of [gmr/httpc-aws](https://github.com/gmr/httpc-aws) for use in building
55
## Supported Erlang Versions
66

77
[Same as RabbitMQ](http://www.rabbitmq.com/which-erlang.html)
8-
8+
99
## Configuration
1010

1111
Configuration for *rabbitmq-aws* is can be provided in multiple ways. It is designed
1212
to behave similarly to the [AWS Command Line Interface](http://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html)
1313
with respect to providing region and configuration information. Additionally it
14-
has two methods, ``rabbitmq_aws:set_region/1`` and ``rabbitmq_aws:set_credentials/2``
14+
has two methods, `rabbitmq_aws:set_region/1` and `rabbitmq_aws:set_credentials/2`
1515
to allow for application specific configuration, bypassing the automatic configuration
1616
behavior.
1717

@@ -40,36 +40,36 @@ and [adds defenses against additional vulnerabilities](https://aws.amazon.com/bl
4040
AWS recommends adopting IMDSv2 and disabling IMDSv1 [by configuring the Instance Metadata Service on the EC2 instances](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html).
4141

4242
By default *rabbitmq-aws* will attempt to use IMDSv2 first and will fallback to use IMDSv1 if calls to IMDSv2 fail. This behavior can be overridden
43-
by setting the ``aws.prefer_imdsv2`` setting to ``false``.
43+
by setting the `aws.prefer_imdsv2` setting to `false`.
4444

4545
### Environment Variables
4646

47-
As with the AWS CLI, the following environment variables can be used to provide
47+
As with the AWS CLI, the following environment variables can be used to provide
4848
configuration or to impact configuration behavior:
4949

50-
- ``AWS_DEFAULT_PROFILE``
51-
- ``AWS_DEFAULT_REGION``
52-
- ``AWS_CONFIG_FILE``
53-
- ``AWS_SHARED_CREDENTIALS_FILE``
54-
- ``AWS_ACCESS_KEY_ID``
55-
- ``AWS_SECRET_ACCESS_KEY``
56-
50+
- `AWS_DEFAULT_PROFILE`
51+
- `AWS_DEFAULT_REGION`
52+
- `AWS_CONFIG_FILE`
53+
- `AWS_SHARED_CREDENTIALS_FILE`
54+
- `AWS_ACCESS_KEY_ID`
55+
- `AWS_SECRET_ACCESS_KEY`
56+
5757
## API Functions
58-
59-
Method | Description
60-
---------------------------------------|--------------------------------------------------------------------------------------------
61-
``rabbitmq_aws:set_region/1`` | Manually specify the AWS region to make requests to.
62-
``rabbitmq_aws:set_credentials/2`` | Manually specify the request credentials to use.
63-
``rabbitmq_aws:refresh_credentials/0`` | Refresh the credentials from the environment, filesystem, or EC2 Instance Metadata Service.
64-
``rabbitmq_aws:ensure_imdsv2_token_valid/0`` | Make sure EC2 IMDSv2 token is active and valid.
65-
``rabbitmq_aws:api_get_request/2`` | Perform an AWS service API request.
66-
``rabbitmq_aws:get/2`` | Perform a GET request to the API specifying the service and request path.
67-
``rabbitmq_aws:get/3`` | Perform a GET request specifying the service, path, and headers.
68-
``rabbitmq_aws:post/4`` | Perform a POST request specifying the service, path, headers, and body.
69-
``rabbitmq_aws:request/5`` | Perform a request specifying the service, method, path, headers, and body.
70-
``rabbitmq_aws:request/6`` | Perform a request specifying the service, method, path, headers, body, and ``httpc:http_options().``
71-
``rabbitmq_aws:request/7`` | Perform a request specifying the service, method, path, headers, body, ``httpc:http_options()``, and override the API endpoint.
72-
58+
59+
Method | Description
60+
-------------------------------------------|--------------------------------------------------------------------------------------------
61+
`rabbitmq_aws:set_region/1` | Manually specify the AWS region to make requests to.
62+
`rabbitmq_aws:set_credentials/2` | Manually specify the request credentials to use.
63+
`rabbitmq_aws:refresh_credentials/0` | Refresh the credentials from the environment, filesystem, or EC2 Instance Metadata Service.
64+
`rabbitmq_aws:ensure_imdsv2_token_valid/0` | Make sure EC2 IMDSv2 token is active and valid.
65+
`rabbitmq_aws:get/2` | Perform a GET request to the API specifying the service and request path.
66+
`rabbitmq_aws:get/3` | Perform a GET request specifying the service, path, and headers.
67+
`rabbitmq_aws:post/4` | Perform a POST request specifying the service, path, headers, and body.
68+
`rabbitmq_aws:request/5` | Perform a request specifying the service, method, path, headers, and body.
69+
`rabbitmq_aws:request/6` | Perform a request specifying the service, method, path, headers, body, and `httpc:http_options().`
70+
`rabbitmq_aws:request/7` | Perform a request specifying the service, method, path, headers, body, `httpc:http_options()`, and override the API endpoint.
71+
`rabbitmq_aws:api_get_request/2` | Perform an AWS service API request with retries.
72+
`rabbitmq_aws:api_post_request/2` | Perform an AWS service API request with retries.
7373

7474
## Example Usage
7575

@@ -80,8 +80,7 @@ you're using the EC2 Instance Metadata Service for credentials:
8080
application:start(rabbitmq_aws).
8181
{ok, {Headers, Response}} = rabbitmq_aws:get("ec2","/?Action=DescribeTags&Version=2015-10-01").
8282
```
83-
84-
To configure credentials, invoke ``rabbitmq_aws:set_credentials/2``:
83+
To configure credentials, invoke `rabbitmq_aws:set_credentials/2`:
8584

8685
```erlang
8786
application:start(rabbitmq_aws).
@@ -90,8 +89,8 @@ rabbitmq_aws:set_credentials("AKIDEXAMPLE", "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMP
9089

9190
RequestHeaders = [{"Content-Type", "application/x-amz-json-1.0"},
9291
{"X-Amz-Target", "DynamoDB_20120810.ListTables"}],
93-
94-
{ok, {Headers, Response}} = rabbitmq_aws:post("dynamodb", "/",
92+
93+
{ok, {Headers, Response}} = rabbitmq_aws:post("dynamodb", "/",
9594
"{\"Limit\": 20}",
9695
RequestHeaders).
9796
```

deps/rabbitmq_aws/priv/schema/rabbitmq_aws.schema

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,5 @@
1515
%% When false, EC2 IMDSv1 will be used first and no attempt will be made to use EC2 IMDSv2.
1616
%% See https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html.
1717

18-
{mapping, "aws.prefer_imdsv2", "rabbit.aws_prefer_imdsv2",
18+
{mapping, "aws.prefer_imdsv2", "rabbitmq_aws.aws_prefer_imdsv2",
1919
[{datatype, {enum, [true, false]}}]}.

deps/rabbitmq_aws/src/rabbitmq_aws.erl

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@
1515
refresh_credentials/0,
1616
request/5, request/6, request/7,
1717
set_credentials/2,
18+
set_credentials/3,
1819
has_credentials/0,
1920
set_region/1,
2021
ensure_imdsv2_token_valid/0,
21-
api_get_request/2
22+
api_get_request/2,
23+
api_post_request/4
2224
]).
2325

2426
%% gen-server exports
@@ -158,6 +160,12 @@ set_credentials(NewState) ->
158160
set_credentials(AccessKey, SecretAccessKey) ->
159161
gen_server:call(rabbitmq_aws, {set_credentials, AccessKey, SecretAccessKey}).
160162

163+
-spec set_credentials(access_key(), secret_access_key(), security_token()) -> ok.
164+
%% @doc Manually set the access credentials with session token for requests.
165+
%% @end
166+
set_credentials(AccessKey, SecretAccessKey, SessionToken) ->
167+
gen_server:call(rabbitmq_aws, {set_credentials, AccessKey, SecretAccessKey, SessionToken}).
168+
161169
-spec set_region(Region :: string()) -> ok.
162170
%% @doc Manually set the AWS region to perform API requests to.
163171
%% @end
@@ -224,6 +232,14 @@ handle_msg({set_credentials, AccessKey, SecretAccessKey}, State) ->
224232
expiration = undefined,
225233
error = undefined
226234
}};
235+
handle_msg({set_credentials, AccessKey, SecretAccessKey, SessionToken}, State) ->
236+
{reply, ok, State#state{
237+
access_key = AccessKey,
238+
secret_access_key = SecretAccessKey,
239+
security_token = SessionToken,
240+
expiration = undefined,
241+
error = undefined
242+
}};
227243
handle_msg({set_credentials, NewState}, State) ->
228244
{reply, ok, State#state{
229245
access_key = NewState#state.access_key,
@@ -607,8 +623,10 @@ ensure_credentials_valid() ->
607623
case has_credentials(State) of
608624
true ->
609625
case expired_credentials(State#state.expiration) of
610-
true -> refresh_credentials(State);
611-
_ -> ok
626+
true ->
627+
refresh_credentials(State);
628+
_ ->
629+
ok
612630
end;
613631
_ ->
614632
refresh_credentials(State)
@@ -618,19 +636,42 @@ ensure_credentials_valid() ->
618636
%% @doc Invoke an API call to an AWS service.
619637
%% @end
620638
api_get_request(Service, Path) ->
621-
?LOG_DEBUG("Invoking AWS request {Service: ~tp; Path: ~tp}...", [Service, Path]),
622-
api_get_request_with_retries(Service, Path, ?MAX_RETRIES, ?LINEAR_BACK_OFF_MILLIS).
639+
?LOG_DEBUG("invoking AWS get request {Service: ~tp; Path: ~tp}...", [Service, Path]),
640+
api_request_with_retries(Service, get, Path, "", [],
641+
?MAX_RETRIES, ?LINEAR_BACK_OFF_MILLIS).
623642

624-
-spec api_get_request_with_retries(string(), path(), integer(), integer()) ->
643+
-spec api_post_request(
644+
Service :: string(),
645+
Path :: path(),
646+
Body :: body(),
647+
Headers :: headers()
648+
) -> result().
649+
%% @doc Perform a HTTP Post request to the AWS API for the specified service. The
650+
%% response will automatically be decoded if it is either in JSON or XML
651+
%% format.
652+
%% @end
653+
api_post_request(Service, Path, Body, Headers) ->
654+
?LOG_DEBUG("invoking AWS post request {Service: ~tp; Path: ~tp}...", [Service, Path]),
655+
api_request_with_retries(Service, post, Path, Body, Headers,
656+
?MAX_RETRIES, ?LINEAR_BACK_OFF_MILLIS).
657+
658+
-spec api_request_with_retries(
659+
Service :: string(),
660+
Method :: method(),
661+
Path :: path(),
662+
Body :: body(),
663+
Headers :: headers(),
664+
Retries :: integer(),
665+
WaitTime :: integer()) ->
625666
{'ok', list()} | {'error', term()}.
626667
%% @doc Invoke an API call to an AWS service with retries.
627668
%% @end
628-
api_get_request_with_retries(_, _, 0, _) ->
629-
?LOG_WARNING("Request to AWS service has failed after ~b retries", [?MAX_RETRIES]),
669+
api_request_with_retries(_, _, _, _, _, 0, _) ->
670+
?LOG_ERROR("Request to AWS service has failed after ~b retries", [?MAX_RETRIES]),
630671
{error, "AWS service is unavailable"};
631-
api_get_request_with_retries(Service, Path, Retries, WaitTimeBetweenRetries) ->
632-
ensure_credentials_valid(),
633-
case get(Service, Path) of
672+
api_request_with_retries(Service, Method, Path, Body, Headers, Retries, WaitTime) ->
673+
ok = ensure_credentials_valid(),
674+
case request(Service, Method, Path, Body, Headers) of
634675
{ok, {_Headers, Payload}} ->
635676
?LOG_DEBUG("AWS request: ~ts~nResponse: ~tp", [Path, Payload]),
636677
{ok, Payload};
@@ -645,6 +686,6 @@ api_get_request_with_retries(Service, Path, Retries, WaitTimeBetweenRetries) ->
645686
ok
646687
end,
647688
?LOG_WARNING("Will retry AWS request, remaining retries: ~b", [Retries]),
648-
timer:sleep(WaitTimeBetweenRetries),
649-
api_get_request_with_retries(Service, Path, Retries - 1, WaitTimeBetweenRetries)
689+
timer:sleep(WaitTime),
690+
api_request_with_retries(Service, Method, Path, Body, Headers, Retries - 1, WaitTime)
650691
end.

deps/rabbitmq_aws/src/rabbitmq_aws_config.erl

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,8 @@ credentials(Profile) ->
134134
lookup_credentials(
135135
Profile,
136136
os:getenv("AWS_ACCESS_KEY_ID"),
137-
os:getenv("AWS_SECRET_ACCESS_KEY")
137+
os:getenv("AWS_SECRET_ACCESS_KEY"),
138+
os:getenv("AWS_SESSION_TOKEN")
138139
).
139140

140141
-spec region() -> {ok, string()}.
@@ -452,43 +453,53 @@ instance_id_url() ->
452453
-spec lookup_credentials(
453454
Profile :: string(),
454455
AccessKey :: string() | false,
455-
SecretKey :: string() | false
456+
SecretKey :: string() | false,
457+
SessionToken :: string() | false
456458
) ->
457459
security_credentials().
458460
%% @doc Return the access key and secret access key if they are set in
459461
%% environment variables, otherwise lookup the credentials from the config
460462
%% file for the specified profile.
461463
%% @end
462-
lookup_credentials(Profile, false, _) ->
464+
lookup_credentials(Profile, false, _, _) ->
463465
lookup_credentials_from_config(
464466
Profile,
465467
value(Profile, aws_access_key_id),
466-
value(Profile, aws_secret_access_key)
468+
value(Profile, aws_secret_access_key),
469+
value(Profile, aws_session_token)
467470
);
468-
lookup_credentials(Profile, _, false) ->
471+
lookup_credentials(Profile, _, false, _) ->
469472
lookup_credentials_from_config(
470473
Profile,
471474
value(Profile, aws_access_key_id),
472-
value(Profile, aws_secret_access_key)
475+
value(Profile, aws_secret_access_key),
476+
value(Profile, aws_session_token)
473477
);
474-
lookup_credentials(_, AccessKey, SecretKey) ->
475-
{ok, AccessKey, SecretKey, undefined, undefined}.
478+
lookup_credentials(_, AccessKey, SecretKey, SessionToken) ->
479+
case SessionToken of
480+
false -> {ok, AccessKey, SecretKey, undefined, undefined};
481+
SessionToken -> {ok, AccessKey, SecretKey, undefined, SessionToken}
482+
end.
476483

477484
-spec lookup_credentials_from_config(
478485
Profile :: string(),
479486
access_key() | {error, Reason :: atom()},
480-
secret_access_key() | {error, Reason :: atom()}
487+
secret_access_key() | {error, Reason :: atom()},
488+
security_token() | {error, Reason :: atom()}
481489
) ->
482490
security_credentials().
483491
%% @doc Return the access key and secret access key if they are set in
484492
%% for the specified profile in the config file, if it exists. If it does
485493
%% not exist or the profile is not set or the values are not set in the
486494
%% profile, look up the values in the shared credentials file
487495
%% @end
488-
lookup_credentials_from_config(Profile, {error, _}, _) ->
496+
lookup_credentials_from_config(Profile, {error, _}, _, _) ->
489497
lookup_credentials_from_file(Profile, credentials_file_data());
490-
lookup_credentials_from_config(_, AccessKey, SecretKey) ->
491-
{ok, AccessKey, SecretKey, undefined, undefined}.
498+
lookup_credentials_from_config(_, AccessKey, SecretKey, SessionToken) ->
499+
case SessionToken of
500+
{error, _} -> {ok, AccessKey, SecretKey, undefined, undefined};
501+
SessionToken -> {ok, AccessKey, SecretKey, undefined, SessionToken}
502+
end.
492503

493504
-spec lookup_credentials_from_file(
494505
Profile :: string(),
@@ -518,22 +529,24 @@ lookup_credentials_from_section(undefined) ->
518529
lookup_credentials_from_section(Credentials) ->
519530
AccessKey = proplists:get_value(aws_access_key_id, Credentials, undefined),
520531
SecretKey = proplists:get_value(aws_secret_access_key, Credentials, undefined),
521-
lookup_credentials_from_proplist(AccessKey, SecretKey).
532+
SessionToken = proplists:get_value(aws_session_token, Credentials, undefined),
533+
lookup_credentials_from_proplist(AccessKey, SecretKey, SessionToken).
522534

523535
-spec lookup_credentials_from_proplist(
524536
AccessKey :: access_key(),
525-
SecretAccessKey :: secret_access_key()
537+
SecretAccessKey :: secret_access_key(),
538+
SessionToken :: security_token()
526539
) ->
527540
security_credentials().
528541
%% @doc Process the contents of the Credentials proplists checking if the
529542
%% access key and secret access key are both set.
530543
%% @end
531-
lookup_credentials_from_proplist(undefined, _) ->
544+
lookup_credentials_from_proplist(undefined, _, _) ->
532545
lookup_credentials_from_instance_metadata();
533-
lookup_credentials_from_proplist(_, undefined) ->
546+
lookup_credentials_from_proplist(_, undefined, _) ->
534547
lookup_credentials_from_instance_metadata();
535-
lookup_credentials_from_proplist(AccessKey, SecretKey) ->
536-
{ok, AccessKey, SecretKey, undefined, undefined}.
548+
lookup_credentials_from_proplist(AccessKey, SecretKey, SessionToken) ->
549+
{ok, AccessKey, SecretKey, undefined, SessionToken}.
537550

538551
-spec lookup_credentials_from_instance_metadata() ->
539552
security_credentials().
@@ -773,7 +786,7 @@ load_imdsv2_token() ->
773786
%% @doc Return headers used for instance metadata service requests.
774787
%% @end
775788
instance_metadata_request_headers() ->
776-
case application:get_env(rabbit, aws_prefer_imdsv2) of
789+
case application:get_env(rabbitmq_aws, aws_prefer_imdsv2) of
777790
{ok, false} ->
778791
[];
779792
%% undefined or {ok, true}

deps/rabbitmq_aws/src/rabbitmq_aws_sign.erl

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ headers(Request) ->
3030
{_, Host, _} = URI#uri.authority,
3131
Headers = append_headers(
3232
RequestTimestamp,
33-
length(Request#request.body),
33+
get_content_length(Request),
3434
PayloadHash,
3535
Host,
3636
Request#request.security_token,
@@ -313,3 +313,9 @@ string_to_sign(RequestTimestamp, RequestDate, Region, Service, RequestHash) ->
313313
%% @end
314314
sort_headers(Headers) ->
315315
lists:sort(fun({A, _}, {B, _}) -> string:to_lower(A) =< string:to_lower(B) end, Headers).
316+
317+
-spec get_content_length(Request :: #request{}) -> non_neg_integer().
318+
get_content_length(#request{body = Body}) when is_binary(Body) ->
319+
byte_size(Body);
320+
get_content_length(#request{body = Body}) when is_list(Body) ->
321+
length(Body).

0 commit comments

Comments
 (0)