diff --git a/.github/instructions/architecture.instructions.md b/.github/instructions/architecture.instructions.md index 27b702b6d8..e995e7eff5 100644 --- a/.github/instructions/architecture.instructions.md +++ b/.github/instructions/architecture.instructions.md @@ -161,6 +161,12 @@ Located in `ConnectionPool/`: - Pool groups manage pools for different connection strings ### Authentication Providers +The provider registry, authentication configuration handlers and `SqlAuthenticationInitializer` +live in `Microsoft.Data.SqlClient.Extensions/Abstractions`, targeting `netstandard2.0;net10.0`. +The driver forwards their public types and delegates authentication lookup directly. +Abstractions' cached feature switches and separate capability guards keep explicit registration +AOT-safe while removing reflective configuration and Azure discovery from trimmed/AOT publishes. + Located in `SSPI/` and authentication-related files: - Windows Authentication (SSPI/Kerberos) - SQL Server Authentication diff --git a/.github/instructions/features.instructions.md b/.github/instructions/features.instructions.md index 4f3db0a8ea..dc13cb9849 100644 --- a/.github/instructions/features.instructions.md +++ b/.github/instructions/features.instructions.md @@ -243,7 +243,8 @@ AppContext switches allow runtime behavior changes without modifying connection | Switch Name | Default | Description | |-------------|---------|-------------| | `Switch.Microsoft.Data.SqlClient.DisableTNIRByDefaultInConnectionString` | `false` | Disables Transparent Network IP Resolution by default | -| `Switch.Microsoft.Data.SqlClient.EnableAppConfig` | `true` | .NET only. Controls whether SqlClient reads app.config: configurable retry logic, authentication providers and switch overrides. See [Trimming](#trimming) | +| `Switch.Microsoft.Data.SqlClient.EnableAppConfig` | `true` | Controls authentication configuration in Abstractions and .NET-only driver retry/switch overrides. Trimmed/AOT authentication paths are removed automatically. See [Trimming](#trimming) | +| `Switch.Microsoft.Data.SqlClient.EnableAzureExtensionDiscovery` | `true` | Controls reflective Azure authentication discovery; trimmed/AOT .NET 10 publishes remove this guarded path | | `Switch.Microsoft.Data.SqlClient.EnableMultiSubnetFailoverByDefault` | `false` | Sets `MultiSubnetFailover=true` as the default for all connections | | `Switch.Microsoft.Data.SqlClient.EnableTransactionIsolationLevelReset` | `false` | Resets a changed session transaction isolation level to `READ COMMITTED` before a pooled connection is reused | | `Switch.Microsoft.Data.SqlClient.EnableUserAgent` | varies | Controls sending user agent information to SQL Server | @@ -288,6 +289,14 @@ Setting `EnableAppConfig` to `false` at publish time removes the configuration r Setting it only at run time, with `AppContext.SetSwitch` or `runtimeconfig.json`, stops the reading but leaves the trim warnings. On .NET Framework the switch has no effect, since trimming does not apply there. ### Guidelines for Adding New Switches +Authentication gates live in Abstractions' `AuthenticationFeatureSwitches.cs`, separately from +the driver's `LocalAppContextSwitches.cs`. Both authentication switches default to `true` and +cache their first value. Separate capability guards remove authentication configuration, +initializer loading and Azure discovery automatically in trimmed/AOT .NET 10 publishes. +Explicit provider registration remains available. Authentication's EnableAppConfig gate also +applies on .NET Framework; the driver's retry/switch-override readers remain .NET-only. +Runtime AppContext changes do not affect publish-time trimming. + 1. Define the switch name constant in `LocalAppContextSwitches.cs` 2. Add a cached property with lazy evaluation pattern (see existing switches) 3. Default to `false` — the switch should opt-in to the new behavior diff --git a/BUILDGUIDE.md b/BUILDGUIDE.md index 6628129b92..4b5aafc192 100644 --- a/BUILDGUIDE.md +++ b/BUILDGUIDE.md @@ -109,7 +109,8 @@ extend the driver's runtime support below .NET Framework 4.6.2 or .NET 10. | Driver reference and unsupported-platform assemblies | `net462;net10.0;netstandard2.0` | | Existing dual-target tests, stress projects, and their shared helpers | `net462;net10.0` | | Modern-only projects, including performance tests | `net10.0` | -| AKV Provider, Abstractions, Logging, and standard-only test utilities | `netstandard2.0` only | +| Abstractions | `netstandard2.0;net10.0` | +| AKV Provider, Logging, and standard-only test utilities | `netstandard2.0` only | | Azure extensions | `net462;netstandard2.0` | | Microsoft.SqlServer.Server (independently versioned; unchanged here) | `net46;netstandard2.0` | | PackageCompatibility tool and tests (xUnit v3) | `net481;net10.0` | @@ -200,6 +201,7 @@ dotnet build -t: [optional_parameters] |----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------| | `Test` | Runs the driver and extension test targets. Without `TestFramework`, all declared frameworks are attempted, including .NET Framework. _This takes considerable time and requires configured servers_. | | `TestAbstractions` | Runs all tests for Microsoft.Data.SqlClient.Extensions.Abstractions | +| `TestAuthenticationConfiguration` | Runs the dedicated current/legacy app.config compatibility scenarios without SQL Server. | | `TestAkvProvider` | Runs the unit test project for Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider. | | `TestAzure` | Runs all tests for Microsoft.Data.SqlClient.Extensions.Azure | | `TestSqlClient` | Runs the unit, functional, and manual test projects for Microsoft.Data.SqlClient. | @@ -214,6 +216,21 @@ PowerShell script tests. The tool test targets use their own runner options rath test parameters below; see the [PackageCompatibility](tools/PackageCompatibility/README.md) and [PackageValidator](tools/PackageValidator/README.md) guides. +Authentication registry, callback, switch, formatter and exception tests are ordinary xUnit tests +under `Extensions/Abstractions/test`. Executable compatibility scenarios live separately under +`src/Microsoft.Data.SqlClient/tests/AuthenticationTests/`. Run these focused targets on Linux: + +```bash +dotnet build build.proj -t:TestAbstractions -p:TestFramework=net10.0 -p:ReferenceType=Project +dotnet build build.proj -t:TestAuthenticationConfiguration -p:TestFramework=net10.0 -p:ReferenceType=Project +``` + +The configuration target supports `ReferenceType=Package` and prepares packages unless +`SkipDependencyPack=true`; use a unique `PackageVersionSqlClient` and an isolated NuGet cache +when validating local packages. It checks transitive configuration compile assets, forwarded +handlers and initializer activation for both section declarations. Executing `net462` scenarios +requires Windows; a Linux cross-build is not runtime validation. + > [!TIP] > In project-reference mode, test targets automatically build their dependencies. In package mode, > prepare the referenced packages first; see [Package Mode Builds](#package-mode-builds). diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationProvider.xml b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationProvider.xml index 7848aaec1a..07fd38db43 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationProvider.xml +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationProvider.xml @@ -129,7 +129,16 @@ See the LICENSE file in the project root for more information. Gets an authentication provider by method. The authentication method. - The authentication provider or if not found. + The authentication provider or if not found or registry initialization fails. + + Bootstraps the registry on first access. Configuration and reflective Azure discovery + are unavailable in trimmed and NativeAOT .NET 10 publishes; explicitly register providers + with SetProvider. Registry initialization failures return null and emit diagnostic traces + through Microsoft.Data.SqlClient.EventSource with the Trace keyword (0x2). + Failures are cached and logged on subsequent accesses, including when tracing is enabled + after the first failure. Correct the reported configuration or package version problem + and restart the application. + Sets an authentication provider by method. @@ -138,6 +147,15 @@ See the LICENSE file in the project root for more information. if the operation succeeded; otherwise, (for example, the existing provider disallows overriding). + + Works directly under NativeAOT. Configuration providers and configured initializers + cannot be overwritten by application registrations. Application registrations replace + the discovered Azure default. A null provider, unsupported method, throwing provider + callback or failed registry initialization returns false and emits diagnostic traces + through Microsoft.Data.SqlClient.EventSource with the Trace keyword (0x2). + Failed registrations do not replace the existing provider. Registry initialization + failures are cached; correct the reported cause and restart the application. + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Abstractions.csproj b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Abstractions.csproj index ba3a673041..122c9445a1 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Abstractions.csproj +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Abstractions.csproj @@ -2,7 +2,9 @@ Microsoft.Data.SqlClient.Extensions.Abstractions - netstandard2.0 + + netstandard2.0;net10.0 + true @@ -83,5 +85,11 @@ + + + + Microsoft.Data.SqlClient.Authentication.%(Filename).resources + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/AuthenticationFeatureSwitches.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/AuthenticationFeatureSwitches.cs new file mode 100644 index 0000000000..a9a3ecc426 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/AuthenticationFeatureSwitches.cs @@ -0,0 +1,126 @@ +// 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.Diagnostics.CodeAnalysis; + +namespace Microsoft.Data.SqlClient; + +/// +/// Defines authentication bootstrap switches and capability guards so configuration loading, Azure +/// extension discovery can be excluded from trimmed or AOT builds. +/// +/// +/// Configuration and discovery default to enabled to preserve existing runtime behavior. Each +/// switch is read on first access and cached for the lifetime of the process. Publish-time switch +/// values allow trimming tools to remove guarded reflection paths; changing an AppContext switch at +/// runtime does not change what was retained during publishing. +/// +internal static class AuthenticationFeatureSwitches +{ + #region Feature Switches + + /// + /// Gets whether authentication providers and initializers may be loaded from app.config. + /// + /// + /// Defaults to true when the AppContext switch is unset. + /// + // Associates this property with the named switch so trimming tools can substitute its + // publish-time value. + [FeatureSwitchDefinition("Switch.Microsoft.Data.SqlClient.EnableAppConfig")] + internal static bool EnableAppConfig => + AcquireAndReturn( + "Switch.Microsoft.Data.SqlClient.EnableAppConfig", + ref s_enableAppConfig, + defaultValue: true); + + /// + /// Gets whether authentication bootstrap may discover the Azure extension provider through + /// reflection. + /// + /// + /// Defaults to true when the AppContext switch is unset. + /// + // Associates this property with the named switch so trimming tools can substitute its + // publish-time value. + [FeatureSwitchDefinition("Switch.Microsoft.Data.SqlClient.EnableAzureExtensionDiscovery")] + internal static bool EnableAzureExtensionDiscovery => + AcquireAndReturn( + "Switch.Microsoft.Data.SqlClient.EnableAzureExtensionDiscovery", + ref s_enableAzureExtensionDiscovery, + defaultValue: true); + + #endregion + + #region Trimming Guards + + /// + /// Guards the reflection-based authentication configuration and initializer loading path. + /// + /// + /// Follows EnableAppConfig. + /// + // Marks this property as guarding calls requiring unreferenced code. Keep it separate from the + // switch definition so trimming removes the guarded path without a publish-time switch value. + [FeatureGuard(typeof(RequiresUnreferencedCodeAttribute))] + // Marks this property as guarding calls requiring dynamic code. + [FeatureGuard(typeof(RequiresDynamicCodeAttribute))] + // Permits a switch-backed guard because trimming replaces this capability guard with false. + [UnconditionalSuppressMessage("Trimming", "IL4000", + Justification = "The separate AppContext feature switch backs this guard; trimming replaces it with false.")] + internal static bool IsAppConfigSupported => EnableAppConfig; + + /// + /// Guards reflection-based Azure extension discovery and provider construction. + /// + /// + /// Follows EnableAzureExtensionDiscovery. + /// + // Marks this property as guarding calls requiring unreferenced code. Keep it separate from the + // switch definition so trimming removes the guarded path without a publish-time switch value. + [FeatureGuard(typeof(RequiresUnreferencedCodeAttribute))] + // Marks this property as guarding calls requiring dynamic code. + [FeatureGuard(typeof(RequiresDynamicCodeAttribute))] + // Permits a switch-backed guard because trimming replaces this capability guard with false. + [UnconditionalSuppressMessage("Trimming", "IL4000", + Justification = "The separate AppContext feature switch backs this guard; trimming replaces it with false.")] + internal static bool IsAzureExtensionDiscoverySupported => EnableAzureExtensionDiscovery; + + #endregion + + #region Helpers + + // The possible states of a feature switch. + private enum SwitchValue : byte + { + None = 0, + True = 1, + False = 2 + } + + // Cached feature switch values. + private static SwitchValue s_enableAppConfig; + private static SwitchValue s_enableAzureExtensionDiscovery; + + // Acquires the feature switch value from AppContext if it hasn't been cached yet. Switches + // that aren't set will assume the default value. + private static bool AcquireAndReturn( + string switchName, ref SwitchValue switchValue, bool defaultValue) + { + // Short circuit if we've already cached the switch value. + if (switchValue != SwitchValue.None) + { + return switchValue == SwitchValue.True; + } + + // Acquire the switch value from AppContext. If not present, use the default value. + bool enabled = + AppContext.TryGetSwitch(switchName, out bool acquiredValue) ? acquiredValue : defaultValue; + + switchValue = enabled ? SwitchValue.True : SwitchValue.False; + return enabled; + } + + #endregion +} diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/AuthenticationStrings.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/AuthenticationStrings.cs new file mode 100644 index 0000000000..46537f7d79 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/AuthenticationStrings.cs @@ -0,0 +1,142 @@ +// 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.Globalization; +using System.Resources; +using System.Text; +using Microsoft.Data.SqlClient.Internal; + +namespace Microsoft.Data.SqlClient; + +/// +/// Creates localized authentication error messages and exceptions using resources owned by +/// Abstractions, without depending on the driver assembly's resource helpers. +/// +internal static class AuthenticationStrings +{ + /// + /// Loads authentication strings from this assembly and its localized satellite assemblies. + /// + private static readonly ResourceManager s_resources = new( + "Microsoft.Data.SqlClient.Authentication.Strings", typeof(AuthenticationStrings).Assembly); + + /// + /// Retrieves a resource using the current UI culture and formats it using the current culture. + /// Recoverable lookup or formatting failures are logged and produce a fallback containing + /// the resource key and argument values. + /// + /// The authentication resource key. + /// Values to substitute into the resource's format placeholders. + /// The localized message, or a generic diagnostic message if formatting fails. + internal static string Format(string key, params object[] args) + { + try + { + string? format = s_resources.GetString(key, CultureInfo.CurrentUICulture); + if (format is not null) + { + return string.Format(CultureInfo.CurrentCulture, format, args); + } + + SqlClientEventSource.Log.TryTraceEvent( + "AuthenticationStrings.Format | Resource '{0}' was not found. Check the authentication resource keys and deployed resource assemblies; using a fallback message.", + key); + } + catch (Exception e) when (ExceptionHelpers.IsRecoverableException(e)) + { + SqlClientEventSource.Log.TryTraceEvent( + "AuthenticationStrings.Format | Resource '{0}' could not be formatted ({1}). Check the deployed resource assemblies, format placeholders and argument values; using a fallback message.", + key, e.GetType().FullName); + } + + args ??= Array.Empty(); + var builder = new StringBuilder(); + builder.Append(key ?? "").Append(": ["); + for (int i = 0; i < args.Length; i++) + { + if (i > 0) + { + builder.Append(", "); + } + + builder.Append(FormatArgument(args[i])); + } + return builder.Append(']').ToString(); + } + + /// + /// Formats an argument using the current culture. Recoverable formatting failures are logged + /// and replaced with a type-name placeholder; fatal runtime errors propagate. + /// + /// The value to format, or null. + /// + /// The formatted value, "<null>" for null, or "<unformattable: type-name>" if the + /// value cannot be formatted. + /// + internal static string FormatArgument(object? argument) + { + if (argument is null) + { + return ""; + } + + try + { + return string.Format(CultureInfo.CurrentCulture, "{0}", argument); + } + catch (Exception e) when (ExceptionHelpers.IsRecoverableException(e)) + { + string typeName = argument.GetType().FullName ?? argument.GetType().Name; + SqlClientEventSource.Log.TryTraceEvent( + "AuthenticationStrings.Format | Argument of type '{0}' could not be formatted ({1}); using a type-name placeholder. Check the argument's formatting implementation.", + typeName, e.GetType().FullName); + return ""; + } + } + + /// + /// Creates an argument exception for a provider construction failure, preserving its cause. + /// + /// The configured authentication method. + /// The configured provider type name. + /// The exception that caused construction to fail. + /// An exception containing the localized message and original exception. + internal static Exception CannotCreateAuthProvider(string authentication, string type, Exception e) => + new ArgumentException(Format("SQL_CannotCreateAuthProvider", authentication, type), e); + + /// + /// Creates an argument exception for an authentication initializer construction failure, + /// preserving its cause. + /// + /// The configured initializer type name. + /// The exception that caused construction to fail. + /// An exception containing the localized message and original exception. + internal static Exception CannotCreateSqlAuthInitializer(string type, Exception e) => + new ArgumentException(Format("SQL_CannotCreateAuthInitializer", type), e); + + /// + /// Creates a not-supported exception when a provider rejects its configured authentication method. + /// + /// The unsupported authentication method. + /// The configured provider type name. + /// An exception identifying the provider and unsupported method. + internal static Exception UnsupportedAuthenticationByProvider(string authentication, string type) => + new NotSupportedException(Format("SQL_UnsupportedAuthenticationByProvider", type, authentication)); + + /// + /// Creates a not-supported exception for an unrecognized authentication method name. + /// + /// The unrecognized configured authentication method. + /// An exception identifying the unsupported method. + internal static Exception UnsupportedAuthentication(string authentication) => + new NotSupportedException(Format("SQL_UnsupportedAuthentication", authentication)); + + /// + /// Creates an invalid-operation exception when the loaded Azure extension cannot support + /// the requested Web Account Manager (WAM) broker option. + /// + /// An exception explaining that the Azure extension must be upgraded. + internal static Exception UseWamBrokerRequiresAzureExtensionUpgrade() => + new InvalidOperationException(Format("SQL_UseWamBrokerRequiresAzureExtensionUpgrade")); +} diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/ExceptionHelpers.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/ExceptionHelpers.cs new file mode 100644 index 0000000000..d05d820ff8 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/ExceptionHelpers.cs @@ -0,0 +1,26 @@ +// 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.Threading; + +namespace Microsoft.Data.SqlClient; + +/// Provides shared exception-handling policy for authentication operations. +internal static class ExceptionHelpers +{ + /// Determines whether a failure can be logged and handled without masking a fatal runtime error. + /// The failure to examine, including its inner exception chain. + /// False if any exception in the chain indicates memory exhaustion, stack overflow, access violation or thread abort; otherwise true. + internal static bool IsRecoverableException(Exception exception) + { + for (Exception? current = exception; current is not null; current = current.InnerException) + { + if (current is OutOfMemoryException or StackOverflowException or AccessViolationException or ThreadAbortException) + { + return false; + } + } + return true; + } +} diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/PackageReadme.md b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/PackageReadme.md index 45cacf13ce..26c5241deb 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/PackageReadme.md +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/PackageReadme.md @@ -17,6 +17,36 @@ The abstractions package allows for: This package supports: - .NET Standard 2.0 (compatible with .NET Framework 4.6.1+, .NET Core 2.0+, and .NET 5+) +- .NET 10.0 (trim/AOT-analyzed authentication registration) + +## Authentication registration + +The provider registry and bootstrap live in this package. `GetProvider` and `SetProvider` +work directly, including under NativeAOT, without loading SqlClient through reflection. +Configuration providers and configured initializers take precedence over application +registrations; application registrations take precedence over the discovered Azure default. +Registry initialization failures return `null` and `false`, respectively. Null providers, +unsupported methods and throwing registration callbacks also return `false`, without replacing +the existing provider. These failures are traced through `Microsoft.Data.SqlClient.EventSource` +(Informational level, Trace keyword `0x2`) with corrective guidance. +Bootstrap failures are cached and logged on subsequent accesses; correct the cause and restart +the application. + +Recoverable resource lookup or formatting failures fall back to the resource key and +culture-formatted argument values, identifying unformattable arguments by type and preserving +the original exception cause. Fatal runtime errors propagate instead of being masked. + +Untrimmed applications retain app.config loading and automatic Azure discovery. These switches +default to `true` and are cached independently on first access for the lifetime of the process: + +- `Switch.Microsoft.Data.SqlClient.EnableAppConfig` +- `Switch.Microsoft.Data.SqlClient.EnableAzureExtensionDiscovery` + +Set switches before first registry access. On .NET 10 trimmed and NativeAOT publishes, both +reflective paths are removed automatically. Explicitly construct and register a provider for +each required authentication method before opening connections. The Azure provider is supplied +by `Microsoft.Data.SqlClient.Extensions.Azure`. Publish-time overrides use +`RuntimeHostConfigurationOption` with `Trim="true"`; runtime changes cannot affect trimming. ## Authentication configuration @@ -29,8 +59,8 @@ remain valid. Section names, properties (`providers`, `initializerType`, `System.Configuration.ConfigurationManager` is a transitive compile dependency, so consumers can use the handlers and their configuration base types without adding a -separate package reference. Authentication initialization and provider loading remain -in the driver. +separate package reference. `SqlAuthenticationInitializer` is also defined in Abstractions +and forwarded from SqlClient, preserving existing compiled consumers and initializer declarations. See the repository's `doc/samples/SqlAuthenticationProviders.config` for a section declaration and custom provider example. diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.cs.resx b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.cs.resx new file mode 100644 index 0000000000..39ff03de6a --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.cs.resx @@ -0,0 +1,26 @@ + + + text/microsoft-resx + System.Resources.ResXResourceReader, System.Windows.Forms, ... + + text/microsoft-resx + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Pro {0} se nepodařilo vytvořit instanci zprostředkovatele ověřování s typem {1}. + + + Nepodařilo se vytvořit instanci SqlAuthenticationInitializer s typem {0}. + + + Zprostředkovatel {0} nepodporuje ověřování {1}. + + + Ověřování {0} není podporované. + + + Nastavení useWamBroker vyžaduje balíček Microsoft.Data.SqlClient.Extensions.Azure, aby se zpřístupnilo Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProviderOptions. Upgradujte balíček Microsoft.Data.SqlClient.Extensions.Azure alespoň na verzi 1.1.0, která zahrnuje tento typ. + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.de.resx b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.de.resx new file mode 100644 index 0000000000..8d5d759a51 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.de.resx @@ -0,0 +1,26 @@ + + + text/microsoft-resx + System.Resources.ResXResourceReader, System.Windows.Forms, ... + + text/microsoft-resx + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Fehler beim Instanziieren eines Authentifizierungsanbieters des Typs "{1}" für "{0}". + + + Fehler beim Instanziieren eines SqlAuthenticationInitializer des Typs "{0}". + + + Die Authentifizierung "{1}" wird vom Anbieter "{0}" nicht unterstützt. + + + Die Authentifizierung "{0}" wird nicht unterstützt. + + + Die Einstellung „useWamBroker“ erfordert das Paket „Microsoft.Data.SqlClient.Extensions.Azure“, um „Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProviderOptions“ bereitzustellen. Aktualisieren Sie das Paket „Microsoft.Data.SqlClient.Extensions.Azure“ auf mindestens Version 1.1.0, die diesen Typ enthält. + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.es.resx b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.es.resx new file mode 100644 index 0000000000..b06a25b6c6 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.es.resx @@ -0,0 +1,26 @@ + + + text/microsoft-resx + System.Resources.ResXResourceReader, System.Windows.Forms, ... + + text/microsoft-resx + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + No se pudo crear una instancia de un proveedor de autenticación con el tipo "{1}" para "{0}". + + + No se pudo crear una instancia de SqlAuthenticationInitializer con el tipo "{0}". + + + El proveedor "{0}" no admite la autenticación "{1}". + + + No se admite la autenticación "{0}". + + + La configuración "useWamBroker" requiere que el paquete "Microsoft.Data.SqlClient.Extensions.Azure" exponga "Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProviderOptions". Actualice el paquete "Microsoft.Data.SqlClient.Extensions.Azure" a, como mínimo, la versión 1.1.0, que incluye este tipo. + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.fr.resx b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.fr.resx new file mode 100644 index 0000000000..e5e11b288f --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.fr.resx @@ -0,0 +1,26 @@ + + + text/microsoft-resx + System.Resources.ResXResourceReader, System.Windows.Forms, ... + + text/microsoft-resx + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Échec de l'instanciation d'un fournisseur d'authentification ayant le type '{1}' pour '{0}'. + + + Échec de l'instanciation d'un SqlAuthenticationInitializer ayant le type '{0}'. + + + Le fournisseur '{0}' ne prend pas en charge l'authentification '{1}'. + + + L'authentification '{0}' n'est pas prise en charge. + + + Le paramètre « useWamBroker » nécessite le package « Microsoft.Data.SqlClient.Extensions.Azure » pour exposer « Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProviderOptions ». Mettez à niveau le package « Microsoft.Data.SqlClient.Extensions.Azure » vers la version 1.1.0 ou ultérieure, qui inclut ce type. + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.it.resx b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.it.resx new file mode 100644 index 0000000000..64122d54cb --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.it.resx @@ -0,0 +1,26 @@ + + + text/microsoft-resx + System.Resources.ResXResourceReader, System.Windows.Forms, ... + + text/microsoft-resx + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Non è stato possibile creare un'istanza di un provider di autenticazione di tipo '{1}' per '{0}'. + + + Non è stato possibile creare un'istanza di un elemento SqlAuthenticationInitializer di tipo '{0}'. + + + Il provider '{0}' non supporta l'autenticazione '{1}'. + + + L'autenticazione '{0}' non è supportata. + + + L'impostazione ''useWamBroker'' richiede che il pacchetto ''Microsoft.Data.SqlClient.Extensions.Azure'' esponga ''Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProviderOptions''. Aggiorna il pacchetto ''Microsoft.Data.SqlClient.Extensions.Azure'' almeno alla versione 1.1.0, che include questo tipo. + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.ja.resx b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.ja.resx new file mode 100644 index 0000000000..6c6cd9ea1a --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.ja.resx @@ -0,0 +1,26 @@ + + + text/microsoft-resx + System.Resources.ResXResourceReader, System.Windows.Forms, ... + + text/microsoft-resx + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + '{0}' の種類 '{1}' の認証プロバイダーをインスタンス化できませんでした。 + + + 種類 '{0}' の SqlAuthenticationInitializer をインスタンス化できませんでした。 + + + プロバイダー '{0}' では認証 '{1}' はサポートされていません。 + + + 認証 '{0}' はサポートされていません。 + + + 'useWamBroker' を設定するには、'Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProviderOptions' を公開する 'Microsoft.Data.SqlClient.Extensions.Azure' パッケージが必要です。この型を含む v1.1.0 以降の 'Microsoft.Data.SqlClient.Extensions.Azure' パッケージにアップグレードしてください。 + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.ko.resx b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.ko.resx new file mode 100644 index 0000000000..f550963c25 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.ko.resx @@ -0,0 +1,26 @@ + + + text/microsoft-resx + System.Resources.ResXResourceReader, System.Windows.Forms, ... + + text/microsoft-resx + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + '{0}'에 대해 '{1}' 형식을 사용하여 인증 공급자를 인스턴스화하지 못했습니다. + + + '{0}' 형식을 사용하여 SqlAuthenticationInitializer를 인스턴스화하지 못했습니다. + + + '{0}' 공급자에서 '{1}' 인증을 지원하지 않습니다. + + + '{0}' 인증을 사용할 수 없습니다. + + + 'useWamBroker'를 설정하려면 'Microsoft.Data.SqlClient.Extensions.Azure' 패키지가 'Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProviderOptions'를 노출해야 합니다. 이 형식을 포함하는 'Microsoft.Data.SqlClient.Extensions.Azure' 패키지를 v1.1.0 이상으로 업그레이드하세요. + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.pl.resx b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.pl.resx new file mode 100644 index 0000000000..dfbe7dc8e1 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.pl.resx @@ -0,0 +1,26 @@ + + + text/microsoft-resx + System.Resources.ResXResourceReader, System.Windows.Forms, ... + + text/microsoft-resx + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Nie można utworzyć wystąpienia dostawcy uwierzytelniania typu {1} dla elementu {0}. + + + Nie można utworzyć wystąpienia elementu SqlAuthenticationInitializer typu {0}. + + + Dostawca „{0}” nie obsługuje uwierzytelniania „{1}”. + + + Uwierzytelnianie „{0}” nie jest obsługiwane. + + + Ustawienie „useWamBroker” wymaga pakietu „Microsoft.Data.SqlClient.Extensions.Azure”, aby uwidoczniono element „Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProviderOptions”. Uaktualnij pakiet „Microsoft.Data.SqlClient.Extensions.Azure” do wersji co najmniej 1.1.0, która zawiera ten typ. + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.pt-BR.resx b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.pt-BR.resx new file mode 100644 index 0000000000..aa028550e4 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.pt-BR.resx @@ -0,0 +1,26 @@ + + + text/microsoft-resx + System.Resources.ResXResourceReader, System.Windows.Forms, ... + + text/microsoft-resx + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Falha ao criar uma instância de um provedor de autenticação com o tipo '{1}' para '{0}'. + + + Falha ao criar um SqlAuthenticationInitializer com o tipo '{0}'. + + + O provedor '{0}' não dá suporte à autenticação '{1}'. + + + A autenticação '{0}' não tem suporte. + + + A definição ''useWamBroker'' exige que o pacote ''Microsoft.Data.SqlClient.Extensions.Azure'' exponha ''Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProviderOptions''. Atualize o pacote ''Microsoft.Data.SqlClient.Extensions.Azure'' para a versão v1.1.0 ou posterior, que inclui esse tipo. + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.resx b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.resx new file mode 100644 index 0000000000..5516220de9 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.resx @@ -0,0 +1,26 @@ + + + text/microsoft-resx + System.Resources.ResXResourceReader, System.Windows.Forms, ... + + text/microsoft-resx + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Failed to instantiate an authentication provider with type '{1}' for '{0}'. + + + Failed to instantiate a SqlAuthenticationInitializer with type '{0}'. + + + The provider '{0}' does not support authentication '{1}'. + + + The authentication '{0}' is not supported. + + + Setting 'useWamBroker' requires the 'Microsoft.Data.SqlClient.Extensions.Azure' package to expose 'Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProviderOptions'. Upgrade the 'Microsoft.Data.SqlClient.Extensions.Azure' package to at least v1.1.0 that includes this type. + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.ru.resx b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.ru.resx new file mode 100644 index 0000000000..821479cdd2 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.ru.resx @@ -0,0 +1,26 @@ + + + text/microsoft-resx + System.Resources.ResXResourceReader, System.Windows.Forms, ... + + text/microsoft-resx + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Не удалось создать экземпляр поставщика проверки подлинности с типом "{1}" для "{0}". + + + Не удалось создать экземпляр SqlAuthenticationInitializer с типом "{0}". + + + Поставщик "{0}" не поддерживает проверку подлинности "{1}". + + + Проверка подлинности "{0}" не поддерживается. + + + Для установки параметра "useWamBroker" требуется, чтобы пакет "Microsoft.Data.SqlClient.Extensions.Azure" предоставлял доступ к "Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProviderOptions". Обновите пакет "Microsoft.Data.SqlClient.Extensions.Azure" как минимум до версии v1.1.0, в которую входит этот тип. + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.tr.resx b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.tr.resx new file mode 100644 index 0000000000..680f4e9f66 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.tr.resx @@ -0,0 +1,26 @@ + + + text/microsoft-resx + System.Resources.ResXResourceReader, System.Windows.Forms, ... + + text/microsoft-resx + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + '{0}' için '{1}' türüne sahip bir kimlik doğrulama sağlayıcısı örneği oluşturulamadı. + + + '{0}' türüne sahip bir SqlAuthenticationInitializer örneği oluşturulamadı. + + + '{0}' adlı sağlayıcı, '{1}' kimlik doğrulamasını desteklemiyor. + + + '{0}' kimlik doğrulaması desteklenmiyor. + + + 'useWamBroker' ayarı, 'Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProviderOptions' türünü kullanıma sunmak için 'Microsoft.Data.SqlClient.Extensions.Azure' paketini gerektirir. Bu türü içeren en az v1.1.0 sürümüne ulaşmak için 'Microsoft.Data.SqlClient.Extensions.Azure' paketini yükseltin. + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.zh-Hans.resx b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.zh-Hans.resx new file mode 100644 index 0000000000..4195615789 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.zh-Hans.resx @@ -0,0 +1,26 @@ + + + text/microsoft-resx + System.Resources.ResXResourceReader, System.Windows.Forms, ... + + text/microsoft-resx + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 未能实例化“{0}”的类型为“{1}”的身份验证提供程序。 + + + 未能实例化类型为“{0}”的 SqlAuthenticationInitializer。 + + + 提供程序“{0}”不支持身份验证“{1}”。 + + + 不支持身份验证“{0}”。 + + + 要设置 "useWamBroker",需要 "Microsoft.Data.SqlClient.Extensions.Azure" 包公开 "Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProviderOptions"。将 "Microsoft.Data.SqlClient.Extensions.Azure" 包升级到至少 v1.1.0,该版本包含此类型。 + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.zh-Hant.resx b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.zh-Hant.resx new file mode 100644 index 0000000000..c1cb57b569 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Resources/Strings.zh-Hant.resx @@ -0,0 +1,26 @@ + + + text/microsoft-resx + System.Resources.ResXResourceReader, System.Windows.Forms, ... + + text/microsoft-resx + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 無法為 '{0}' 具現化類型為 '{1}' 的驗證提供者。 + + + 無法將類型為 '{0}' 的 SqlAuthenticationInitializer 具現化。 + + + 提供者 '{0}' 不支援驗證 '{1}'。 + + + 不支援驗證 '{0}'。 + + + 設定 'useWamBroker' 需要 'Microsoft.Data.SqlClient.Extensions.Azure' 套件以公開 'Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProviderOptions'。請將 'Microsoft.Data.SqlClient.Extensions.Azure' 套件升級至至少 v1.1.0,其中包含此類型。 + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationInitializer.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationInitializer.cs new file mode 100644 index 0000000000..e783b1ca3a --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationInitializer.cs @@ -0,0 +1,15 @@ +// 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. + +namespace Microsoft.Data.SqlClient; + +/// Initializes authentication providers configured in app.config. +public abstract class SqlAuthenticationInitializer +{ + /// Creates an authentication initializer. + protected SqlAuthenticationInitializer() { } + + /// Registers application-configured authentication providers. + public abstract void Initialize(); +} diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProvider.Internal.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProvider.Internal.cs deleted file mode 100644 index 5007384465..0000000000 --- a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProvider.Internal.cs +++ /dev/null @@ -1,198 +0,0 @@ -// 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.Reflection; -using System.Runtime.InteropServices; -using Microsoft.Data.SqlClient.Internal; - -namespace Microsoft.Data.SqlClient; - -/// -// -// This part of the SqlAuthenticationProvider class implements the static -// GetProvider and SetProvider methods by reflection into the Microsoft.Data.SqlClient -// package's SqlAuthenticationProviderManager class, if that assembly is present. -// -public abstract partial class SqlAuthenticationProvider -{ - /// - /// This class implements the static GetProvider and SetProvider methods by - /// using reflection to call into the Microsoft.Data.SqlClient package's - /// SqlAuthenticationProviderManager class, if that assembly is present. - /// - private static class Internal - { - /// - /// Our handle to the reflected GetProvider() method. - /// - private static readonly MethodInfo? _getProvider = null; - - /// - /// Our handle to the reflected SetProvider() method. - /// - private static readonly MethodInfo? _setProvider = null; - - /// - /// Static construction performs the reflection lookups. - /// - static Internal() - { - const string assemblyName = "Microsoft.Data.SqlClient"; - - // If the MDS package is present, load its - // SqlAuthenticationProviderManager class and get/set methods. - try - { - // Try to load the MDS assembly. - var assembly = Assembly.Load(assemblyName); - - if (assembly is null) - { - Log($"MDS assembly={assemblyName} not found; " + - "Get/SetProvider() will not function"); - return; - } - - // TODO(https://sqlclientdrivers.visualstudio.com/ADO.Net/_workitems/edit/39845): - // Verify the assembly is signed by us? - - // Look for the manager class. - const string className = "Microsoft.Data.SqlClient.SqlAuthenticationProviderManager"; - Type? manager = assembly.GetType(className); - - if (manager is null) - { - Log($"MDS auth manager manager class={className} not found; " + - "Get/SetProvider() will not function"); - return; - } - - // Get handles to the get/set static methods. - _getProvider = manager.GetMethod( - "GetProvider", - BindingFlags.NonPublic | BindingFlags.Static); - - if (_getProvider is null) - { - Log($"MDS GetProvider() method not found; " + - "GetProvider() will not function"); - } - - _setProvider = manager.GetMethod( - "SetProvider", - BindingFlags.NonPublic | BindingFlags.Static); - - if (_setProvider is null) - { - Log($"MDS SetProvider() method not found; " + - "SetProvider() will not function"); - } - } - // All of these exceptions mean we couldn't find the get/set - // methods. - catch (Exception ex) - when (ex is AmbiguousMatchException - or BadImageFormatException - or FileLoadException - or FileNotFoundException) - { - Log($"MDS assembly={assemblyName} not found or not usable; " + - $"Get/SetProvider() will not function: {ex} "); - } - // Any other exceptions are fatal. - } - - /// - /// Call the reflected GetProvider method. - /// - /// - /// The authentication method whose provider to get. - /// - /// - /// Returns null if reflection failed or any exceptions occur. - /// Otherwise, returns as the reflected method does. - /// - internal static SqlAuthenticationProvider? GetProvider( - SqlAuthenticationMethod authenticationMethod) - { - if (_getProvider is null) - { - return null; - } - - try - { - return _getProvider.Invoke(null, [authenticationMethod]) - as SqlAuthenticationProvider; - } - catch (Exception ex) - when (ex is InvalidOperationException - or MemberAccessException - or MethodAccessException - or NotSupportedException - or TargetInvocationException) - { - Log($"GetProvider() invocation failed: " + - $"{ex.GetType().Name}: {ex.Message}"); - return null; - } - } - - /// - /// Call the reflected SetProvider method. - /// - /// - /// The authentication method whose provider to set. - /// - /// - /// The provider to set. - /// - /// - /// Returns false if reflection failed, invocation fails, or any - /// exceptions occur. Otherwise, returns as the reflected method - /// does. - /// - internal static bool SetProvider( - SqlAuthenticationMethod authenticationMethod, - SqlAuthenticationProvider provider) - { - if (_setProvider is null) - { - return false; - } - - try - { - bool? result = - _setProvider.Invoke(null, [authenticationMethod, provider]) - as bool?; - - if (!result.HasValue) - { - Log($"SetProvider() invocation returned null; " + - "translating to false"); - return false; - } - - return result.Value; - } - catch (Exception ex) - when (ex is InvalidOperationException - or MemberAccessException - or MethodAccessException - or NotSupportedException - or TargetInvocationException) - { - Log($"SetProvider() invocation failed: " + - $"{ex.GetType().Name}: {ex.Message}"); - return false; - } - } - - private static void Log(string message) - { - SqlClientEventSource.Log.TryTraceEvent("SqlAuthenticationProvider.Internal | {0}", message); - } - } -} diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProvider.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProvider.cs index 86c045efc0..1a9c3139f7 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProvider.cs +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProvider.cs @@ -21,25 +21,17 @@ public virtual void BeforeUnload(SqlAuthenticationMethod authenticationMethod) { /// - // - // We would like to deprecate this method in favour of - // SqlAuthenticationProviderManager.GetProvider(). - // public static SqlAuthenticationProvider? GetProvider( SqlAuthenticationMethod authenticationMethod) { - return Internal.GetProvider(authenticationMethod); + return SqlAuthenticationProviderRegistry.GetProvider(authenticationMethod); } /// - // - // We would like to deprecate this method in favour of - // SqlAuthenticationProviderManager.SetProvider(). - // public static bool SetProvider( SqlAuthenticationMethod authenticationMethod, SqlAuthenticationProvider provider) { - return Internal.SetProvider(authenticationMethod, provider); + return SqlAuthenticationProviderRegistry.SetProvider(authenticationMethod, provider); } } diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProviderRegistry.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProviderRegistry.cs new file mode 100644 index 0000000000..771d839e38 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProviderRegistry.cs @@ -0,0 +1,729 @@ +// 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.Generic; +using System.Configuration; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.ExceptionServices; +using Microsoft.Data.SqlClient.Internal; + +namespace Microsoft.Data.SqlClient; + +/// +/// Owns authentication provider registration and bootstrap in Abstractions so the public +/// registration API can access providers directly, including under NativeAOT. +/// +/// +/// Serializes registry access and gives configuration registrations precedence over application +/// registrations and discovered Azure defaults. Recoverable bootstrap failures are cached and +/// logged while public registry operations preserve their null/false failure results. +/// +internal sealed class SqlAuthenticationProviderRegistry +{ + /// The optional Azure extension assembly discovered during bootstrap. + private const string azureAssemblyName = "Microsoft.Data.SqlClient.Extensions.Azure"; + + /// The expected Azure extension public key token used by strong-name verification. + private static readonly byte[] s_azurePublicKeyToken = [0x23, 0xec, 0x7f, 0xc2, 0xd6, 0xea, 0xa4, 0xa5]; + + /// Synchronizes bootstrap, provider lookup, registration and replacement callbacks. + private readonly object _sync = new(); + /// Indicates that all enabled bootstrap steps completed successfully. + private bool _initialized; + /// Prevents recursive bootstrap when an initializer accesses the registry. + private bool _initializing; + /// Assigns configuration precedence to registrations made by the configured initializer. + private bool _runningInitializer; + /// Preserves the original bootstrap failure for subsequent registry accesses. + private ExceptionDispatchInfo? _bootstrapFailure; + + /// + /// Runs the enabled configuration and Azure discovery steps on first access. + /// + /// + /// Reentrant initializer registrations can use the registry while bootstrap is in progress. + /// A recoverable failure is cached and rethrown internally; GetProvider and SetProvider log it + /// and return null or false instead of exposing it through the public API. + /// + private void EnsureInitialized() + { + lock (_sync) + { + _bootstrapFailure?.Throw(); + if (_initialized || _initializing) + { + return; + } + _initializing = true; + try + { + if (AuthenticationFeatureSwitches.IsAppConfigSupported) + { + LoadConfiguration(); + } + if (AuthenticationFeatureSwitches.IsAzureExtensionDiscoverySupported) + { + LoadAzureExtension(); + } + _initialized = true; + } + catch (Exception e) when (ExceptionHelpers.IsRecoverableException(e)) + { + // Keep the original exception, rather than poisoning a static constructor. + SqlClientEventSource.Log.TryTraceEvent( + "Authentication bootstrap failed: {0}", e); + _bootstrapFailure = ExceptionDispatchInfo.Capture(e); + throw; + } + finally + { + _initializing = false; + } + } + } + + /// + /// Loads authentication settings from the current app.config section, falling back to the legacy section. + /// + /// + /// Configuration retrieval errors are logged and ignored. Provider and initializer failures + /// from applying a retrieved section propagate to bootstrap failure handling. + /// + [RequiresUnreferencedCode("Authentication configuration loads provider types by name.")] + [RequiresDynamicCode("Authentication configuration constructs providers dynamically.")] + private void LoadConfiguration() + { + ConfigurationSection? configurationSection = null; + + try + { + // New configuration section "SqlClientAuthenticationProviders" for Microsoft.Data.SqlClient accepted to avoid conflicts with older one. + configurationSection = FetchConfigurationSection("SqlClientAuthenticationProviders", + "Microsoft.Data.SqlClient.SqlClientAuthenticationProviderConfigurationSection"); + if (configurationSection == null) + { + // If configuration section is not yet found, try with old Configuration Section name for backwards compatibility + configurationSection = FetchConfigurationSection("SqlAuthenticationProviders", + "Microsoft.Data.SqlClient.SqlAuthenticationProviderConfigurationSection"); + } + } + catch (ConfigurationErrorsException e) + { + // Don't throw an error for invalid config files + SqlClientEventSource.Log.TryTraceEvent("static SqlAuthenticationProviderRegistry: Unable to load custom SqlAuthenticationProviders or SqlClientAuthenticationProviders. Correct app.config syntax and section handler declarations. ConfigurationManager failed to load due to configuration errors: {0}", e); + } + + ApplyConfiguration(configurationSection); + } + + /// + /// Discovers the optional Azure extension and registers its provider as the authentication default. + /// + /// + /// Validates the extension's public key token in signed builds. + /// Defaults do not replace existing registrations. Handled loading or construction failures + /// are logged and leave the application responsible for supplying a provider. + /// + [RequiresUnreferencedCode("Azure extension discovery loads types and constructors by name.")] + [RequiresDynamicCode("Azure extension discovery constructs providers dynamically.")] + private void LoadAzureExtension() + { + // If our Azure extensions package is present, use its authentication provider as our + // default. + try + { + // Try to load our Azure extension. +#if STRONG_NAME_SIGNING + + // When strong-name signing is enabled, build a fully-qualified AssemblyName + // that includes the expected public key token. + + SqlClientEventSource.Log.TryTraceEvent( + nameof(SqlAuthenticationProviderRegistry) + + ": Attempting to load Azure extension assembly={0} with " + + "expected public key token={1}", + azureAssemblyName, + BitConverter.ToString(s_azurePublicKeyToken).Replace("-", "")); + + var qualifiedName = new AssemblyName(azureAssemblyName); + qualifiedName.SetPublicKeyToken(s_azurePublicKeyToken); + + // The .NET Framework runtime will enforce the token during binding, causing Load() + // to throw. This prevents an untrusted assembly from being loaded and having its + // module initializers run. This will throw if the public key token doesn't match. + // + // The .NET runtime ignores the public key token and will happily load any assembly + // with the same simple name. + // + var assembly = Assembly.Load(qualifiedName); + +#if !NETFRAMEWORK + // For the .NET runtime, we will check the public key token ourselves. + // + // Note that a null assembly is handled below. + if (assembly is not null) + { + byte[]? actualToken = assembly.GetName().GetPublicKeyToken(); + + if (actualToken is null || !actualToken.SequenceEqual(s_azurePublicKeyToken)) + { + SqlClientEventSource.Log.TryTraceEvent( + nameof(SqlAuthenticationProviderRegistry) + + ": Azure extension assembly={0} has an " + + "unexpected public key token; " + + "no default Active Directory provider installed", + assembly.GetName()); + return; + } + } +#endif + +#else + + SqlClientEventSource.Log.TryTraceEvent( + nameof(SqlAuthenticationProviderRegistry) + + ": Attempting to load Azure extension assembly={0} without " + + "strong name verification; ensure this assembly is from a trusted source", + azureAssemblyName); + + var assembly = Assembly.Load(azureAssemblyName); + +#endif + + if (assembly is null) + { + SqlClientEventSource.Log.TryTraceEvent( + nameof(SqlAuthenticationProviderRegistry) + + ": Azure extension assembly={0} not found; " + + "no default Active Directory provider installed", + azureAssemblyName); + return; + } + + SqlClientEventSource.Log.TryTraceEvent( + nameof(SqlAuthenticationProviderRegistry) + + ": Azure extension assembly={0} found; " + + "attempting to set as default provider for all Active " + + "Directory authentication methods", + assembly.GetName()); + + // Look for the authentication provider class. + const string className = "Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProvider"; + Type? type = assembly.GetType(className); + + if (type is null) + { + SqlClientEventSource.Log.TryTraceEvent( + nameof(SqlAuthenticationProviderRegistry) + + ": Azure extension does not contain class={0}; " + + "no default Active Directory provider installed", + className); + + return; + } + + // Try to instantiate it. Behavior depends on what the app + // configured in : + // * Neither applicationClientId nor useWamBroker -> use the + // parameterless constructor (defaults to the SqlClient + // first-party app id and enables WAM brokering on Windows). + // * applicationClientId only -> prefer the + // (ActiveDirectoryAuthenticationProviderOptions) constructor + // when the Azure extension exposes it; otherwise fall back + // to the legacy (string applicationClientId) constructor so + // older Azure extension versions keep working. + // * useWamBroker (with or without applicationClientId) -> + // requires the (Options) constructor because there is no + // positional analog. If the Azure extension is too old to + // expose Options, throw to surface the misconfiguration. + const string optionsTypeName = "Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProviderOptions"; + Type? optionsType = assembly.GetType(optionsTypeName); + + SqlAuthenticationProvider? instance = CreateAzureAuthenticationProvider( + type, + optionsType, + Instance._applicationClientId, + Instance._useWamBroker); + + if (instance is null) + { + SqlClientEventSource.Log.TryTraceEvent( + nameof(SqlAuthenticationProviderRegistry) + + ": Failed to instantiate Azure extension class={0}; " + + "no default Active Directory provider installed", + className); + + return; + } + + // We successfully instantiated the provider, so set it as the + // default for all Active Directory authentication methods. + // + // Note that SetProvider() will refuse to clobber an application + // specified provider, so these defaults will only be applied + // for methods that do not already have a provider. + Register(SqlAuthenticationMethod.ActiveDirectoryIntegrated, instance, RegistrationTier.Default); +#pragma warning disable 0618 // Type or member is obsolete + Register(SqlAuthenticationMethod.ActiveDirectoryPassword, instance, RegistrationTier.Default); +#pragma warning restore 0618 // Type or member is obsolete + Register(SqlAuthenticationMethod.ActiveDirectoryInteractive, instance, RegistrationTier.Default); + Register(SqlAuthenticationMethod.ActiveDirectoryServicePrincipal, instance, RegistrationTier.Default); + Register(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, instance, RegistrationTier.Default); + Register(SqlAuthenticationMethod.ActiveDirectoryManagedIdentity, instance, RegistrationTier.Default); + Register(SqlAuthenticationMethod.ActiveDirectoryMSI, instance, RegistrationTier.Default); + Register(SqlAuthenticationMethod.ActiveDirectoryDefault, instance, RegistrationTier.Default); + Register(SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity, instance, RegistrationTier.Default); + + SqlClientEventSource.Log.TryTraceEvent( + nameof(SqlAuthenticationProviderRegistry) + + ": Azure extension class={0} installed as " + + "provider for all Active Directory authentication methods", + className); + } + // All of these exceptions mean we couldn't find or instantiate the + // Azure extension's authentication provider, in which case we + // simply have no default and the app must provide one if they + // attempt to use Active Directory authentication. + catch (Exception ex) + when (ex is + AmbiguousMatchException or + ArgumentException or + BadImageFormatException or + FileLoadException or + FileNotFoundException or + MemberAccessException or + MethodAccessException or + MissingMethodException or + NotSupportedException or + TargetInvocationException or + TypeInitializationException or + TypeLoadException) + { + SqlClientEventSource.Log.TryTraceEvent( + nameof(SqlAuthenticationProviderRegistry) + + ": Azure extension assembly={0} not found or " + + "not usable; no default provider installed; " + + "{1}: {2}", + azureAssemblyName, + ex.GetType().Name, + ex.Message); + } + // Any other exceptions are fatal. + } + + /// The shared registry, available before bootstrap invokes a configured initializer. + private static readonly SqlAuthenticationProviderRegistry Instance = new(); + + /// Registration precedence, ordered from lowest to highest. + private enum RegistrationTier + { + /// A discovered Azure provider that cannot replace an existing registration. + Default, + /// An application registration that can replace a discovered default. + User, + /// A configured provider or initializer registration that application code cannot override. + Config + } + + /// Providers indexed by the authentication methods they serve. + private readonly Dictionary _providers = new(); + /// The precedence assigned to each registered authentication method. + private readonly Dictionary _tiers = new(); + /// The optional application client ID read from authentication configuration. + private string? _applicationClientId; + + /// The optional Web Account Manager (WAM) broker override read from app.config. + /// + /// Null preserves the Azure provider's default broker behavior rather than explicitly enabling + /// or disabling it. + /// + private bool? _useWamBroker; + + /// + /// Applies Azure options, invokes a configured initializer and registers configured providers. + /// + /// The authentication configuration section, or null if none was found. + /// Initializer and provider registrations receive configuration precedence. + /// A configured provider or initializer could not be created or initialized. + /// A configured method is unrecognized or rejected by its provider. + [RequiresUnreferencedCode("Configured initializer and provider types are loaded by name.")] + [RequiresDynamicCode("Configured initializer and providers are constructed dynamically.")] + private void ApplyConfiguration(ConfigurationSection? configSection) + { + if (configSection == null) + { + SqlClientEventSource.Log.TryTraceEvent("SqlAuthenticationProviderRegistry | {0}", "Neither SqlClientAuthenticationProviders nor SqlAuthenticationProviders configuration section found."); + return; + } + + string applicationClientId = ReadString(configSection, "applicationClientId"); + string useWamBrokerValue = ReadString(configSection, "useWamBroker"); + string initializerTypeName = ReadString(configSection, "initializerType"); + var providers = configSection.ElementInformation.Properties["providers"].Value + as ProviderSettingsCollection; + + if (!string.IsNullOrEmpty(applicationClientId)) + { + _applicationClientId = applicationClientId; + SqlClientEventSource.Log.TryTraceEvent("SqlAuthenticationProviderRegistry | {0}", "Received user-defined Application Client Id"); + } + else + { + SqlClientEventSource.Log.TryTraceEvent("SqlAuthenticationProviderRegistry | {0}", "No user-defined Application Client Id found."); + } + + if (!string.IsNullOrEmpty(useWamBrokerValue)) + { + if (bool.TryParse(useWamBrokerValue, out bool useWamBroker)) + { + _useWamBroker = useWamBroker; + SqlClientEventSource.Log.TryTraceEvent("SqlAuthenticationProviderRegistry | {0}", $"Received user-defined UseWamBroker={useWamBroker}."); + } + else + { + SqlClientEventSource.Log.TryTraceEvent("SqlAuthenticationProviderRegistry | {0}", $"Ignoring user-defined UseWamBroker='{useWamBrokerValue}': not a valid boolean."); + } + } + else + { + SqlClientEventSource.Log.TryTraceEvent("SqlAuthenticationProviderRegistry | {0}", "No user-defined UseWamBroker found."); + } + + // Create user-defined auth initializer, if any. + if (!string.IsNullOrEmpty(initializerTypeName)) + { + try + { + var initializerType = Type.GetType(initializerTypeName, true); + if (initializerType is not null) + { + var initializer = (SqlAuthenticationInitializer?)Activator.CreateInstance(initializerType); + if (initializer is not null) + { + _runningInitializer = true; + try + { + initializer.Initialize(); + } + finally + { + _runningInitializer = false; + } + } + } + } + catch (Exception e) + { + throw AuthenticationStrings.CannotCreateSqlAuthInitializer(initializerTypeName, e); + } + SqlClientEventSource.Log.TryTraceEvent("SqlAuthenticationProviderRegistry | {0}", "Created user-defined SqlAuthenticationInitializer."); + } + else + { + SqlClientEventSource.Log.TryTraceEvent("SqlAuthenticationProviderRegistry | {0}", "No user-defined SqlAuthenticationInitializer found."); + } + + // add user-defined providers, if any. + if (providers != null && providers.Count > 0) + { + foreach (ProviderSettings providerSettings in providers) + { + SqlAuthenticationMethod authentication = AuthenticationEnumFromString(providerSettings.Name); + SqlAuthenticationProvider? provider; + try + { + var providerType = Type.GetType(providerSettings.Type, true); + if (providerType is null) + { + continue; + } + provider = (SqlAuthenticationProvider?)Activator.CreateInstance(providerType); + } + catch (Exception e) + { + throw AuthenticationStrings.CannotCreateAuthProvider(authentication.ToString(), providerSettings.Type, e); + } + if (provider is null) + { + continue; + } + if (!provider.IsSupported(authentication)) + { + throw AuthenticationStrings.UnsupportedAuthenticationByProvider(authentication.ToString(), providerSettings.Type); + } + + Register(authentication, provider, RegistrationTier.Config); + SqlClientEventSource.Log.TryTraceEvent("SqlAuthenticationProviderRegistry | {0}", string.Format("Added user-defined auth provider: {0} for authentication {1}.", providerSettings?.Type, authentication)); + } + } + else + { + SqlClientEventSource.Log.TryTraceEvent("SqlAuthenticationProviderRegistry | {0}", "No user-defined auth providers."); + } + } + + /// Reads a string-valued property from a configuration section. + /// The section containing the property. + /// The configuration property name. + /// The string value, or an empty string if the property's value is null or not a string. + private static string ReadString(ConfigurationSection section, string name) => + section.ElementInformation.Properties[name].Value as string ?? string.Empty; + + /// Gets the registered provider for an authentication method after ensuring bootstrap. + /// The authentication method to look up. + /// The provider, or null if none is registered or bootstrap has a recoverable failure. + /// Recoverable failures, including cached bootstrap failures, emit actionable diagnostic traces. + internal static SqlAuthenticationProvider? GetProvider(SqlAuthenticationMethod authenticationMethod) + { + lock (Instance._sync) + { + try + { + Instance.EnsureInitialized(); + return Instance._providers.TryGetValue(authenticationMethod, out SqlAuthenticationProvider? value) ? value : null; + } + catch (Exception e) when (ExceptionHelpers.IsRecoverableException(e)) + { + SqlClientEventSource.Log.TryTraceEvent( + "SqlAuthenticationProvider.GetProvider failed for {0}; returning null. " + + "Check app.config provider and initializer type names, public parameterless constructors, " + + "initializer implementations and supported authentication methods. Upgrade all SqlClient family packages together " + + "to the same version. Bootstrap failures are cached; correct the cause and restart the application. Error: {1}", + authenticationMethod, e); + return null; + } + } + } + + /// Reflectively constructs an Azure authentication provider using the configured overrides. + /// The Azure authentication provider type to instantiate. + /// The Azure options type, or null if the extension does not expose it. + /// The application client ID override, or null to retain the default. + /// The WAM broker override, or null to retain the default. + /// + /// The constructed provider, or null if a client-ID-only override has no compatible constructor + /// or reflective construction does not produce an authentication provider. + /// + /// + /// Uses the parameterless constructor when neither override is set. A client-ID-only override + /// prefers the options constructor and falls back to the legacy string constructor. + /// A broker override requires the options constructor; during bootstrap, incompatibility is + /// cached and logged while public GetProvider/SetProvider return null/false. + /// + /// A broker override was requested but the extension lacks the required options constructor. + [RequiresUnreferencedCode("Azure provider constructors and properties are discovered by name.")] + [RequiresDynamicCode("Azure providers are constructed dynamically.")] + internal static SqlAuthenticationProvider? CreateAzureAuthenticationProvider( + Type providerType, + Type? optionsType, + string? applicationClientId, + bool? useWamBroker) + { + if (applicationClientId is null && useWamBroker is null) + { + return Activator.CreateInstance(providerType) as SqlAuthenticationProvider; + } + + ConstructorInfo? optionsCtor = optionsType is null + ? null + : providerType.GetConstructor([optionsType]); + + if (useWamBroker is bool useWam) + { + if (optionsType is null || optionsCtor is null) + { + throw AuthenticationStrings.UseWamBrokerRequiresAzureExtensionUpgrade(); + } + + var options = Activator.CreateInstance(optionsType); + if (options is null) + { + return null; + } + + if (applicationClientId is not null) + { + optionsType.GetProperty("ApplicationClientId") + ?.SetValue(options, applicationClientId); + } + optionsType.GetProperty("UseWamBroker") + ?.SetValue(options, useWam); + + return optionsCtor.Invoke([options]) as SqlAuthenticationProvider; + } + + // applicationClientId-only: prefer Options when the extension exposes it, + // otherwise fall back to the legacy (string) ctor for backward compatibility + // with older Azure extension versions. + if (optionsType is not null && optionsCtor is not null) + { + var options = Activator.CreateInstance(optionsType); + if (options is null) + { + return null; + } + optionsType.GetProperty("ApplicationClientId") + ?.SetValue(options, applicationClientId); + return optionsCtor.Invoke([options]) as SqlAuthenticationProvider; + } + + ConstructorInfo? legacyCtor = providerType.GetConstructor([typeof(string)]); + if (legacyCtor is not null) + { + return legacyCtor.Invoke([applicationClientId]) as SqlAuthenticationProvider; + } + + return null; + } + + /// + /// Registers an authentication provider with application or configured-initializer precedence. + /// + /// Authentication method. + /// Authentication provider. + /// + /// True if registration succeeds; false on recoverable initialization, argument or callback + /// errors, or if an existing registration has higher precedence. + /// + /// + /// Recoverable failures and precedence refusals emit diagnostic traces. Registrations made + /// by a configured initializer receive configuration precedence rather than application precedence. + /// + internal static bool SetProvider(SqlAuthenticationMethod authenticationMethod, SqlAuthenticationProvider provider) + { + lock (Instance._sync) + { + try + { + Instance.EnsureInitialized(); + if (provider is null) + { + throw new ArgumentNullException(nameof(provider)); + } + return Instance.Register(authenticationMethod, provider, + Instance._runningInitializer ? RegistrationTier.Config : RegistrationTier.User); + } + catch (Exception e) when (ExceptionHelpers.IsRecoverableException(e)) + { + SqlClientEventSource.Log.TryTraceEvent( + "SqlAuthenticationProvider.SetProvider failed for {0}; returning false. " + + "Pass a non-null provider that supports this method and fix any throwing IsSupported, " + + "BeforeLoad or BeforeUnload callbacks. For bootstrap errors, check app.config provider and " + + "initializer type names, public parameterless constructors and initializer implementations, " + + "and upgrade all SqlClient family " + + "packages together to the same version. Bootstrap failures are cached; correct the cause and " + + "restart the application. Error: {1}", + authenticationMethod, e); + return false; + } + } + } + + /// Validates and publishes a provider registration according to its precedence. + /// The authentication method to register. + /// The provider that must support the method. + /// The precedence assigned to this registration. + /// True if registered; false if precedence prevents replacement. + /// + /// Called while holding the registry lock. Replacement invokes the old provider's BeforeUnload + /// and the new provider's BeforeLoad before updating the registration; first registrations do + /// not invoke these callbacks. Callback failures propagate without publishing the replacement. + /// + /// The provider does not support the authentication method. + private bool Register(SqlAuthenticationMethod authenticationMethod, + SqlAuthenticationProvider provider, RegistrationTier tier) + { + if (!provider.IsSupported(authenticationMethod)) + { + throw AuthenticationStrings.UnsupportedAuthenticationByProvider(authenticationMethod.ToString(), provider.GetType().Name); + } + if (_tiers.TryGetValue(authenticationMethod, out RegistrationTier oldTier) && + (oldTier > tier || (tier == RegistrationTier.Default && oldTier == tier))) + { + SqlClientEventSource.Log.TryTraceEvent( + "Authentication registration refused for {0}: existing tier {1} takes precedence over {2}. " + + "To change a configuration-owned provider, update app.config or its initializer rather than " + + "overriding it with an application registration.", + authenticationMethod, oldTier, tier); + + // The app has already specified a Provider for this + // authentication method, so we won't override it. + return false; + } + if (_providers.TryGetValue(authenticationMethod, out SqlAuthenticationProvider? oldProvider)) + { + oldProvider.BeforeUnload(authenticationMethod); + provider.BeforeLoad(authenticationMethod); + } + _providers[authenticationMethod] = provider; + _tiers[authenticationMethod] = tier; + return true; + } + + /// + /// Fetches an app.config section only if its handler has the expected full type name. + /// + /// The configuration section name. + /// The expected section handler's full type name. + /// The matching section, or null if it is absent or has a different handler type. + /// Does not read appsettings.json. Handler mismatches are logged. + /// The configuration system cannot load the section. + [RequiresUnreferencedCode("Configuration section handlers are resolved by name.")] + [RequiresDynamicCode("Configuration section handlers are constructed dynamically.")] + private static ConfigurationSection? FetchConfigurationSection(string name, string typeName) + { + // TODO: Support reading configuration from appsettings.json for .NET runtime applications. + object section = ConfigurationManager.GetSection(name); + if (section != null) + { + if (section is ConfigurationSection configSection && configSection.GetType().FullName == typeName) + { + return configSection; + } + else + { + SqlClientEventSource.Log.TryTraceEvent("Found a custom {0} configuration but it is not of type {1}.", name, typeName); + } + } + return default; + } + + /// Maps a configured authentication method name to its enum value, ignoring case. + /// The authentication method name from a configured provider entry. + /// The corresponding supported Active Directory authentication method. + /// The configured name is not recognized. + private static SqlAuthenticationMethod AuthenticationEnumFromString(string authentication) + { + switch (authentication.ToLowerInvariant()) + { + case "active directory integrated": + return SqlAuthenticationMethod.ActiveDirectoryIntegrated; + case "active directory password": +#pragma warning disable 0618 // Type or member is obsolete + return SqlAuthenticationMethod.ActiveDirectoryPassword; +#pragma warning restore 0618 // Type or member is obsolete + case "active directory interactive": + return SqlAuthenticationMethod.ActiveDirectoryInteractive; + case "active directory service principal": + return SqlAuthenticationMethod.ActiveDirectoryServicePrincipal; + case "active directory device code flow": + return SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow; + case "active directory managed identity": + return SqlAuthenticationMethod.ActiveDirectoryManagedIdentity; + case "active directory msi": + return SqlAuthenticationMethod.ActiveDirectoryMSI; + case "active directory default": + return SqlAuthenticationMethod.ActiveDirectoryDefault; + case "active directory workload identity": + return SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity; + default: + throw AuthenticationStrings.UnsupportedAuthentication(authentication); + } + } + +} diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/TrimmingAttributes.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/TrimmingAttributes.cs new file mode 100644 index 0000000000..63e34c142d --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/TrimmingAttributes.cs @@ -0,0 +1,85 @@ +// 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 NETSTANDARD2_0 + +namespace System.Diagnostics.CodeAnalysis; + +/// +/// Provides the trimming annotation missing from .NET Standard 2.0 so shared code can mark +/// members that may fail when unreferenced code is removed. +/// +/// +/// Stores the warning message and optional guidance URL for trimming tools. +/// Modern targets use the framework-provided attribute instead. +/// +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Class, + Inherited = false)] +internal sealed class RequiresUnreferencedCodeAttribute(string message) : Attribute +{ + public string Message { get; } = message; + public string? Url { get; set; } +} + +/// +/// Provides the Native AOT annotation missing from .NET Standard 2.0 so shared code can mark +/// members that require runtime code generation. +/// +/// +/// Stores the warning message and optional guidance URL for AOT analysis tools. +/// Modern targets use the framework-provided attribute instead. +/// +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Class, + Inherited = false)] +internal sealed class RequiresDynamicCodeAttribute(string message) : Attribute +{ + public string Message { get; } = message; + public string? Url { get; set; } +} + +/// +/// Provides the feature-switch annotation missing from .NET Standard 2.0, associating a +/// Boolean property with a named switch that trimming tools can substitute at publish time. +/// +/// +/// Stores the switch name as metadata; it does not read or set the switch at runtime. +/// Modern targets use the framework-provided attribute instead. +/// +[AttributeUsage(AttributeTargets.Property, Inherited = false)] +internal sealed class FeatureSwitchDefinitionAttribute(string switchName) : Attribute +{ + public string SwitchName { get; } = switchName; +} + +/// +/// Provides the feature-guard annotation missing from .NET Standard 2.0, identifying a +/// Boolean property whose true value indicates that an annotated capability is available. +/// +/// +/// Stores the capability's attribute type so analysis tools can recognize guarded code paths. +/// Modern targets use the framework-provided attribute instead. +/// +[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = false)] +internal sealed class FeatureGuardAttribute(Type featureType) : Attribute +{ + public Type FeatureType { get; } = featureType; +} + +/// +/// Provides the diagnostic-suppression annotation missing from .NET Standard 2.0 so shared +/// code can record suppressions that remain in assembly metadata for trimming and AOT tools. +/// +/// +/// Stores the diagnostic category, identifier, and optional justification without changing +/// runtime behavior. Modern targets use the framework-provided attribute instead. +/// +[AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)] +internal sealed class UnconditionalSuppressMessageAttribute(string category, string checkId) : Attribute +{ + public string Category { get; } = category; + public string CheckId { get; } = checkId; + public string? Justification { get; set; } +} + +#endif diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/AuthenticationFeatureSwitchesTest.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/AuthenticationFeatureSwitchesTest.cs new file mode 100644 index 0000000000..5bf434f348 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/AuthenticationFeatureSwitchesTest.cs @@ -0,0 +1,58 @@ +// 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.Reflection; + +namespace Microsoft.Data.SqlClient.Extensions.Abstractions.Test; + +/// Serializes switch mutations against authentication bootstrap in other tests. +[CollectionDefinition(Name, DisableParallelization = true)] +public class AuthenticationFeatureSwitchTestCollection +{ + public const string Name = "Authentication feature switches"; +} + +/// Verifies authentication switches retain their first value through their capability guards. +[Collection(AuthenticationFeatureSwitchTestCollection.Name)] +public class AuthenticationFeatureSwitchesTest +{ + /// Changing AppContext after first access must not change the switch or its guard. + [Theory] + [InlineData("EnableAppConfig", "s_enableAppConfig", true)] + [InlineData("EnableAppConfig", "s_enableAppConfig", false)] + [InlineData("EnableAzureExtensionDiscovery", "s_enableAzureExtensionDiscovery", true)] + [InlineData("EnableAzureExtensionDiscovery", "s_enableAzureExtensionDiscovery", false)] + public void SwitchRetainsFirstValue(string propertyName, string fieldName, bool initialValue) + { + string switchName = "Switch.Microsoft.Data.SqlClient." + propertyName; + Type type = typeof(AuthenticationFeatureSwitches); + FieldInfo field = type.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Static)!; + object cachedValue = field.GetValue(null)!; + bool originalValue = !AppContext.TryGetSwitch(switchName, out bool value) || value; + try + { + field.SetValue(null, Enum.ToObject(field.FieldType, 0)); + AppContext.SetSwitch(switchName, initialValue); + Assert.Equal(initialValue, ReadSwitch(propertyName)); + AppContext.SetSwitch(switchName, !initialValue); + Assert.Equal(initialValue, ReadSwitch(propertyName)); + Assert.Equal(initialValue, propertyName == "EnableAppConfig" + ? AuthenticationFeatureSwitches.IsAppConfigSupported + : AuthenticationFeatureSwitches.IsAzureExtensionDiscoverySupported); + } + finally + { + AppContext.SetSwitch(switchName, originalValue); + field.SetValue(null, cachedValue); + } + } + + /// Reads a feature switch directly without triggering registry bootstrap. + /// The switch property to read. + /// The cached switch value. + private static bool ReadSwitch(string propertyName) => + propertyName == "EnableAppConfig" + ? AuthenticationFeatureSwitches.EnableAppConfig + : AuthenticationFeatureSwitches.EnableAzureExtensionDiscovery; +} diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/AuthenticationRegistryTest.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/AuthenticationRegistryTest.cs new file mode 100644 index 0000000000..51a7439e31 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/AuthenticationRegistryTest.cs @@ -0,0 +1,133 @@ +// 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.Collections.Concurrent; +using System.Diagnostics.Tracing; + +namespace Microsoft.Data.SqlClient.Extensions.Abstractions.Test; + +/// Validates errors and callback compatibility of the direct provider registry. +public class AuthenticationRegistryTest +{ + /// Unsupported registrations preserve the false result and explain how to fix the provider. + [Fact] + public void UnsupportedProviderReturnsFalseAndLogs() + { + using var listener = new AuthenticationTraceListener(); + Assert.False(SqlAuthenticationProvider.SetProvider( + SqlAuthenticationMethod.NotSpecified, new Provider(false))); + Assert.Contains(listener.Messages, message => + message.Contains("SetProvider") && message.Contains("NotSpecified") && + message.Contains("returning false") && message.Contains("supports")); + } + + /// Null registrations preserve the false result and explain the required argument. + [Fact] + public void NullProviderReturnsFalseAndLogs() + { + using var listener = new AuthenticationTraceListener(); + Assert.False(SqlAuthenticationProvider.SetProvider( + SqlAuthenticationMethod.NotSpecified, null!)); + Assert.Contains(listener.Messages, message => + message.Contains("SetProvider") && message.Contains("returning false") && + message.Contains("non-null")); + } + + /// Provider callback failures are logged without replacing the existing registration. + [Theory] + [InlineData("IsSupported")] + [InlineData("BeforeLoad")] + [InlineData("BeforeUnload")] + public void CallbackFailureReturnsFalseAndPreservesProvider(string callback) + { + using var listener = new AuthenticationTraceListener(); + var first = new ThrowingProvider(callback == "BeforeUnload" ? callback : null); + var replacement = new ThrowingProvider(callback == "BeforeUnload" ? null : callback); + Assert.True(SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.NotSpecified, first)); + + Assert.False(SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.NotSpecified, replacement)); + + Assert.Same(first, SqlAuthenticationProvider.GetProvider(SqlAuthenticationMethod.NotSpecified)); + Assert.Contains(listener.Messages, message => + message.Contains("SetProvider") && message.Contains(callback) && + message.Contains("returning false") && message.Contains("callbacks")); + first.Throw = false; + var valid = new Provider(true); + Assert.True(SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.NotSpecified, valid)); + Assert.Same(valid, SqlAuthenticationProvider.GetProvider(SqlAuthenticationMethod.NotSpecified)); + } + + /// The registry preserves initial insertion and replacement callback behavior. + [Fact] + public void ReplacementInvokesCallbacks() + { + var first = new Provider(true); + var second = new Provider(true); + Assert.True(SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.SqlPassword, first)); + Assert.True(SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.SqlPassword, second)); + Assert.Equal(1, first.Unloads); + Assert.Equal(1, second.Loads); + Assert.Same(second, SqlAuthenticationProvider.GetProvider(SqlAuthenticationMethod.SqlPassword)); + } + + private sealed class Provider(bool supported) : SqlAuthenticationProvider + { + internal int Loads; + internal int Unloads; + public override bool IsSupported(SqlAuthenticationMethod method) => supported; + public override void BeforeLoad(SqlAuthenticationMethod method) => Loads++; + public override void BeforeUnload(SqlAuthenticationMethod method) => Unloads++; + public override Task AcquireTokenAsync(SqlAuthenticationParameters parameters) => + throw new NotSupportedException(); + } + + private sealed class ThrowingProvider(string? callback) : SqlAuthenticationProvider + { + internal bool Throw { get; set; } = true; + + public override bool IsSupported(SqlAuthenticationMethod method) + { + ThrowIfRequested(nameof(IsSupported)); + return true; + } + + public override void BeforeLoad(SqlAuthenticationMethod method) => + ThrowIfRequested(nameof(BeforeLoad)); + + public override void BeforeUnload(SqlAuthenticationMethod method) => + ThrowIfRequested(nameof(BeforeUnload)); + + public override Task AcquireTokenAsync(SqlAuthenticationParameters parameters) => + throw new NotSupportedException(); + + private void ThrowIfRequested(string name) + { + if (Throw && callback == name) + { + throw new InvalidOperationException("Test callback failure: " + name); + } + } + } + + private sealed class AuthenticationTraceListener : EventListener + { + internal ConcurrentQueue Messages { get; } = new(); + + protected override void OnEventSourceCreated(EventSource eventSource) + { + if (eventSource.Name == "Microsoft.Data.SqlClient.EventSource") + { + EnableEvents(eventSource, EventLevel.Informational, (EventKeywords)2); + } + } + + protected override void OnEventWritten(EventWrittenEventArgs eventData) + { + if (eventData.EventId == 3 && eventData.Payload?[0] is string message) + { + Messages.Enqueue(message); + } + } + } +} diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/AuthenticationStringsTest.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/AuthenticationStringsTest.cs new file mode 100644 index 0000000000..dfd66b4cfc --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/AuthenticationStringsTest.cs @@ -0,0 +1,219 @@ +// 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.Collections.Concurrent; +using System.Diagnostics.Tracing; +using System.Globalization; + +namespace Microsoft.Data.SqlClient.Extensions.Abstractions.Test; + +/// Verifies message-generation failures do not mask authentication errors. +public class AuthenticationStringsTest +{ + /// Available resources retain their localized formatting. + [Fact] + public void ExistingResourceUsesLocalizedMessage() + { + CultureInfo originalCulture = CultureInfo.CurrentUICulture; + try + { + CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture; + Assert.Equal("The authentication 'unsupported' is not supported.", + AuthenticationStrings.Format("SQL_UnsupportedAuthentication", "unsupported")); + } + finally + { + CultureInfo.CurrentUICulture = originalCulture; + } + } + + /// Missing keys produce a diagnostic message with culture-formatted arguments. + [Theory] + [InlineData("en-US")] + [InlineData("fr-FR")] + public void MissingResourceIncludesArgumentsAndLogs(string cultureName) + { + using var listener = new AuthenticationTraceListener(); + CultureInfo originalCulture = CultureInfo.CurrentCulture; + try + { + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo(cultureName); + decimal value = 1234.5m; + string message = AuthenticationStrings.Format("MissingAuthenticationResource", + "provider-type", value, null!); + + Assert.Equal("MissingAuthenticationResource: [provider-type, " + + value.ToString(CultureInfo.CurrentCulture) + ", ]", message); + Assert.Contains(listener.Messages, trace => + trace.Contains("AuthenticationStrings.Format") && + trace.Contains("MissingAuthenticationResource")); + } + finally + { + CultureInfo.CurrentCulture = originalCulture; + } + } + + /// Incorrect placeholder counts fall back without replacing the intended exception. + [Fact] + public void FormattingFailurePreservesIntendedException() + { + using var listener = new AuthenticationTraceListener(); + var cause = new InvalidOperationException("Provider construction failed."); + var error = new ArgumentException( + AuthenticationStrings.Format("SQL_CannotCreateAuthProvider", "authentication"), cause); + + Assert.Equal("SQL_CannotCreateAuthProvider: [authentication]", error.Message); + Assert.Same(cause, error.InnerException); + Assert.Contains(listener.Messages, trace => trace.Contains(nameof(FormatException))); + } + + /// Lookup failures and missing argument arrays still produce a usable message. + [Theory] + [InlineData(null)] + [InlineData("SQL_UnsupportedAuthentication")] + public void NullKeyAndArgumentsProduceFallback(string? key) + { + Assert.Equal((key ?? "") + ": []", AuthenticationStrings.Format(key!, null!)); + } + + /// An argument that cannot format must not prevent rendering the remaining values. + [Theory] + [InlineData("MissingAuthenticationResource")] + [InlineData("SQL_UnsupportedAuthentication")] + public void ThrowingArgumentDoesNotEscape(string key) + { + using var listener = new AuthenticationTraceListener(); + string message = AuthenticationStrings.Format(key, new ThrowingArgument(), "provider-type"); + + Assert.Equal(key + ": [, provider-type]", message); + Assert.Contains(listener.Messages, trace => + trace.Contains("AuthenticationStrings.Format") && + trace.Contains(nameof(InvalidOperationException))); + } + + /// Provider and initializer exception factories preserve the original construction cause. + [Fact] + public void ExceptionFactoriesPreserveCause() + { + var cause = new InvalidOperationException("Construction failed."); + Assert.Same(cause, AuthenticationStrings.CannotCreateAuthProvider( + "authentication", "provider-type", cause).InnerException); + Assert.Same(cause, AuthenticationStrings.CannotCreateSqlAuthInitializer( + "initializer-type", cause).InnerException); + } + + /// Null and string arguments retain their defined representations without resource lookup. + [Theory] + [InlineData(null, "")] + [InlineData("", "")] + [InlineData("provider-type", "provider-type")] + public void FormatArgumentPreservesNullAndStrings(object? argument, string expected) + { + Assert.Equal(expected, AuthenticationStrings.FormatArgument(argument)); + } + + /// Numeric argument formatting follows the current culture rather than an invariant default. + [Theory] + [InlineData("en-US", "1234.5")] + [InlineData("fr-FR", "1234,5")] + public void FormatArgumentUsesCurrentCulture(string cultureName, string expected) + { + CultureInfo originalCulture = CultureInfo.CurrentCulture; + try + { + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo(cultureName); + Assert.Equal(expected, AuthenticationStrings.FormatArgument(1234.5m)); + } + finally + { + CultureInfo.CurrentCulture = originalCulture; + } + } + + /// Custom formattable arguments receive the current culture and the default format. + [Fact] + public void FormatArgumentUsesIFormattable() + { + var argument = new FormattableArgument(); + + Assert.Equal("formatted", AuthenticationStrings.FormatArgument(argument)); + Assert.Same(CultureInfo.CurrentCulture, argument.Provider); + Assert.Null(argument.Format); + } + + /// Recoverable argument failures return a type-name placeholder and emit a diagnostic. + [Fact] + public void FormatArgumentFailureReturnsPlaceholderAndLogs() + { + using var listener = new AuthenticationTraceListener(); + + Assert.Equal("", + AuthenticationStrings.FormatArgument(new ThrowingArgument())); + Assert.Contains(listener.Messages, trace => + trace.Contains("Argument of type") && + trace.Contains(typeof(ThrowingArgument).FullName!) && + trace.Contains(nameof(InvalidOperationException))); + } + + /// Fatal argument failures propagate unchanged instead of being hidden by a placeholder. + [Fact] + public void FormatArgumentFatalFailurePropagates() + { + var cause = new OutOfMemoryException("Simulated formatting failure."); + + Assert.Same(cause, Assert.Throws(() => + AuthenticationStrings.FormatArgument(new ThrowingArgument(cause)))); + } + + private sealed class ThrowingArgument(Exception? exception = null) + { + /// Simulates a failure while producing an argument's textual value. + public override string ToString() => + throw exception ?? new InvalidOperationException("Cannot format argument."); + } + + private sealed class FormattableArgument : IFormattable + { + internal string? Format { get; private set; } + internal IFormatProvider? Provider { get; private set; } + + /// Records the requested format and culture to verify composite formatting. + /// The requested format string. + /// The culture used for formatting. + /// A recognizable formatted value. + public string ToString(string? format, IFormatProvider? formatProvider) + { + Format = format; + Provider = formatProvider; + return "formatted"; + } + + /// Rejects ordinary formatting so tests require the IFormattable path. + public override string ToString() => throw new InvalidOperationException("Use IFormattable."); + } + + /// Captures message-generation diagnostics without altering the registry. + private sealed class AuthenticationTraceListener : EventListener + { + internal ConcurrentQueue Messages { get; } = new(); + + protected override void OnEventSourceCreated(EventSource eventSource) + { + if (eventSource.Name == "Microsoft.Data.SqlClient.EventSource") + { + EnableEvents(eventSource, EventLevel.Informational, (EventKeywords)2); + } + } + + protected override void OnEventWritten(EventWrittenEventArgs eventData) + { + if (eventData.EventId == 3 && eventData.Payload?[0] is string message) + { + Messages.Enqueue(message); + } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlAuthenticationProviderManagerTests.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/AzureProviderConstructionTest.cs similarity index 58% rename from src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlAuthenticationProviderManagerTests.cs rename to src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/AzureProviderConstructionTest.cs index 08089abe98..665397e7c2 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlAuthenticationProviderManagerTests.cs +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/AzureProviderConstructionTest.cs @@ -6,105 +6,11 @@ using System.Threading.Tasks; using Xunit; -namespace Microsoft.Data.SqlClient.UnitTests; +namespace Microsoft.Data.SqlClient.Extensions.Abstractions.Test; -public class SqlAuthenticationProviderManagerTests +/// Preserves Azure constructor selection for configured client IDs and broker options. +public class AzureProviderConstructionTest { - private class Provider : SqlAuthenticationProvider - { - public override Task AcquireTokenAsync( - SqlAuthenticationParameters parameters) - { - return Task.FromResult( - new SqlAuthenticationToken( - "SampleAccessToken", DateTimeOffset.UtcNow.AddMinutes(5))); - } - - public override bool IsSupported(SqlAuthenticationMethod authenticationMethod) - { - return authenticationMethod == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow; - } - } - - // Verify that we can get and set providers via both the Abstractions - // package and Manager class interchangeably. - // - // This tests the dynamic assembly loading code in the Abstractions - // package. - [Fact] - public void Abstractions_And_Manager_GetSetProvider_Equivalent() - { - // Set via Manager, get via both. - Provider provider1 = new(); - - Assert.True( - SqlAuthenticationProviderManager.SetProvider( - // GOTCHA: On .NET Framework, the dummy provider is already - // registered as the default provider for Interactive, so we - // use DeviceCodeFlow instead. - SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, - provider1)); - - Assert.Same( - provider1, - SqlAuthenticationProviderManager.GetProvider( - SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow)); - - Assert.Same( - provider1, - SqlAuthenticationProvider.GetProvider( - SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow)); - - // Set via Abstractions, get via both. - Provider provider2 = new(); - - Assert.True( - SqlAuthenticationProvider.SetProvider( - SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, - provider2)); - - Assert.Same( - provider2, - SqlAuthenticationProviderManager.GetProvider( - SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow)); - - Assert.Same( - provider2, - SqlAuthenticationProvider.GetProvider( - SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow)); - } - - // Regression: the manager's static initializer reflectively constructs the Azure extension's - // ActiveDirectoryAuthenticationProvider. That class has overlapping 1-arg constructors - // ((string) and (ProviderOptions)), so calling Activator.CreateInstance(type, [null]) used - // to throw AmbiguousMatchException -- which surfaced as TypeInitializationException from - // GetProvider and broke every AD-authenticated connection. Calling GetProvider for an AD - // method must succeed (returning either the registered provider or null) and must not throw. - [Fact] - public void GetProvider_ForActiveDirectoryMethod_DoesNotThrow() - { - foreach (SqlAuthenticationMethod method in new[] - { - SqlAuthenticationMethod.ActiveDirectoryIntegrated, - #pragma warning disable CS0618 // ActiveDirectoryPassword is obsolete. - SqlAuthenticationMethod.ActiveDirectoryPassword, - #pragma warning restore CS0618 - SqlAuthenticationMethod.ActiveDirectoryInteractive, - SqlAuthenticationMethod.ActiveDirectoryServicePrincipal, - SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, - SqlAuthenticationMethod.ActiveDirectoryManagedIdentity, - SqlAuthenticationMethod.ActiveDirectoryMSI, - SqlAuthenticationMethod.ActiveDirectoryDefault, - SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity, - }) - { - // No assertion on the value -- the provider may or may not be installed depending on - // whether the Azure extension is on disk. We only assert no throw (which is what a - // TypeInitializationException from the static initializer would do). - _ = SqlAuthenticationProviderManager.GetProvider(method); - } - } - // CreateAzureAuthenticationProvider tests ---------------------------------------------- // // Each Stub* container mimics one shape the real Azure extension might expose: @@ -179,10 +85,11 @@ public sealed class ActiveDirectoryAuthenticationProvider : StubProviderBase } } + /// Unconfigured discovery avoids ambiguous null constructor arguments. [Fact] public void CreateAzureAuthenticationProvider_NeitherConfigured_UsesParameterlessCtor() { - var instance = SqlAuthenticationProviderManager.CreateAzureAuthenticationProvider( + var instance = SqlAuthenticationProviderRegistry.CreateAzureAuthenticationProvider( typeof(StubModern.ActiveDirectoryAuthenticationProvider), typeof(StubModern.ActiveDirectoryAuthenticationProviderOptions), applicationClientId: null, @@ -196,10 +103,11 @@ public void CreateAzureAuthenticationProvider_NeitherConfigured_UsesParameterles Assert.Null(stub.CapturedUseWamBroker); } + /// Modern Azure extensions receive client IDs through the options constructor. [Fact] public void CreateAzureAuthenticationProvider_AppIdOnly_OptionsAvailable_UsesOptionsCtor() { - var instance = SqlAuthenticationProviderManager.CreateAzureAuthenticationProvider( + var instance = SqlAuthenticationProviderRegistry.CreateAzureAuthenticationProvider( typeof(StubModern.ActiveDirectoryAuthenticationProvider), typeof(StubModern.ActiveDirectoryAuthenticationProviderOptions), applicationClientId: "app-123", @@ -212,10 +120,11 @@ public void CreateAzureAuthenticationProvider_AppIdOnly_OptionsAvailable_UsesOpt Assert.Equal(false, stub.CapturedUseWamBroker); } + /// Legacy constructor selection remains compatible until phase 2 removes the fallback. [Fact] public void CreateAzureAuthenticationProvider_AppIdOnly_OptionsMissing_FallsBackToStringCtor() { - var instance = SqlAuthenticationProviderManager.CreateAzureAuthenticationProvider( + var instance = SqlAuthenticationProviderRegistry.CreateAzureAuthenticationProvider( typeof(StubLegacy.ActiveDirectoryAuthenticationProvider), optionsType: null, applicationClientId: "legacy-456", @@ -229,10 +138,11 @@ public void CreateAzureAuthenticationProvider_AppIdOnly_OptionsMissing_FallsBack Assert.Null(stub.CapturedUseWamBroker); } + /// Incompatible Azure types are not constructed with invalid arguments. [Fact] public void CreateAzureAuthenticationProvider_AppIdOnly_NoCompatibleCtor_ReturnsNull() { - var instance = SqlAuthenticationProviderManager.CreateAzureAuthenticationProvider( + var instance = SqlAuthenticationProviderRegistry.CreateAzureAuthenticationProvider( typeof(StubMinimal.ActiveDirectoryAuthenticationProvider), optionsType: null, applicationClientId: "no-ctor", @@ -241,11 +151,12 @@ public void CreateAzureAuthenticationProvider_AppIdOnly_NoCompatibleCtor_Returns Assert.Null(instance); } + /// A broker override without Azure options produces a localized error. [Fact] public void CreateAzureAuthenticationProvider_UseWamBroker_OptionsMissing_Throws() { InvalidOperationException ex = Assert.Throws(() => - SqlAuthenticationProviderManager.CreateAzureAuthenticationProvider( + SqlAuthenticationProviderRegistry.CreateAzureAuthenticationProvider( typeof(StubLegacy.ActiveDirectoryAuthenticationProvider), optionsType: null, applicationClientId: null, @@ -255,10 +166,11 @@ public void CreateAzureAuthenticationProvider_UseWamBroker_OptionsMissing_Throws Assert.Contains("Microsoft.Data.SqlClient.Extensions.Azure", ex.Message); } + /// The configured broker flag and client ID reach the modern options constructor. [Fact] public void CreateAzureAuthenticationProvider_UseWamBroker_OptionsAvailable_UsesOptionsCtor() { - var instance = SqlAuthenticationProviderManager.CreateAzureAuthenticationProvider( + var instance = SqlAuthenticationProviderRegistry.CreateAzureAuthenticationProvider( typeof(StubModern.ActiveDirectoryAuthenticationProvider), typeof(StubModern.ActiveDirectoryAuthenticationProviderOptions), applicationClientId: "app-789", diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/ExceptionHelpersTest.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/ExceptionHelpersTest.cs new file mode 100644 index 0000000000..f24d8801c9 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/ExceptionHelpersTest.cs @@ -0,0 +1,44 @@ +// 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.Reflection; +using System.Threading; + +namespace Microsoft.Data.SqlClient.Extensions.Abstractions.Test; + +/// Verifies fatal errors are never treated as recoverable, including through exception wrappers. +public class ExceptionHelpersTest +{ + /// Fatal runtime errors remain non-recoverable even when wrapped by other exceptions. + [Theory] + [InlineData(typeof(OutOfMemoryException), false)] + [InlineData(typeof(OutOfMemoryException), true)] + [InlineData(typeof(StackOverflowException), false)] + [InlineData(typeof(StackOverflowException), true)] + [InlineData(typeof(AccessViolationException), false)] + [InlineData(typeof(AccessViolationException), true)] + [InlineData(typeof(ThreadAbortException), false)] + [InlineData(typeof(ThreadAbortException), true)] + public void FatalExceptionIsNotRecoverable(Type exceptionType, bool wrapped) + { + var exception = (Exception)Activator.CreateInstance(exceptionType, nonPublic: true)!; + if (wrapped) + { + exception = new TargetInvocationException(new InvalidOperationException("Wrapper", exception)); + } + + Assert.False(ExceptionHelpers.IsRecoverableException(exception)); + } + + /// Ordinary failures remain recoverable throughout an inner-exception chain. + [Fact] + public void OrdinaryExceptionChainIsRecoverable() + { + var exception = new ArgumentException("Wrapper", + new InvalidOperationException("Wrapper", new FormatException("Invalid format"))); + + Assert.True(ExceptionHelpers.IsRecoverableException(exception)); + Assert.True(ExceptionHelpers.IsRecoverableException(new FormatException("Invalid format"))); + } +} diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/SqlAuthenticationProviderTest.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/SqlAuthenticationProviderTest.cs index c5872a8391..7bb749e2ba 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/SqlAuthenticationProviderTest.cs +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/SqlAuthenticationProviderTest.cs @@ -6,6 +6,7 @@ namespace Microsoft.Data.SqlClient.Extensions.Abstractions.Test; +/// Registration no longer depends on reflective access to a SqlClient assembly. public class SqlAuthenticationProviderTest { #region Test Setup @@ -25,8 +26,7 @@ public SqlAuthenticationProviderTest() #region Tests /// - /// Test that GetProvider fails predictably when the MDS assembly can't be - /// found. + /// Without SqlClient, a method starts without a provider and supports direct registration and lookup. /// [Theory] #pragma warning disable CS0618 // Type or member is obsolete @@ -40,35 +40,13 @@ public SqlAuthenticationProviderTest() [InlineData(SqlAuthenticationMethod.ActiveDirectoryMSI)] [InlineData(SqlAuthenticationMethod.ActiveDirectoryDefault)] [InlineData(SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity)] - public void GetProvider_NoMdsAssembly(SqlAuthenticationMethod method) + public void GetAndSetProvider_NoMdsAssembly(SqlAuthenticationMethod method) { - // GetProvider() should return null when the MDS assembly can't be - // found. Assert.Null(SqlAuthenticationProvider.GetProvider(method)); - } - /// - /// Test that SetProvider fails predictably when the MDS assembly can't be - /// found. - /// - [Theory] - #pragma warning disable CS0618 // Type or member is obsolete - [InlineData(SqlAuthenticationMethod.ActiveDirectoryPassword)] - #pragma warning restore CS0618 // Type or member is obsolete - [InlineData(SqlAuthenticationMethod.ActiveDirectoryIntegrated)] - [InlineData(SqlAuthenticationMethod.ActiveDirectoryInteractive)] - [InlineData(SqlAuthenticationMethod.ActiveDirectoryServicePrincipal)] - [InlineData(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow)] - [InlineData(SqlAuthenticationMethod.ActiveDirectoryManagedIdentity)] - [InlineData(SqlAuthenticationMethod.ActiveDirectoryMSI)] - [InlineData(SqlAuthenticationMethod.ActiveDirectoryDefault)] - [InlineData(SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity)] - public void SetProvider_NoMdsAssembly(SqlAuthenticationMethod method) - { - // SetProvider() should return false when the MDS assembly can't be - // found. - Assert.False( - SqlAuthenticationProvider.SetProvider(method, new Provider())); + var provider = new Provider(); + Assert.True(SqlAuthenticationProvider.SetProvider(method, provider)); + Assert.Same(provider, SqlAuthenticationProvider.GetProvider(method)); } #endregion diff --git a/src/Microsoft.Data.SqlClient.Extensions/Azure/test/DefaultAuthProviderTests.cs b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/DefaultAuthProviderTests.cs index fa426141c9..494a96434f 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Azure/test/DefaultAuthProviderTests.cs +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/DefaultAuthProviderTests.cs @@ -11,11 +11,9 @@ public class DefaultAuthProviderTests // authentication methods, and not for any other methods. // // Note that this isn't testing anything in the Azure package. It actually - // tests the static constructor of the SqlAuthenticationProviderManager - // class in the MDS package and the static GetProvider() and SetProvider() - // methods of the SqlAuthenticationProvider class in the Abstractions - // package. We're testing this here because this test project uses both of - // those packages, and this is a convenient place to put such a test. + // tests registry bootstrap and the static GetProvider() and SetProvider() + // methods in Abstractions. We're testing this here because this project + // includes the real Azure provider for discovery. // // TODO(https://sqlclientdrivers.visualstudio.com/ADO.Net/_workitems/edit/41888): // Move this test to a more appropriate location once we have one. diff --git a/src/Microsoft.Data.SqlClient.Extensions/Azure/test/WamBrokerTests.cs b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/WamBrokerTests.cs index 9b2aa5dcf7..b2a2a46c5c 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Azure/test/WamBrokerTests.cs +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/WamBrokerTests.cs @@ -88,7 +88,7 @@ public void Ctor_AppClientId_DefaultsUseWamBrokerToFalse() /// Mirrors the previous test for the /// constructor: a caller (or app.config) that sets only ApplicationClientId and skips /// UseWamBroker must get the documented default of . This is - /// the contract SqlAuthenticationProviderManager relies on when reflecting onto the + /// the contract the Abstractions authentication registry relies on when reflecting onto the /// Options ctor and only forwarding the properties that were explicitly configured. /// [Fact] diff --git a/src/Microsoft.Data.SqlClient/ref/Microsoft.Data.SqlClient.cs b/src/Microsoft.Data.SqlClient/ref/Microsoft.Data.SqlClient.cs index 7b8a60147f..eaa0bf6893 100644 --- a/src/Microsoft.Data.SqlClient/ref/Microsoft.Data.SqlClient.cs +++ b/src/Microsoft.Data.SqlClient/ref/Microsoft.Data.SqlClient.cs @@ -38,15 +38,6 @@ public enum SortOrder Descending = 1 } -/// -public abstract class SqlAuthenticationInitializer -{ - /// - protected SqlAuthenticationInitializer() { } - /// - public abstract void Initialize(); -} - /// #if NET public class SqlBatch : System.Data.Common.DbBatch diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs index e5d4a61a5f..acfad5114f 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs @@ -3025,7 +3025,7 @@ private SqlFedAuthToken GetFedAuthToken(SqlFedAuthInfo fedAuthInfo) // Username to use in error messages. string? username = null; - SqlAuthenticationProvider? authProvider = SqlAuthenticationProviderManager.GetProvider(ConnectionOptions.Authentication); + SqlAuthenticationProvider? authProvider = SqlAuthenticationProvider.GetProvider(ConnectionOptions.Authentication); if (authProvider == null && _accessTokenCallback == null) { throw SQL.CannotFindAuthProvider(ConnectionOptions.Authentication); diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs deleted file mode 100644 index 9ff301b805..0000000000 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs +++ /dev/null @@ -1,587 +0,0 @@ -// 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 System.Collections.Generic; -using System.Configuration; -using System.IO; -using System.Reflection; -using Microsoft.Data.SqlClient.Internal; - -#nullable enable - -namespace Microsoft.Data.SqlClient -{ - internal sealed class SqlAuthenticationProviderManager - { - [Obsolete("ActiveDirectoryPassword is deprecated, use a more secure authentication method. See https://aka.ms/SqlClientEntraIDAuthentication for more details.")] - private const string ActiveDirectoryPassword = "active directory password"; - private const string ActiveDirectoryIntegrated = "active directory integrated"; - private const string ActiveDirectoryInteractive = "active directory interactive"; - private const string ActiveDirectoryServicePrincipal = "active directory service principal"; - private const string ActiveDirectoryDeviceCodeFlow = "active directory device code flow"; - private const string ActiveDirectoryManagedIdentity = "active directory managed identity"; - private const string ActiveDirectoryMSI = "active directory msi"; - private const string ActiveDirectoryDefault = "active directory default"; - private const string ActiveDirectoryWorkloadIdentity = "active directory workload identity"; - - // The name of our Azure extension assembly. - private const string azureAssemblyName = "Microsoft.Data.SqlClient.Extensions.Azure"; - - // The public key token of our Azure extension assembly, used to avoid loading imposter - // assemblies. - private static readonly byte[] s_azurePublicKeyToken = [ 0x23, 0xec, 0x7f, 0xc2, 0xd6, 0xea, 0xa4, 0xa5 ]; - - static SqlAuthenticationProviderManager() - { - TryReadConfigurationSection(out SqlAuthenticationProviderConfigurationSection? configurationSection); - - Instance = new SqlAuthenticationProviderManager(configurationSection); - - // If our Azure extensions package is present, use its authentication provider as our - // default. - try - { - // Try to load our Azure extension. - #if STRONG_NAME_SIGNING - - // When strong-name signing is enabled, build a fully-qualified AssemblyName - // that includes the expected public key token. - - SqlClientEventSource.Log.TryTraceEvent( - nameof(SqlAuthenticationProviderManager) + - ": Attempting to load Azure extension assembly={0} with " + - "expected public key token={1}", - azureAssemblyName, - BitConverter.ToString(s_azurePublicKeyToken).Replace("-", "")); - - var qualifiedName = new AssemblyName(azureAssemblyName); - qualifiedName.SetPublicKeyToken(s_azurePublicKeyToken); - - // The .NET Framework runtime will enforce the token during binding, causing Load() - // to throw. This prevents an untrusted assembly from being loaded and having its - // module initializers run. This will throw if the public key token doesn't match. - // - // The .NET runtime ignores the public key token and will happily load any assembly - // with the same simple name. - // - var assembly = Assembly.Load(qualifiedName); - - #if NET - // For the .NET runtime, we will check the public key token ourselves. - // - // Note that a null assembly is handled below. - if (assembly is not null) - { - byte[]? actualToken = assembly.GetName().GetPublicKeyToken(); - - if (actualToken is null || !actualToken.AsSpan().SequenceEqual(s_azurePublicKeyToken)) - { - SqlClientEventSource.Log.TryTraceEvent( - nameof(SqlAuthenticationProviderManager) + - ": Azure extension assembly={0} has an " + - "unexpected public key token; " + - "no default Active Directory provider installed", - assembly.GetName()); - return; - } - } - #endif - - #else - - SqlClientEventSource.Log.TryTraceEvent( - nameof(SqlAuthenticationProviderManager) + - ": Attempting to load Azure extension assembly={0} without " + - "strong name verification; ensure this assembly is from a trusted source", - azureAssemblyName); - - var assembly = Assembly.Load(azureAssemblyName); - - #endif - - if (assembly is null) - { - SqlClientEventSource.Log.TryTraceEvent( - nameof(SqlAuthenticationProviderManager) + - ": Azure extension assembly={0} not found; " + - "no default Active Directory provider installed", - azureAssemblyName); - return; - } - - SqlClientEventSource.Log.TryTraceEvent( - nameof(SqlAuthenticationProviderManager) + - ": Azure extension assembly={0} found; " + - "attempting to set as default provider for all Active " + - "Directory authentication methods", - assembly.GetName()); - - // Look for the authentication provider class. - const string className = "Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProvider"; - Type? type = assembly.GetType(className); - - if (type is null) - { - SqlClientEventSource.Log.TryTraceEvent( - nameof(SqlAuthenticationProviderManager) + - ": Azure extension does not contain class={0}; " + - "no default Active Directory provider installed", - className); - - return; - } - - // Try to instantiate it. Behavior depends on what the app - // configured in : - // * Neither applicationClientId nor useWamBroker -> use the - // parameterless constructor (defaults to the SqlClient - // first-party app id and enables WAM brokering on Windows). - // * applicationClientId only -> prefer the - // (ActiveDirectoryAuthenticationProviderOptions) constructor - // when the Azure extension exposes it; otherwise fall back - // to the legacy (string applicationClientId) constructor so - // older Azure extension versions keep working. - // * useWamBroker (with or without applicationClientId) -> - // requires the (Options) constructor because there is no - // positional analog. If the Azure extension is too old to - // expose Options, throw to surface the misconfiguration. - const string optionsTypeName = "Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProviderOptions"; - Type? optionsType = assembly.GetType(optionsTypeName); - - SqlAuthenticationProvider? instance = CreateAzureAuthenticationProvider( - type, - optionsType, - Instance._applicationClientId, - Instance._useWamBroker); - - if (instance is null) - { - SqlClientEventSource.Log.TryTraceEvent( - nameof(SqlAuthenticationProviderManager) + - ": Failed to instantiate Azure extension class={0}; " + - "no default Active Directory provider installed", - className); - - return; - } - - // We successfully instantiated the provider, so set it as the - // default for all Active Directory authentication methods. - // - // Note that SetProvider() will refuse to clobber an application - // specified provider, so these defaults will only be applied - // for methods that do not already have a provider. - SetProvider(SqlAuthenticationMethod.ActiveDirectoryIntegrated, instance); - #pragma warning disable 0618 // Type or member is obsolete - SetProvider(SqlAuthenticationMethod.ActiveDirectoryPassword, instance); - #pragma warning restore 0618 // Type or member is obsolete - SetProvider(SqlAuthenticationMethod.ActiveDirectoryInteractive, instance); - SetProvider(SqlAuthenticationMethod.ActiveDirectoryServicePrincipal, instance); - SetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, instance); - SetProvider(SqlAuthenticationMethod.ActiveDirectoryManagedIdentity, instance); - SetProvider(SqlAuthenticationMethod.ActiveDirectoryMSI, instance); - SetProvider(SqlAuthenticationMethod.ActiveDirectoryDefault, instance); - SetProvider(SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity, instance); - - SqlClientEventSource.Log.TryTraceEvent( - nameof(SqlAuthenticationProviderManager) + - ": Azure extension class={0} installed as " + - "provider for all Active Directory authentication methods", - className); - } - // All of these exceptions mean we couldn't find or instantiate the - // Azure extension's authentication provider, in which case we - // simply have no default and the app must provide one if they - // attempt to use Active Directory authentication. - catch (Exception ex) - when (ex is - AmbiguousMatchException or - ArgumentException or - BadImageFormatException or - FileLoadException or - FileNotFoundException or - MemberAccessException or - MethodAccessException or - MissingMethodException or - NotSupportedException or - TargetInvocationException or - TypeInitializationException or - TypeLoadException) - { - SqlClientEventSource.Log.TryTraceEvent( - nameof(SqlAuthenticationProviderManager) + - ": Azure extension assembly={0} not found or " + - "not usable; no default provider installed; " + - "{1}: {2}", - azureAssemblyName, - ex.GetType().Name, - ex.Message); - } - // Any other exceptions are fatal. - } - - private static readonly SqlAuthenticationProviderManager Instance; - - private readonly HashSet _authenticationsWithAppSpecifiedProvider = new(); - private readonly ConcurrentDictionary _providers = new(); - private readonly SqlClientLogger _sqlAuthLogger = new SqlClientLogger(); - private readonly string? _applicationClientId = null; - - // Optional override for ActiveDirectoryAuthenticationProviderOptions.UseWamBroker - // read from the app.config attribute. - // null means the app did not configure the value, in which case we leave the - // provider's default behavior (WAM is implied by the SqlClient first-party app id and - // off otherwise) untouched. - private readonly bool? _useWamBroker = null; - - /// - /// Constructor. - /// - private SqlAuthenticationProviderManager(SqlAuthenticationProviderConfigurationSection? configSection) - { - var methodName = "Ctor"; - - if (configSection == null) - { - _sqlAuthLogger.LogInfo(nameof(SqlAuthenticationProviderManager), methodName, "Neither SqlClientAuthenticationProviders nor SqlAuthenticationProviders configuration section found."); - return; - } - - if (!string.IsNullOrEmpty(configSection.ApplicationClientId)) - { - _applicationClientId = configSection.ApplicationClientId; - _sqlAuthLogger.LogInfo(nameof(SqlAuthenticationProviderManager), methodName, "Received user-defined Application Client Id"); - } - else - { - _sqlAuthLogger.LogInfo(nameof(SqlAuthenticationProviderManager), methodName, "No user-defined Application Client Id found."); - } - - if (!string.IsNullOrEmpty(configSection.UseWamBroker)) - { - if (bool.TryParse(configSection.UseWamBroker, out bool useWamBroker)) - { - _useWamBroker = useWamBroker; - _sqlAuthLogger.LogInfo(nameof(SqlAuthenticationProviderManager), methodName, $"Received user-defined UseWamBroker={useWamBroker}."); - } - else - { - _sqlAuthLogger.LogError(nameof(SqlAuthenticationProviderManager), methodName, $"Ignoring user-defined UseWamBroker='{configSection.UseWamBroker}': not a valid boolean."); - } - } - else - { - _sqlAuthLogger.LogInfo(nameof(SqlAuthenticationProviderManager), methodName, "No user-defined UseWamBroker found."); - } - - // Create user-defined auth initializer, if any. - if (!string.IsNullOrEmpty(configSection.InitializerType)) - { - try - { - var initializerType = Type.GetType(configSection.InitializerType, true); - if (initializerType is not null) - { - var initializer = (SqlAuthenticationInitializer?)Activator.CreateInstance(initializerType); - if (initializer is not null) - { - initializer.Initialize(); - } - } - } - catch (Exception e) - { - throw SQL.CannotCreateSqlAuthInitializer(configSection.InitializerType, e); - } - _sqlAuthLogger.LogInfo(nameof(SqlAuthenticationProviderManager), methodName, "Created user-defined SqlAuthenticationInitializer."); - } - else - { - _sqlAuthLogger.LogInfo(nameof(SqlAuthenticationProviderManager), methodName, "No user-defined SqlAuthenticationInitializer found."); - } - - // add user-defined providers, if any. - if (configSection.Providers != null && configSection.Providers.Count > 0) - { - foreach (ProviderSettings providerSettings in configSection.Providers) - { - SqlAuthenticationMethod authentication = AuthenticationEnumFromString(providerSettings.Name); - SqlAuthenticationProvider? provider; - try - { - var providerType = Type.GetType(providerSettings.Type, true); - if (providerType is null) - { - continue; - } - provider = (SqlAuthenticationProvider?)Activator.CreateInstance(providerType); - } - catch (Exception e) - { - throw SQL.CannotCreateAuthProvider(authentication.ToString(), providerSettings.Type, e); - } - if (provider is null) - { - continue; - } - if (!provider.IsSupported(authentication)) - { - throw SQL.UnsupportedAuthenticationByProvider(authentication.ToString(), providerSettings.Type); - } - - _providers[authentication] = provider; - _authenticationsWithAppSpecifiedProvider.Add(authentication); - _sqlAuthLogger.LogInfo(nameof(SqlAuthenticationProviderManager), methodName, string.Format("Added user-defined auth provider: {0} for authentication {1}.", providerSettings?.Type, authentication)); - } - } - else - { - _sqlAuthLogger.LogInfo(nameof(SqlAuthenticationProviderManager), methodName, "No user-defined auth providers."); - } - } - - /// - /// Get an authentication provider by method. - /// - /// Authentication method. - /// Authentication provider or null if not found. - internal static SqlAuthenticationProvider? GetProvider(SqlAuthenticationMethod authenticationMethod) - { - return Instance._providers.TryGetValue(authenticationMethod, out SqlAuthenticationProvider? value) ? value : null; - } - - // Reflectively constructs the Azure extension's ActiveDirectoryAuthenticationProvider, - // selecting the constructor that matches what the app configured. Extracted from the - // static initializer so it can be unit-tested with stub provider/options shapes. - // - // Returns null when no compatible constructor is available (e.g. a custom assembly - // that lacks both the (Options) and (string) ctors). - // - // Throws InvalidOperationException when useWamBroker is configured but the Azure - // extension is too old to expose ActiveDirectoryAuthenticationProviderOptions; that - // signals user-actionable misconfiguration and intentionally escapes the static ctor's - // catch-when filter so it surfaces as a TypeInitializationException. - internal static SqlAuthenticationProvider? CreateAzureAuthenticationProvider( - Type providerType, - Type? optionsType, - string? applicationClientId, - bool? useWamBroker) - { - if (applicationClientId is null && useWamBroker is null) - { - return Activator.CreateInstance(providerType) as SqlAuthenticationProvider; - } - - ConstructorInfo? optionsCtor = optionsType is null - ? null - : providerType.GetConstructor([optionsType]); - - if (useWamBroker is bool useWam) - { - if (optionsType is null || optionsCtor is null) - { - throw SQL.UseWamBrokerRequiresAzureExtensionUpgrade(); - } - - var options = Activator.CreateInstance(optionsType); - if (options is null) - { - return null; - } - - if (applicationClientId is not null) - { - optionsType.GetProperty("ApplicationClientId") - ?.SetValue(options, applicationClientId); - } - optionsType.GetProperty("UseWamBroker") - ?.SetValue(options, useWam); - - return optionsCtor.Invoke([options]) as SqlAuthenticationProvider; - } - - // applicationClientId-only: prefer Options when the extension exposes it, - // otherwise fall back to the legacy (string) ctor for backward compatibility - // with older Azure extension versions. - if (optionsType is not null && optionsCtor is not null) - { - var options = Activator.CreateInstance(optionsType); - if (options is null) - { - return null; - } - optionsType.GetProperty("ApplicationClientId") - ?.SetValue(options, applicationClientId); - return optionsCtor.Invoke([options]) as SqlAuthenticationProvider; - } - - ConstructorInfo? legacyCtor = providerType.GetConstructor([typeof(string)]); - if (legacyCtor is not null) - { - return legacyCtor.Invoke([applicationClientId]) as SqlAuthenticationProvider; - } - - return null; - } - - /// - /// Set an authentication provider by method. - /// - /// Authentication method. - /// Authentication provider. - /// - /// True if succeeded, false on any errors or if the authentication method has already - /// been claimed via app configuration. - /// - internal static bool SetProvider(SqlAuthenticationMethod authenticationMethod, SqlAuthenticationProvider provider) - { - if (!provider.IsSupported(authenticationMethod)) - { - throw SQL.UnsupportedAuthenticationByProvider(authenticationMethod.ToString(), provider.GetType().Name); - } - var methodName = "SetProvider"; - if (Instance._authenticationsWithAppSpecifiedProvider.Contains(authenticationMethod)) - { - Instance._sqlAuthLogger.LogError(nameof(SqlAuthenticationProviderManager), methodName, $"Failed to add provider {GetProviderType(provider)} because a user-defined provider with type {GetProviderType(Instance._providers[authenticationMethod])} already existed for authentication {authenticationMethod}."); - - // The app has already specified a Provider for this - // authentication method, so we won't override it. - return false; - } - Instance._providers.AddOrUpdate( - authenticationMethod, - provider, - (SqlAuthenticationMethod key, SqlAuthenticationProvider oldProvider) => - { - if (oldProvider != null) - { - oldProvider.BeforeUnload(authenticationMethod); - } - - provider.BeforeLoad(authenticationMethod); - - Instance._sqlAuthLogger.LogInfo(nameof(SqlAuthenticationProviderManager), methodName, $"Added auth provider {GetProviderType(provider)}, overriding existed provider {GetProviderType(oldProvider)} for authentication {authenticationMethod}."); - return provider; - }); - return true; - } - - /// - /// Reads the authentication provider configuration section from app.config, - /// unless app.config reading is disabled. - /// - /// - /// The section that was found, or null if there is none or app.config was not read. - /// - /// - /// True when app.config was read, false when the EnableAppConfig switch is - /// disabled and it was not. - /// - /// - /// Separated from the static constructor so that it can be tested: the - /// constructor runs once per process, before any test can observe it. - /// - internal static bool TryReadConfigurationSection(out SqlAuthenticationProviderConfigurationSection? configurationSection) - { - configurationSection = null; - - if (!LocalAppContextSwitches.EnableAppConfig) - { - return false; - } - - try - { - // New configuration section "SqlClientAuthenticationProviders" for Microsoft.Data.SqlClient accepted to avoid conflicts with older one. - configurationSection = FetchConfigurationSection(SqlClientAuthenticationProviderConfigurationSection.Name); - if (configurationSection == null) - { - // If configuration section is not yet found, try with old Configuration Section name for backwards compatibility - configurationSection = FetchConfigurationSection(SqlAuthenticationProviderConfigurationSection.Name); - } - } - catch (ConfigurationErrorsException e) - { - // Don't throw an error for invalid config files - SqlClientEventSource.Log.TryTraceEvent("static SqlAuthenticationProviderManager: Unable to load custom SqlAuthenticationProviders or SqlClientAuthenticationProviders. ConfigurationManager failed to load due to configuration errors: {0}", e); - } - - return true; - } - - /// - /// Fetches provided configuration section from app.config file. - /// Does not support reading from appsettings.json yet. - /// - /// - /// - /// - private static T? FetchConfigurationSection(string name) where T : class - { - Type t = typeof(T); - - // TODO: Support reading configuration from appsettings.json for .NET runtime applications. - object section = ConfigurationManager.GetSection(name); - if (section != null) - { - if (section is ConfigurationSection configSection && configSection.GetType() == t) - { - return (T)section; - } - else - { - SqlClientEventSource.Log.TraceEvent("Found a custom {0} configuration but it is not of type {1}.", name, t.FullName); - } - } - return default; - } - - private static SqlAuthenticationMethod AuthenticationEnumFromString(string authentication) - { - switch (authentication.ToLowerInvariant()) - { - case ActiveDirectoryIntegrated: - return SqlAuthenticationMethod.ActiveDirectoryIntegrated; - #pragma warning disable 0618 // Type or member is obsolete - case ActiveDirectoryPassword: - return SqlAuthenticationMethod.ActiveDirectoryPassword; - #pragma warning restore 0618 // Type or member is obsolete - case ActiveDirectoryInteractive: - return SqlAuthenticationMethod.ActiveDirectoryInteractive; - case ActiveDirectoryServicePrincipal: - return SqlAuthenticationMethod.ActiveDirectoryServicePrincipal; - case ActiveDirectoryDeviceCodeFlow: - return SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow; - case ActiveDirectoryManagedIdentity: - return SqlAuthenticationMethod.ActiveDirectoryManagedIdentity; - case ActiveDirectoryMSI: - return SqlAuthenticationMethod.ActiveDirectoryMSI; - case ActiveDirectoryDefault: - return SqlAuthenticationMethod.ActiveDirectoryDefault; - case ActiveDirectoryWorkloadIdentity: - return SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity; - default: - throw SQL.UnsupportedAuthentication(authentication); - } - } - - private static string GetProviderType(SqlAuthenticationProvider? provider) - { - if (provider is null) - { - return "null"; - } - return provider.GetType().FullName ?? "unknown"; - } - } - - /// - public abstract class SqlAuthenticationInitializer - { - /// - public abstract void Initialize(); - } -} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs index c9388a42f1..ddae9c54f9 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs @@ -242,36 +242,11 @@ internal static Exception NonLocalSSEInstance() // SQL.ActiveDirectoryAuth // - internal static Exception UnsupportedAuthentication(string authentication) - { - return ADP.NotSupported(StringsHelper.GetString(Strings.SQL_UnsupportedAuthentication, authentication)); - } - internal static Exception UnsupportedAuthenticationSpecified(SqlAuthenticationMethod authentication) { return ADP.InvalidOperation(StringsHelper.GetString(Strings.SQL_UnsupportedAuthenticationSpecified, authentication)); } - internal static Exception CannotCreateAuthProvider(string authentication, string type, Exception e) - { - return ADP.Argument(StringsHelper.GetString(Strings.SQL_CannotCreateAuthProvider, authentication, type), e); - } - - internal static Exception CannotCreateSqlAuthInitializer(string type, Exception e) - { - return ADP.Argument(StringsHelper.GetString(Strings.SQL_CannotCreateAuthInitializer, type), e); - } - - internal static Exception UnsupportedAuthenticationByProvider(string authentication, string type) - { - return ADP.NotSupported(StringsHelper.GetString(Strings.SQL_UnsupportedAuthenticationByProvider, type, authentication)); - } - - internal static Exception UseWamBrokerRequiresAzureExtensionUpgrade() - { - return ADP.InvalidOperation(StringsHelper.GetString(Strings.SQL_UseWamBrokerRequiresAzureExtensionUpgrade)); - } - internal static Exception CannotFindAuthProvider(SqlAuthenticationMethod authentication) { string authName = authentication.ToString(); @@ -2150,4 +2125,3 @@ public static MethodInfo GetPromotedToken } } } - diff --git a/src/Microsoft.Data.SqlClient/src/PackageReadme.md b/src/Microsoft.Data.SqlClient/src/PackageReadme.md index 0209b118ab..d299d594a9 100644 --- a/src/Microsoft.Data.SqlClient/src/PackageReadme.md +++ b/src/Microsoft.Data.SqlClient/src/PackageReadme.md @@ -63,6 +63,21 @@ Install-Package Microsoft.Data.SqlClient.Extensions.Azure This package provides the `ActiveDirectoryAuthenticationProvider`, which uses `Azure.Identity` credential types supplied by [Azure.Core](https://www.nuget.org/packages/Azure.Core) to handle token acquisition, caching, and credential management. +The authentication registry and `SqlAuthenticationInitializer` now live in +`Microsoft.Data.SqlClient.Extensions.Abstractions`; existing driver type names are forwarded. +`SqlAuthenticationProvider.GetProvider` and `SetProvider` work directly without a reflective +bridge to the driver. Configuration registrations take precedence over application registrations, +which take precedence over discovered Azure defaults. + +In trimmed and NativeAOT .NET 10 applications, explicitly construct and register a provider for +each required method before opening connections: reflective Azure discovery and authentication +app.config loading are removed. Untrimmed applications retain both paths. The switches +`Switch.Microsoft.Data.SqlClient.EnableAppConfig` and +`Switch.Microsoft.Data.SqlClient.EnableAzureExtensionDiscovery` default to `true` and are cached +on first access. Failed initialization or registration returns `null`/`false` and emits +diagnostic guidance through `Microsoft.Data.SqlClient.EventSource` (Informational level, +Trace keyword `0x2`). Bootstrap failures are cached; correct the cause and restart the application. + With this package reference, you can continue to use Entra ID authentication modes directly in your connection string: ```csharp diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs b/src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs index 83cdfeb47d..5fd835df24 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs @@ -3039,24 +3039,6 @@ internal static string SQL_BulkLoadUnspecifiedSortOrder { } } - /// - /// Looks up a localized string similar to Failed to instantiate a SqlAuthenticationInitializer with type '{0}'.. - /// - internal static string SQL_CannotCreateAuthInitializer { - get { - return ResourceManager.GetString("SQL_CannotCreateAuthInitializer", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Failed to instantiate an authentication provider with type '{1}' for '{0}'.. - /// - internal static string SQL_CannotCreateAuthProvider { - get { - return ResourceManager.GetString("SQL_CannotCreateAuthProvider", resourceCulture); - } - } - /// /// Looks up a localized string similar to Cannot create normalizer for '{0}'.. /// @@ -3075,15 +3057,6 @@ internal static string SQL_CannotFindActiveDirectoryAuthProvider { } } - /// - /// Looks up a localized string similar to Setting 'useWamBroker' requires the 'Microsoft.Data.SqlClient.Extensions.Azure' package to expose 'Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProviderOptions'. Upgrade the 'Microsoft.Data.SqlClient.Extensions.Azure' package to a version that includes this type.. - /// - internal static string SQL_UseWamBrokerRequiresAzureExtensionUpgrade { - get { - return ResourceManager.GetString("SQL_UseWamBrokerRequiresAzureExtensionUpgrade", resourceCulture); - } - } - /// /// Looks up a localized string similar to Cannot find an authentication provider for '{0}'.. /// @@ -4236,24 +4209,6 @@ internal static string SQL_UnknownSysTxIsolationLevel { } } - /// - /// Looks up a localized string similar to The authentication '{0}' is not supported.. - /// - internal static string SQL_UnsupportedAuthentication { - get { - return ResourceManager.GetString("SQL_UnsupportedAuthentication", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The provider '{0}' does not support authentication '{1}'.. - /// - internal static string SQL_UnsupportedAuthenticationByProvider { - get { - return ResourceManager.GetString("SQL_UnsupportedAuthenticationByProvider", resourceCulture); - } - } - /// /// Looks up a localized string similar to Unsupported authentication specified in this context: {0}. /// diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.cs.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.cs.resx index 891b54cf3e..c9cc4be2c3 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.cs.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.cs.resx @@ -591,27 +591,12 @@ Přesměrování instance SSE není podporováno u uživatelských instancí, které nejsou místní. - - Ověřování {0} není podporované. - - - Pro {0} se nepodařilo vytvořit instanci zprostředkovatele ověřování s typem {1}. - - - Nepodařilo se vytvořit instanci SqlAuthenticationInitializer s typem {0}. - - - Zprostředkovatel {0} nepodporuje ověřování {1}. - Pro {0} nelze najít zprostředkovatele ověřování. Pro {0} nelze najít zprostředkovatele ověřování. Nainstalujte balíček NuGet Microsoft.Data.SqlClient.Extensions.Azure (https://www.nuget.org/packages/Microsoft.Data.SqlClient.Extensions.Azure), abyste mohli používat metody ověřování Active Directory (Entra ID). - - Nastavení useWamBroker vyžaduje balíček Microsoft.Data.SqlClient.Extensions.Azure, aby se zpřístupnilo Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProviderOptions. Upgradujte balíček Microsoft.Data.SqlClient.Extensions.Azure alespoň na verzi 1.1.0, která zahrnuje tento typ. - Parametr {0} nesmí obsahovat hodnotu Null ani nesmí být prázdný. diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.de.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.de.resx index e9c1277930..26af71d22e 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.de.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.de.resx @@ -591,27 +591,12 @@ Die Umleitung der SSE-Instanz wird bei nicht lokalen Benutzerinstanzen nicht unterstützt. - - Die Authentifizierung "{0}" wird nicht unterstützt. - - - Fehler beim Instanziieren eines Authentifizierungsanbieters des Typs "{1}" für "{0}". - - - Fehler beim Instanziieren eines SqlAuthenticationInitializer des Typs "{0}". - - - Die Authentifizierung "{1}" wird vom Anbieter "{0}" nicht unterstützt. - Für "{0}" wurde kein Authentifizierungsanbieter gefunden. Für „{0}“ wurde kein Authentifizierungsanbieter gefunden. Installieren Sie das NuGet-Paket „Microsoft.Data.SqlClient.Extensions.Azure“ (https://www.nuget.org/packages/Microsoft.Data.SqlClient.Extensions.Azure), um Active Directory (Entra ID)-Authentifizierungsmethoden zu verwenden. - - Die Einstellung „useWamBroker“ erfordert das Paket „Microsoft.Data.SqlClient.Extensions.Azure“, um „Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProviderOptions“ bereitzustellen. Aktualisieren Sie das Paket „Microsoft.Data.SqlClient.Extensions.Azure“ auf mindestens Version 1.1.0, die diesen Typ enthält. - Der Parameter "{0}" darf nicht NULL oder leer sein. diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.es.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.es.resx index abbf3d5d5e..f368d77dc9 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.es.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.es.resx @@ -591,27 +591,12 @@ Las instancias de usuario no locales no admiten la redirección SSE Instance. - - No se admite la autenticación "{0}". - - - No se pudo crear una instancia de un proveedor de autenticación con el tipo "{1}" para "{0}". - - - No se pudo crear una instancia de SqlAuthenticationInitializer con el tipo "{0}". - - - El proveedor "{0}" no admite la autenticación "{1}". - No se encuentra ningún proveedor de autenticación para "{0}". No se encuentra un proveedor de autenticación para ''{0}''. Instale el paquete NuGet "Microsoft.Data.SqlClient.Extensions.Azure" (https://www.nuget.org/packages/Microsoft.Data.SqlClient.Extensions.Azure) para usar métodos de autenticación Active Directory (Id. de Entra). - - La configuración "useWamBroker" requiere que el paquete "Microsoft.Data.SqlClient.Extensions.Azure" exponga "Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProviderOptions". Actualice el paquete "Microsoft.Data.SqlClient.Extensions.Azure" a, como mínimo, la versión 1.1.0, que incluye este tipo. - El parámetro "{0}" no puede ser nulo ni estar vacío. diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.fr.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.fr.resx index 9586cbc67e..f8a3ea9570 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.fr.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.fr.resx @@ -591,27 +591,12 @@ La redirection d'instance SSE n'est pas prise en charge pour les instances d'utilisateurs non locaux. - - L'authentification '{0}' n'est pas prise en charge. - - - Échec de l'instanciation d'un fournisseur d'authentification ayant le type '{1}' pour '{0}'. - - - Échec de l'instanciation d'un SqlAuthenticationInitializer ayant le type '{0}'. - - - Le fournisseur '{0}' ne prend pas en charge l'authentification '{1}'. - Le fournisseur d'authentification est introuvable pour '{0}'. Impossible de trouver un fournisseur d'authentification pour « {0} ». Installez le package NuGet « Microsoft.Data.SqlClient.Extensions.Azure » (https://www.nuget.org/packages/Microsoft.Data.SqlClient.Extensions.Azure) pour utiliser les méthodes d'authentification Active Directory Domain Services (Entra ID). - - Le paramètre « useWamBroker » nécessite le package « Microsoft.Data.SqlClient.Extensions.Azure » pour exposer « Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProviderOptions ». Mettez à niveau le package « Microsoft.Data.SqlClient.Extensions.Azure » vers la version 1.1.0 ou ultérieure, qui inclut ce type. - Le paramètre « {0} » ne peut pas être Null ou vide. diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.it.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.it.resx index 9dca930330..3ddc230d9f 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.it.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.it.resx @@ -591,27 +591,12 @@ Il reindirizzamento dell'istanza SSE non è supportato per le istanze di utenti non locali. - - L'autenticazione '{0}' non è supportata. - - - Non è stato possibile creare un'istanza di un provider di autenticazione di tipo '{1}' per '{0}'. - - - Non è stato possibile creare un'istanza di un elemento SqlAuthenticationInitializer di tipo '{0}'. - - - Il provider '{0}' non supporta l'autenticazione '{1}'. - Non è possibile trovare un provider di autenticazione per '{0}'. Non è possibile trovare un provider di autenticazione per '{0}'. Installare il pacchetto NuGet 'Microsoft.Data.SqlClient.Extensions.Azure' (https://www.nuget.org/packages/Microsoft.Data.SqlClient.Extensions.Azure) per usare i metodi di autenticazione di Active Directory (Entra ID). - - L'impostazione ''useWamBroker'' richiede che il pacchetto ''Microsoft.Data.SqlClient.Extensions.Azure'' esponga ''Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProviderOptions''. Aggiorna il pacchetto ''Microsoft.Data.SqlClient.Extensions.Azure'' almeno alla versione 1.1.0, che include questo tipo. - Il parametro '{0}' non può essere Null o vuoto. diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.ja.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.ja.resx index ada7543ef3..16fd1b8063 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.ja.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.ja.resx @@ -591,27 +591,12 @@ 非ローカル ユーザーのインスタンスでは、SSE インスタンスのリダイレクトを行うことはできません。 - - 認証 '{0}' はサポートされていません。 - - - '{0}' の種類 '{1}' の認証プロバイダーをインスタンス化できませんでした。 - - - 種類 '{0}' の SqlAuthenticationInitializer をインスタンス化できませんでした。 - - - プロバイダー '{0}' では認証 '{1}' はサポートされていません。 - '{0}' の認証プロバイダーが見つかりません。 '{0}' の認証プロバイダーが見つかりません。Active Directory (Entra ID) 認証方法を使用するには、'Microsoft.Data.SqlClient.Extensions.Azure' NuGet パッケージ (https://www.nuget.org/packages/Microsoft.Data.SqlClient.Extensions.Azure) をインストールしてください。 - - 'useWamBroker' を設定するには、'Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProviderOptions' を公開する 'Microsoft.Data.SqlClient.Extensions.Azure' パッケージが必要です。この型を含む v1.1.0 以降の 'Microsoft.Data.SqlClient.Extensions.Azure' パッケージにアップグレードしてください。 - パラメーター '{0}' を null や空にすることはできません。 diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.ko.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.ko.resx index cddfe278bf..3bfeba1b78 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.ko.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.ko.resx @@ -591,27 +591,12 @@ SSE 인스턴스 리디렉션은 로컬이 아닌 사용자 인스턴스에는 지원되지 않습니다. - - '{0}' 인증을 사용할 수 없습니다. - - - '{0}'에 대해 '{1}' 형식을 사용하여 인증 공급자를 인스턴스화하지 못했습니다. - - - '{0}' 형식을 사용하여 SqlAuthenticationInitializer를 인스턴스화하지 못했습니다. - - - '{0}' 공급자에서 '{1}' 인증을 지원하지 않습니다. - '{0}'에 대한 인증 공급자를 찾을 수 없습니다. '{0}'에 대한 인증 공급자를 찾을 수 없습니다. Active Directory(Entra ID) 인증 방법을 사용하려면 'Microsoft.Data.SqlClient.Extensions.Azure' NuGet 패키지(https://www.nuget.org/packages/Microsoft.Data.SqlClient.Extensions.Azure)를 설치하세요. - - 'useWamBroker'를 설정하려면 'Microsoft.Data.SqlClient.Extensions.Azure' 패키지가 'Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProviderOptions'를 노출해야 합니다. 이 형식을 포함하는 'Microsoft.Data.SqlClient.Extensions.Azure' 패키지를 v1.1.0 이상으로 업그레이드하세요. - '{0}' 매개 변수는 null이거나 비워 둘 수 없습니다. diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.pl.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.pl.resx index 79edb911b0..e994703634 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.pl.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.pl.resx @@ -591,27 +591,12 @@ Przekierowywanie wystąpienia SSE nie jest obsługiwane dla wystąpień użytkowników nielokalnych. - - Uwierzytelnianie „{0}” nie jest obsługiwane. - - - Nie można utworzyć wystąpienia dostawcy uwierzytelniania typu {1} dla elementu {0}. - - - Nie można utworzyć wystąpienia elementu SqlAuthenticationInitializer typu {0}. - - - Dostawca „{0}” nie obsługuje uwierzytelniania „{1}”. - Nie można odnaleźć dostawcy uwierzytelniania elementu {0}. Nie można odnaleźć dostawcy uwierzytelniania dla elementu „{0}”. Zainstaluj pakiet NuGet „Microsoft.Data.SqlClient.Extensions.Azure” (https://www.nuget.org/packages/Microsoft.Data.SqlClient.Extensions.Azure), aby korzystać z metod uwierzytelniania usługi Active Directory (Entra ID). - - Ustawienie „useWamBroker” wymaga pakietu „Microsoft.Data.SqlClient.Extensions.Azure”, aby uwidoczniono element „Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProviderOptions”. Uaktualnij pakiet „Microsoft.Data.SqlClient.Extensions.Azure” do wersji co najmniej 1.1.0, która zawiera ten typ. - Parametr „{0}” nie może mieć wartości null ani być pusty. diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.pt-BR.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.pt-BR.resx index 1e0f99cc1e..646da15655 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.pt-BR.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.pt-BR.resx @@ -591,27 +591,12 @@ Não há suporte para redirecionamento da Instância SSE para instâncias de usuários que não sejam locais. - - A autenticação '{0}' não tem suporte. - - - Falha ao criar uma instância de um provedor de autenticação com o tipo '{1}' para '{0}'. - - - Falha ao criar um SqlAuthenticationInitializer com o tipo '{0}'. - - - O provedor '{0}' não dá suporte à autenticação '{1}'. - Não é possível localizar um provedor de autenticação para '{0}'. Não é possível localizar um provedor de autenticação para '{0}'. Instale o pacote NuGet 'Microsoft.Data.SqlClient.Extensions.Azure' (https://www.nuget.org/packages/Microsoft.Data.SqlClient.Extensions.Azure) para usar métodos de autenticação do Active Directory (Entra ID). - - A definição ''useWamBroker'' exige que o pacote ''Microsoft.Data.SqlClient.Extensions.Azure'' exponha ''Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProviderOptions''. Atualize o pacote ''Microsoft.Data.SqlClient.Extensions.Azure'' para a versão v1.1.0 ou posterior, que inclui esse tipo. - O parâmetro '{0}' não pode ser nulo ou vazio. diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.resx index de17e0d809..cf1b662136 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.resx @@ -591,27 +591,12 @@ SSE Instance re-direction is not supported for non-local user instances. - - The authentication '{0}' is not supported. - - - Failed to instantiate an authentication provider with type '{1}' for '{0}'. - - - Failed to instantiate a SqlAuthenticationInitializer with type '{0}'. - - - The provider '{0}' does not support authentication '{1}'. - Cannot find an authentication provider for '{0}'. Cannot find an authentication provider for '{0}'. Install the 'Microsoft.Data.SqlClient.Extensions.Azure' NuGet package (https://www.nuget.org/packages/Microsoft.Data.SqlClient.Extensions.Azure) to use Active Directory (Entra ID) authentication methods. - - Setting 'useWamBroker' requires the 'Microsoft.Data.SqlClient.Extensions.Azure' package to expose 'Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProviderOptions'. Upgrade the 'Microsoft.Data.SqlClient.Extensions.Azure' package to at least v1.1.0 that includes this type. - Parameter '{0}' cannot be null or empty. diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.ru.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.ru.resx index d560e1902f..e7f7771c22 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.ru.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.ru.resx @@ -591,27 +591,12 @@ Перенаправление экземпляра SSE не поддерживается для нелокальных пользовательских интерфейсов. - - Проверка подлинности "{0}" не поддерживается. - - - Не удалось создать экземпляр поставщика проверки подлинности с типом "{1}" для "{0}". - - - Не удалось создать экземпляр SqlAuthenticationInitializer с типом "{0}". - - - Поставщик "{0}" не поддерживает проверку подлинности "{1}". - Не удается найти поставщик проверки подлинности для "{0}". Не удается найти поставщик проверки подлинности для "{0}". Установите пакет NuGet "Microsoft.Data.SqlClient.Extensions.Azure" (https://www.nuget.org/packages/Microsoft.Data.SqlClient.Extensions.Azure) для использования методов проверки подлинности Active Directory (Entra ID). - - Для установки параметра "useWamBroker" требуется, чтобы пакет "Microsoft.Data.SqlClient.Extensions.Azure" предоставлял доступ к "Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProviderOptions". Обновите пакет "Microsoft.Data.SqlClient.Extensions.Azure" как минимум до версии v1.1.0, в которую входит этот тип. - Параметр «{0}» не может иметь значение NULL или быть пустым. diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.tr.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.tr.resx index 00c4ccc94d..119b4b7ecd 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.tr.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.tr.resx @@ -591,27 +591,12 @@ SSE Örneği yeniden yönlendirmesi, yerel olmayan kullanıcı örnekleri için desteklenmiyor. - - '{0}' kimlik doğrulaması desteklenmiyor. - - - '{0}' için '{1}' türüne sahip bir kimlik doğrulama sağlayıcısı örneği oluşturulamadı. - - - '{0}' türüne sahip bir SqlAuthenticationInitializer örneği oluşturulamadı. - - - '{0}' adlı sağlayıcı, '{1}' kimlik doğrulamasını desteklemiyor. - '{0}' için kimlik doğrulama sağlayıcısı bulunamıyor. '{0}' için kimlik doğrulaması sağlayıcısı bulunamıyor. Active Directory (Entra ID) kimlik doğrulaması yöntemlerini kullanmak için ‘Microsoft.Data.SqlClient.Extensions.Azure’ NuGet paketini (https://www.nuget.org/packages/Microsoft.Data.SqlClient.Extensions.Azure) yükleyin. - - 'useWamBroker' ayarı, 'Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProviderOptions' türünü kullanıma sunmak için 'Microsoft.Data.SqlClient.Extensions.Azure' paketini gerektirir. Bu türü içeren en az v1.1.0 sürümüne ulaşmak için 'Microsoft.Data.SqlClient.Extensions.Azure' paketini yükseltin. - '{0}' parametresi null veya boş olamaz. diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.zh-Hans.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.zh-Hans.resx index c32097660d..95522e9f5d 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.zh-Hans.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.zh-Hans.resx @@ -591,27 +591,12 @@ 非本地用户实例不支持 SSE 实例重定向。 - - 不支持身份验证“{0}”。 - - - 未能实例化“{0}”的类型为“{1}”的身份验证提供程序。 - - - 未能实例化类型为“{0}”的 SqlAuthenticationInitializer。 - - - 提供程序“{0}”不支持身份验证“{1}”。 - 找不到“{0}”的身份验证提供程序。 找不到 ‘{0}’ 的身份验证提供程序。安装 'Microsoft.Data.SqlClient.Extensions.Azure' NuGet 包(https://www.nuget.org/packages/Microsoft.Data.SqlClient.Extensions.Azure)以使用 Active Directory (Entra ID)身份验证方法。 - - 要设置 "useWamBroker",需要 "Microsoft.Data.SqlClient.Extensions.Azure" 包公开 "Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProviderOptions"。将 "Microsoft.Data.SqlClient.Extensions.Azure" 包升级到至少 v1.1.0,该版本包含此类型。 - 参数“{0}”不能为 null 或为空。 diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.zh-Hant.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.zh-Hant.resx index a369957b12..b7b42f827b 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.zh-Hant.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.zh-Hant.resx @@ -591,27 +591,12 @@ 不支援非本端使用者執行個體的 SSE 執行個體重新導向。 - - 不支援驗證 '{0}'。 - - - 無法為 '{0}' 具現化類型為 '{1}' 的驗證提供者。 - - - 無法將類型為 '{0}' 的 SqlAuthenticationInitializer 具現化。 - - - 提供者 '{0}' 不支援驗證 '{1}'。 - 找不到 '{0}' 的驗證提供者。 找不到 '{0}' 的驗證提供者。請安裝 'Microsoft.Data.SqlClient.Extensions.Azure' NuGet 套件 (https://www.nuget.org/packages/Microsoft.Data.SqlClient.Extensions.Azure),以使用 Active Directory (Entra ID) 認證方法。 - - 設定 'useWamBroker' 需要 'Microsoft.Data.SqlClient.Extensions.Azure' 套件以公開 'Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProviderOptions'。請將 'Microsoft.Data.SqlClient.Extensions.Azure' 套件升級至至少 v1.1.0,其中包含此類型。 - 參數 '{0}' 不能是 Null 或空白。 diff --git a/src/Microsoft.Data.SqlClient/src/TypeForwards.Abstractions.cs b/src/Microsoft.Data.SqlClient/src/TypeForwards.Abstractions.cs index eeaf2dfb51..86ce61707d 100644 --- a/src/Microsoft.Data.SqlClient/src/TypeForwards.Abstractions.cs +++ b/src/Microsoft.Data.SqlClient/src/TypeForwards.Abstractions.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(Microsoft.Data.SqlClient.SqlAuthenticationMethod))] +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(Microsoft.Data.SqlClient.SqlAuthenticationInitializer))] [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(Microsoft.Data.SqlClient.SqlAuthenticationParameters))] [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(Microsoft.Data.SqlClient.SqlAuthenticationProvider))] [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(Microsoft.Data.SqlClient.SqlAuthenticationProviderException))] diff --git a/src/Microsoft.Data.SqlClient/tests/AuthenticationTests/ConfigurationTest/Program.cs b/src/Microsoft.Data.SqlClient/tests/AuthenticationTests/ConfigurationTest/Program.cs index d8273770f7..e383b9ffa5 100644 --- a/src/Microsoft.Data.SqlClient/tests/AuthenticationTests/ConfigurationTest/Program.cs +++ b/src/Microsoft.Data.SqlClient/tests/AuthenticationTests/ConfigurationTest/Program.cs @@ -21,8 +21,10 @@ Require(section.GetType().Assembly.GetName().Name == "Microsoft.Data.SqlClient.Extensions.Abstractions", "The assembly-qualified handler was not forwarded to Abstractions."); Require(SqlAuthenticationProvider.GetProvider(SqlAuthenticationMethod.ActiveDirectoryInteractive) is TestProvider, - "The driver's typed section retrieval did not load the configured provider."); -Require(TestInitializer.Initialized, "The configured driver initializer did not run."); + "The Abstractions registry did not load the configured provider."); +Require(TestInitializer.Initialized, "The configured initializer did not run."); +Require(typeof(SqlAuthenticationInitializer).Assembly.GetName().Name == "Microsoft.Data.SqlClient.Extensions.Abstractions", + "The initializer type did not relocate to Abstractions."); Require(!SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryInteractive, new TestProvider()), "Application registration replaced the configured provider."); Console.WriteLine("PASS: " + scenario); @@ -36,7 +38,7 @@ static void Require(bool condition, string message) } } -/// Proves initializer activation still uses the driver-owned type in this intermediate layer. +/// Proves configured initializers activate through the forwarded Abstractions type. public sealed class TestInitializer : SqlAuthenticationInitializer { public static bool Initialized; diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AADAuthenticationTests.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AADAuthenticationTests.cs index dc6d5b3d7a..54fae22310 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AADAuthenticationTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AADAuthenticationTests.cs @@ -52,15 +52,14 @@ private void InvalidCombinationCheck(SqlCredential credential) /// /// Tests whether a dummy SQL Auth provider is registered due to - /// configuration in an app.config file. Only .NET Framework reads - /// from the app.config file, so this test is only valid for that - /// runtime. + /// configuration in an app.config file. This test host supplies the + /// configuration only on .NET Framework; the dedicated AuthenticationTests + /// harness also covers untrimmed .NET configuration loading. /// /// See the app.config file in the same directory as this file. /// - /// .NET (Core) reads similar configuration from appsettings.json, but - /// our SqlAuthenticationProviderManager does not currently support - /// that configuration source. + /// The Abstractions authentication registry does not currently support + /// appsettings.json as a configuration source. /// [ConditionalFact(typeof(TestUtility), nameof(TestUtility.IsNetFramework))] public async Task IsDummySqlAuthenticationProviderSetByDefault() diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/EnableAppConfigSwitchTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/EnableAppConfigSwitchTest.cs index c611d16f79..372b73e70e 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/EnableAppConfigSwitchTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/EnableAppConfigSwitchTest.cs @@ -12,7 +12,8 @@ namespace Microsoft.Data.SqlClient.UnitTests; /// /// Tests that the EnableAppConfig switch gates every app.config reader: the /// configurable retry logic providers, the switch overrides applied during -/// static initialization, and the authentication provider section. +/// static initialization. Authentication configuration is covered by the +/// dedicated AuthenticationTests configuration-process harness. /// [Collection(AppContextSwitchTestCollection.Name)] public class EnableAppConfigSwitchTest @@ -85,25 +86,6 @@ public void SwitchOverridesAreReadOnlyWhenEnabled(bool enabled) Assert.Equal(enabled, LocalAppContextSwitches.ApplyAppConfigSwitchOverrides()); } - /// - /// The switch gates the authentication provider section read during static - /// initialization. - /// - [Theory] - [InlineData(true)] - [InlineData(false)] - public void AuthenticationProvidersAreReadOnlyWhenEnabled(bool enabled) - { - using LocalAppContextSwitchesHelper switchesHelper = new(); - switchesHelper.EnableAppConfig = enabled; - - Assert.Equal( - enabled, - SqlAuthenticationProviderManager.TryReadConfigurationSection(out SqlAuthenticationProviderConfigurationSection? section)); - - // No test host supplies the section, so it is null either way. - Assert.Null(section); - } } #endif diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/TypeForwardTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/TypeForwardTests.cs index ed11feb494..5f9e27237a 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/TypeForwardTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/TypeForwardTests.cs @@ -16,6 +16,7 @@ public class TypeForwardTests /// Type forwards preserve resolution through the original assembly name. [Theory] [InlineData("Microsoft.Data.SqlClient.SqlAuthenticationMethod", true)] + [InlineData("Microsoft.Data.SqlClient.SqlAuthenticationInitializer", false)] [InlineData("Microsoft.Data.SqlClient.SqlAuthenticationParameters", false)] [InlineData("Microsoft.Data.SqlClient.SqlAuthenticationProvider", false)] [InlineData("Microsoft.Data.SqlClient.SqlAuthenticationProviderException", false)]