Skip to content

Introduce [Try]AddAWSCredentials extension methods #3758

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 8 commits into
base: development
Choose a base branch
from
14 changes: 1 addition & 13 deletions extensions/src/AWSSDK.Extensions.NETCore.Setup/AWSOptions.cs
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@
* permissions and limitations under the License.
*/
using Amazon.Runtime;

using AWSSDK.Extensions.NETCore.Setup;
using Microsoft.Extensions.Logging;

namespace Amazon.Extensions.NETCore.Setup
@@ -92,18 +92,6 @@ internal set
/// </summary>
public LoggingSetting Logging { get; set; }

/// <summary>
/// Create a service client for the specified service interface using the options set in this instance.
/// For example if T is set to IAmazonS3 then the AmazonS3ServiceClient which implements IAmazonS3 is created
/// and returned.
/// </summary>
/// <typeparam name="T">The service interface that a service client will be created for.</typeparam>
/// <returns>The service client that implements the service interface.</returns>
public T CreateServiceClient<T>() where T : class, IAmazonService
{
return new ClientFactory<T>(this).CreateServiceClient((ILogger)null, this) as T;
}

/// <summary>
/// Container for logging settings of the SDK
/// </summary>
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Amazon.Extensions.NETCore.Setup;
using Amazon.Runtime;

namespace AWSSDK.Extensions.NETCore.Setup
{
/// <summary>
///
/// </summary>
public static class AWSOptionsExtensions
{
/// <summary>
/// Create a service client for the specified service interface using the options set in this instance.
/// For example if T is set to IAmazonS3 then the AmazonS3ServiceClient which implements IAmazonS3 is created
/// and returned.
/// </summary>
/// <typeparam name="T">The service interface that a service client will be created for.</typeparam>
/// <returns>The service client that implements the service interface.</returns>
public static T CreateServiceClient<T>(this AWSOptions options)
where T : class, IAmazonService
{
var credentials = new DefaultAWSCredentials(options, null);
var clientFactory = new ClientFactory<T>(options, credentials, null);

return clientFactory.CreateServiceClient() as T;
}
}
}
106 changes: 19 additions & 87 deletions extensions/src/AWSSDK.Extensions.NETCore.Setup/ClientFactory.cs
Original file line number Diff line number Diff line change
@@ -21,6 +21,7 @@
using Amazon.Runtime;
using Amazon.Runtime.CredentialManagement;
using Amazon.Runtime.Credentials.Internal;
using AWSSDK.Extensions.NETCore.Setup;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -36,68 +37,45 @@ internal class ClientFactory<T>
private static readonly Type[] EMPTY_TYPES = Array.Empty<Type>();
private static readonly object[] EMPTY_PARAMETERS = Array.Empty<object>();

private AWSOptions _awsOptions;
private readonly AWSOptions _options;
private readonly AWSCredentials _credentials;
private readonly ILogger _logger;

/// <summary>
/// Constructs an instance of the ClientFactory
/// </summary>
/// <param name="awsOptions">The AWS options used for creating service clients.</param>
internal ClientFactory(AWSOptions awsOptions)
{
_awsOptions = awsOptions;
}

/// <summary>
/// Creates the AWS service client that implements the service client interface. The AWSOptions object
/// will be searched for in the IServiceProvider.
/// </summary>
/// <param name="provider">The dependency injection provider.</param>
/// <returns>The AWS service client</returns>
internal object CreateServiceClient(IServiceProvider provider)
/// <param name="credentials"></param>
/// <param name="logger"></param>
internal ClientFactory(AWSOptions awsOptions, AWSCredentials credentials, ILogger logger)
{
var loggerFactory = provider.GetService<Microsoft.Extensions.Logging.ILoggerFactory>();
var logger = loggerFactory?.CreateLogger("AWSSDK");

var options = _awsOptions ?? provider.GetService<AWSOptions>();
if(options == null)
{
var configuration = provider.GetService<IConfiguration>();
if(configuration != null)
{
options = configuration.GetAWSOptions();
if (options != null)
logger?.LogInformation("Found AWS options in IConfiguration");
}
}

return CreateServiceClient(logger, options);
_options = awsOptions ?? throw new ArgumentNullException(nameof(awsOptions));
_credentials = credentials ?? throw new ArgumentNullException(nameof(credentials));
_logger = logger;
}

