diff --git a/BUILDGUIDE.md b/BUILDGUIDE.md index 0313de22a3..6628129b92 100644 --- a/BUILDGUIDE.md +++ b/BUILDGUIDE.md @@ -56,6 +56,24 @@ package the project. The `build.proj` file provides convenient targets to accomp > This section is not exhaustive of all targets or parameters to `build.proj`. Complete documentation is available in > [`build.proj`](build.proj). +### Authentication configuration compatibility checks + +The executable fixtures under `src/Microsoft.Data.SqlClient/tests/AuthenticationTests/` +run in separate processes to validate both current and legacy app.config section +declarations, their forwarded handler types, initializer activation and configured +provider precedence, without SQL Server or identity-service access. + +```bash +dotnet build build.proj -t:TestAuthenticationConfiguration -p:Configuration=Release -p:TestFramework=net10.0 -p:ReferenceType=Project +dotnet build build.proj -t:TestAuthenticationConfiguration -p:Configuration=Release -p:TestFramework=net10.0 -p:ReferenceType=Package +``` + +Package mode prepares local packages unless `-p:SkipDependencyPack=true` is supplied. +Use `-p:PackageVersionSqlClient=` for prebuilt packages and an isolated +`NUGET_PACKAGES` directory when validating package changes to avoid stale cache entries. +On Windows, select `-p:TestFramework=net462` to execute .NET Framework checks. +Cross-building .NET Framework on Linux does not execute those scenarios. + ### Building Projects From the root of your repository, run `dotnet build` against `build.proj` with a build target, following this pattern: diff --git a/build.proj b/build.proj index dc740a3db2..bbca4a8a50 100644 --- a/build.proj +++ b/build.proj @@ -604,6 +604,7 @@ $(SqlClientSrcRoot)tests/FunctionalTests/Microsoft.Data.SqlClient.FunctionalTests.csproj $(SqlClientSrcRoot)tests/ManualTests/Microsoft.Data.SqlClient.ManualTests.csproj $(SqlClientSrcRoot)tests/UnitTests/Microsoft.Data.SqlClient.UnitTests.csproj + $(SqlClientSrcRoot)tests/AuthenticationTests/RunAuthenticationTests.ps1 $(RepoRoot)tools/GenAPI/Microsoft.DotNet.GenAPI/ @@ -611,6 +612,17 @@ + + $(TestFramework) + net10.0 + -PackageVersionSqlClient "$(PackageVersionSqlClient)" + PackSqlClient + + + + + diff --git a/doc/samples/SqlAuthenticationProviders.config b/doc/samples/SqlAuthenticationProviders.config new file mode 100644 index 0000000000..ec10cc4627 --- /dev/null +++ b/doc/samples/SqlAuthenticationProviders.config @@ -0,0 +1,22 @@ + + + + + +
+ + + + + + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Abstractions.csproj b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Abstractions.csproj index 1c9b5cc8b6..ba3a673041 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Abstractions.csproj +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Abstractions.csproj @@ -78,6 +78,8 @@ Condition="'$(ReferenceType)' == 'Project'" /> + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/PackageReadme.md b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/PackageReadme.md index 69a2fa1b26..45cacf13ce 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/PackageReadme.md +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/PackageReadme.md @@ -18,6 +18,23 @@ This package supports: - .NET Standard 2.0 (compatible with .NET Framework 4.6.1+, .NET Core 2.0+, and .NET 5+) +## Authentication configuration + +The public `SqlAuthenticationProviderConfigurationSection` (legacy +`SqlAuthenticationProviders`) and `SqlClientAuthenticationProviderConfigurationSection` +(`SqlClientAuthenticationProviders`) handlers are defined in this package and forwarded +from SqlClient. Existing app.config section declarations naming `Microsoft.Data.SqlClient` +remain valid. Section names, properties (`providers`, `initializerType`, +`applicationClientId`, `useWamBroker`) and empty defaults are unchanged. + +`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. + +See the repository's `doc/samples/SqlAuthenticationProviders.config` for a section +declaration and custom provider example. + ## Installation Install the package via NuGet: diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProviderConfigurationSection.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProviderConfigurationSection.cs new file mode 100644 index 0000000000..c744406f14 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProviderConfigurationSection.cs @@ -0,0 +1,39 @@ +// 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.Configuration; + +namespace Microsoft.Data.SqlClient +{ + /// Defines the legacy SqlAuthenticationProviders section for authentication configuration in app.config. + public class SqlAuthenticationProviderConfigurationSection : ConfigurationSection + { + /// The name of the legacy authentication configuration section. + public const string Name = "SqlAuthenticationProviders"; + + /// Gets the user-defined authentication providers. + [ConfigurationProperty("providers")] + public ProviderSettingsCollection Providers => (ProviderSettingsCollection)this["providers"]; + + /// Gets the assembly-qualified initializer type name, or an empty string when unset. + [ConfigurationProperty("initializerType")] + public string InitializerType => this["initializerType"] as string ?? string.Empty; + + /// Gets the application client ID, or an empty string when unset. + [ConfigurationProperty("applicationClientId", IsRequired = false)] + public string ApplicationClientId => this["applicationClientId"] as string ?? string.Empty; + + /// Gets the Windows Authentication Manager broker preference, or an empty string when unset. + /// The value is a string to distinguish an unset preference from an explicit false value. + [ConfigurationProperty("useWamBroker", IsRequired = false)] + public string UseWamBroker => this["useWamBroker"] as string ?? string.Empty; + } + + /// Defines the current SqlClientAuthenticationProviders section for authentication configuration in app.config. + public class SqlClientAuthenticationProviderConfigurationSection : SqlAuthenticationProviderConfigurationSection + { + /// The name of the current authentication configuration section. + public new const string Name = "SqlClientAuthenticationProviders"; + } +} diff --git a/src/Microsoft.Data.SqlClient.slnx b/src/Microsoft.Data.SqlClient.slnx index 9c03addd51..6dde1aff04 100644 --- a/src/Microsoft.Data.SqlClient.slnx +++ b/src/Microsoft.Data.SqlClient.slnx @@ -202,6 +202,9 @@ + + + diff --git a/src/Microsoft.Data.SqlClient/notsupported/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/notsupported/Microsoft.Data.SqlClient.csproj index a55fb0ff86..c5d7ce8bd5 100644 --- a/src/Microsoft.Data.SqlClient/notsupported/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/notsupported/Microsoft.Data.SqlClient.csproj @@ -105,6 +105,10 @@ + + + + + + + - + @@ -78,7 +78,7 @@ - + diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs index de87702ffa..9ff301b805 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs @@ -578,49 +578,6 @@ private static string GetProviderType(SqlAuthenticationProvider? provider) } } - /// - /// The configuration section definition for reading app.config. - /// - internal class SqlAuthenticationProviderConfigurationSection : ConfigurationSection - { - public const string Name = "SqlAuthenticationProviders"; - - /// - /// User-defined auth providers. - /// - [ConfigurationProperty("providers")] - public ProviderSettingsCollection Providers => (ProviderSettingsCollection)this["providers"]; - - /// - /// User-defined initializer. - /// - [ConfigurationProperty("initializerType")] - public string InitializerType => this["initializerType"] as string ?? string.Empty; - - /// - /// Application Client Id - /// - [ConfigurationProperty("applicationClientId", IsRequired = false)] - public string ApplicationClientId => this["applicationClientId"] as string ?? string.Empty; - - /// - /// Forwarded to ActiveDirectoryAuthenticationProviderOptions.UseWamBroker - /// when the Azure extension's default provider is auto-installed. Stored as a string so - /// that an unset attribute can be distinguished from useWamBroker="false"; the - /// runtime parses it with . - /// - [ConfigurationProperty("useWamBroker", IsRequired = false)] - public string UseWamBroker => this["useWamBroker"] as string ?? string.Empty; - } - - /// - /// The configuration section definition for reading app.config. - /// - internal class SqlClientAuthenticationProviderConfigurationSection : SqlAuthenticationProviderConfigurationSection - { - public new const string Name = "SqlClientAuthenticationProviders"; - } - /// public abstract class SqlAuthenticationInitializer { diff --git a/src/Microsoft.Data.SqlClient/src/TypeForwards.Abstractions.cs b/src/Microsoft.Data.SqlClient/src/TypeForwards.Abstractions.cs index f8272c5d2b..eeaf2dfb51 100644 --- a/src/Microsoft.Data.SqlClient/src/TypeForwards.Abstractions.cs +++ b/src/Microsoft.Data.SqlClient/src/TypeForwards.Abstractions.cs @@ -7,3 +7,5 @@ [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(Microsoft.Data.SqlClient.SqlAuthenticationProvider))] [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(Microsoft.Data.SqlClient.SqlAuthenticationProviderException))] [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(Microsoft.Data.SqlClient.SqlAuthenticationToken))] +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(Microsoft.Data.SqlClient.SqlAuthenticationProviderConfigurationSection))] +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(Microsoft.Data.SqlClient.SqlClientAuthenticationProviderConfigurationSection))] diff --git a/src/Microsoft.Data.SqlClient/tests/AuthenticationTests/ConfigurationTest/ConfigurationTest.csproj b/src/Microsoft.Data.SqlClient/tests/AuthenticationTests/ConfigurationTest/ConfigurationTest.csproj new file mode 100644 index 0000000000..9b8b7bea80 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/AuthenticationTests/ConfigurationTest/ConfigurationTest.csproj @@ -0,0 +1,19 @@ + + + AuthenticationConfigurationTest + Exe + net462;net10.0 + enable + enable + false + + + + + + + + + + + diff --git a/src/Microsoft.Data.SqlClient/tests/AuthenticationTests/ConfigurationTest/Program.cs b/src/Microsoft.Data.SqlClient/tests/AuthenticationTests/ConfigurationTest/Program.cs new file mode 100644 index 0000000000..d8273770f7 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/AuthenticationTests/ConfigurationTest/Program.cs @@ -0,0 +1,57 @@ +// 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 Microsoft.Data.SqlClient; +using System.Configuration; + +string scenario = args.Single(); +string sectionName = scenario switch +{ + "configured" => "SqlClientAuthenticationProviders", + "legacy" => "SqlAuthenticationProviders", + _ => throw new ArgumentException("Unknown configuration scenario: " + scenario) +}; + +var section = ConfigurationManager.GetSection(sectionName) as SqlAuthenticationProviderConfigurationSection + ?? throw new InvalidOperationException("The section declaration did not resolve."); +ConfigurationSection configurationSection = section; +Require(configurationSection.ElementInformation.Properties.Count == 4 && section.Providers.Count == 1, + "The public configuration schema did not compile or load through its transitive dependencies."); +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."); +Require(!SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryInteractive, new TestProvider()), + "Application registration replaced the configured provider."); +Console.WriteLine("PASS: " + scenario); + +/// Fails the executable scenario when its required compatibility contract is not preserved. +static void Require(bool condition, string message) +{ + if (!condition) + { + throw new InvalidOperationException(message); + } +} + +/// Proves initializer activation still uses the driver-owned type in this intermediate layer. +public sealed class TestInitializer : SqlAuthenticationInitializer +{ + public static bool Initialized; + + /// + public override void Initialize() => Initialized = true; +} + +/// Registers a provider without contacting an identity service or a database. +public sealed class TestProvider : SqlAuthenticationProvider +{ + /// + public override bool IsSupported(SqlAuthenticationMethod method) => true; + + /// + public override Task AcquireTokenAsync(SqlAuthenticationParameters parameters) => + throw new NotSupportedException(); +} diff --git a/src/Microsoft.Data.SqlClient/tests/AuthenticationTests/ConfigurationTest/app.config b/src/Microsoft.Data.SqlClient/tests/AuthenticationTests/ConfigurationTest/app.config new file mode 100644 index 0000000000..8259d66e2d --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/AuthenticationTests/ConfigurationTest/app.config @@ -0,0 +1,12 @@ + + + +
+ + + + + + + diff --git a/src/Microsoft.Data.SqlClient/tests/AuthenticationTests/ConfigurationTest/legacy.config b/src/Microsoft.Data.SqlClient/tests/AuthenticationTests/ConfigurationTest/legacy.config new file mode 100644 index 0000000000..62dd88dc42 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/AuthenticationTests/ConfigurationTest/legacy.config @@ -0,0 +1,12 @@ + + + +
+ + + + + + + diff --git a/src/Microsoft.Data.SqlClient/tests/AuthenticationTests/RunAuthenticationTests.ps1 b/src/Microsoft.Data.SqlClient/tests/AuthenticationTests/RunAuthenticationTests.ps1 new file mode 100644 index 0000000000..1628b6451f --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/AuthenticationTests/RunAuthenticationTests.ps1 @@ -0,0 +1,55 @@ +# 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. + +param( + [ValidateSet('net462', 'net10.0')] + [string]$Framework = 'net10.0', + [ValidateSet('Project', 'Package')] + [string]$ReferenceType = 'Project', + [ValidateSet('Debug', 'Release')] + [string]$Configuration = 'Release', + [string]$PackageVersionSqlClient +) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest + +if ($Framework -eq 'net462' -and ![System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform( + [System.Runtime.InteropServices.OSPlatform]::Windows)) { + throw 'Executing net462 authentication scenarios requires Windows; cross-building is not runtime validation.' +} + +function Invoke-DotNet { + param([string[]]$CommandArguments) + & dotnet @CommandArguments + if ($LASTEXITCODE -ne 0) { + throw "dotnet failed ($LASTEXITCODE): $($CommandArguments -join ' ')" + } +} + +$project = Join-Path $PSScriptRoot 'ConfigurationTest/ConfigurationTest.csproj' +$buildOptions = @("-p:ReferenceType=$ReferenceType") +if ($PackageVersionSqlClient) { + $buildOptions += "-p:SqlClientPackageVersion=$PackageVersionSqlClient" +} +Invoke-DotNet -CommandArguments (@('build', $project, '-c', $Configuration, '-f', $Framework) + $buildOptions) +$output = Join-Path $PSScriptRoot "ConfigurationTest/bin/$Configuration/$Framework" +$extension = if ($Framework -eq 'net462') { 'exe' } else { 'dll' } +$app = Join-Path $output "AuthenticationConfigurationTest.$extension" +$configPath = "$app.config" +$originalConfig = [System.IO.File]::ReadAllBytes($configPath) +try { + foreach ($scenario in @('configured', 'legacy')) { + $config = if ($scenario -eq 'configured') { 'app.config' } else { 'legacy.config' } + Copy-Item -LiteralPath (Join-Path $PSScriptRoot "ConfigurationTest/$config") -Destination $configPath -Force + if ($Framework -eq 'net462') { + & $app $scenario + if ($LASTEXITCODE -ne 0) { throw "Configuration test failed: $scenario" } + } else { + Invoke-DotNet -CommandArguments @($app, $scenario) + } + } +} finally { + [System.IO.File]::WriteAllBytes($configPath, $originalConfig) +} diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/AuthenticationConfigurationSchemaTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/AuthenticationConfigurationSchemaTest.cs new file mode 100644 index 0000000000..25b73c3a1b --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/AuthenticationConfigurationSchemaTest.cs @@ -0,0 +1,35 @@ +// 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.Configuration; +using System.Linq; +using Xunit; + +namespace Microsoft.Data.SqlClient.UnitTests; + +/// Pins section identity and schema for existing assembly-qualified app.config declarations. +public class AuthenticationConfigurationSchemaTest +{ + /// Both public handlers preserve their original names, configuration properties and empty defaults. + [Theory] + [InlineData("SqlAuthenticationProviderConfigurationSection", "SqlAuthenticationProviders")] + [InlineData("SqlClientAuthenticationProviderConfigurationSection", "SqlClientAuthenticationProviders")] + public void HandlerContractIsPreserved(string typeName, string sectionName) + { + Type type = typeof(SqlConnection).Assembly.GetType("Microsoft.Data.SqlClient." + typeName, throwOnError: true)!; + Assert.Equal("Microsoft.Data.SqlClient.Extensions.Abstractions", type.Assembly.GetName().Name); + Assert.True(type.IsPublic); + Assert.Same(type, Type.GetType(type.FullName + ", Microsoft.Data.SqlClient", throwOnError: true)); + var section = Assert.IsAssignableFrom(Activator.CreateInstance(type)); + Assert.Equal(new[] { "applicationClientId", "initializerType", "providers", "useWamBroker" }, + section.ElementInformation.Properties.Cast() + .Select(property => property.Name).OrderBy(name => name, StringComparer.Ordinal)); + Assert.IsType(section.ElementInformation.Properties["providers"].Value); + Assert.Equal(sectionName, type.GetField("Name")!.GetRawConstantValue()); + Assert.Equal(string.Empty, section.ElementInformation.Properties["initializerType"].Value); + Assert.Equal(string.Empty, section.ElementInformation.Properties["applicationClientId"].Value); + Assert.Equal(string.Empty, section.ElementInformation.Properties["useWamBroker"].Value); + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/TypeForwardTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/TypeForwardTests.cs index 8cb9811933..ed11feb494 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/TypeForwardTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/TypeForwardTests.cs @@ -8,16 +8,20 @@ namespace Microsoft.Data.SqlClient.UnitTests { + /// Verifies relocated public types remain resolvable through the driver assembly. public class TypeForwardTests { private static readonly Assembly s_sqlClientAssembly = typeof(SqlConnection).Assembly; + /// Type forwards preserve resolution through the original assembly name. [Theory] [InlineData("Microsoft.Data.SqlClient.SqlAuthenticationMethod", true)] [InlineData("Microsoft.Data.SqlClient.SqlAuthenticationParameters", false)] [InlineData("Microsoft.Data.SqlClient.SqlAuthenticationProvider", false)] [InlineData("Microsoft.Data.SqlClient.SqlAuthenticationProviderException", false)] [InlineData("Microsoft.Data.SqlClient.SqlAuthenticationToken", false)] + [InlineData("Microsoft.Data.SqlClient.SqlAuthenticationProviderConfigurationSection", false)] + [InlineData("Microsoft.Data.SqlClient.SqlClientAuthenticationProviderConfigurationSection", false)] public void AbstractionsType_CanBeLoadedFromSqlClientAssembly(string typeName, bool isEnum) { // Types moved to the Abstractions assembly must remain loadable via the @@ -26,6 +30,8 @@ public void AbstractionsType_CanBeLoadedFromSqlClientAssembly(string typeName, b Assert.NotNull(type); Assert.Equal(isEnum, type.IsEnum); + Assert.True(type.IsPublic); + Assert.Same(type, Type.GetType(typeName + ", Microsoft.Data.SqlClient", throwOnError: true)); // Assert that the assembly containing the type is the Abstractions assembly. Assert.Equal("Microsoft.Data.SqlClient.Extensions.Abstractions", type.Assembly.GetName().Name);