Skip to content
Draft
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
374 changes: 374 additions & 0 deletions aot-auth-provider-proposal.md

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@paulmedynski - Remove this before we merge the PR.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ See the LICENSE file in the project root for more information.
<remarks>
Avoid performing long-waiting tasks in this method, since it can block other threads from accessing the provider registry.

This method must be idempotent. It may be invoked more than once for the same provider instance, and more than once for a single <see cref="M:Microsoft.Data.SqlClient.SqlAuthenticationProvider.SetProvider(Microsoft.Data.SqlClient.SqlAuthenticationMethod,Microsoft.Data.SqlClient.SqlAuthenticationProvider)" /> call, because registration uses a concurrent registry whose update logic may run multiple times under contention.

This method must not throw.
</remarks>
<param name="authenticationMethod">The authentication method.</param>
Expand All @@ -108,6 +110,8 @@ See the LICENSE file in the project root for more information.
<remarks>
For example, this method is called when a different provider with the same authentication method overrides this provider in the SQL authentication provider registry. Avoid performing long-waiting task in this method, since it can block other threads from accessing the provider registry.

This method must be idempotent. It may be invoked more than once for the same provider instance, and more than once for a single <see cref="M:Microsoft.Data.SqlClient.SqlAuthenticationProvider.SetProvider(Microsoft.Data.SqlClient.SqlAuthenticationMethod,Microsoft.Data.SqlClient.SqlAuthenticationProvider)" /> call, because registration uses a concurrent registry whose update logic may run multiple times under contention.

This method must not throw.
</remarks>
<param name="authenticationMethod">The authentication method.</param>
Expand All @@ -130,6 +134,9 @@ See the LICENSE file in the project root for more information.
<summary>Gets an authentication provider by method.</summary>
<param name="authenticationMethod">The authentication method.</param>
<returns>The authentication provider or <see langword="null" /> if not found.</returns>
<remarks>
This is the canonical way to retrieve a registered authentication provider.
</remarks>
</GetProvider>
<SetProvider>
<summary>Sets an authentication provider by method.</summary>
Expand All @@ -138,6 +145,9 @@ See the LICENSE file in the project root for more information.
<returns>
<see langword="true" /> if the operation succeeded; otherwise, <see langword="false" /> (for example, the existing provider disallows overriding).
</returns>
<remarks>
This is the canonical way to register an authentication provider.
</remarks>
</SetProvider>
</members>
</docs>
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,13 @@
<!-- Strong name signing ============================================= -->
<!-- This is done in Directory.Build.props -->

<!-- Unsigned: expose internals to the test assembly in any reference mode. -->
<!-- Unsigned: expose internals to the test assembly and to the SqlClient driver in any
reference mode. The driver hosts the authentication provider bootstrapper, which seeds
the AuthenticationProviderRegistry (internal to this assembly). -->
<ItemGroup Condition="'$(SigningKeyPath)' == ''">
<InternalsVisibleTo Include="$(AssemblyName).Test" />
<InternalsVisibleTo Include="UnitTests" />

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Give SqlClient access to Abstractions internals so it can call SetPermanentProvider().

Give UnitTests access to Abstractions internals so it can use non-global isloated provider registry instances.

<InternalsVisibleTo Include="Microsoft.Data.SqlClient" />
</ItemGroup>