/// <summary>
/// Creates the AWS service client that implements the service client interface. The AWSOptions object
/// will be searched for in the IServiceProvider.
/// Creates the AWS service client that implements the service client interface.
/// </summary>
/// <param name="logger">Logger instance for writing diagnostic logs.</param>
/// <param name="options">The AWS options used for creating the service client.</param>
/// <returns>The AWS service client</returns>
internal IAmazonService CreateServiceClient(ILogger logger, AWSOptions options)
internal IAmazonService CreateServiceClient()
{
PerformGlobalConfig(logger, options);
var credentials = CreateCredentials(logger, options);
PerformGlobalConfig(_logger, _options);
var credentials = _credentials;

if (!string.IsNullOrEmpty(options?.SessionRoleArn))
if (!string.IsNullOrEmpty(_options?.SessionRoleArn))
{
if (string.IsNullOrEmpty(options?.ExternalId))
if (string.IsNullOrEmpty(_options?.ExternalId))
{
credentials = new AssumeRoleAWSCredentials(credentials, options.SessionRoleArn, options.SessionName);
credentials = new AssumeRoleAWSCredentials(credentials, _options.SessionRoleArn, _options.SessionName);
}
else
{
credentials = new AssumeRoleAWSCredentials(credentials, options.SessionRoleArn, options.SessionName, new AssumeRoleAWSCredentialsOptions() { ExternalId = options.ExternalId });
credentials = new AssumeRoleAWSCredentials(credentials, _options.SessionRoleArn, _options.SessionName, new AssumeRoleAWSCredentialsOptions() { ExternalId = _options.ExternalId });
}
}

var config = CreateConfig(options);
var config = CreateConfig(_options);
var client = CreateClient(credentials, config);
return client as IAmazonService;
}
@@ -165,52 +143,6 @@ private static AmazonServiceClient CreateClient(AWSCredentials credentials, Clie
#endif
}

/// <summary>
/// Creates the AWSCredentials using either the profile indicated from the AWSOptions object
/// of the SDK fallback credentials search.
/// </summary>
/// <param name="logger"></param>
/// <param name="options"></param>
/// <returns></returns>
private static AWSCredentials CreateCredentials(ILogger logger, AWSOptions options)
{
if (options != null)
{
if (options.Credentials != null)
{
logger?.LogInformation("Using AWS credentials specified with the AWSOptions.Credentials property");
return options.Credentials;
}
if (!string.IsNullOrEmpty(options.Profile))
{
var chain = new CredentialProfileStoreChain(options.ProfilesLocation);
AWSCredentials result;
if (chain.TryGetAWSCredentials(options.Profile, out result))
{
logger?.LogInformation($"Found AWS credentials for the profile {options.Profile}");
return result;
}
else
{
logger?.LogInformation($"Failed to find AWS credentials for the profile {options.Profile}");
}
}
}

var credentials = DefaultIdentityResolverConfiguration.ResolveDefaultIdentity<AWSCredentials>();
if (credentials == null)
{
logger?.LogError("Last effort to find AWS Credentials with AWS SDK's default credential search failed");
throw new AmazonClientException("Failed to find AWS Credentials for constructing AWS service client");
}
else
{
logger?.LogInformation("Found credentials using the AWS SDK's default credential search");
}

return credentials;
}

/// <summary>
/// Creates the ClientConfig object for the service client.
/// </summary>
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using Amazon.Extensions.NETCore.Setup;
using Amazon.Runtime;
using Amazon.Runtime.CredentialManagement;
using Amazon.Runtime.Credentials.Internal;
using Microsoft.Extensions.Logging;

