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: Support AAD RBAC via TokenCredential #28644

Merged
1 commit merged into from
Aug 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/TestCosmos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,10 @@ jobs:
- name: Test on Cosmos
run: test.cmd /p:Projects=${{ github.workspace }}\test\EFCore.Cosmos.FunctionalTests\EFCore.Cosmos.FunctionalTests.csproj
shell: cmd

- name: Publish Test Results
uses: actions/upload-artifact@v3
if: always()
with:
name: test-results
path: artifacts/TestResults/Debug/*
22 changes: 21 additions & 1 deletion src/EFCore.Cosmos/Extensions/CosmosDatabaseFacadeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ public static CosmosClient GetCosmosClient(this DatabaseFacade databaseFacade)
=> GetService<ISingletonCosmosClientWrapper>(databaseFacade).Client;

private static TService GetService<TService>(IInfrastructure<IServiceProvider> databaseFacade)
where TService : class
{
var service = databaseFacade.Instance.GetService<TService>();
var service = databaseFacade.GetService<TService>();
if (service == null)
{
throw new InvalidOperationException(CosmosStrings.CosmosNotInUse);
Expand All @@ -36,6 +37,25 @@ private static TService GetService<TService>(IInfrastructure<IServiceProvider> d
return service;
}

/// <summary>
/// Gets the configured database name for this <see cref="DbContext" />.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-cosmos">Accessing Azure Cosmos DB with EF Core</see> for more information and examples.
/// </remarks>
/// <param name="databaseFacade">The <see cref="DatabaseFacade" /> for the context.</param>
/// <returns>The database name.</returns>
public static string GetCosmosDatabaseId(this DatabaseFacade databaseFacade)
{
var cosmosOptions = databaseFacade.GetService<IDbContextOptions>().FindExtension<CosmosOptionsExtension>();
if (cosmosOptions == null)
{
throw new InvalidOperationException(CosmosStrings.CosmosNotInUse);
}

return cosmosOptions.DatabaseName;
}

/// <summary>
/// Returns <see langword="true" /> if the database provider currently in use is the Cosmos provider.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Azure.Core;
using Microsoft.EntityFrameworkCore.Cosmos.Infrastructure.Internal;

// ReSharper disable once CheckNamespace
Expand Down Expand Up @@ -83,6 +84,74 @@ public static DbContextOptionsBuilder UseCosmos(
return optionsBuilder;
}

/// <summary>
/// Configures the context to connect to an Azure Cosmos database.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-dbcontext-options">Using DbContextOptions</see>, and
/// <see href="https://aka.ms/efcore-docs-cosmos">Accessing Azure Cosmos DB with EF Core</see> for more information and examples.
/// </remarks>
/// <typeparam name="TContext">The type of context to be configured.</typeparam>
/// <param name="optionsBuilder">The builder being used to configure the context.</param>
/// <param name="accountEndpoint">The account end-point to connect to.</param>
/// <param name="tokenCredential">The Azure authentication token.</param>
/// <param name="databaseName">The database name.</param>
/// <param name="cosmosOptionsAction">An optional action to allow additional Cosmos-specific configuration.</param>
/// <returns>The options builder so that further configuration can be chained.</returns>
public static DbContextOptionsBuilder<TContext> UseCosmos<TContext>(
this DbContextOptionsBuilder<TContext> optionsBuilder,
string accountEndpoint,
TokenCredential tokenCredential,
string databaseName,
Action<CosmosDbContextOptionsBuilder>? cosmosOptionsAction = null)
where TContext : DbContext
=> (DbContextOptionsBuilder<TContext>)UseCosmos(
(DbContextOptionsBuilder)optionsBuilder,
accountEndpoint,
tokenCredential,
databaseName,
cosmosOptionsAction);

/// <summary>
/// Configures the context to connect to an Azure Cosmos database.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-dbcontext-options">Using DbContextOptions</see>, and
/// <see href="https://aka.ms/efcore-docs-cosmos">Accessing Azure Cosmos DB with EF Core</see> for more information and examples.
/// </remarks>
/// <param name="optionsBuilder">The builder being used to configure the context.</param>
/// <param name="accountEndpoint">The account end-point to connect to.</param>
/// <param name="tokenCredential">The Azure authentication token.</param>
/// <param name="databaseName">The database name.</param>
/// <param name="cosmosOptionsAction">An optional action to allow additional Cosmos-specific configuration.</param>
/// <returns>The options builder so that further configuration can be chained.</returns>
public static DbContextOptionsBuilder UseCosmos(
this DbContextOptionsBuilder optionsBuilder,
string accountEndpoint,
TokenCredential tokenCredential,
string databaseName,
Action<CosmosDbContextOptionsBuilder>? cosmosOptionsAction = null)
{
Check.NotNull(optionsBuilder, nameof(optionsBuilder));
Check.NotNull(accountEndpoint, nameof(accountEndpoint));
Check.NotNull(tokenCredential, nameof(tokenCredential));
Check.NotEmpty(databaseName, nameof(databaseName));

var extension = optionsBuilder.Options.FindExtension<CosmosOptionsExtension>()
?? new CosmosOptionsExtension();

extension = extension
.WithAccountEndpoint(accountEndpoint)
.WithTokenCredential(tokenCredential)
.WithDatabaseName(databaseName);

((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension);

cosmosOptionsAction?.Invoke(new CosmosDbContextOptionsBuilder(optionsBuilder));

return optionsBuilder;
}

/// <summary>
/// Configures the context to connect to an Azure Cosmos database.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Globalization;
using System.Net;
using System.Text;
using Azure.Core;
using Microsoft.EntityFrameworkCore.Cosmos.Internal;

namespace Microsoft.EntityFrameworkCore.Cosmos.Infrastructure.Internal;
Expand All @@ -18,6 +19,7 @@ public class CosmosOptionsExtension : IDbContextOptionsExtension
{
private string? _accountEndpoint;
private string? _accountKey;
private TokenCredential? _tokenCredential;
private string? _connectionString;
private string? _databaseName;
private string? _region;
Expand Down Expand Up @@ -55,6 +57,7 @@ protected CosmosOptionsExtension(CosmosOptionsExtension copyFrom)
{
_accountEndpoint = copyFrom._accountEndpoint;
_accountKey = copyFrom._accountKey;
_tokenCredential = copyFrom._tokenCredential;
_databaseName = copyFrom._databaseName;
_connectionString = copyFrom._connectionString;
_region = copyFrom._region;
Expand Down Expand Up @@ -138,6 +141,35 @@ public virtual CosmosOptionsExtension WithAccountKey(string? accountKey)
return clone;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual TokenCredential? TokenCredential
=> _tokenCredential;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual CosmosOptionsExtension WithTokenCredential(TokenCredential? tokenCredential)
{
if (tokenCredential is not null && _connectionString is not null)
{
throw new InvalidOperationException(CosmosStrings.ConnectionStringConflictingConfiguration);
}

var clone = Clone();

clone._tokenCredential = tokenCredential;

return clone;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
Expand All @@ -155,7 +187,7 @@ public virtual string? ConnectionString
/// </summary>
public virtual CosmosOptionsExtension WithConnectionString(string? connectionString)
{
if (connectionString is not null && (_accountEndpoint != null || _accountKey != null))
if (connectionString is not null && (_accountEndpoint != null || _accountKey != null || _tokenCredential != null))
{
throw new InvalidOperationException(CosmosStrings.ConnectionStringConflictingConfiguration);
}
Expand Down Expand Up @@ -565,6 +597,7 @@ public override int GetServiceProviderHashCode()
{
hashCode.Add(Extension._accountEndpoint);
hashCode.Add(Extension._accountKey);
hashCode.Add(Extension._tokenCredential);
}

hashCode.Add(Extension._region);
Expand All @@ -591,6 +624,7 @@ public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo
&& Extension._connectionString == otherInfo.Extension._connectionString
&& Extension._accountEndpoint == otherInfo.Extension._accountEndpoint
&& Extension._accountKey == otherInfo.Extension._accountKey
&& Extension._tokenCredential == otherInfo.Extension._tokenCredential
&& Extension._region == otherInfo.Extension._region
&& Extension._connectionMode == otherInfo.Extension._connectionMode
&& Extension._limitToEndpoint == otherInfo.Extension._limitToEndpoint
Expand All @@ -615,8 +649,17 @@ public override void PopulateDebugInfo(IDictionary<string, string> debugInfo)
{
debugInfo["Cosmos:" + nameof(AccountEndpoint)] =
(Extension._accountEndpoint?.GetHashCode() ?? 0L).ToString(CultureInfo.InvariantCulture);
debugInfo["Cosmos:" + nameof(AccountKey)] =
(Extension._accountKey?.GetHashCode() ?? 0L).ToString(CultureInfo.InvariantCulture);

if (Extension._accountKey == null)
{
debugInfo["Cosmos:" + nameof(TokenCredential)] =
(Extension._tokenCredential?.GetHashCode() ?? 0L).ToString(CultureInfo.InvariantCulture);
}
else
{
debugInfo["Cosmos:" + nameof(AccountKey)] =
(Extension._accountKey?.GetHashCode() ?? 0L).ToString(CultureInfo.InvariantCulture);
}
}

debugInfo["Cosmos:" + nameof(CosmosDbContextOptionsBuilder.Region)] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Net;
using Azure.Core;

namespace Microsoft.EntityFrameworkCore.Cosmos.Infrastructure.Internal;

Expand Down Expand Up @@ -29,6 +30,14 @@ public class CosmosSingletonOptions : ICosmosSingletonOptions
/// </summary>
public virtual string? AccountKey { get; private set; }

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual TokenCredential? TokenCredential { get; private set; }

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
Expand Down Expand Up @@ -146,6 +155,7 @@ public virtual void Initialize(IDbContextOptions options)
{
AccountEndpoint = cosmosOptions.AccountEndpoint;
AccountKey = cosmosOptions.AccountKey;
TokenCredential = cosmosOptions.TokenCredential;
ConnectionString = cosmosOptions.ConnectionString;
Region = cosmosOptions.Region;
LimitToEndpoint = cosmosOptions.LimitToEndpoint;
Expand Down Expand Up @@ -175,6 +185,7 @@ public virtual void Validate(IDbContextOptions options)
if (cosmosOptions != null
&& (AccountEndpoint != cosmosOptions.AccountEndpoint
|| AccountKey != cosmosOptions.AccountKey
|| TokenCredential != cosmosOptions.TokenCredential
|| ConnectionString != cosmosOptions.ConnectionString
|| Region != cosmosOptions.Region
|| LimitToEndpoint != cosmosOptions.LimitToEndpoint
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Net;
using Azure.Core;

namespace Microsoft.EntityFrameworkCore.Cosmos.Infrastructure.Internal;

Expand Down Expand Up @@ -35,6 +36,14 @@ public interface ICosmosSingletonOptions : ISingletonOptions
/// </summary>
string? AccountKey { get; }

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
TokenCredential? TokenCredential { get; }

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public override ConventionSet CreateConventionSet()
conventionSet.Add(new ContextContainerConvention(Dependencies));
conventionSet.Add(new ETagPropertyConvention());
conventionSet.Add(new StoreKeyConvention(Dependencies));

conventionSet.Replace<ValueGenerationConvention>(new CosmosValueGenerationConvention(Dependencies));
conventionSet.Replace<KeyDiscoveryConvention>(new CosmosKeyDiscoveryConvention(Dependencies));
conventionSet.Replace<InversePropertyAttributeConvention>(new CosmosInversePropertyAttributeConvention(Dependencies));
Expand Down
2 changes: 1 addition & 1 deletion src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/EFCore.Cosmos/Properties/CosmosStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@
<value>The Cosmos database does not support 'CanConnect' or 'CanConnectAsync'.</value>
</data>
<data name="ConnectionStringConflictingConfiguration" xml:space="preserve">
<value>Both the connection string and account key or account endpoint were specified. Specify only one set of connection details.</value>
<value>Both the connection string and CredentialToken, account key or account endpoint were specified. Specify only one set of connection details.</value>
</data>
<data name="CosmosNotInUse" xml:space="preserve">
<value>Cosmos-specific methods can only be used when the context is using the Cosmos provider.</value>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Azure.Core;
using Microsoft.EntityFrameworkCore.Cosmos.Infrastructure.Internal;

namespace Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal;
Expand All @@ -18,6 +19,7 @@ public class SingletonCosmosClientWrapper : ISingletonCosmosClientWrapper
private readonly string? _endpoint;
private readonly string? _key;
private readonly string? _connectionString;
private readonly TokenCredential? _tokenCredential;
private CosmosClient? _client;

/// <summary>
Expand All @@ -31,6 +33,7 @@ public SingletonCosmosClientWrapper(ICosmosSingletonOptions options)
_endpoint = options.AccountEndpoint;
_key = options.AccountKey;
_connectionString = options.ConnectionString;
_tokenCredential = options.TokenCredential;
var configuration = new CosmosClientOptions { ApplicationName = UserAgent, Serializer = new JsonCosmosSerializer() };

if (options.Region != null)
Expand Down Expand Up @@ -99,7 +102,9 @@ public SingletonCosmosClientWrapper(ICosmosSingletonOptions options)
/// </summary>
public virtual CosmosClient Client
=> _client ??= string.IsNullOrEmpty(_connectionString)
? new CosmosClient(_endpoint, _key, _options)
? _tokenCredential == null
? new CosmosClient(_endpoint, _key, _options)
: new CosmosClient(_endpoint, _tokenCredential, _options)
: new CosmosClient(_connectionString, _options);

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,9 @@ public override ConventionSet CreateConventionSet()
conventionSet.Add(new RelationalMapToJsonConvention(Dependencies, RelationalDependencies));

conventionSet.Replace<ValueGenerationConvention>(
new RelationalValueGenerationConvention(Dependencies, RelationalDependencies));
new RelationalValueGenerationConvention(Dependencies, RelationalDependencies));
conventionSet.Replace<QueryFilterRewritingConvention>(
new RelationalQueryFilterRewritingConvention(Dependencies, RelationalDependencies));
new RelationalQueryFilterRewritingConvention(Dependencies, RelationalDependencies));
conventionSet.Replace<RuntimeModelConvention>(new RelationalRuntimeModelConvention(Dependencies, RelationalDependencies));

return conventionSet;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,15 @@ public override ConventionSet CreateConventionSet()
conventionSet.Replace<ValueGenerationConvention>(
new SqlServerValueGenerationConvention(Dependencies, RelationalDependencies));
conventionSet.Replace<RuntimeModelConvention>(new SqlServerRuntimeModelConvention(Dependencies, RelationalDependencies));

var sqlServerTemporalConvention = new SqlServerTemporalConvention(Dependencies, RelationalDependencies);
ConventionSet.AddBefore(
conventionSet.EntityTypeAnnotationChangedConventions,
sqlServerTemporalConvention,
typeof(SqlServerValueGenerationConvention));
conventionSet.SkipNavigationForeignKeyChangedConventions.Add(sqlServerTemporalConvention);
conventionSet.ModelFinalizingConventions.Add(sqlServerTemporalConvention);

return conventionSet;
}

Expand Down
Loading