From f536f077fb5695b1398761f49a32916ca307e5cc Mon Sep 17 00:00:00 2001 From: Thomas Farr Date: Thu, 19 Jan 2023 16:45:55 +1300 Subject: [PATCH] Add test to validate service name is passed through signing Signed-off-by: Thomas Farr --- .../AwsSigV4HttpClientHandler.cs | 6 +- .../AwsSigV4HttpConnection.cs | 27 +++++--- .../AwsSigV4HttpConnectionTests.cs | 69 +++++++++++++++++++ .../Utils/FixedDateTimeProvider.cs | 20 ++++++ .../Utils/HttpRequestMessageAssertions.cs | 17 +++++ .../Utils/MockHttpMessageHandler.cs | 24 +++++++ .../Utils/TestableAwsSigV4HttpConnection.cs | 25 +++++++ 7 files changed, 177 insertions(+), 11 deletions(-) create mode 100644 tests/Tests.Auth.AwsSigV4/AwsSigV4HttpConnectionTests.cs create mode 100644 tests/Tests.Auth.AwsSigV4/Utils/FixedDateTimeProvider.cs create mode 100644 tests/Tests.Auth.AwsSigV4/Utils/HttpRequestMessageAssertions.cs create mode 100644 tests/Tests.Auth.AwsSigV4/Utils/MockHttpMessageHandler.cs create mode 100644 tests/Tests.Auth.AwsSigV4/Utils/TestableAwsSigV4HttpConnection.cs diff --git a/src/OpenSearch.Net.Auth.AwsSigV4/AwsSigV4HttpClientHandler.cs b/src/OpenSearch.Net.Auth.AwsSigV4/AwsSigV4HttpClientHandler.cs index d9b75dfd80..a61e461333 100644 --- a/src/OpenSearch.Net.Auth.AwsSigV4/AwsSigV4HttpClientHandler.cs +++ b/src/OpenSearch.Net.Auth.AwsSigV4/AwsSigV4HttpClientHandler.cs @@ -21,20 +21,22 @@ internal class AwsSigV4HttpClientHandler : DelegatingHandler private readonly AWSCredentials _credentials; private readonly RegionEndpoint _region; private readonly string _service; + private readonly IDateTimeProvider _dateTimeProvider; - public AwsSigV4HttpClientHandler(AWSCredentials credentials, RegionEndpoint region, string service, HttpMessageHandler innerHandler) + public AwsSigV4HttpClientHandler(AWSCredentials credentials, RegionEndpoint region, string service, IDateTimeProvider dateTimeProvider, HttpMessageHandler innerHandler) : base(innerHandler) { _credentials = credentials ?? throw new ArgumentNullException(nameof(credentials)); _region = region ?? throw new ArgumentNullException(nameof(region)); _service = service ?? throw new ArgumentNullException(nameof(service)); + _dateTimeProvider = dateTimeProvider ?? throw new ArgumentNullException(nameof(dateTimeProvider)); } protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var credentials = await _credentials.GetCredentialsAsync().ConfigureAwait(false); - await AwsSigV4Util.SignRequest(request, credentials, _region, DateTime.UtcNow, _service).ConfigureAwait(false); + await AwsSigV4Util.SignRequest(request, credentials, _region, _dateTimeProvider.Now(), _service).ConfigureAwait(false); return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); } diff --git a/src/OpenSearch.Net.Auth.AwsSigV4/AwsSigV4HttpConnection.cs b/src/OpenSearch.Net.Auth.AwsSigV4/AwsSigV4HttpConnection.cs index 3d58ec41c4..8c690c3d2e 100644 --- a/src/OpenSearch.Net.Auth.AwsSigV4/AwsSigV4HttpConnection.cs +++ b/src/OpenSearch.Net.Auth.AwsSigV4/AwsSigV4HttpConnection.cs @@ -22,29 +22,33 @@ public class AwsSigV4HttpConnection : HttpConnection private readonly AWSCredentials _credentials; private readonly RegionEndpoint _region; private readonly string _service; + private readonly IDateTimeProvider _dateTimeProvider; /// /// Construct a new connection discovering both the credentials and region from the environment. /// /// The service code to use when signing, defaults to the service code for the Amazon OpenSearch Service ("es"). - /// - public AwsSigV4HttpConnection(string service = OpenSearchService) : this(null, null, service) { } + /// The date time proved to use, safe to pass null to use the default + /// + public AwsSigV4HttpConnection(string service = OpenSearchService, IDateTimeProvider dateTimeProvider = null) : this(null, null, service, dateTimeProvider) { } /// /// Construct a new connection configured with the specified credentials and using the region discovered from the environment. /// /// The credentials to use when signing. /// The service code to use when signing, defaults to the service code for the Amazon OpenSearch Service ("es"). - /// - public AwsSigV4HttpConnection(AWSCredentials credentials, string service = OpenSearchService) : this(credentials, null, service) { } + /// The date time proved to use, safe to pass null to use the default + /// + public AwsSigV4HttpConnection(AWSCredentials credentials, string service = OpenSearchService, IDateTimeProvider dateTimeProvider = null) : this(credentials, null, service, dateTimeProvider) { } /// /// Construct a new connection configured with a specified region and using credentials discovered from the environment. /// /// The region to use when signing. /// The service code to use when signing, defaults to the service code for the Amazon OpenSearch Service ("es"). - /// - public AwsSigV4HttpConnection(RegionEndpoint region, string service = OpenSearchService) : this(null, region, service) { } + /// The date time proved to use, safe to pass null to use the default + /// + public AwsSigV4HttpConnection(RegionEndpoint region, string service = OpenSearchService, IDateTimeProvider dateTimeProvider = null) : this(null, region, service, dateTimeProvider) { } /// /// Construct a new connection configured with the given credentials and region. @@ -52,31 +56,36 @@ public AwsSigV4HttpConnection(RegionEndpoint region, string service = OpenSearch /// The credentials to use when signing, or null to have them discovered automatically by the AWS SDK. /// The region to use when signing, or null to have it discovered automatically by the AWS SDK. /// The service code to use when signing, defaults to the service code for the Amazon OpenSearch Service ("es"). + /// The date time proved to use, safe to pass null to use the default /// Thrown if region is null and is unable to be automatically discovered by the AWS SDK. /// /// The same logic as the AWS SDK for .NET /// is used to automatically discover the credentials and region to use if not provided explicitly. /// - public AwsSigV4HttpConnection(AWSCredentials credentials, RegionEndpoint region, string service = OpenSearchService) + public AwsSigV4HttpConnection(AWSCredentials credentials, RegionEndpoint region, string service = OpenSearchService, IDateTimeProvider dateTimeProvider = null) { _credentials = credentials ?? FallbackCredentialsFactory.GetCredentials(); // FallbackCredentialsFactory throws in case of not finding credentials. _region = region ?? FallbackRegionFactory.GetRegionEndpoint() // FallbackRegionFactory can return null. ?? throw new ArgumentNullException(nameof(region), "A RegionEndpoint was not provided and was unable to be determined from the environment."); _service = service ?? OpenSearchService; + _dateTimeProvider = dateTimeProvider ?? DateTimeProvider.Default; } #if DOTNETCORE + protected virtual System.Net.Http.HttpMessageHandler InnerCreateHttpClientHandler(RequestData requestData) => + base.CreateHttpClientHandler(requestData); + protected override System.Net.Http.HttpMessageHandler CreateHttpClientHandler(RequestData requestData) => - new AwsSigV4HttpClientHandler(_credentials, _region, _service, base.CreateHttpClientHandler(requestData)); + new AwsSigV4HttpClientHandler(_credentials, _region, _service, _dateTimeProvider, InnerCreateHttpClientHandler(requestData)); #else protected override System.Net.HttpWebRequest CreateHttpWebRequest(RequestData requestData) { var request = base.CreateHttpWebRequest(requestData); - AwsSigV4Util.SignRequest(request, requestData, _credentials.GetCredentials(), _region, DateTime.UtcNow, _service); + AwsSigV4Util.SignRequest(request, requestData, _credentials.GetCredentials(), _region, _dateTimeProvider.Now(), _service); return request; } diff --git a/tests/Tests.Auth.AwsSigV4/AwsSigV4HttpConnectionTests.cs b/tests/Tests.Auth.AwsSigV4/AwsSigV4HttpConnectionTests.cs new file mode 100644 index 0000000000..f63fec215c --- /dev/null +++ b/tests/Tests.Auth.AwsSigV4/AwsSigV4HttpConnectionTests.cs @@ -0,0 +1,69 @@ +/* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +*/ + +using System; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Amazon; +using Amazon.Runtime; +using FluentAssertions; +using OpenSearch.Client; +using OpenSearch.OpenSearch.Xunit.XunitPlumbing; +using Tests.Auth.AwsSigV4.Utils; +using Xunit; + +namespace Tests.Auth.AwsSigV4; + +public class AwsSigV4HttpConnectionTests +{ + private static readonly BasicAWSCredentials TestCredentials = new("test-access-key", "test-secret-key"); + private static readonly RegionEndpoint TestRegion = RegionEndpoint.APSoutheast2; + private static readonly DateTime TestSigningTime = new(2023, 01, 13, 16, 08, 37, DateTimeKind.Utc); + + [TU] + [InlineData("es", "275d72b784a3183861ae13b4fcb486ff31a7a9dd7f2d6ebb780f89008aa689cc")] + [InlineData("aoss", "89fb38096ed2e9f4a2908d1e40c3e1b7ac68fc4ab2f723feddec61b4ada4607a")] + [InlineData("arbitrary", "9bdf7d1cf23d8ca4b41f84cd15aafbc7b665dd4985cf68258c9e7ac40311521b")] + public async Task SignsRequestCorrectly(string service, string expectedSignature) + { + var response = new HttpResponseMessage(HttpStatusCode.OK); + response.Content = new StringContent(@"{ + ""acknowledged"": true, + ""shards_acknowledged"": true, + ""index"": ""sample-index1"" +}", Encoding.UTF8, "application/json"); + + HttpRequestMessage sentRequest = null; + + var client = CreateClient(r => + { + sentRequest = r; + return response; + }, $"https://aaabbbcccddd111222333.ap-southeast-2.{service}.amazonaws.com", service); + + await client.Indices.CreateAsync("sample-index1", d => + d.Settings(s => + s.NumberOfShards(2).NumberOfReplicas(1)) + .Map(t => + t.Properties(p => + p.Number(n => + n.Name("age").Type(NumberType.Integer)))) + .Aliases(a => a.Alias("sample-alias1"))); + + sentRequest.ShouldHaveHeader("x-amz-date", "20230113T160837Z"); + sentRequest.ShouldHaveHeader("x-amz-content-sha256", "4c770eaed349122a28302ff73d34437cad600acda5a9dd373efc7da2910f8564"); + sentRequest.ShouldHaveHeader("Authorization", $"AWS4-HMAC-SHA256 Credential=test-access-key/20230113/ap-southeast-2/{service}/aws4_request, SignedHeaders=accept;content-type;host;opensearch-client-meta;x-amz-content-sha256;x-amz-date, Signature={expectedSignature}"); + } + + private static OpenSearchClient CreateClient(MockHttpMessageHandler.Handler handler, string uri, string service) => + new(new ConnectionSettings( + new Uri(uri), + new TestableAwsSigV4HttpConnection(TestCredentials, TestRegion, service, new FixedDateTimeProvider(TestSigningTime), handler) + )); +} diff --git a/tests/Tests.Auth.AwsSigV4/Utils/FixedDateTimeProvider.cs b/tests/Tests.Auth.AwsSigV4/Utils/FixedDateTimeProvider.cs new file mode 100644 index 0000000000..8f671c90d9 --- /dev/null +++ b/tests/Tests.Auth.AwsSigV4/Utils/FixedDateTimeProvider.cs @@ -0,0 +1,20 @@ +/* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +*/ + +using System; +using OpenSearch.Net; + +namespace Tests.Auth.AwsSigV4.Utils; + +internal class FixedDateTimeProvider : DateTimeProvider +{ + private readonly DateTime _now; + + public FixedDateTimeProvider(DateTime now) => _now = now; + + public override DateTime Now() => _now; +} diff --git a/tests/Tests.Auth.AwsSigV4/Utils/HttpRequestMessageAssertions.cs b/tests/Tests.Auth.AwsSigV4/Utils/HttpRequestMessageAssertions.cs new file mode 100644 index 0000000000..c6305d5524 --- /dev/null +++ b/tests/Tests.Auth.AwsSigV4/Utils/HttpRequestMessageAssertions.cs @@ -0,0 +1,17 @@ +/* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +*/ + +using System.Net.Http; +using FluentAssertions; + +namespace Tests.Auth.AwsSigV4.Utils; + +internal static class HttpRequestMessageAssertions +{ + public static void ShouldHaveHeader(this HttpRequestMessage request, string name, string value) => + request.Headers.GetValues(name).Should().BeEquivalentTo(value); +} diff --git a/tests/Tests.Auth.AwsSigV4/Utils/MockHttpMessageHandler.cs b/tests/Tests.Auth.AwsSigV4/Utils/MockHttpMessageHandler.cs new file mode 100644 index 0000000000..81199b740d --- /dev/null +++ b/tests/Tests.Auth.AwsSigV4/Utils/MockHttpMessageHandler.cs @@ -0,0 +1,24 @@ +/* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +*/ + +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Tests.Auth.AwsSigV4.Utils; + +internal class MockHttpMessageHandler : HttpMessageHandler +{ + public delegate HttpResponseMessage Handler(HttpRequestMessage request); + + private readonly Handler _handler; + + public MockHttpMessageHandler(Handler handler) => _handler = handler; + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => + Task.FromResult(_handler(request)); +} diff --git a/tests/Tests.Auth.AwsSigV4/Utils/TestableAwsSigV4HttpConnection.cs b/tests/Tests.Auth.AwsSigV4/Utils/TestableAwsSigV4HttpConnection.cs new file mode 100644 index 0000000000..d18e554a71 --- /dev/null +++ b/tests/Tests.Auth.AwsSigV4/Utils/TestableAwsSigV4HttpConnection.cs @@ -0,0 +1,25 @@ +/* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +*/ + +using System.Net.Http; +using Amazon; +using Amazon.Runtime; +using OpenSearch.Net; +using OpenSearch.Net.Auth.AwsSigV4; + +namespace Tests.Auth.AwsSigV4.Utils; + +internal class TestableAwsSigV4HttpConnection : AwsSigV4HttpConnection +{ + private readonly MockHttpMessageHandler _handler; + + public TestableAwsSigV4HttpConnection(AWSCredentials credentials, RegionEndpoint region, string service, IDateTimeProvider dateTimeProvider, MockHttpMessageHandler.Handler handler) + : base(credentials, region, service, dateTimeProvider) => + _handler = new MockHttpMessageHandler(handler); + + protected override HttpMessageHandler InnerCreateHttpClientHandler(RequestData requestData) => _handler; +}