namespace AWSSDK.Extensions.NETCore.Setup
{
/// <summary>
///
/// </summary>
public class DefaultAWSCredentials : AWSCredentials
{
private readonly AWSOptions _options;
private readonly ILogger _logger;

/// <summary>
///
/// </summary>
/// <param name="awsOptions"></param>
/// <param name="logger"></param>
public DefaultAWSCredentials(AWSOptions awsOptions, ILogger logger)
{
_options = awsOptions;
_logger = logger;
}

/// <summary>
///
/// </summary>
/// <returns></returns>
public override ImmutableCredentials GetCredentials()
{
if (_options != null)
{
if (_options.Credentials != null)
{
_logger?.LogInformation("Using AWS credentials specified with the AWSOptions.Credentials property");
return _options.Credentials.GetCredentials();
}
if (!string.IsNullOrEmpty(_options.Profile))
{
var chain = new CredentialProfileStoreChain(_options.ProfilesLocation);
AWSCredentials result;
if (chain.TryGetAWSCredentials(_options.Profile, out result))
{
_logger?.LogInformation($"Found AWS credentials for the profile {_options.Profile}");
return result.GetCredentials();
}
else
{
_logger?.LogInformation($"Failed to find AWS credentials for the profile {_options.Profile}");
}
}
}

var credentials = DefaultIdentityResolverConfiguration.ResolveDefaultIdentity<AWSCredentials>();
if (credentials == null)
{
_logger?.LogError("Last effort to find AWS Credentials with AWS SDK's default credential search failed");
throw new AmazonClientException("Failed to find AWS Credentials for constructing AWS service client");
}
else
{
_logger?.LogInformation("Found credentials using the AWS SDK's default credential search");
}

return credentials.GetCredentials();
}
}
}
Original file line number Diff line number Diff line change
@@ -21,7 +21,9 @@

using Amazon.Runtime;
using Amazon.Extensions.NETCore.Setup;
using AWSSDK.Extensions.NETCore.Setup;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;