<!--
Expand All @@ -48,6 +52,13 @@
-->
<ItemGroup Condition="'$(SigningKeyPath)' != '' AND '$(ReferenceType)' == 'Package'">
<InternalsVisibleTo Include="$(AssemblyName).Test, PublicKey=00240000048000001401000006020000002400005253413100080000010001003D19684676DA365F331D00CE7BD4B8EF03E74102F39A5681B40622703D68F0298ECACECC723D3FFC1EA9365AF4958578550EA1EBEEC084B0B3757F3762449F5365E872802A4B548056760764FAD062BFEE81ED26183109AD46810E7E6E965419D0A10473680144D20C1BFE1027A5F586CA987523C06F5C126C44EA7D4F51EB023867A9F294315F95775ACEFD2D678186919458DFCCB4DE2E9F53AEFC766C7CBCEC474ED21C1616E5A9414D366D91D121C39F5FE6641295ADC058EF3FB10593BCDE2E82D9F217C2634909EEF496CD53AE78ABBEA572B871D72EBFC5378205950ABA97C7CCC2B9635D96933D5F9C9624D71FF53EE2094CF3A6BD38534D66E414B7" />
<InternalsVisibleTo Include="UnitTests, PublicKey=00240000048000001401000006020000002400005253413100080000010001003D19684676DA365F331D00CE7BD4B8EF03E74102F39A5681B40622703D68F0298ECACECC723D3FFC1EA9365AF4958578550EA1EBEEC084B0B3757F3762449F5365E872802A4B548056760764FAD062BFEE81ED26183109AD46810E7E6E965419D0A10473680144D20C1BFE1027A5F586CA987523C06F5C126C44EA7D4F51EB023867A9F294315F95775ACEFD2D678186919458DFCCB4DE2E9F53AEFC766C7CBCEC474ED21C1616E5A9414D366D91D121C39F5FE6641295ADC058EF3FB10593BCDE2E82D9F217C2634909EEF496CD53AE78ABBEA572B871D72EBFC5378205950ABA97C7CCC2B9635D96933D5F9C9624D71FF53EE2094CF3A6BD38534D66E414B7" />
<!--
Expose internals to the SqlClient driver, which hosts the authentication provider
bootstrapper that seeds the AuthenticationProviderRegistry. The driver is signed with the
product key (public key token 23ec7fc2d6eaa4a5), which differs from the test key above.
-->
<InternalsVisibleTo Include="Microsoft.Data.SqlClient, PublicKey=0024000004800000940000000602000000240000525341310004000001000100C14C6D7EE398F4910F849A9511EC66BBB621E7FFF7E0802860E283AAA4F3B7FAD06A3AFA95628D8907176D3C61E3F4DF845CDD821B3751A9917FC7A5EC2F191C887AAF72732B5FD85F6D82DE8461BCBA93ECCE724737D48683A62089597DE4EA24F263FB9A70B04DD3F4B9A003CE883DAC51DE59A6DB2BAF429A42BCD72112EE" />
</ItemGroup>

<!-- Build Output ==================================================== -->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Concurrent;
using Microsoft.Data.SqlClient.Internal;

namespace Microsoft.Data.SqlClient;

