diff --git a/src/Microsoft.Health.Fhir.Azure/FhirServerBuilderAzureRegistrationExtensions.cs b/src/Microsoft.Health.Fhir.Azure/FhirServerBuilderAzureRegistrationExtensions.cs index d37b26ff58..6d628fd90b 100644 --- a/src/Microsoft.Health.Fhir.Azure/FhirServerBuilderAzureRegistrationExtensions.cs +++ b/src/Microsoft.Health.Fhir.Azure/FhirServerBuilderAzureRegistrationExtensions.cs @@ -51,10 +51,6 @@ public static IFhirServerBuilder AddAzureExportClientInitializer(this IFhirServe fhirServerBuilder.Services.Add() .Transient() .AsService>(); - - fhirServerBuilder.Services.Add() - .Transient() - .AsService(); } else { diff --git a/src/Microsoft.Health.Fhir.Azure/ExportDestinationClient/AzureAccessTokenProvider.cs b/src/Microsoft.Health.Fhir.Core/Features/Operations/AzureAccessTokenProvider.cs similarity index 80% rename from src/Microsoft.Health.Fhir.Azure/ExportDestinationClient/AzureAccessTokenProvider.cs rename to src/Microsoft.Health.Fhir.Core/Features/Operations/AzureAccessTokenProvider.cs index a4011f72ae..60f2b0e219 100644 --- a/src/Microsoft.Health.Fhir.Azure/ExportDestinationClient/AzureAccessTokenProvider.cs +++ b/src/Microsoft.Health.Fhir.Core/Features/Operations/AzureAccessTokenProvider.cs @@ -4,15 +4,15 @@ // ------------------------------------------------------------------------------------------------- using System; +using System.Globalization; using System.Threading; using System.Threading.Tasks; using Azure.Core; using Azure.Identity; using EnsureThat; using Microsoft.Extensions.Logging; -using Microsoft.Health.Fhir.Core.Features.Operations; -namespace Microsoft.Health.Fhir.Azure.ExportDestinationClient +namespace Microsoft.Health.Fhir.Core.Features.Operations { public class AzureAccessTokenProvider : IAccessTokenProvider { @@ -27,12 +27,14 @@ public AzureAccessTokenProvider(ILogger logger) _logger = logger; } + public TokenCredential TokenCredential => _azureServiceTokenProvider; + public async Task GetAccessTokenForResourceAsync(Uri resourceUri, CancellationToken cancellationToken) { EnsureArg.IsNotNull(resourceUri, nameof(resourceUri)); // https://learn.microsoft.com/en-us/dotnet/api/overview/azure/app-auth-migration?view=azure-dotnet - var accessTokenContext = new TokenRequestContext(scopes: new[] { resourceUri + "/.default" }); + var accessTokenContext = new TokenRequestContext(scopes: [resourceUri + "/.default"]); AccessToken accessToken; try @@ -42,14 +44,14 @@ public async Task GetAccessTokenForResourceAsync(Uri resourceUri, Cancel catch (CredentialUnavailableException ex) { _logger.LogWarning(ex, "Failed to retrieve access token"); - throw new AccessTokenProviderException(Resources.CannotGetAccessToken); + throw new AccessTokenProviderException(string.Format(CultureInfo.InvariantCulture, Core.Resources.CannotGetAccessToken, resourceUri)); } if (string.IsNullOrEmpty(accessToken.Token)) { _logger.LogWarning("Failed to retrieve access token"); - throw new AccessTokenProviderException(Resources.CannotGetAccessToken); + throw new AccessTokenProviderException(string.Format(CultureInfo.InvariantCulture, Core.Resources.CannotGetAccessToken, resourceUri)); } else { diff --git a/src/Microsoft.Health.Fhir.Core/Features/Operations/IAccessTokenProvider.cs b/src/Microsoft.Health.Fhir.Core/Features/Operations/IAccessTokenProvider.cs index f4b7aec814..ec5515d3e0 100644 --- a/src/Microsoft.Health.Fhir.Core/Features/Operations/IAccessTokenProvider.cs +++ b/src/Microsoft.Health.Fhir.Core/Features/Operations/IAccessTokenProvider.cs @@ -6,11 +6,14 @@ using System; using System.Threading; using System.Threading.Tasks; +using Azure.Core; namespace Microsoft.Health.Fhir.Core.Features.Operations { public interface IAccessTokenProvider { + TokenCredential TokenCredential { get; } + /// /// Gets the access token for the resource. /// diff --git a/src/Microsoft.Health.Fhir.Core/Resources.Designer.cs b/src/Microsoft.Health.Fhir.Core/Resources.Designer.cs index def19f3c2c..5f873578c1 100644 --- a/src/Microsoft.Health.Fhir.Core/Resources.Designer.cs +++ b/src/Microsoft.Health.Fhir.Core/Resources.Designer.cs @@ -1,7 +1,6 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -78,6 +77,15 @@ internal static string BundleRequiredForBatchOrTransaction { } } + /// + /// Looks up a localized string similar to Unable to get an access token for '{0}'.. + /// + internal static string CannotGetAccessToken { + get { + return ResourceManager.GetString("CannotGetAccessToken", resourceCulture); + } + } + /// /// Looks up a localized string similar to Can't find '{0}' in type '{1}'. /// diff --git a/src/Microsoft.Health.Fhir.Core/Resources.resx b/src/Microsoft.Health.Fhir.Core/Resources.resx index 5290df064d..4f5af5e210 100644 --- a/src/Microsoft.Health.Fhir.Core/Resources.resx +++ b/src/Microsoft.Health.Fhir.Core/Resources.resx @@ -744,4 +744,7 @@ The FHIR Server ran out of memory while processing an export job. Please use the _maxCount parameter when requesting an export job to reduce the number of resources exported at one time. The count used in this job was {0} '{0}' the value of _maxCount used for this export job. + + Unable to get an access token for '{0}'. + \ No newline at end of file diff --git a/src/Microsoft.Health.Fhir.CosmosDb.UnitTests/Features/Storage/FhirCosmosClientInitializerTests.cs b/src/Microsoft.Health.Fhir.CosmosDb.UnitTests/Features/Storage/FhirCosmosClientInitializerTests.cs index 527c388106..503b8af40b 100644 --- a/src/Microsoft.Health.Fhir.CosmosDb.UnitTests/Features/Storage/FhirCosmosClientInitializerTests.cs +++ b/src/Microsoft.Health.Fhir.CosmosDb.UnitTests/Features/Storage/FhirCosmosClientInitializerTests.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Health.Core.Features.Context; using Microsoft.Health.Fhir.Core.Features.Context; +using Microsoft.Health.Fhir.Core.Features.Operations; using Microsoft.Health.Fhir.CosmosDb.Core.Configs; using Microsoft.Health.Fhir.CosmosDb.Core.Features.Storage; using Microsoft.Health.Fhir.CosmosDb.Features.Storage; @@ -40,7 +41,7 @@ public FhirCosmosClientInitializerTests() clientTestProvider, () => new[] { new TestRequestHandler() }, new RetryExceptionPolicyFactory(_cosmosDataStoreConfiguration, Substitute.For>()), - new Lazy(() => Substitute.For()), + Substitute.For(), NullLogger.Instance); _collectionInitializers = new List { _collectionInitializer1, _collectionInitializer2 }; diff --git a/src/Microsoft.Health.Fhir.CosmosDb/Features/Storage/CosmosAccessTokenProviderFactory.cs b/src/Microsoft.Health.Fhir.CosmosDb/Features/Storage/CosmosAccessTokenProviderFactory.cs new file mode 100644 index 0000000000..89e62b3cd4 --- /dev/null +++ b/src/Microsoft.Health.Fhir.CosmosDb/Features/Storage/CosmosAccessTokenProviderFactory.cs @@ -0,0 +1,13 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using Microsoft.Health.Fhir.Core.Features.Operations; + +namespace Microsoft.Health.Fhir.CosmosDb.Features.Storage; + +/// +/// Factory for creating instances for access to Cosmos. +/// +public delegate IAccessTokenProvider CosmosAccessTokenProviderFactory(); diff --git a/src/Microsoft.Health.Fhir.CosmosDb/Features/Storage/CosmosDbCollectionPhysicalPartitionInfo.cs b/src/Microsoft.Health.Fhir.CosmosDb/Features/Storage/CosmosDbCollectionPhysicalPartitionInfo.cs index cc14323285..9b06ceb67d 100644 --- a/src/Microsoft.Health.Fhir.CosmosDb/Features/Storage/CosmosDbCollectionPhysicalPartitionInfo.cs +++ b/src/Microsoft.Health.Fhir.CosmosDb/Features/Storage/CosmosDbCollectionPhysicalPartitionInfo.cs @@ -41,7 +41,7 @@ public CosmosDbCollectionPhysicalPartitionInfo( CosmosDataStoreConfiguration dataStoreConfiguration, IOptionsMonitor collectionConfiguration, IHttpClientFactory httpClientFactory, - IAccessTokenProvider accessTokenProvider, + CosmosAccessTokenProviderFactory accessTokenProviderFactory, ILogger logger) { EnsureArg.IsNotNull(dataStoreConfiguration, nameof(dataStoreConfiguration)); @@ -52,7 +52,7 @@ public CosmosDbCollectionPhysicalPartitionInfo( _dataStoreConfiguration = dataStoreConfiguration; _collectionConfiguration = collectionConfiguration.Get(Constants.CollectionConfigurationName); _httpClientFactory = httpClientFactory; - _accessTokenProvider = accessTokenProvider; + _accessTokenProvider = accessTokenProviderFactory.Invoke(); _logger = logger; } diff --git a/src/Microsoft.Health.Fhir.CosmosDb/Features/Storage/FhirCosmosClientInitializer.cs b/src/Microsoft.Health.Fhir.CosmosDb/Features/Storage/FhirCosmosClientInitializer.cs index f943f58653..df1344f058 100644 --- a/src/Microsoft.Health.Fhir.CosmosDb/Features/Storage/FhirCosmosClientInitializer.cs +++ b/src/Microsoft.Health.Fhir.CosmosDb/Features/Storage/FhirCosmosClientInitializer.cs @@ -14,6 +14,7 @@ using Microsoft.Azure.Cosmos.Fluent; using Microsoft.Extensions.Logging; using Microsoft.Health.Abstractions.Exceptions; +using Microsoft.Health.Fhir.Core.Features.Operations; using Microsoft.Health.Fhir.CosmosDb.Core.Configs; using Microsoft.Health.Fhir.CosmosDb.Core.Features.Storage; using Microsoft.IO; @@ -27,11 +28,11 @@ public class FhirCosmosClientInitializer : ICosmosClientInitializer private const StringComparison _hashCodeStringComparison = StringComparison.Ordinal; private readonly ICosmosClientTestProvider _testProvider; - private readonly ILogger _logger; private readonly Func> _requestHandlerFactory; private readonly RetryExceptionPolicyFactory _retryExceptionPolicyFactory; - private readonly Lazy _tokenCredential; + private readonly CosmosAccessTokenProviderFactory _cosmosAccessTokenProviderFactory; private readonly object _lockObject; + private readonly ILogger _logger; private CosmosClient _cosmosClient; private int _cosmosKeyHashCode; @@ -40,19 +41,19 @@ public FhirCosmosClientInitializer( ICosmosClientTestProvider testProvider, Func> requestHandlerFactory, RetryExceptionPolicyFactory retryExceptionPolicyFactory, - Lazy tokenCredential, + CosmosAccessTokenProviderFactory cosmosAccessTokenProviderFactory, ILogger logger) { EnsureArg.IsNotNull(testProvider, nameof(testProvider)); EnsureArg.IsNotNull(requestHandlerFactory, nameof(requestHandlerFactory)); EnsureArg.IsNotNull(retryExceptionPolicyFactory, nameof(retryExceptionPolicyFactory)); - EnsureArg.IsNotNull(tokenCredential, nameof(tokenCredential)); + EnsureArg.IsNotNull(cosmosAccessTokenProviderFactory, nameof(cosmosAccessTokenProviderFactory)); EnsureArg.IsNotNull(logger, nameof(logger)); _testProvider = testProvider; _requestHandlerFactory = requestHandlerFactory; _retryExceptionPolicyFactory = retryExceptionPolicyFactory; - _tokenCredential = tokenCredential; + _cosmosAccessTokenProviderFactory = cosmosAccessTokenProviderFactory; _logger = logger; _lockObject = new object(); @@ -127,7 +128,7 @@ private CosmosClient CreateCosmosClientInternal(CosmosDataStoreConfiguration con CosmosClientBuilder builder; builder = configuration.UseManagedIdentity ? - new CosmosClientBuilder(host, _tokenCredential.Value) : + new CosmosClientBuilder(host, _cosmosAccessTokenProviderFactory.Invoke().TokenCredential) : new CosmosClientBuilder(host, key); builder diff --git a/src/Microsoft.Health.Fhir.CosmosDb/Registration/FhirServerBuilderCosmosDbRegistrationExtensions.cs b/src/Microsoft.Health.Fhir.CosmosDb/Registration/FhirServerBuilderCosmosDbRegistrationExtensions.cs index 787968e0f6..f631f06a61 100644 --- a/src/Microsoft.Health.Fhir.CosmosDb/Registration/FhirServerBuilderCosmosDbRegistrationExtensions.cs +++ b/src/Microsoft.Health.Fhir.CosmosDb/Registration/FhirServerBuilderCosmosDbRegistrationExtensions.cs @@ -15,6 +15,7 @@ using Microsoft.Extensions.Options; using Microsoft.Health.Extensions.DependencyInjection; using Microsoft.Health.Fhir.Core.Extensions; +using Microsoft.Health.Fhir.Core.Features.Operations; using Microsoft.Health.Fhir.Core.Features.Operations.Export; using Microsoft.Health.Fhir.Core.Features.Parameters; using Microsoft.Health.Fhir.Core.Features.Search.Expressions; @@ -271,6 +272,10 @@ private static IFhirServerBuilder AddCosmosDbPersistence(this IFhirServerBuilder .AsImplementedInterfaces() .AsFactory>(); + services.Add(c => c.GetRequiredService) + .Transient() + .AsSelf(); + IEnumerable jobs = services.TypesInSameAssemblyAs() .AssignableTo() .Transient() diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Modules/OperationsModule.cs b/src/Microsoft.Health.Fhir.Shared.Api/Modules/OperationsModule.cs index 12a5df0c53..ede83f6bd0 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Modules/OperationsModule.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api/Modules/OperationsModule.cs @@ -10,6 +10,7 @@ using Microsoft.Health.Fhir.Api.Features.Operations; using Microsoft.Health.Fhir.Core.Extensions; using Microsoft.Health.Fhir.Core.Features.Conformance; +using Microsoft.Health.Fhir.Core.Features.Operations; using Microsoft.Health.Fhir.Core.Features.Operations.Everything; using Microsoft.Health.Fhir.Core.Features.Operations.Export; using Microsoft.Health.Fhir.Core.Features.Operations.Import; @@ -88,6 +89,10 @@ public void Load(IServiceCollection services) .Transient() .AsSelf() .AsImplementedInterfaces(); + + services.Add() + .Transient() + .AsService(); } } } diff --git a/test/Microsoft.Health.Fhir.Shared.Tests.Integration/Persistence/CosmosDbFhirStorageTestsFixture.cs b/test/Microsoft.Health.Fhir.Shared.Tests.Integration/Persistence/CosmosDbFhirStorageTestsFixture.cs index 8a84759b39..8cd2204e52 100644 --- a/test/Microsoft.Health.Fhir.Shared.Tests.Integration/Persistence/CosmosDbFhirStorageTestsFixture.cs +++ b/test/Microsoft.Health.Fhir.Shared.Tests.Integration/Persistence/CosmosDbFhirStorageTestsFixture.cs @@ -142,6 +142,9 @@ public virtual async Task InitializeAsync() var cosmosResponseProcessor = Substitute.For(); + var cosmosAccessor = Substitute.For(); + cosmosAccessor.TokenCredential.Returns(GetTokenCredential()); + var responseProcessor = new CosmosResponseProcessor(_fhirRequestContextAccessor, mediator, Substitute.For(), NullLogger.Instance); var handler = new FhirCosmosResponseHandler(() => new NonDisposingScope(_container), _cosmosDataStoreConfiguration, _fhirRequestContextAccessor, responseProcessor); var retryExceptionPolicyFactory = new RetryExceptionPolicyFactory(_cosmosDataStoreConfiguration, _fhirRequestContextAccessor); @@ -149,7 +152,7 @@ public virtual async Task InitializeAsync() testProvider, () => new[] { handler }, retryExceptionPolicyFactory, - new Lazy(GetTokenCredential), + () => cosmosAccessor, NullLogger.Instance); _cosmosClient = documentClientInitializer.CreateCosmosClient(_cosmosDataStoreConfiguration);