-
Notifications
You must be signed in to change notification settings - Fork 330
AOT-safe auth provider with feature switch and trimmer support #4348
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
base: dev/paul/assembly-signing-core
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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" /> | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
|
|
||
| <!-- | ||
|
|
@@ -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 ==================================================== --> | ||
|
|
||
| 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 | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This lets the C# 14 compiler write the |
||
| { | ||
| } | ||
|
|
||
| #endif | ||
There was a problem hiding this comment.
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.