namespace Microsoft.Extensions.DependencyInjection
{
@@ -63,6 +65,94 @@ public static IServiceCollection AddDefaultAWSOptions(
return collection;
}

/// <summary>
///
/// </summary>
/// <param name="collection"></param>
/// <param name="lifetime"></param>
/// <returns></returns>
public static IServiceCollection AddAWSCredentials(
this IServiceCollection collection,
ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
collection.Add(new ServiceDescriptor(typeof(AWSCredentials), sp => sp.CreateDefaultAWSCredentials(), lifetime));
return collection;
}

/// <summary>
///
/// </summary>
/// <param name="collection"></param>
/// <param name="credentials"></param>
/// <returns></returns>
public static IServiceCollection AddAWSCredentials(
this IServiceCollection collection,
AWSCredentials credentials)
{
collection.Add(new ServiceDescriptor(typeof(AWSCredentials), credentials));
return collection;
}

/// <summary>
///
/// </summary>
/// <param name="collection"></param>
/// <param name="credentialsFunc"></param>
/// <param name="lifetime"></param>
/// <returns></returns>
public static IServiceCollection AddAWSCredentials(
this IServiceCollection collection,
Func<IServiceProvider, AWSCredentials> credentialsFunc,
ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
collection.Add(new ServiceDescriptor(typeof(AWSCredentials), credentialsFunc, lifetime));
return collection;
}

/// <summary>
///
/// </summary>
/// <param name="collection"></param>
/// <param name="lifetime"></param>
/// <returns></returns>
public static IServiceCollection TryAddAWSCredentials(
this IServiceCollection collection,
ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
collection.TryAdd(new ServiceDescriptor(typeof(AWSCredentials), sp => sp.CreateDefaultAWSCredentials(), lifetime));
return collection;
}

/// <summary>
///
/// </summary>
/// <param name="collection"></param>
/// <param name="credentials"></param>
/// <returns></returns>
public static IServiceCollection TryAddAWSCredentials(
this IServiceCollection collection,
AWSCredentials credentials)
{
collection.TryAdd(new ServiceDescriptor(typeof(AWSCredentials), credentials));
return collection;
}

/// <summary>
///
/// </summary>
/// <param name="collection"></param>
/// <param name="credentialsFunc"></param>
/// <param name="lifetime"></param>
/// <returns></returns>
public static IServiceCollection TryAddAWSCredentials(
this IServiceCollection collection,
Func<IServiceProvider, AWSCredentials> credentialsFunc,
ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
collection.TryAdd(new ServiceDescriptor(typeof(AWSCredentials), credentialsFunc, lifetime));
return collection;
}

/// <summary>
/// Adds the AWSOptions object to the dependency injection framework providing information
/// that will be used to construct Amazon service clients if they haven't already been registered.
@@ -106,7 +196,7 @@ public static IServiceCollection TryAddDefaultAWSOptions(
/// <returns>Returns back the IServiceCollection to continue the fluent system of IServiceCollection.</returns>
public static IServiceCollection AddAWSService<T>(this IServiceCollection collection, ServiceLifetime lifetime = ServiceLifetime.Singleton) where T : IAmazonService
{
return AddAWSService<T>(collection, null, lifetime);
return AddAWSService<T>(collection, options: null, lifetime);
}

/// <summary>
@@ -121,10 +211,24 @@ public static IServiceCollection AddAWSService<T>(this IServiceCollection collec
/// <returns>Returns back the IServiceCollection to continue the fluent system of IServiceCollection.</returns>
public static IServiceCollection AddAWSService<T>(this IServiceCollection collection, AWSOptions options, ServiceLifetime lifetime = ServiceLifetime.Singleton) where T : IAmazonService
{
Func<IServiceProvider, object> factory =
new ClientFactory<T>(options).CreateServiceClient;
var descriptor = new ServiceDescriptor(typeof(T), sp => CreateServiceClient<T>(options, sp), lifetime);
collection.Add(descriptor);
return collection;
}

var descriptor = new ServiceDescriptor(typeof(T), factory, lifetime);
/// <summary>
/// Adds the Amazon service client to the dependency injection framework. The Amazon service client is not
/// created until it is requested. If the ServiceLifetime property is set to Singleton, the default, then the same
/// instance will be reused for the lifetime of the process and the object should not be disposed.
/// </summary>
/// <typeparam name="T">The AWS service interface, like IAmazonS3.</typeparam>
/// <param name="collection"></param>
/// <param name="optionsFunc">A func that resolves the AWS options used to create the service client overriding the default AWS options added using AddDefaultAWSOptions.</param>
/// <param name="lifetime">The lifetime of the service client created. The default is Singleton.</param>
/// <returns>Returns back the IServiceCollection to continue the fluent system of IServiceCollection.</returns>
public static IServiceCollection AddAWSService<T>(this IServiceCollection collection, Func<IServiceProvider, AWSOptions> optionsFunc, ServiceLifetime lifetime = ServiceLifetime.Singleton) where T : IAmazonService
{
var descriptor = new ServiceDescriptor(typeof(T), sp => CreateServiceClient<T>(optionsFunc(sp), sp), lifetime);
collection.Add(descriptor);
return collection;
}
@@ -140,7 +244,7 @@ public static IServiceCollection AddAWSService<T>(this IServiceCollection collec
/// <returns>Returns back the IServiceCollection to continue the fluent system of IServiceCollection.</returns>
public static IServiceCollection TryAddAWSService<T>(this IServiceCollection collection, ServiceLifetime lifetime = ServiceLifetime.Singleton) where T : IAmazonService
{
return TryAddAWSService<T>(collection, null, lifetime);
return TryAddAWSService<T>(collection, options: null, lifetime);
}

/// <summary>
@@ -155,14 +259,28 @@ public static IServiceCollection TryAddAWSService<T>(this IServiceCollection col
/// <returns>Returns back the IServiceCollection to continue the fluent system of IServiceCollection.</returns>
public static IServiceCollection TryAddAWSService<T>(this IServiceCollection collection, AWSOptions options, ServiceLifetime lifetime = ServiceLifetime.Singleton) where T : IAmazonService
{
Func<IServiceProvider, object> factory =
new ClientFactory<T>(options).CreateServiceClient;
var descriptor = new ServiceDescriptor(typeof(T), sp => CreateServiceClient<T>(options, sp), lifetime);
collection.TryAdd(descriptor);
return collection;
}

var descriptor = new ServiceDescriptor(typeof(T), factory, lifetime);
/// <summary>
/// Adds the Amazon service client to the dependency injection framework if the service type hasn't already been registered.
/// The Amazon service client is not created until it is requested. If the ServiceLifetime property is set to Singleton,
/// the default, then the same instance will be reused for the lifetime of the process and the object should not be disposed.
/// </summary>
/// <typeparam name="T">The AWS service interface, like IAmazonS3.</typeparam>
/// <param name="collection"></param>
/// <param name="optionsFunc">A func that resolves the AWS options used to create the service client overriding the default AWS options added using AddDefaultAWSOptions.</param>
/// <param name="lifetime">The lifetime of the service client created. The default is Singleton.</param>
/// <returns>Returns back the IServiceCollection to continue the fluent system of IServiceCollection.</returns>
public static IServiceCollection TryAddAWSService<T>(this IServiceCollection collection, Func<IServiceProvider, AWSOptions> optionsFunc, ServiceLifetime lifetime = ServiceLifetime.Singleton) where T : IAmazonService
{
var descriptor = new ServiceDescriptor(typeof(T), sp => CreateServiceClient<T>(optionsFunc(sp), sp), lifetime);
collection.TryAdd(descriptor);
return collection;
}

#if NET8_0_OR_GREATER

/// <summary>
@@ -177,7 +295,7 @@ public static IServiceCollection TryAddAWSService<T>(this IServiceCollection col
/// <returns>Returns back the IServiceCollection to continue the fluent system of IServiceCollection.</returns>
public static IServiceCollection AddKeyedAWSService<T>(this IServiceCollection collection, object serviceKey, ServiceLifetime lifetime = ServiceLifetime.Singleton) where T : IAmazonService
{
return AddKeyedAWSService<T>(collection, serviceKey, null, lifetime);
return AddKeyedAWSService<T>(collection, serviceKey, options: null, lifetime);
}

/// <summary>
@@ -193,9 +311,25 @@ public static IServiceCollection AddKeyedAWSService<T>(this IServiceCollection c
/// <returns>Returns back the IServiceCollection to continue the fluent system of IServiceCollection.</returns>
public static IServiceCollection AddKeyedAWSService<T>(this IServiceCollection collection, object serviceKey, AWSOptions options, ServiceLifetime lifetime = ServiceLifetime.Singleton) where T : IAmazonService
{
object Factory(IServiceProvider sp, object key) => new ClientFactory<T>(options).CreateServiceClient(sp);
var descriptor = new ServiceDescriptor(typeof(T), serviceKey, (sp, _) => CreateServiceClient<T>(options, sp), lifetime);
collection.Add(descriptor);
return collection;
}

var descriptor = new ServiceDescriptor(typeof(T), serviceKey, Factory, lifetime);
/// <summary>
/// Adds the Amazon service client to the dependency injection framework with a key. The Amazon service client is not
/// created until it is requested. If the ServiceLifetime property is set to Singleton, the default, then the same
/// instance will be reused for the lifetime of the process and the object should not be disposed.
/// </summary>
/// <typeparam name="T">The AWS service interface, like IAmazonS3</typeparam>
/// <param name="collection"></param>
/// <param name="serviceKey">The key with which the service will be added in the dependency injection framework.</param>
/// <param name="optionsFunc">A func that resolves the AWS options used to create the service client overriding the default AWS options added using AddDefaultAWSOptions.</param>
/// <param name="lifetime">The lifetime of the service client created. The default is Singleton.</param>
/// <returns>Returns back the IServiceCollection to continue the fluent system of IServiceCollection.</returns>
public static IServiceCollection AddKeyedAWSService<T>(this IServiceCollection collection, object serviceKey, Func<IServiceProvider, AWSOptions> optionsFunc, ServiceLifetime lifetime = ServiceLifetime.Singleton) where T : IAmazonService
{
var descriptor = new ServiceDescriptor(typeof(T), serviceKey, (sp, _) => CreateServiceClient<T>(optionsFunc(sp), sp), lifetime);
collection.Add(descriptor);
return collection;
}
@@ -212,7 +346,7 @@ public static IServiceCollection AddKeyedAWSService<T>(this IServiceCollection c
/// <returns>Returns back the IServiceCollection to continue the fluent system of IServiceCollection.</returns>
public static IServiceCollection TryAddKeyedAWSService<T>(this IServiceCollection collection, object serviceKey, ServiceLifetime lifetime = ServiceLifetime.Singleton) where T : IAmazonService
{
return TryAddKeyedAWSService<T>(collection, serviceKey, null, lifetime);
return TryAddKeyedAWSService<T>(collection, serviceKey, options: null, lifetime);
}

/// <summary>
@@ -228,12 +362,45 @@ public static IServiceCollection TryAddKeyedAWSService<T>(this IServiceCollectio
/// <returns>Returns back the IServiceCollection to continue the fluent system of IServiceCollection.</returns>
public static IServiceCollection TryAddKeyedAWSService<T>(this IServiceCollection collection, object serviceKey, AWSOptions options, ServiceLifetime lifetime = ServiceLifetime.Singleton) where T : IAmazonService
{
object Factory(IServiceProvider sp, object key) => new ClientFactory<T>(options).CreateServiceClient(sp);
var descriptor = new ServiceDescriptor(typeof(T), serviceKey, (sp, _) => CreateServiceClient<T>(options, sp), lifetime);
collection.TryAdd(descriptor);
return collection;
}

var descriptor = new ServiceDescriptor(typeof(T), serviceKey, Factory, lifetime);
/// <summary>
/// Adds the Amazon service client to the dependency injection framework with a key if the service type hasn't already been registered with the same key.
/// The Amazon service client is not created until it is requested. If the ServiceLifetime property is set to Singleton, the default, then the same
/// instance will be reused for the lifetime of the process and the object should not be disposed.
/// </summary>
/// <typeparam name="T">The AWS service interface, like IAmazonS3</typeparam>
/// <param name="collection"></param>
/// <param name="serviceKey">The key with which the service will be added in the dependency injection framework.</param>
/// <param name="optionsFunc">A func that resolves the AWS options used to create the service client overriding the default AWS options added using AddDefaultAWSOptions.</param>
/// <param name="lifetime">The lifetime of the service client created. The default is Singleton.</param>
/// <returns>Returns back the IServiceCollection to continue the fluent system of IServiceCollection.</returns>
public static IServiceCollection TryAddKeyedAWSService<T>(this IServiceCollection collection, object serviceKey, Func<IServiceProvider, AWSOptions> optionsFunc, ServiceLifetime lifetime = ServiceLifetime.Singleton) where T : IAmazonService
{
var descriptor = new ServiceDescriptor(typeof(T), serviceKey, (sp, _) => CreateServiceClient<T>(optionsFunc(sp), sp), lifetime);
collection.TryAdd(descriptor);
return collection;
}
#endif

private static object CreateServiceClient<T>(AWSOptions options, IServiceProvider sp) where T : IAmazonService
{
var logger = sp.GetService<ILogger>();
var awsOptions = options ?? sp.GetService<AWSOptions>() ?? new AWSOptions();
var credentialsFactory = sp.GetService<AWSCredentials>() ?? sp.CreateDefaultAWSCredentials(awsOptions);

var factory = new ClientFactory<T>(awsOptions, credentialsFactory, logger);

return factory.CreateServiceClient();
}

private static AWSCredentials CreateDefaultAWSCredentials(this IServiceProvider sp, AWSOptions options = null)
{
options = options ?? sp.GetService<AWSOptions>() ?? new AWSOptions();
return new DefaultAWSCredentials(options, sp.GetService<ILogger>());
}
}
}
1 change: 1 addition & 0 deletions extensions/test/NETCore.SetupTests/ConfigurationTests.cs
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@
using Amazon;
using Amazon.S3;
using Amazon.Runtime;
using AWSSDK.Extensions.NETCore.Setup;

namespace NETCore.SetupTests
{