Skip to content

Commit

Permalink
Add test to validate service name is passed through signing
Browse files Browse the repository at this point in the history
Signed-off-by: Thomas Farr <[email protected]>
  • Loading branch information
Xtansia committed Jan 19, 2023
1 parent 0b18d6b commit f536f07
Show file tree
Hide file tree
Showing 7 changed files with 177 additions and 11 deletions.
6 changes: 4 additions & 2 deletions src/OpenSearch.Net.Auth.AwsSigV4/AwsSigV4HttpClientHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<HttpResponseMessage> 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);
}
Expand Down
27 changes: 18 additions & 9 deletions src/OpenSearch.Net.Auth.AwsSigV4/AwsSigV4HttpConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,61 +22,70 @@ public class AwsSigV4HttpConnection : HttpConnection
private readonly AWSCredentials _credentials;
private readonly RegionEndpoint _region;
private readonly string _service;
private readonly IDateTimeProvider _dateTimeProvider;

/// <summary>
/// Construct a new connection discovering both the credentials and region from the environment.
/// </summary>
/// <param name="service">The service code to use when signing, defaults to the service code for the Amazon OpenSearch Service (<c>"es"</c>).</param>
/// <seealso cref="AwsSigV4HttpConnection(AWSCredentials, RegionEndpoint, string)"/>
public AwsSigV4HttpConnection(string service = OpenSearchService) : this(null, null, service) { }
/// <param name="dateTimeProvider">The date time proved to use, safe to pass null to use the default</param>
/// <seealso cref="AwsSigV4HttpConnection(AWSCredentials, RegionEndpoint, string, IDateTimeProvider)"/>
public AwsSigV4HttpConnection(string service = OpenSearchService, IDateTimeProvider dateTimeProvider = null) : this(null, null, service, dateTimeProvider) { }

/// <summary>
/// Construct a new connection configured with the specified credentials and using the region discovered from the environment.
/// </summary>
/// <param name="credentials">The credentials to use when signing.</param>
/// <param name="service">The service code to use when signing, defaults to the service code for the Amazon OpenSearch Service (<c>"es"</c>).</param>
/// <seealso cref="AwsSigV4HttpConnection(AWSCredentials, RegionEndpoint, string)"/>
public AwsSigV4HttpConnection(AWSCredentials credentials, string service = OpenSearchService) : this(credentials, null, service) { }
/// <param name="dateTimeProvider">The date time proved to use, safe to pass null to use the default</param>
/// <seealso cref="AwsSigV4HttpConnection(AWSCredentials, RegionEndpoint, string, IDateTimeProvider)"/>
public AwsSigV4HttpConnection(AWSCredentials credentials, string service = OpenSearchService, IDateTimeProvider dateTimeProvider = null) : this(credentials, null, service, dateTimeProvider) { }

/// <summary>
/// Construct a new connection configured with a specified region and using credentials discovered from the environment.
/// </summary>
/// <param name="region">The region to use when signing.</param>
/// <param name="service">The service code to use when signing, defaults to the service code for the Amazon OpenSearch Service (<c>"es"</c>).</param>
/// <seealso cref="AwsSigV4HttpConnection(AWSCredentials, RegionEndpoint, string)"/>
public AwsSigV4HttpConnection(RegionEndpoint region, string service = OpenSearchService) : this(null, region, service) { }
/// <param name="dateTimeProvider">The date time proved to use, safe to pass null to use the default</param>
/// <seealso cref="AwsSigV4HttpConnection(AWSCredentials, RegionEndpoint, string, IDateTimeProvider)"/>
public AwsSigV4HttpConnection(RegionEndpoint region, string service = OpenSearchService, IDateTimeProvider dateTimeProvider = null) : this(null, region, service, dateTimeProvider) { }

/// <summary>
/// Construct a new connection configured with the given credentials and region.
/// </summary>
/// <param name="credentials">The credentials to use when signing, or null to have them discovered automatically by the AWS SDK.</param>
/// <param name="region">The region to use when signing, or null to have it discovered automatically by the AWS SDK.</param>
/// <param name="service">The service code to use when signing, defaults to the service code for the Amazon OpenSearch Service (<c>"es"</c>).</param>
/// <param name="dateTimeProvider">The date time proved to use, safe to pass null to use the default</param>
/// <exception cref="ArgumentNullException">Thrown if region is null and is unable to be automatically discovered by the AWS SDK.</exception>
/// <remarks>
/// The same logic as the <a href="https://docs.aws.amazon.com/sdk-for-net/v3/developer-guide/creds-assign.html">AWS SDK for .NET</a>
/// is used to automatically discover the credentials and region to use if not provided explicitly.
/// </remarks>
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;
}

Expand Down
69 changes: 69 additions & 0 deletions tests/Tests.Auth.AwsSigV4/AwsSigV4HttpConnectionTests.cs
Original file line number Diff line number Diff line change
@@ -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)
));
}
20 changes: 20 additions & 0 deletions tests/Tests.Auth.AwsSigV4/Utils/FixedDateTimeProvider.cs
Original file line number Diff line number Diff line change
@@ -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;
}
17 changes: 17 additions & 0 deletions tests/Tests.Auth.AwsSigV4/Utils/HttpRequestMessageAssertions.cs
Original file line number Diff line number Diff line change
@@ -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);
}
24 changes: 24 additions & 0 deletions tests/Tests.Auth.AwsSigV4/Utils/MockHttpMessageHandler.cs
Original file line number Diff line number Diff line change
@@ -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<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) =>
Task.FromResult(_handler(request));
}
25 changes: 25 additions & 0 deletions tests/Tests.Auth.AwsSigV4/Utils/TestableAwsSigV4HttpConnection.cs
Original file line number Diff line number Diff line change
@@ -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;
}

0 comments on commit f536f07

Please sign in to comment.