Skip to content
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

Cosmos managed Identity Authentication Changes #3950

Closed
wants to merge 11 commits into from
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ public class CollectionUpgradeManagerTests
ConnectionMode = ConnectionMode.Direct,
DatabaseId = "testdatabaseid",
Host = "https://fakehost",
Key = "ZmFrZWtleQ==", // "fakekey"
PreferredLocations = null,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ public class DataPlaneCollectionSetupTests
ConnectionMode = ConnectionMode.Direct,
DatabaseId = "testdatabaseid",
Host = "https://fakehost",
Key = "ZmFrZWtleQ==", // "fakekey"
PreferredLocations = null,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Net.Http;
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Security.Cryptography.Xml;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
Expand All @@ -18,6 +19,7 @@
using Microsoft.Health.Abstractions.Exceptions;
using Microsoft.Health.Extensions.DependencyInjection;
using Microsoft.Health.Fhir.Core.Extensions;
using Microsoft.Health.Fhir.Core.Features.Operations;
using Microsoft.Health.Fhir.CosmosDb.Core.Configs;

namespace Microsoft.Health.Fhir.CosmosDb.Features.Storage
Expand All @@ -33,19 +35,23 @@ internal class CosmosDbCollectionPhysicalPartitionInfo : IRequireInitializationO
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<CosmosDbCollectionPhysicalPartitionInfo> _logger;
private readonly CancellationTokenSource _backgroundLoopCancellationTokenSource = new();
private readonly IAccessTokenProvider _aadTokenProvider;
private Task _backgroundLoopTask;

public CosmosDbCollectionPhysicalPartitionInfo(
IAccessTokenProvider aadTokenProvider,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What "add" in "aadTokenProvider" stands for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@abiisnn "aad" stands here for Azure Active Directory.

CosmosDataStoreConfiguration dataStoreConfiguration,
IOptionsMonitor<CosmosCollectionConfiguration> collectionConfiguration,
IHttpClientFactory httpClientFactory,
ILogger<CosmosDbCollectionPhysicalPartitionInfo> logger)
{
EnsureArg.IsNotNull(aadTokenProvider, nameof(aadTokenProvider));
EnsureArg.IsNotNull(dataStoreConfiguration, nameof(dataStoreConfiguration));
EnsureArg.IsNotNull(collectionConfiguration, nameof(collectionConfiguration));
EnsureArg.IsNotNull(httpClientFactory, nameof(httpClientFactory));
EnsureArg.IsNotNull(logger, nameof(logger));

_aadTokenProvider = aadTokenProvider;
_dataStoreConfiguration = dataStoreConfiguration;
_collectionConfiguration = collectionConfiguration.Get(Constants.CollectionConfigurationName);
_httpClientFactory = httpClientFactory;
Expand Down Expand Up @@ -92,46 +98,18 @@ private async Task<int> GetPhysicalPartitionCount(CancellationToken cancellation
using HttpClient client = _httpClientFactory.CreateClient();

string host = _dataStoreConfiguration.Host;
string key = _dataStoreConfiguration.Key;

if (string.IsNullOrWhiteSpace(host))
{
if (string.IsNullOrWhiteSpace(key))
{
host = CosmosDbLocalEmulator.Host;
key = CosmosDbLocalEmulator.Key;
}
else
{
Ensure.That(host, $"{nameof(CosmosDataStoreConfiguration)}.{nameof(CosmosDataStoreConfiguration.Host)}").IsNotNullOrEmpty();
}
}
else if (string.IsNullOrWhiteSpace(key))
{
Ensure.That(key, $"{nameof(CosmosDataStoreConfiguration)}.{nameof(CosmosDataStoreConfiguration.Key)}").IsNotNullOrEmpty();
}
Comment on lines -95 to -112
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should keep the same behavior but using the new tokenProvider.

We might need to add a new variable called "token" in CosmosDBLocalEmuletor and make sure we clean up "key" in case it is not needed anymore. If "token" is not longer needed (because now you use new DefaultAzureCredential()), then can we clean up all key references?

I think we can keep the same structure because Ensure.That() will throw an exception in case host or token are null or white space, as now, it will just check host and not token.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still need "key"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@abiisnn We had removed "Key" reference from everywhere.
Also, in CosmosDBLocalEmulator class, we do not need token property as we are using DefaultAzureCredentials, and it generates token internally.

Ensure.That(host, $"{nameof(CosmosDataStoreConfiguration)}.{nameof(CosmosDataStoreConfiguration.Host)}").IsNotNullOrEmpty();

string date = DateTime.UtcNow.ToString("R");

bool isResourceToken = IsResourceToken(key);

string authToken = HttpUtility.UrlEncode(
isResourceToken
? key
: GenerateAuthToken(
"get",
"pkranges",
$"dbs/{_dataStoreConfiguration.DatabaseId}/colls/{_collectionConfiguration.CollectionId}",
date,
key));
string accessToken = await GenerateAccessToken(host, cancellationToken);

using var httpRequestMessage = new HttpRequestMessage(
HttpMethod.Get,
$"{host}/dbs/{_dataStoreConfiguration.DatabaseId}/colls/{_collectionConfiguration.CollectionId}/pkranges")
{
Headers =
{
{ "authorization", authToken },
{ "authorization", accessToken },
{ "x-ms-version", "2018-12-31" },
{ "x-ms-date", date },
},
Expand Down Expand Up @@ -175,15 +153,21 @@ public async ValueTask DisposeAsync()
}
}

private static string GenerateAuthToken(string verb, string resourceType, string resourceId, string date, string key)
private async Task<string> GenerateAccessToken(string host, CancellationToken cancellationToken)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we check that host is not null, empty or white space?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@abiisnn We are already checking for "host" as not null and empty or white space in line number 101.
Only after the check is successful, GenerateAccessToken function will get invoked.

{
string payLoad = $"{verb.ToLowerInvariant()}\n{resourceType.ToLowerInvariant()}\n{resourceId}\n{date.ToLowerInvariant()}\n\n";

using var hmacSha256 = new HMACSHA256 { Key = Convert.FromBase64String(key) };
byte[] hashPayLoad = hmacSha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(payLoad));
string signature = Convert.ToBase64String(hashPayLoad);
string accessToken = string.Empty;
var resourceURI = new Uri(host);
try
{
accessToken = await _aadTokenProvider.GetAccessTokenForResourceAsync(resourceURI, cancellationToken);
accessToken = HttpUtility.UrlEncode($"type=aad&ver=1.0&sig={accessToken}");
}
catch (AccessTokenProviderException ex)
{
_logger.LogError(ex, "Failed to get access token from managed identity.");
}

return $"type=master&ver=1.0&sig={signature}";
return accessToken;
}