/// <summary>
/// Holds the registry of <see cref="SqlAuthenticationProvider"/> instances keyed by
/// <see cref="SqlAuthenticationMethod"/>. This is the shared store that backs the public
/// <see cref="SqlAuthenticationProvider.GetProvider"/> and
/// <see cref="SqlAuthenticationProvider.SetProvider"/> methods.
/// </summary>
/// <remarks>
/// Providers fall into two categories:
/// <list type="bullet">
/// <item>
/// <description>
/// Permanent providers, registered via <see cref="SetPermanentProvider"/> (e.g. from an
/// application's configuration). These take precedence and cannot be overridden by a later
/// call to <see cref="SetProvider"/>.
/// </description>
/// </item>
/// <item>
/// <description>
/// Overridable providers, registered via <see cref="SetProvider"/>. These can be replaced
/// by subsequent <see cref="SetProvider"/> calls, but never override a permanent provider.
/// </description>
/// </item>
/// </list>
/// </remarks>
internal sealed class AuthenticationProviderRegistry
{
#region Private Fields

/// <summary>
/// The singleton instance backing the public static
/// <see cref="SqlAuthenticationProvider.GetProvider"/> and
/// <see cref="SqlAuthenticationProvider.SetProvider"/> accessors.
/// </summary>
/// <remarks>
/// Production code uses this shared instance. Tests can instead construct an isolated

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think we can apply this pattern to other globals and singletons (like LocalAppContextSwitches) to avoid tests clobbering each other.

/// instance via the internal constructor to avoid mutating global state.
/// </remarks>
internal static AuthenticationProviderRegistry Instance { get; } = new();

/// <summary>
/// A registered provider together with whether it was registered as permanent (via
/// <see cref="SetPermanentProvider"/>) and therefore not overridable by <see cref="SetProvider"/>.
/// </summary>
/// <param name="Provider">The registered provider. Never <see langword="null"/>.</param>
/// <param name="IsPermanent">Whether the provider must not be overridden by <see cref="SetProvider"/>.</param>
private readonly record struct ProviderEntry(SqlAuthenticationProvider Provider, bool IsPermanent);

/// <summary>
/// The registered providers keyed by authentication method. Each entry records whether the
/// provider was registered as permanent (e.g. application specified); permanent providers are
/// not overridable via <see cref="SetProvider"/>.
/// </summary>
private readonly ConcurrentDictionary<SqlAuthenticationMethod, ProviderEntry> _providers = new();

#endregion

#region Construction

/// <summary>
/// Initializes a new, empty registry. Production code uses the shared <see cref="Instance"/>;
/// the constructor is exposed to tests so they can exercise registry behavior in isolation.
/// </summary>
internal AuthenticationProviderRegistry()
{
}

#endregion

#region Internal API

/// <summary>
/// Gets the provider registered for the given authentication method, or <see langword="null"/>
/// if none is registered.
/// </summary>
internal SqlAuthenticationProvider? GetProvider(SqlAuthenticationMethod authenticationMethod)
{
return _providers.TryGetValue(authenticationMethod, out ProviderEntry entry)
? entry.Provider
: null;
}

/// <summary>
/// Registers an overridable provider for the given authentication method.
/// </summary>
/// <returns>
/// <see langword="true"/> if the provider was registered; <see langword="false"/> if a
/// permanent provider is already registered for the authentication method.
/// </returns>
/// <exception cref="NotSupportedException">
/// The provider does not support the given authentication method.
/// </exception>
/// <exception cref="NullReferenceException">
/// <paramref name="provider"/> is <see langword="null"/>.
/// </exception>
internal bool SetProvider(SqlAuthenticationMethod authenticationMethod, SqlAuthenticationProvider provider)
{
if (!provider.IsSupported(authenticationMethod))
{
throw new NotSupportedException(
string.Format(
AbstractionsStrings.SQL_UnsupportedAuthenticationByProvider,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This string and its existing localizations were pulled into Abstractions.

@paulmedynski - Do we need to update our localization pipelines?

provider.GetType().Name,
authenticationMethod.ToString()));
}

ProviderEntry result = _providers.AddOrUpdate(
authenticationMethod,
// addValueFactory: no provider is registered for this method yet.
(SqlAuthenticationMethod key) =>
{
InvokeProviderCallback(provider, provider.BeforeLoad, key, nameof(SqlAuthenticationProvider.BeforeLoad));

SqlClientEventSource.Log.TryTraceEvent(
"AuthenticationProviderRegistry.SetProvider | Added auth provider {0} for authentication {1}.",
GetProviderType(provider),
key);

return new ProviderEntry(provider, IsPermanent: false);
},
// updateValueFactory: a provider is already registered for this method.
(SqlAuthenticationMethod key, ProviderEntry existing) =>
{
// Permanent providers cannot be replaced. Return the existing entry unchanged so
// AddOrUpdate keeps it; SetProvider detects this from the returned entry below.
if (existing.IsPermanent)
{
SqlClientEventSource.Log.TryTraceEvent(
"AuthenticationProviderRegistry.SetProvider | Failed to add provider {0} because a " +
"permanent provider with type {1} already existed for authentication {2}.",
GetProviderType(provider),
GetProviderType(existing.Provider),
key);

return existing;
}

InvokeProviderCallback(existing.Provider, existing.Provider.BeforeUnload, key, nameof(SqlAuthenticationProvider.BeforeUnload));
InvokeProviderCallback(provider, provider.BeforeLoad, key, nameof(SqlAuthenticationProvider.BeforeLoad));

SqlClientEventSource.Log.TryTraceEvent(
"AuthenticationProviderRegistry.SetProvider | Added auth provider {0}, overriding " +
"existing provider {1} for authentication {2}.",
GetProviderType(provider),
GetProviderType(existing.Provider),
key);

return new ProviderEntry(provider, IsPermanent: false);
});

// The new provider is always stored non-permanent; if a permanent provider blocked the
// update, AddOrUpdate returned that (permanent) entry instead.
return !result.IsPermanent;
}

/// <summary>
/// Registers a permanent provider for the given authentication method. Permanent providers
/// take precedence and cannot be overridden by <see cref="SetProvider"/>.
/// </summary>
/// <remarks>
/// Callers are responsible for verifying that the provider supports the authentication method
/// before registering it.
/// <para>
/// This is a last-in-wins operation: a later <see cref="SetPermanentProvider"/> call for the

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This behaviour isn't documented anywhere public, but it is preserved from the original Manager implementation.

/// same authentication method unconditionally replaces any previously registered provider
/// (permanent or not). Only <see cref="SetProvider"/> is blocked by an existing permanent
/// provider; <see cref="SetPermanentProvider"/> itself always overwrites.
/// </para>
/// </remarks>
internal void SetPermanentProvider(SqlAuthenticationMethod authenticationMethod, SqlAuthenticationProvider provider)
{
_providers[authenticationMethod] = new ProviderEntry(provider, IsPermanent: true);
}

#endregion

#region Private Helpers

/// <summary>
/// Returns a human-readable type name for the given provider, for use in trace messages.
/// </summary>
/// <param name="provider">The provider to describe, or <see langword="null"/>.</param>
/// <returns>
/// The provider's full type name; <c>"null"</c> if <paramref name="provider"/> is
/// <see langword="null"/>; or <c>"unknown"</c> if the type name is unavailable.
/// </returns>
private static string GetProviderType(SqlAuthenticationProvider? provider)
{
if (provider is null)
{
return "null";
}
return provider.GetType().FullName ?? "unknown";
}

/// <summary>
/// Invokes a provider lifecycle callback (<see cref="SqlAuthenticationProvider.BeforeLoad"/> or
/// <see cref="SqlAuthenticationProvider.BeforeUnload"/>), isolating the registry from a
/// misbehaving provider: any exception the callback throws is logged and swallowed so it
/// cannot corrupt registration.
/// </summary>
/// <param name="provider">The provider whose callback is being invoked (used for logging).</param>
/// <param name="callback">The callback to invoke.</param>
/// <param name="authenticationMethod">The authentication method passed to the callback.</param>
/// <param name="callbackName">The callback name, used in trace messages.</param>
private static void InvokeProviderCallback(
SqlAuthenticationProvider provider,
Action<SqlAuthenticationMethod> callback,
SqlAuthenticationMethod authenticationMethod,
string callbackName)
{
try
{
callback(authenticationMethod);
}
catch (Exception ex)
{
SqlClientEventSource.Log.TryTraceEvent(
"AuthenticationProviderRegistry.SetProvider | {0} threw for provider {1} with " +
"authentication {2}; ignoring: {3}",
callbackName,
GetProviderType(provider),
authenticationMethod,
ex);
}
}

#endregion
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#if !NET

namespace System.Runtime.CompilerServices;

/// <summary>
/// Polyfill for the marker type the C# compiler requires to emit <c>init</c>-only setters
/// (used by records and init-only properties). It is provided by the BCL on .NET, but not on
/// the netstandard2.0 / .NET Framework targets this assembly supports, so we define it here.
/// </summary>
internal static class IsExternalInit

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This lets the C# 14 compiler write the ProviderEntry getters/setters properly when targeting .NET Standard 2.0.

{
}

#endif
Loading
Loading