private record PartitionKeyRange;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,5 @@ namespace Microsoft.Health.Fhir.CosmosDb.Features.Storage
public static class CosmosDbLocalEmulator
{
public const string Host = "https://localhost:8081";
public const string Key = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Azure.Identity;
using EnsureThat;
using Microsoft.Azure.Cosmos;
using Microsoft.Azure.Cosmos.Fluent;
Expand Down Expand Up @@ -105,21 +106,19 @@ await _retryExceptionPolicyFactory.RetryPolicy.ExecuteAsync(async () =>
private CosmosClient CreateCosmosClientInternal(CosmosDataStoreConfiguration configuration)
{
var host = configuration.Host;
var key = configuration.Key;

if (string.IsNullOrWhiteSpace(host) && string.IsNullOrWhiteSpace(key))
if (string.IsNullOrWhiteSpace(host))
{
_logger.LogWarning("No connection string provided, attempting to connect to local emulator.");

host = CosmosDbLocalEmulator.Host;
key = CosmosDbLocalEmulator.Key;
}

_logger.LogInformation("Creating CosmosClient instance for {DatabaseId}, Host: {Host}", configuration.DatabaseId, host);

IEnumerable<RequestHandler> requestHandlers = _requestHandlerFactory.Invoke();

var builder = new CosmosClientBuilder(host, key)
var builder = new CosmosClientBuilder(host, new DefaultAzureCredential())
.WithConnectionModeDirect(enableTcpConnectionEndpointRediscovery: true)
.WithCustomSerializer(new FhirCosmosSerializer(_logger))
.WithThrottlingRetryOptions(TimeSpan.FromSeconds(configuration.RetryOptions.MaxWaitTimeInSeconds), configuration.RetryOptions.MaxNumberOfRetries)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Azure.Identity;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Hosting;
Expand Down Expand Up @@ -113,10 +114,9 @@ string ValueOrFallback(string configKey, string fallbackValue)
}

var host = ValueOrFallback("CosmosDb:Host", CosmosDbLocalEmulator.Host);
var key = ValueOrFallback("CosmosDb:Key", CosmosDbLocalEmulator.Key);
var databaseId = ValueOrFallback("CosmosDb:DatabaseId", null) ?? throw new InvalidOperationException("expected CosmosDb:DatabaseId to be set in configuration");

using var client = new CosmosClient(host, key);
using var client = new CosmosClient(host, new DefaultAzureCredential());
Container container = client.GetContainer(databaseId, collectionId);
await container.DeleteContainerAsync();
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ public CosmosDbFhirStorageTestsFixture()
_cosmosDataStoreConfiguration = new CosmosDataStoreConfiguration
{
Host = Environment.GetEnvironmentVariable("CosmosDb:Host") ?? CosmosDbLocalEmulator.Host,
Key = Environment.GetEnvironmentVariable("CosmosDb:Key") ?? CosmosDbLocalEmulator.Key,
DatabaseId = Environment.GetEnvironmentVariable("CosmosDb:DatabaseId") ?? "FhirTests",
AllowDatabaseCreation = true,
PreferredLocations = Environment.GetEnvironmentVariable("CosmosDb:PreferredLocations")?.Split(';', StringSplitOptions.RemoveEmptyEntries),
Expand Down
Loading