diff --git a/build.proj b/build.proj index 31795fc6a7..fe652c024d 100644 --- a/build.proj +++ b/build.proj @@ -719,7 +719,12 @@ "$(DotnetPath)dotnet" test "$(SqlClientFunctionalTestProjectPath)" + + -p:Configuration=$(Configuration) + $(SigningKeyPathArgument) + + $(TestBlameArgument) $(TestCodeCoverageArgument) $(TestFiltersArgument) @@ -763,7 +768,12 @@ "$(DotnetPath)dotnet" test "$(SqlClientManualTestProjectPath)" + + -p:Configuration=$(Configuration) + $(SigningKeyPathArgument) + + $(TestBlameArgument) $(TestCodeCoverageArgument) $(ManualTestFiltersArgument) @@ -792,17 +802,24 @@ "$(DotnetPath)dotnet" test "$(SqlClientUnitTestProjectPath)" + + -p:Configuration=$(Configuration) + $(SigningKeyPathArgument) + $(TestSigningKeyPathArgument) + + $(TestBlameArgument) $(TestCodeCoverageArgument) $(TestFiltersArgument) $(TestFrameworkArgument) + --results-directory "$(TestResultsFolderPath)" + --logger:"trx;LogFilePrefix=$(LogFilePrefix)" + + $(ReferenceTypeArgument) - $(TestSigningKeyPathArgument) $(PackageVersionSqlClientArgument) $(PackageVersionSqlServerArgument) - --results-directory "$(TestResultsFolderPath)" - --logger:"trx;LogFilePrefix=$(LogFilePrefix)" @@ -999,22 +1016,28 @@ - AbstractionsTests-$(OS) $(LogFilePrefix)-$(TestFramework) "$(DotnetPath)dotnet" test "$(AbstractionsTestProjectPath)" + + -p:Configuration=$(Configuration) + $(SigningKeyPathArgument) + $(TestSigningKeyPathArgument) + + $(TestBlameArgument) $(TestCodeCoverageArgument) $(TestFiltersArgument) $(TestFrameworkArgument) --results-directory "$(TestResultsFolderPath)" --logger:"trx;LogFilePrefix=$(LogFilePrefix)" + + + $(ReferenceTypeArgument) + $(PackageVersionSqlClientArgument) $([System.Text.RegularExpressions.Regex]::Replace($(DotnetCommand), "\s+", " ")) @@ -1109,7 +1132,13 @@ "$(DotnetPath)dotnet" test "$(AzureTestProjectPath)" + + -p:Configuration=$(Configuration) + $(SigningKeyPathArgument) + $(TestSigningKeyPathArgument) + + $(TestBlameArgument) $(TestCodeCoverageArgument) $(TestFiltersArgument) @@ -1117,7 +1146,7 @@ --results-directory "$(TestResultsFolderPath)" --logger:"trx;LogFilePrefix=$(LogFilePrefix)" - + $(ReferenceTypeArgument) $(PackageVersionSqlClientArgument) $(PackageVersionSqlServerArgument) diff --git a/eng/pipelines/ci/package/sqlclient-ci-package-pipeline.yml b/eng/pipelines/ci/package/sqlclient-ci-package-pipeline.yml index ada32431b2..577aae5f61 100644 --- a/eng/pipelines/ci/package/sqlclient-ci-package-pipeline.yml +++ b/eng/pipelines/ci/package/sqlclient-ci-package-pipeline.yml @@ -81,7 +81,7 @@ variables: value: ${{ eq(variables['System.TeamProject'], 'ADO.Net') }} # Signing key argument passed to build.proj. On internal builds this references the secure file - # downloaded by DownloadSecureFile@1; on public builds it expands to empty. + # downloaded by download-driver-signing-key-step.yml; on public builds it expands to empty. - name: signingKeyArg ${{ if eq(variables.isInternalBuild, true) }}: value: -p:SigningKeyPath="$(driverKeyFile.secureFilePath)" @@ -125,13 +125,9 @@ jobs: Write-Host 'Done.' displayName: Clean Packages Directory - # On internal builds, download the strong-name signing key. + # On internal builds, download the driver strong-name signing key. - ${{ if eq(variables.isInternalBuild, true) }}: - - task: DownloadSecureFile@1 - displayName: Download Driver Signing Key - inputs: - secureFile: netfxKeypair.snk - name: driverKeyFile + - template: /eng/pipelines/common/steps/download-driver-signing-key-step.yml@self # Run the Pack target via build.proj. - task: DotNetCoreCLI@2 diff --git a/eng/pipelines/common/steps/download-driver-signing-key-step.yml b/eng/pipelines/common/steps/download-driver-signing-key-step.yml new file mode 100644 index 0000000000..b7b35000a2 --- /dev/null +++ b/eng/pipelines/common/steps/download-driver-signing-key-step.yml @@ -0,0 +1,22 @@ +#################################################################################################### +# 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. +#################################################################################################### + +# This template downloads the driver strong-name signing key from ADO secure files. This key is +# used to sign the shipping driver assemblies. +# +# The secure file is only available to the internal ADO.Net project, so this template must only be +# used by internal builds. +# +# Downstream steps reference the key's path via: +# +# $(driverKeyFile.secureFilePath) + +steps: + + - task: DownloadSecureFile@1 + displayName: Download Driver Signing Key + inputs: + secureFile: netfxKeypair.snk + name: driverKeyFile diff --git a/eng/pipelines/common/steps/download-test-signing-key-step.yml b/eng/pipelines/common/steps/download-test-signing-key-step.yml new file mode 100644 index 0000000000..b9d11791ec --- /dev/null +++ b/eng/pipelines/common/steps/download-test-signing-key-step.yml @@ -0,0 +1,22 @@ +#################################################################################################### +# 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. +#################################################################################################### + +# This template downloads the test strong-name signing key from ADO secure files. This key is used +# to sign the test assemblies so that they satisfy the driver's InternalsVisibleTo declarations. +# +# The secure file is only available to the internal ADO.Net project, so this template must only be +# used by internal builds. +# +# Downstream steps reference the key's path via: +# +# $(testKeyFile.secureFilePath) + +steps: + + - task: DownloadSecureFile@1 + displayName: Download Test Signing Key + inputs: + secureFile: sqlclient-test-key.snk + name: testKeyFile diff --git a/eng/pipelines/common/templates/jobs/ci-build-nugets-job.yml b/eng/pipelines/common/templates/jobs/ci-build-nugets-job.yml index 95d5d0b7d0..04ee722bbc 100644 --- a/eng/pipelines/common/templates/jobs/ci-build-nugets-job.yml +++ b/eng/pipelines/common/templates/jobs/ci-build-nugets-job.yml @@ -76,6 +76,10 @@ parameters: type: string default: SqlServer.Artifacts + # True when building on the internal ADO.Net project. + - name: isInternalBuild + type: boolean + jobs: - job: build_mds_akv_packages_job displayName: Build MDS & AKV Packages @@ -126,6 +130,10 @@ jobs: # Restore dotnet CLI tools (e.g. pwsh, apicompat) before building. - template: /eng/pipelines/common/steps/restore-dotnet-tools.yml@self + # Download the driver strong-name signing key for internal Package-mode builds. + - ${{ if and(eq(parameters.isInternalBuild, true), eq(parameters.referenceType, 'Package')) }}: + - template: /eng/pipelines/common/steps/download-driver-signing-key-step.yml@self + # When we're performing a Debug build, we still want to try _compiling_ the # code in Release mode to ensure downstream pipelines don't encounter # compilation errors. We won't use the Release artifacts for anything else @@ -147,6 +155,8 @@ jobs: build: MDS packageVersion: ${{ parameters.packageVersion }} sqlServerPackageVersion: ${{ parameters.sqlServerPackageVersion }} + ${{ if and(eq(parameters.isInternalBuild, true), eq(parameters.referenceType, 'Package')) }}: + signingKeyPath: $(driverKeyFile.secureFilePath) - task: DotNetCoreCLI@2 displayName: 'Create MDS NuGet Package' @@ -189,6 +199,8 @@ jobs: build: AkvProvider packageVersion: ${{ parameters.packageVersion }} sqlServerPackageVersion: ${{ parameters.sqlServerPackageVersion }} + ${{ if and(eq(parameters.isInternalBuild, true), eq(parameters.referenceType, 'Package')) }}: + signingKeyPath: $(driverKeyFile.secureFilePath) - task: DotNetCoreCLI@2 displayName: 'Create AKV Provider NuGet Package' diff --git a/eng/pipelines/common/templates/jobs/ci-run-tests-job.yml b/eng/pipelines/common/templates/jobs/ci-run-tests-job.yml index 4752e09e43..4f2f5814d4 100644 --- a/eng/pipelines/common/templates/jobs/ci-run-tests-job.yml +++ b/eng/pipelines/common/templates/jobs/ci-run-tests-job.yml @@ -148,6 +148,10 @@ parameters: - name: saPassword type: string + # True when building on the internal ADO.Net project. + - name: isInternalBuild + type: boolean + jobs: - job: ${{ format('{0}', coalesce(parameters.jobDisplayName, parameters.image, 'unknown_image')) }} @@ -205,6 +209,11 @@ jobs: # Restore dotnet CLI tools (e.g. pwsh, apicompat) before building. - template: /eng/pipelines/common/steps/restore-dotnet-tools.yml@self + # Download the driver and test strong-name signing keys for internal Package-mode builds. + - ${{ if and(eq(parameters.isInternalBuild, true), eq(parameters.referenceType, 'Package')) }}: + - template: /eng/pipelines/common/steps/download-driver-signing-key-step.yml@self + - template: /eng/pipelines/common/steps/download-test-signing-key-step.yml@self + - ${{ if ne(parameters.prebuildSteps, '') }}: - ${{ parameters.prebuildSteps }} # extra steps to run before the build like downloading sni and the required configuration @@ -358,6 +367,9 @@ jobs: operatingSystem: ${{ parameters.operatingSystem }} packageVersion: ${{ parameters.packageVersion }} sqlServerPackageVersion: ${{ parameters.sqlServerPackageVersion }} + ${{ if and(eq(parameters.isInternalBuild, true), eq(parameters.referenceType, 'Package')) }}: + signingKeyPath: $(driverKeyFile.secureFilePath) + testSigningKeyPath: $(testKeyFile.secureFilePath) - ${{ if and(eq(parameters.enableX86Test, true), eq(parameters.operatingSystem, 'Windows')) }}: - template: /eng/pipelines/common/templates/steps/run-all-tests-step.yml@self @@ -372,6 +384,9 @@ jobs: operatingSystem: ${{ parameters.operatingSystem }} packageVersion: ${{ parameters.packageVersion }} sqlServerPackageVersion: ${{ parameters.sqlServerPackageVersion }} + ${{ if and(eq(parameters.isInternalBuild, true), eq(parameters.referenceType, 'Package')) }}: + signingKeyPath: $(driverKeyFile.secureFilePath) + testSigningKeyPath: $(testKeyFile.secureFilePath) - template: /eng/pipelines/common/templates/steps/publish-test-results-step.yml@self parameters: diff --git a/eng/pipelines/common/templates/stages/ci-run-tests-stage.yml b/eng/pipelines/common/templates/stages/ci-run-tests-stage.yml index 7ed67c1af9..77cee04802 100644 --- a/eng/pipelines/common/templates/stages/ci-run-tests-stage.yml +++ b/eng/pipelines/common/templates/stages/ci-run-tests-stage.yml @@ -71,6 +71,10 @@ parameters: - name: testJobTimeout type: number + # True when building on the internal ADO.Net project. + - name: isInternalBuild + type: boolean + stages: - ${{ each config in parameters.testConfigurations }}: - ${{ each image in config.value.images }}: @@ -114,6 +118,7 @@ stages: packageVersion: $(packageVersion) loggingArtifactsName: ${{ parameters.loggingArtifactsName }} mdsArtifactsName: ${{ parameters.mdsArtifactsName }} + isInternalBuild: ${{ parameters.isInternalBuild }} sqlServerArtifactsName: ${{ parameters.sqlServerArtifactsName }} sqlServerPackageVersion: $(sqlServerPackageVersion) prebuildSteps: ${{ parameters.prebuildSteps }} @@ -149,6 +154,7 @@ stages: packageVersion: $(packageVersion) loggingArtifactsName: ${{ parameters.loggingArtifactsName }} mdsArtifactsName: ${{ parameters.mdsArtifactsName }} + isInternalBuild: ${{ parameters.isInternalBuild }} sqlServerArtifactsName: ${{ parameters.sqlServerArtifactsName }} sqlServerPackageVersion: $(sqlServerPackageVersion) prebuildSteps: ${{ parameters.prebuildSteps }} diff --git a/eng/pipelines/common/templates/steps/ci-project-build-step.yml b/eng/pipelines/common/templates/steps/ci-project-build-step.yml index c80de64fd4..01a8836f5b 100644 --- a/eng/pipelines/common/templates/steps/ci-project-build-step.yml +++ b/eng/pipelines/common/templates/steps/ci-project-build-step.yml @@ -58,6 +58,12 @@ parameters: type: string default: $(sqlServerPackageVersion) + # Path to the driver strong-name signing key file. When non-empty, passed as SigningKeyPath to + # the build. The calling job is responsible for downloading the key. + - name: signingKeyPath + type: string + default: '' + steps: # Build MDS - ${{ if or(eq(parameters.build, 'MDS'), eq(parameters.build, 'all'), eq(parameters.build, 'allNoDocs')) }}: @@ -74,6 +80,7 @@ steps: -p:BuildNumber=${{ parameters.buildNumber }} -p:PackageVersionSqlClient=${{ parameters.packageVersion }} -p:PackageVersionSqlServer=${{ parameters.sqlServerPackageVersion }} + -p:SigningKeyPath="${{ parameters.signingKeyPath }}" # Build AKV Provider - ${{ if or(eq(parameters.build, 'AkvProvider'), eq(parameters.build, 'all'), eq(parameters.build, 'allNoDocs')) }}: @@ -90,3 +97,4 @@ steps: -p:BuildNumber=${{ parameters.buildNumber }} -p:PackageVersionSqlClient=${{ parameters.packageVersion }} -p:PackageVersionSqlServer=${{ parameters.sqlServerPackageVersion }} + -p:SigningKeyPath="${{ parameters.signingKeyPath }}" diff --git a/eng/pipelines/common/templates/steps/run-all-tests-step.yml b/eng/pipelines/common/templates/steps/run-all-tests-step.yml index 928c2e3abc..ef31add54d 100644 --- a/eng/pipelines/common/templates/steps/run-all-tests-step.yml +++ b/eng/pipelines/common/templates/steps/run-all-tests-step.yml @@ -68,6 +68,20 @@ parameters: type: number default: 2 + # Path to the driver strong-name signing key file. When non-empty, passed to build.proj so that + # the driver assemblies built for the test run are strong named. The calling job is responsible + # for downloading the key. + - name: signingKeyPath + type: string + default: '' + + # Path to the test strong-name signing key file. When non-empty, passed to build.proj so that + # test assemblies are strong named and can satisfy InternalsVisibleTo grants from signed driver + # assemblies. The calling job is responsible for downloading the key. + - name: testSigningKeyPath + type: string + default: '' + steps: - ${{ if parameters.debug }}: - powershell: 'dotnet sdk check' @@ -90,6 +104,8 @@ steps: -p:PackageVersionSqlClient=${{ parameters.packageVersion }} -p:PackageVersionSqlServer=${{ parameters.sqlServerPackageVersion }} -p:TestResultsFolderPath=TestResults + -p:SigningKeyPath="${{ parameters.signingKeyPath }}" + -p:TestSigningKeyPath="${{ parameters.testSigningKeyPath }}" ${{ else }}: # x86 arguments: >- -t:TestSqlClientUnit @@ -100,6 +116,8 @@ steps: -p:PackageVersionSqlServer=${{ parameters.sqlServerPackageVersion }} -p:DotnetPath=${{ parameters.dotnetx86RootPath }} -p:TestResultsFolderPath=TestResults + -p:SigningKeyPath="${{ parameters.signingKeyPath }}" + -p:TestSigningKeyPath="${{ parameters.testSigningKeyPath }}" - task: DotNetCoreCLI@2 displayName: 'Run Flaky Unit Tests ${{parameters.msbuildArchitecture }}' @@ -118,6 +136,8 @@ steps: -p:TestFilters="category=flaky" -p:TestResultsFolderPath=TestResults -p:TestCodeCoverage=false + -p:SigningKeyPath="${{ parameters.signingKeyPath }}" + -p:TestSigningKeyPath="${{ parameters.testSigningKeyPath }}" ${{ else }}: # x86 arguments: >- -t:TestSqlClientUnit @@ -130,6 +150,8 @@ steps: -p:TestFilters="category=flaky" -p:TestResultsFolderPath=TestResults -p:TestCodeCoverage=false + -p:SigningKeyPath="${{ parameters.signingKeyPath }}" + -p:TestSigningKeyPath="${{ parameters.testSigningKeyPath }}" continueOnError: true - task: DotNetCoreCLI@2 @@ -147,6 +169,8 @@ steps: -p:PackageVersionSqlClient=${{ parameters.packageVersion }} -p:PackageVersionSqlServer=${{ parameters.sqlServerPackageVersion }} -p:TestResultsFolderPath=TestResults + -p:SigningKeyPath="${{ parameters.signingKeyPath }}" + -p:TestSigningKeyPath="${{ parameters.testSigningKeyPath }}" ${{ else }}: # x86 arguments: >- -t:TestSqlClientFunctional @@ -157,6 +181,8 @@ steps: -p:PackageVersionSqlServer=${{ parameters.sqlServerPackageVersion }} -p:DotnetPath=${{ parameters.dotnetx86RootPath }} -p:TestResultsFolderPath=TestResults + -p:SigningKeyPath="${{ parameters.signingKeyPath }}" + -p:TestSigningKeyPath="${{ parameters.testSigningKeyPath }}" - task: DotNetCoreCLI@2 displayName: 'Run Flaky Functional Tests ${{parameters.msbuildArchitecture }}' @@ -175,6 +201,8 @@ steps: -p:TestFilters="category=flaky" -p:TestResultsFolderPath=TestResults -p:TestCodeCoverage=false + -p:SigningKeyPath="${{ parameters.signingKeyPath }}" + -p:TestSigningKeyPath="${{ parameters.testSigningKeyPath }}" ${{ else }}: # x86 arguments: >- -t:TestSqlClientFunctional @@ -187,6 +215,8 @@ steps: -p:TestFilters="category=flaky" -p:TestResultsFolderPath=TestResults -p:TestCodeCoverage=false + -p:SigningKeyPath="${{ parameters.signingKeyPath }}" + -p:TestSigningKeyPath="${{ parameters.testSigningKeyPath }}" continueOnError: true - task: DotNetCoreCLI@2 @@ -205,6 +235,8 @@ steps: -p:PackageVersionSqlClient=${{ parameters.packageVersion }} -p:PackageVersionSqlServer=${{ parameters.sqlServerPackageVersion }} -p:TestResultsFolderPath=TestResults + -p:SigningKeyPath="${{ parameters.signingKeyPath }}" + -p:TestSigningKeyPath="${{ parameters.testSigningKeyPath }}" ${{ else }}: # x86 arguments: >- -t:TestSqlClientManual @@ -216,6 +248,8 @@ steps: -p:PackageVersionSqlServer=${{ parameters.sqlServerPackageVersion }} -p:DotnetPath=${{ parameters.dotnetx86RootPath }} -p:TestResultsFolderPath=TestResults + -p:SigningKeyPath="${{ parameters.signingKeyPath }}" + -p:TestSigningKeyPath="${{ parameters.testSigningKeyPath }}" retryCountOnTaskFailure: ${{parameters.retryCountOnManualTests }} - task: DotNetCoreCLI@2 @@ -236,6 +270,8 @@ steps: -p:TestFilters="category=flaky" -p:TestResultsFolderPath=TestResults -p:TestCodeCoverage=false + -p:SigningKeyPath="${{ parameters.signingKeyPath }}" + -p:TestSigningKeyPath="${{ parameters.testSigningKeyPath }}" ${{ else }}: # x86 arguments: >- -t:TestSqlClientManual @@ -249,6 +285,8 @@ steps: -p:TestFilters="category=flaky" -p:TestResultsFolderPath=TestResults -p:TestCodeCoverage=false + -p:SigningKeyPath="${{ parameters.signingKeyPath }}" + -p:TestSigningKeyPath="${{ parameters.testSigningKeyPath }}" continueOnError: true - ${{ else }}: # Linux or macOS @@ -266,6 +304,8 @@ steps: -p:PackageVersionSqlClient=${{ parameters.packageVersion }} -p:PackageVersionSqlServer=${{ parameters.sqlServerPackageVersion }} -p:TestResultsFolderPath=TestResults + -p:SigningKeyPath="${{ parameters.signingKeyPath }}" + -p:TestSigningKeyPath="${{ parameters.testSigningKeyPath }}" - task: DotNetCoreCLI@2 displayName: 'Run Flaky Unit Tests' @@ -283,6 +323,8 @@ steps: -p:TestFilters="category=flaky" -p:TestResultsFolderPath=TestResults -p:TestCodeCoverage=false + -p:SigningKeyPath="${{ parameters.signingKeyPath }}" + -p:TestSigningKeyPath="${{ parameters.testSigningKeyPath }}" continueOnError: true - task: DotNetCoreCLI@2 @@ -299,6 +341,8 @@ steps: -p:PackageVersionSqlClient=${{ parameters.packageVersion }} -p:PackageVersionSqlServer=${{ parameters.sqlServerPackageVersion }} -p:TestResultsFolderPath=TestResults + -p:SigningKeyPath="${{ parameters.signingKeyPath }}" + -p:TestSigningKeyPath="${{ parameters.testSigningKeyPath }}" - task: DotNetCoreCLI@2 displayName: 'Run Flaky Functional Tests' @@ -316,6 +360,8 @@ steps: -p:TestFilters="category=flaky" -p:TestResultsFolderPath=TestResults -p:TestCodeCoverage=false + -p:SigningKeyPath="${{ parameters.signingKeyPath }}" + -p:TestSigningKeyPath="${{ parameters.testSigningKeyPath }}" continueOnError: true - task: DotNetCoreCLI@2 displayName: 'Run Manual Tests' @@ -332,6 +378,8 @@ steps: -p:PackageVersionSqlClient=${{ parameters.packageVersion }} -p:PackageVersionSqlServer=${{ parameters.sqlServerPackageVersion }} -p:TestResultsFolderPath=TestResults + -p:SigningKeyPath="${{ parameters.signingKeyPath }}" + -p:TestSigningKeyPath="${{ parameters.testSigningKeyPath }}" retryCountOnTaskFailure: ${{parameters.retryCountOnManualTests }} - task: DotNetCoreCLI@2 @@ -351,4 +399,6 @@ steps: -p:TestFilters="category=flaky" -p:TestResultsFolderPath=TestResults -p:TestCodeCoverage=false + -p:SigningKeyPath="${{ parameters.signingKeyPath }}" + -p:TestSigningKeyPath="${{ parameters.testSigningKeyPath }}" continueOnError: true diff --git a/eng/pipelines/dotnet-sqlclient-ci-core.yml b/eng/pipelines/dotnet-sqlclient-ci-core.yml index 335e98b575..f20948c71d 100644 --- a/eng/pipelines/dotnet-sqlclient-ci-core.yml +++ b/eng/pipelines/dotnet-sqlclient-ci-core.yml @@ -129,6 +129,11 @@ parameters: - detailed - diagnostic + # True when building on the internal ADO.Net project. Internal builds may perform additional or + # different steps, such as strong-name signing. + - name: isInternalBuild + type: boolean + variables: - template: /eng/pipelines/libraries/ci-build-variables.yml@self @@ -169,6 +174,8 @@ stages: buildConfiguration: ${{ parameters.buildConfiguration }} debug: ${{ parameters.debug }} dotnetVerbosity: ${{ parameters.dotnetVerbosity }} + referenceType: ${{ parameters.referenceType }} + isInternalBuild: ${{ parameters.isInternalBuild }} # Build the Logging package, and publish it to the pipeline artifacts # under the given artifact name. This runs in parallel with the Secrets @@ -179,6 +186,8 @@ stages: buildConfiguration: ${{ parameters.buildConfiguration }} debug: ${{ parameters.debug }} dotnetVerbosity: ${{ parameters.dotnetVerbosity }} + referenceType: ${{ parameters.referenceType }} + isInternalBuild: ${{ parameters.isInternalBuild }} # Build the Abstractions package, and publish it to the pipeline artifacts # under the given artifact name. @@ -193,6 +202,7 @@ stages: dotnetVerbosity: ${{ parameters.dotnetVerbosity }} loggingArtifactsName: $(loggingArtifactsName) referenceType: ${{ parameters.referenceType }} + isInternalBuild: ${{ parameters.isInternalBuild }} # When building Abstractions via packages, we must depend on the Logging # package. ${{ if eq(parameters.referenceType, 'Package') }}: @@ -210,6 +220,7 @@ stages: loggingArtifactsName: $(loggingArtifactsName) mdsArtifactsName: $(mdsArtifactsName) referenceType: ${{ parameters.referenceType }} + isInternalBuild: ${{ parameters.isInternalBuild }} sqlServerArtifactsName: $(sqlServerArtifactsName) SNIVersion: ${{ parameters.SNIVersion }} SNIValidationFeed: ${{ parameters.SNIValidationFeed }} @@ -229,6 +240,7 @@ stages: azureArtifactsName: $(azureArtifactsName) buildConfiguration: ${{ parameters.buildConfiguration }} debug: ${{ parameters.debug }} + isInternalBuild: ${{ parameters.isInternalBuild }} # When building via packages, we must depend on the Abstractions, Logging, # SqlServer, and MDS packages. ${{ if eq(parameters.referenceType, 'Package') }}: @@ -263,6 +275,7 @@ stages: abstractionsArtifactsName: $(abstractionsArtifactsName) loggingArtifactsName: $(loggingArtifactsName) mdsArtifactsName: $(mdsArtifactsName) + isInternalBuild: ${{ parameters.isInternalBuild }} sqlServerArtifactsName: $(sqlServerArtifactsName) testJobTimeout: ${{ parameters.testJobTimeout }} diff --git a/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml b/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml index 74fa022a6c..9a22b71dc6 100644 --- a/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml +++ b/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml @@ -183,3 +183,4 @@ extends: testJobTimeout: ${{ parameters.testJobTimeout }} testSets: ${{ parameters.testSets }} useManagedSNI: ${{ parameters.useManagedSNI }} + isInternalBuild: ${{ eq(variables['System.TeamProject'], 'ADO.Net') }} diff --git a/eng/pipelines/dotnet-sqlclient-ci-project-reference-pipeline.yml b/eng/pipelines/dotnet-sqlclient-ci-project-reference-pipeline.yml index ce4db1ce3c..3ada68a210 100644 --- a/eng/pipelines/dotnet-sqlclient-ci-project-reference-pipeline.yml +++ b/eng/pipelines/dotnet-sqlclient-ci-project-reference-pipeline.yml @@ -205,3 +205,4 @@ extends: testJobTimeout: ${{ parameters.testJobTimeout }} testSets: ${{ parameters.testSets }} useManagedSNI: ${{ parameters.useManagedSNI }} + isInternalBuild: ${{ eq(variables['System.TeamProject'], 'ADO.Net') }} diff --git a/eng/pipelines/jobs/pack-abstractions-package-ci-job.yml b/eng/pipelines/jobs/pack-abstractions-package-ci-job.yml index 8bf43bd997..1db5027718 100644 --- a/eng/pipelines/jobs/pack-abstractions-package-ci-job.yml +++ b/eng/pipelines/jobs/pack-abstractions-package-ci-job.yml @@ -15,7 +15,7 @@ parameters: # The name to apply to the published pipeline artifacts. - name: abstractionsArtifactsName type: string - default: Abstractions.Artifact + default: Abstractions.Artifacts # The version to apply to the Abstractions NuGet package and its assemblies. Every package in the # SqlClient family shares this version. @@ -60,13 +60,16 @@ parameters: # The C# project reference type to use when building and packing the packages. - name: referenceType type: string - default: Project values: # Reference sibling packages as NuGet packages. - Package # Reference sibling packages as C# projects. - Project + # True when building on the internal ADO.Net project. + - name: isInternalBuild + type: boolean + jobs: - job: pack_abstractions_package_job @@ -130,12 +133,25 @@ jobs: parameters: debug: ${{ parameters.debug }} - # Create the NuGet packages. - # - # When referenceType is Package, we must pass ReferenceType and the - # dependency version so that Directory.Packages.props applies version - # ranges to sibling package dependencies. - - ${{ if eq(parameters.referenceType, 'Package') }}: + # Create the NuGet packages. Internal Package-mode builds strong-name sign the assemblies + # with the driver key. + - ${{ if and(eq(parameters.isInternalBuild, true), eq(parameters.referenceType, 'Package')) }}: + - template: /eng/pipelines/common/steps/download-driver-signing-key-step.yml@self + + - task: DotNetCoreCLI@2 + displayName: Create NuGet Package + inputs: + command: pack + packagesToPack: $(project) + configurationToPack: ${{ parameters.buildConfiguration }} + packDirectory: $(dotnetPackagesDir) + verbosityToPack: ${{ parameters.dotnetVerbosity }} + # BuildNumber supplies the revision component of FileVersion + # (Major.Minor.Patch.Revision). Without it, FileVersionBuildNumber + # defaults to 0 and the assembly is stamped Major.Minor.Patch.0. + buildProperties: SqlClientPackageVersion=${{ parameters.packageVersion }};ReferenceType=Package;BuildNumber=$(Build.BuildNumber);SigningKeyPath="$(driverKeyFile.secureFilePath)" + + - ${{ elseif eq(parameters.referenceType, 'Package') }}: - task: DotNetCoreCLI@2 displayName: Create NuGet Package inputs: @@ -144,8 +160,9 @@ jobs: configurationToPack: ${{ parameters.buildConfiguration }} packDirectory: $(dotnetPackagesDir) verbosityToPack: ${{ parameters.dotnetVerbosity }} - # BuildNumber supplies the revision component of FileVersion; without - # it the assembly is stamped Major.Minor.Patch.0 (see Project branch). + # BuildNumber supplies the revision component of FileVersion + # (Major.Minor.Patch.Revision). Without it, FileVersionBuildNumber + # defaults to 0 and the assembly is stamped Major.Minor.Patch.0. buildProperties: SqlClientPackageVersion=${{ parameters.packageVersion }};ReferenceType=Package;BuildNumber=$(Build.BuildNumber) - ${{ else }}: @@ -159,8 +176,7 @@ jobs: verbosityToPack: ${{ parameters.dotnetVerbosity }} # BuildNumber supplies the revision component of FileVersion # (Major.Minor.Patch.Revision). Without it, FileVersionBuildNumber - # defaults to 0 and the assembly is stamped Major.Minor.Patch.0, - # inconsistent with the MDS/AKV packages that pass it. + # defaults to 0 and the assembly is stamped Major.Minor.Patch.0. buildProperties: SqlClientPackageVersion=${{ parameters.packageVersion }};BuildNumber=$(Build.BuildNumber) # Publish the NuGet packages as a named pipeline artifact. diff --git a/eng/pipelines/jobs/pack-azure-package-ci-job.yml b/eng/pipelines/jobs/pack-azure-package-ci-job.yml index 38eaf58fe9..e982859bfd 100644 --- a/eng/pipelines/jobs/pack-azure-package-ci-job.yml +++ b/eng/pipelines/jobs/pack-azure-package-ci-job.yml @@ -73,6 +73,10 @@ parameters: # Reference sibling packages as C# projects. - Project + # True when building on the internal ADO.Net project. + - name: isInternalBuild + type: boolean + jobs: - job: pack_azure_package_job @@ -141,12 +145,15 @@ jobs: parameters: debug: ${{ parameters.debug }} - # Create the NuGet packages. + # Create the NuGet packages. Internal Package-mode builds strong-name sign the assemblies + # with the driver key. # # When referenceType is Package, we must pass ReferenceType and the # dependency versions so that Directory.Packages.props applies version # ranges to sibling package dependencies. - - ${{ if eq(parameters.referenceType, 'Package') }}: + - ${{ if and(eq(parameters.isInternalBuild, true), eq(parameters.referenceType, 'Package')) }}: + - template: /eng/pipelines/common/steps/download-driver-signing-key-step.yml@self + - task: DotNetCoreCLI@2 displayName: Create NuGet Package inputs: @@ -155,8 +162,23 @@ jobs: configurationToPack: ${{ parameters.buildConfiguration }} packDirectory: $(dotnetPackagesDir) verbosityToPack: ${{ parameters.dotnetVerbosity }} - # BuildNumber supplies the revision component of FileVersion; without - # it the assembly is stamped Major.Minor.Patch.0 (see Project branch). + # BuildNumber supplies the revision component of FileVersion + # (Major.Minor.Patch.Revision). Without it, FileVersionBuildNumber + # defaults to 0 and the assembly is stamped Major.Minor.Patch.0. + buildProperties: SqlClientPackageVersion=${{ parameters.packageVersion }};ReferenceType=Package;BuildNumber=$(Build.BuildNumber);SigningKeyPath="$(driverKeyFile.secureFilePath)" + + - ${{ elseif eq(parameters.referenceType, 'Package') }}: + - task: DotNetCoreCLI@2 + displayName: Create NuGet Package + inputs: + command: pack + packagesToPack: $(project) + configurationToPack: ${{ parameters.buildConfiguration }} + packDirectory: $(dotnetPackagesDir) + verbosityToPack: ${{ parameters.dotnetVerbosity }} + # BuildNumber supplies the revision component of FileVersion + # (Major.Minor.Patch.Revision). Without it, FileVersionBuildNumber + # defaults to 0 and the assembly is stamped Major.Minor.Patch.0. buildProperties: SqlClientPackageVersion=${{ parameters.packageVersion }};ReferenceType=Package;BuildNumber=$(Build.BuildNumber) - ${{ else }}: @@ -170,8 +192,7 @@ jobs: verbosityToPack: ${{ parameters.dotnetVerbosity }} # BuildNumber supplies the revision component of FileVersion # (Major.Minor.Patch.Revision). Without it, FileVersionBuildNumber - # defaults to 0 and the assembly is stamped Major.Minor.Patch.0, - # inconsistent with the MDS/AKV packages that pass it. + # defaults to 0 and the assembly is stamped Major.Minor.Patch.0. buildProperties: SqlClientPackageVersion=${{ parameters.packageVersion }};BuildNumber=$(Build.BuildNumber) # Publish the NuGet packages as a named pipeline artifact. diff --git a/eng/pipelines/jobs/pack-logging-package-ci-job.yml b/eng/pipelines/jobs/pack-logging-package-ci-job.yml index 0f0faa1859..03e67066d8 100644 --- a/eng/pipelines/jobs/pack-logging-package-ci-job.yml +++ b/eng/pipelines/jobs/pack-logging-package-ci-job.yml @@ -50,6 +50,19 @@ parameters: - detailed - diagnostic + # The C# project reference type to use when building and packing the packages. + - name: referenceType + type: string + values: + # Reference sibling packages as NuGet packages. + - Package + # Reference sibling packages as C# projects. + - Project + + # True when building on the internal ADO.Net project. + - name: isInternalBuild + type: boolean + jobs: - job: pack_logging_package_job @@ -95,20 +108,37 @@ jobs: parameters: debug: ${{ parameters.debug }} - # Create the NuGet packages. - - task: DotNetCoreCLI@2 - displayName: Create NuGet Package - inputs: - command: pack - packagesToPack: $(project) - configurationToPack: ${{ parameters.buildConfiguration }} - packDirectory: $(dotnetPackagesDir) - verbosityToPack: ${{ parameters.dotnetVerbosity }} - # BuildNumber supplies the revision component of FileVersion - # (Major.Minor.Patch.Revision). Without it, FileVersionBuildNumber - # defaults to 0 and the assembly is stamped Major.Minor.Patch.0, - # inconsistent with the MDS/AKV packages that pass it. - buildProperties: SqlClientPackageVersion=${{ parameters.packageVersion }};BuildNumber=$(Build.BuildNumber) + # Create the NuGet packages. Internal Package-mode builds strong-name sign the assemblies + # with the driver key. + - ${{ if and(eq(parameters.isInternalBuild, true), eq(parameters.referenceType, 'Package')) }}: + - template: /eng/pipelines/common/steps/download-driver-signing-key-step.yml@self + + - task: DotNetCoreCLI@2 + displayName: Create NuGet Package + inputs: + command: pack + packagesToPack: $(project) + configurationToPack: ${{ parameters.buildConfiguration }} + packDirectory: $(dotnetPackagesDir) + verbosityToPack: ${{ parameters.dotnetVerbosity }} + # BuildNumber supplies the revision component of FileVersion + # (Major.Minor.Patch.Revision). Without it, FileVersionBuildNumber + # defaults to 0 and the assembly is stamped Major.Minor.Patch.0. + buildProperties: SqlClientPackageVersion=${{ parameters.packageVersion }};BuildNumber=$(Build.BuildNumber);SigningKeyPath="$(driverKeyFile.secureFilePath)" + + - ${{ else }}: + - task: DotNetCoreCLI@2 + displayName: Create NuGet Package + inputs: + command: pack + packagesToPack: $(project) + configurationToPack: ${{ parameters.buildConfiguration }} + packDirectory: $(dotnetPackagesDir) + verbosityToPack: ${{ parameters.dotnetVerbosity }} + # BuildNumber supplies the revision component of FileVersion + # (Major.Minor.Patch.Revision). Without it, FileVersionBuildNumber + # defaults to 0 and the assembly is stamped Major.Minor.Patch.0. + buildProperties: SqlClientPackageVersion=${{ parameters.packageVersion }};BuildNumber=$(Build.BuildNumber) # Publish the NuGet packages as a named pipeline artifact. - task: PublishPipelineArtifact@1 diff --git a/eng/pipelines/jobs/pack-sqlserver-package-ci-job.yml b/eng/pipelines/jobs/pack-sqlserver-package-ci-job.yml index 4f619b3f1b..722b3d24f3 100644 --- a/eng/pipelines/jobs/pack-sqlserver-package-ci-job.yml +++ b/eng/pipelines/jobs/pack-sqlserver-package-ci-job.yml @@ -49,6 +49,19 @@ parameters: - detailed - diagnostic + # The C# project reference type to use when building and packing the packages. + - name: referenceType + type: string + values: + # Reference sibling packages as NuGet packages. + - Package + # Reference sibling packages as C# projects. + - Project + + # True when building on the internal ADO.Net project. + - name: isInternalBuild + type: boolean + jobs: - job: pack_sqlserver_package_job @@ -94,19 +107,37 @@ jobs: parameters: debug: ${{ parameters.debug }} - # Create the NuGet packages. - - task: DotNetCoreCLI@2 - displayName: Create NuGet Package - inputs: - command: pack - packagesToPack: $(project) - configurationToPack: ${{ parameters.buildConfiguration }} - packDirectory: $(dotnetPackagesDir) - verbosityToPack: ${{ parameters.dotnetVerbosity }} - # BuildNumber supplies the revision component of FileVersion - # (Major.Minor.Patch.Revision). Without it, FileVersionBuildNumber - # defaults to 0 and the assembly is stamped Major.Minor.Patch.0. - buildProperties: SqlServerPackageVersion=${{ parameters.sqlServerPackageVersion }};BuildNumber=$(Build.BuildNumber) + # Create the NuGet packages. Internal Package-mode builds strong-name sign the assemblies + # with the driver key. + - ${{ if and(eq(parameters.isInternalBuild, true), eq(parameters.referenceType, 'Package')) }}: + - template: /eng/pipelines/common/steps/download-driver-signing-key-step.yml@self + + - task: DotNetCoreCLI@2 + displayName: Create NuGet Package + inputs: + command: pack + packagesToPack: $(project) + configurationToPack: ${{ parameters.buildConfiguration }} + packDirectory: $(dotnetPackagesDir) + verbosityToPack: ${{ parameters.dotnetVerbosity }} + # BuildNumber supplies the revision component of FileVersion + # (Major.Minor.Patch.Revision). Without it, FileVersionBuildNumber + # defaults to 0 and the assembly is stamped Major.Minor.Patch.0. + buildProperties: SqlServerPackageVersion=${{ parameters.sqlServerPackageVersion }};BuildNumber=$(Build.BuildNumber);SigningKeyPath="$(driverKeyFile.secureFilePath)" + + - ${{ else }}: + - task: DotNetCoreCLI@2 + displayName: Create NuGet Package + inputs: + command: pack + packagesToPack: $(project) + configurationToPack: ${{ parameters.buildConfiguration }} + packDirectory: $(dotnetPackagesDir) + verbosityToPack: ${{ parameters.dotnetVerbosity }} + # BuildNumber supplies the revision component of FileVersion + # (Major.Minor.Patch.Revision). Without it, FileVersionBuildNumber + # defaults to 0 and the assembly is stamped Major.Minor.Patch.0. + buildProperties: SqlServerPackageVersion=${{ parameters.sqlServerPackageVersion }};BuildNumber=$(Build.BuildNumber) - task: PublishPipelineArtifact@1 displayName: Publish Pipeline Artifact diff --git a/eng/pipelines/jobs/test-abstractions-package-ci-job.yml b/eng/pipelines/jobs/test-abstractions-package-ci-job.yml index 83366b52a9..a3792396e5 100644 --- a/eng/pipelines/jobs/test-abstractions-package-ci-job.yml +++ b/eng/pipelines/jobs/test-abstractions-package-ci-job.yml @@ -13,6 +13,18 @@ parameters: + # The name of the Logging pipeline artifacts to download. + # + # This is used when the referenceType is 'Package'. + - name: loggingArtifactsName + type: string + default: Logging.Artifacts + + # The version to apply to the SqlClient family packages. This is used when + # referenceType is 'Package'. + - name: packageVersion + type: string + # The type of build to test (Release or Debug) - name: buildConfiguration type: string @@ -61,6 +73,20 @@ parameters: - name: poolName type: string + # True when building on the internal ADO.Net project. When set, the Abstractions assembly is + # strong-name signed with the driver key and the test assembly with the test key. + - name: isInternalBuild + type: boolean + + # The C# project reference type to use when building. + - name: referenceType + type: string + values: + # Reference sibling packages as NuGet packages. + - Package + # Reference sibling packages as C# projects. + - Project + # The pool VM image to use. - name: vmImage type: string @@ -88,12 +114,24 @@ jobs: - name: project value: src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/Abstractions.Test.csproj - # dotnet CLI arguments for build/test/pack commands - - name: buildArguments + # dotnet CLI arguments for build/test commands. + - name: dotnetBuildOpts value: >- -p:Configuration=${{ parameters.buildConfiguration }} + -p:ReferenceType=${{ parameters.referenceType }} + -p:SqlClientPackageVersion=${{ parameters.packageVersion }} --verbosity ${{ parameters.dotnetVerbosity }} + # Strong-name signing arguments — only set for internal Package-mode builds. + - ${{ if and(eq(parameters.isInternalBuild, true), eq(parameters.referenceType, 'Package')) }}: + - name: signingArguments + value: >- + -p:SigningKeyPath="$(driverKeyFile.secureFilePath)" + -p:TestSigningKeyPath="$(testKeyFile.secureFilePath)" + - ${{ else }}: + - name: signingArguments + value: '' + # Explicitly unset the $PLATFORM environment variable that is set by the # 'ADO Build properties' Library in the ADO SqlClientDrivers public project. # This is defined with a non-standard Platform of 'AnyCPU', and will fail @@ -121,6 +159,19 @@ jobs: - pwsh: 'Get-ChildItem Env: | Sort-Object Name' displayName: '[Debug] Print Environment Variables' + # Download the driver and test strong-name signing keys for internal Package-mode builds. + - ${{ if and(eq(parameters.isInternalBuild, true), eq(parameters.referenceType, 'Package')) }}: + - template: /eng/pipelines/common/steps/download-driver-signing-key-step.yml@self + - template: /eng/pipelines/common/steps/download-test-signing-key-step.yml@self + + # For Package reference builds, download the Logging dependency into packages/. + - ${{ if eq(parameters.referenceType, 'Package') }}: + - task: DownloadPipelineArtifact@2 + displayName: Download Logging Package Artifacts + inputs: + artifactName: ${{ parameters.loggingArtifactsName }} + targetPath: $(Build.SourcesDirectory)/packages + # Install the .NET SDK and Runtimes. - template: /eng/pipelines/common/steps/install-dotnet.yml@self parameters: @@ -136,7 +187,7 @@ jobs: inputs: command: build projects: $(project) - arguments: $(buildArguments) + arguments: $(dotnetBuildOpts) $(signingArguments) # Run the tests for each .NET runtime. - ${{ each runtime in parameters.netRuntimes }}: @@ -146,7 +197,7 @@ jobs: command: test projects: $(project) arguments: >- - $(buildArguments) + $(dotnetBuildOpts) --no-build -f ${{ runtime }} --filter "category != failing & category != flaky & category != interactive" @@ -157,7 +208,7 @@ jobs: command: test projects: $(project) arguments: >- - $(buildArguments) + $(dotnetBuildOpts) --no-build -f ${{ runtime }} --filter "category = flaky" @@ -170,7 +221,7 @@ jobs: command: test projects: $(project) arguments: >- - $(buildArguments) + $(dotnetBuildOpts) --no-build -f ${{ runtime }} --filter "category != failing & category != flaky & category != interactive" @@ -181,7 +232,7 @@ jobs: command: test projects: $(project) arguments: >- - $(buildArguments) + $(dotnetBuildOpts) --no-build -f ${{ runtime }} --filter "category = flaky" diff --git a/eng/pipelines/jobs/test-azure-package-ci-job.yml b/eng/pipelines/jobs/test-azure-package-ci-job.yml index 104401b585..a1eba2f4bb 100644 --- a/eng/pipelines/jobs/test-azure-package-ci-job.yml +++ b/eng/pipelines/jobs/test-azure-package-ci-job.yml @@ -127,6 +127,11 @@ parameters: type: boolean default: false + # True when building on the internal ADO.Net project. When set, the Azure assembly is + # strong-name signed with the driver key and the test assembly with the test key. + - name: isInternalBuild + type: boolean + # The pool VM image to use. - name: vmImage type: string @@ -154,8 +159,10 @@ jobs: - name: project value: src/Microsoft.Data.SqlClient.Extensions/Azure/test/Azure.Test.csproj - # dotnet CLI arguments for build/test/pack commands. - - name: buildArguments + # dotnet CLI arguments for build/test commands. + # + # Not named 'buildArguments': dotnet injects $BUILDARGUMENTS into 'dotnet build'. + - name: dotnetBuildOpts value: >- -p:Configuration=${{ parameters.buildConfiguration }} --verbosity ${{ parameters.dotnetVerbosity }} @@ -163,6 +170,16 @@ jobs: -p:SqlClientPackageVersion=${{ parameters.packageVersion }} -p:SqlServerPackageVersion=${{ parameters.sqlServerPackageVersion }} + # Strong-name signing arguments - only set for internal Package-mode builds. + - ${{ if and(eq(parameters.isInternalBuild, true), eq(parameters.referenceType, 'Package')) }}: + - name: signingArguments + value: >- + -p:SigningKeyPath="$(driverKeyFile.secureFilePath)" + -p:TestSigningKeyPath="$(testKeyFile.secureFilePath)" + - ${{ else }}: + - name: signingArguments + value: '' + # Explicitly unset the $PLATFORM environment variable that is set by the # 'ADO Build properties' Library in the ADO SqlClientDrivers public # project. This is defined with a non-standard Platform of 'AnyCPU', and @@ -232,6 +249,11 @@ jobs: debug: ${{ parameters.debug }} runtimes: [8.x, 9.x] + # Download the driver and test strong-name signing keys for internal Package-mode builds. + - ${{ if and(eq(parameters.isInternalBuild, true), eq(parameters.referenceType, 'Package')) }}: + - template: /eng/pipelines/common/steps/download-driver-signing-key-step.yml@self + - template: /eng/pipelines/common/steps/download-test-signing-key-step.yml@self + # The Windows agent images include a suitable .NET Framework runtime, so # we don't have to install one explicitly. @@ -273,7 +295,7 @@ jobs: inputs: command: build projects: $(project) - arguments: $(buildArguments) + arguments: $(dotnetBuildOpts) $(signingArguments) # List the DLLs in the output directory for debugging purposes. - ${{ if eq(parameters.debug, true) }}: @@ -308,7 +330,7 @@ jobs: command: test projects: $(project) arguments: >- - $(buildArguments) + $(dotnetBuildOpts) --no-build -f ${{ runtime }} --filter "category != failing & category != flaky & category != interactive" @@ -326,7 +348,7 @@ jobs: command: test projects: $(project) arguments: >- - $(buildArguments) + $(dotnetBuildOpts) --no-build -f ${{ runtime }} --filter "category = flaky" @@ -346,7 +368,7 @@ jobs: command: test projects: $(project) arguments: >- - $(buildArguments) + $(dotnetBuildOpts) --no-build -f ${{ runtime }} --filter "category != failing & category != flaky & category != interactive" @@ -364,7 +386,7 @@ jobs: command: test projects: $(project) arguments: >- - $(buildArguments) + $(dotnetBuildOpts) --no-build -f ${{ runtime }} --filter "category = flaky" diff --git a/eng/pipelines/onebranch/jobs/validate-signed-package-job.yml b/eng/pipelines/onebranch/jobs/validate-signed-package-job.yml index afa5aa5918..0769e2093c 100644 --- a/eng/pipelines/onebranch/jobs/validate-signed-package-job.yml +++ b/eng/pipelines/onebranch/jobs/validate-signed-package-job.yml @@ -157,8 +157,8 @@ jobs: $nugetPackageInstallPath = "${{ variables.nugetPackageInstallPath }}" echo "nugetPackageInstallPath= $nugetPackageInstallPath" - # Verify strong name signing ##################################### - echo "> 1. Verifying strong name signing of DLLs ..." + # Verify strong-name signing ################################### + echo "> 1. Verifying strong-name signing of DLLs ..." # @TODO: This path seems brittle to VS upgrades, can we make it more flexible? $snPath = "C:\Program Files (x86)\Microsoft SDKs\Windows\*\bin\NETFX 4.8.1 Tools\sn.exe" diff --git a/eng/pipelines/onebranch/steps/build-buildproj-step.yml b/eng/pipelines/onebranch/steps/build-buildproj-step.yml index 5cfc809772..7c2682440c 100644 --- a/eng/pipelines/onebranch/steps/build-buildproj-step.yml +++ b/eng/pipelines/onebranch/steps/build-buildproj-step.yml @@ -57,12 +57,8 @@ parameters: type: string steps: - # Download the strong name signing key from secure file storage - - task: DownloadSecureFile@1 - displayName: 'Download Signing Key' - inputs: - secureFile: 'netfxKeypair.snk' - name: keyFile + # Download the driver strong-name signing key from secure file storage. + - template: /eng/pipelines/common/steps/download-driver-signing-key-step.yml@self - task: DotNetCoreCLI@2 displayName: 'build.proj - Build${{ parameters.packageShortName }}' @@ -74,7 +70,7 @@ steps: -p:Configuration=${{ parameters.buildConfiguration }} -p:ReferenceType=Package -p:SkipDependencyPack=true - -p:SigningKeyPath="$(keyFile.secureFilePath)" + -p:SigningKeyPath="$(driverKeyFile.secureFilePath)" -p:BuildNumber="${{ parameters.revision }}" -p:PackageVersion${{ parameters.versionPropertySuffix }}="${{ parameters.packageVersion }}" ${{ parameters.dependencyArguments }} diff --git a/eng/pipelines/sqlclient-pr-package-ref-pipeline.yml b/eng/pipelines/sqlclient-pr-package-ref-pipeline.yml index abb0ef5f39..fc532179d8 100644 --- a/eng/pipelines/sqlclient-pr-package-ref-pipeline.yml +++ b/eng/pipelines/sqlclient-pr-package-ref-pipeline.yml @@ -143,6 +143,8 @@ extends: testJobTimeout: ${{ parameters.testJobTimeout }} testSets: ${{ parameters.testSets }} useManagedSNI: ${{ parameters.useManagedSNI }} + # PR builds run in the public project. + isInternalBuild: false # Legacy SQL Server tests (2016/2017) run in CI only, not on PRs. runLegacySqlTests: false # Don't run the AE tests in Debug mode; they rarely succeed. diff --git a/eng/pipelines/sqlclient-pr-project-ref-pipeline.yml b/eng/pipelines/sqlclient-pr-project-ref-pipeline.yml index f9578e6548..be66c1cc7d 100644 --- a/eng/pipelines/sqlclient-pr-project-ref-pipeline.yml +++ b/eng/pipelines/sqlclient-pr-project-ref-pipeline.yml @@ -143,6 +143,8 @@ extends: testJobTimeout: ${{ parameters.testJobTimeout }} testSets: ${{ parameters.testSets }} useManagedSNI: ${{ parameters.useManagedSNI }} + # PR builds run in the public project. + isInternalBuild: false # Legacy SQL Server tests (2016/2017) run in CI only, not on PRs. runLegacySqlTests: false # Don't run the AE tests in Debug mode; they rarely succeed. diff --git a/eng/pipelines/stages/build-abstractions-package-ci-stage.yml b/eng/pipelines/stages/build-abstractions-package-ci-stage.yml index 3df0ef9843..bdb0305e0e 100644 --- a/eng/pipelines/stages/build-abstractions-package-ci-stage.yml +++ b/eng/pipelines/stages/build-abstractions-package-ci-stage.yml @@ -69,13 +69,16 @@ parameters: # The C# project reference type to use when building and packing the packages. - name: referenceType type: string - default: Project values: # Reference sibling packages as NuGet packages. - Package # Reference sibling packages as C# projects. - Project + # True when building on the internal ADO.Net project. + - name: isInternalBuild + type: boolean + stages: - stage: build_abstractions_package_stage @@ -98,14 +101,18 @@ stages: - template: /eng/pipelines/jobs/test-abstractions-package-ci-job.yml@self parameters: + packageVersion: $(packageVersion) buildConfiguration: ${{ parameters.buildConfiguration }} debug: ${{ parameters.debug }} displayNamePrefix: Linux dotnetVerbosity: ${{ parameters.dotnetVerbosity }} + isInternalBuild: ${{ parameters.isInternalBuild }} jobNameSuffix: linux + loggingArtifactsName: ${{ parameters.loggingArtifactsName }} netFrameworkRuntimes: [] netRuntimes: [net8.0, net9.0, net10.0] poolName: Azure Pipelines + referenceType: ${{ parameters.referenceType }} vmImage: ubuntu-latest # ------------------------------------------------------------------------ @@ -113,14 +120,18 @@ stages: - template: /eng/pipelines/jobs/test-abstractions-package-ci-job.yml@self parameters: + packageVersion: $(packageVersion) buildConfiguration: ${{ parameters.buildConfiguration }} debug: ${{ parameters.debug }} displayNamePrefix: Win dotnetVerbosity: ${{ parameters.dotnetVerbosity }} + isInternalBuild: ${{ parameters.isInternalBuild }} jobNameSuffix: windows + loggingArtifactsName: ${{ parameters.loggingArtifactsName }} netFrameworkRuntimes: [net462] netRuntimes: [net8.0, net9.0, net10.0] poolName: Azure Pipelines + referenceType: ${{ parameters.referenceType }} vmImage: windows-latest # ------------------------------------------------------------------------ @@ -128,14 +139,18 @@ stages: - template: /eng/pipelines/jobs/test-abstractions-package-ci-job.yml@self parameters: + packageVersion: $(packageVersion) buildConfiguration: ${{ parameters.buildConfiguration }} debug: ${{ parameters.debug }} displayNamePrefix: macOS dotnetVerbosity: ${{ parameters.dotnetVerbosity }} + isInternalBuild: ${{ parameters.isInternalBuild }} jobNameSuffix: macos + loggingArtifactsName: ${{ parameters.loggingArtifactsName }} netFrameworkRuntimes: [] netRuntimes: [net8.0, net9.0, net10.0] poolName: Azure Pipelines + referenceType: ${{ parameters.referenceType }} vmImage: macos-latest # ------------------------------------------------------------------------ @@ -157,3 +172,4 @@ stages: dotnetVerbosity: ${{ parameters.dotnetVerbosity }} loggingArtifactsName: ${{ parameters.loggingArtifactsName }} referenceType: ${{ parameters.referenceType }} + isInternalBuild: ${{ parameters.isInternalBuild }} diff --git a/eng/pipelines/stages/build-azure-package-ci-stage.yml b/eng/pipelines/stages/build-azure-package-ci-stage.yml index b907d2fcda..ec21f33349 100644 --- a/eng/pipelines/stages/build-azure-package-ci-stage.yml +++ b/eng/pipelines/stages/build-azure-package-ci-stage.yml @@ -118,6 +118,10 @@ parameters: # Reference sibling packages as C# projects. - Project + # True when building on the internal ADO.Net project. + - name: isInternalBuild + type: boolean + stages: - stage: build_azure_package_stage @@ -153,6 +157,7 @@ stages: displayNamePrefix: Linux dotnetVerbosity: ${{ parameters.dotnetVerbosity }} jobNameSuffix: linux + isInternalBuild: ${{ parameters.isInternalBuild }} loggingArtifactsName: ${{ parameters.loggingArtifactsName }} mdsArtifactsName: ${{ parameters.mdsArtifactsName }} sqlServerArtifactsName: ${{ parameters.sqlServerArtifactsName }} @@ -173,6 +178,7 @@ stages: displayNamePrefix: Linux Integration dotnetVerbosity: ${{ parameters.dotnetVerbosity }} jobNameSuffix: linux_integration + isInternalBuild: ${{ parameters.isInternalBuild }} loggingArtifactsName: ${{ parameters.loggingArtifactsName }} mdsArtifactsName: ${{ parameters.mdsArtifactsName }} sqlServerArtifactsName: ${{ parameters.sqlServerArtifactsName }} @@ -202,6 +208,7 @@ stages: displayNamePrefix: Win dotnetVerbosity: ${{ parameters.dotnetVerbosity }} jobNameSuffix: windows + isInternalBuild: ${{ parameters.isInternalBuild }} loggingArtifactsName: ${{ parameters.loggingArtifactsName }} mdsArtifactsName: ${{ parameters.mdsArtifactsName }} sqlServerArtifactsName: ${{ parameters.sqlServerArtifactsName }} @@ -222,6 +229,7 @@ stages: displayNamePrefix: Win Integration dotnetVerbosity: ${{ parameters.dotnetVerbosity }} jobNameSuffix: windows_integration + isInternalBuild: ${{ parameters.isInternalBuild }} loggingArtifactsName: ${{ parameters.loggingArtifactsName }} mdsArtifactsName: ${{ parameters.mdsArtifactsName }} sqlServerArtifactsName: ${{ parameters.sqlServerArtifactsName }} @@ -260,6 +268,7 @@ stages: displayNamePrefix: macOS dotnetVerbosity: ${{ parameters.dotnetVerbosity }} jobNameSuffix: macos + isInternalBuild: ${{ parameters.isInternalBuild }} loggingArtifactsName: ${{ parameters.loggingArtifactsName }} mdsArtifactsName: ${{ parameters.mdsArtifactsName }} sqlServerArtifactsName: ${{ parameters.sqlServerArtifactsName }} @@ -293,3 +302,4 @@ stages: dotnetVerbosity: ${{ parameters.dotnetVerbosity }} loggingArtifactsName: ${{ parameters.loggingArtifactsName }} referenceType: ${{ parameters.referenceType }} + isInternalBuild: ${{ parameters.isInternalBuild }} diff --git a/eng/pipelines/stages/build-logging-package-ci-stage.yml b/eng/pipelines/stages/build-logging-package-ci-stage.yml index b9d5feb082..60d07f128c 100644 --- a/eng/pipelines/stages/build-logging-package-ci-stage.yml +++ b/eng/pipelines/stages/build-logging-package-ci-stage.yml @@ -59,6 +59,19 @@ parameters: - detailed - diagnostic + # The C# project reference type to use when building and packing the packages. + - name: referenceType + type: string + values: + # Reference sibling packages as NuGet packages. + - Package + # Reference sibling packages as C# projects. + - Project + + # True when building on the internal ADO.Net project. + - name: isInternalBuild + type: boolean + stages: - stage: build_logging_package_stage @@ -90,3 +103,5 @@ stages: buildConfiguration: ${{ parameters.buildConfiguration }} debug: ${{ parameters.debug }} dotnetVerbosity: ${{ parameters.dotnetVerbosity }} + referenceType: ${{ parameters.referenceType }} + isInternalBuild: ${{ parameters.isInternalBuild }} diff --git a/eng/pipelines/stages/build-sqlclient-package-ci-stage.yml b/eng/pipelines/stages/build-sqlclient-package-ci-stage.yml index 70fa122d5f..9887e2b91c 100644 --- a/eng/pipelines/stages/build-sqlclient-package-ci-stage.yml +++ b/eng/pipelines/stages/build-sqlclient-package-ci-stage.yml @@ -64,6 +64,10 @@ parameters: type: string default: '' + # True when building on the internal ADO.Net project. + - name: isInternalBuild + type: boolean + stages: - stage: build_sqlclient_package_stage @@ -93,6 +97,7 @@ stages: packageVersion: $(packageVersion) sqlServerArtifactsName: ${{ parameters.sqlServerArtifactsName }} sqlServerPackageVersion: $(sqlServerPackageVersion) + isInternalBuild: ${{ parameters.isInternalBuild }} ${{ if ne(parameters.SNIVersion, '') }}: prebuildSteps: - template: /eng/pipelines/common/templates/steps/override-sni-version.yml@self diff --git a/eng/pipelines/stages/build-sqlserver-package-ci-stage.yml b/eng/pipelines/stages/build-sqlserver-package-ci-stage.yml index 3a1417185d..0687bd1267 100644 --- a/eng/pipelines/stages/build-sqlserver-package-ci-stage.yml +++ b/eng/pipelines/stages/build-sqlserver-package-ci-stage.yml @@ -59,6 +59,19 @@ parameters: - detailed - diagnostic + # The C# project reference type to use when building and packing the packages. + - name: referenceType + type: string + values: + # Reference sibling packages as NuGet packages. + - Package + # Reference sibling packages as C# projects. + - Project + + # True when building on the internal ADO.Net project. + - name: isInternalBuild + type: boolean + stages: - stage: build_sqlserver_package_stage @@ -83,3 +96,5 @@ stages: # The version is computed by this stage (see the sqlServerPackageVersion variable above). sqlServerPackageVersion: $(sqlServerPackageVersion) dotnetVerbosity: ${{ parameters.dotnetVerbosity }} + referenceType: ${{ parameters.referenceType }} + isInternalBuild: ${{ parameters.isInternalBuild }} diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationProvider.xml b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationProvider.xml index 7848aaec1a..ce19c6de12 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationProvider.xml +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationProvider.xml @@ -99,6 +99,8 @@ See the LICENSE file in the project root for more information. Avoid performing long-waiting tasks in this method, since it can block other threads from accessing the provider registry. + This method must be idempotent. It may be invoked more than once for the same provider instance, and more than once for a single call, because registration uses a concurrent registry whose update logic may run multiple times under contention. + This method must not throw. The authentication method. @@ -108,6 +110,8 @@ See the LICENSE file in the project root for more information. For example, this method is called when a different provider with the same authentication method overrides this provider in the SQL authentication provider registry. Avoid performing long-waiting task in this method, since it can block other threads from accessing the provider registry. + This method must be idempotent. It may be invoked more than once for the same provider instance, and more than once for a single call, because registration uses a concurrent registry whose update logic may run multiple times under contention. + This method must not throw. The authentication method. @@ -130,6 +134,9 @@ 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. + + This is the canonical way to retrieve a registered authentication provider. + Sets an authentication provider by method. @@ -138,6 +145,9 @@ See the LICENSE file in the project root for more information. if the operation succeeded; otherwise, (for example, the existing provider disallows overriding). + + This is the canonical way to register an authentication provider. + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Abstractions.csproj b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Abstractions.csproj index e8b3c55ba0..3b7c99ac5a 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Abstractions.csproj +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Abstractions.csproj @@ -33,8 +33,31 @@ + + + + + + + + + + + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/AuthenticationProviderRegistry.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/AuthenticationProviderRegistry.cs new file mode 100644 index 0000000000..542fd3e38e --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/AuthenticationProviderRegistry.cs @@ -0,0 +1,238 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Concurrent; +using Microsoft.Data.SqlClient.Internal; + +namespace Microsoft.Data.SqlClient; + +/// +/// Holds the registry of instances keyed by +/// . This is the shared store that backs the public +/// and +/// methods. +/// +/// +/// Providers fall into two categories: +/// +/// +/// +/// Permanent providers, registered via (e.g. from an +/// application's configuration). These take precedence and cannot be overridden by a later +/// call to . +/// +/// +/// +/// +/// Overridable providers, registered via . These can be replaced +/// by subsequent calls, but never override a permanent provider. +/// +/// +/// +/// +internal sealed class AuthenticationProviderRegistry +{ + #region Private Fields + + /// + /// The singleton instance backing the public static + /// and + /// accessors. + /// + /// + /// Production code uses this shared instance. Tests can instead construct an isolated + /// instance via the internal constructor to avoid mutating global state. + /// + internal static AuthenticationProviderRegistry Instance { get; } = new(); + + /// + /// A registered provider together with whether it was registered as permanent (via + /// ) and therefore not overridable by . + /// + /// The registered provider. Never . + /// Whether the provider must not be overridden by . + private readonly record struct ProviderEntry(SqlAuthenticationProvider Provider, bool IsPermanent); + + /// + /// The registered providers keyed by authentication method. Each entry records whether the + /// provider was registered as permanent (e.g. application specified); permanent providers are + /// not overridable via . + /// + private readonly ConcurrentDictionary _providers = new(); + + #endregion + + #region Construction + + /// + /// Initializes a new, empty registry. Production code uses the shared ; + /// the constructor is exposed to tests so they can exercise registry behavior in isolation. + /// + internal AuthenticationProviderRegistry() + { + } + + #endregion + + #region Internal API + + /// + /// Gets the provider registered for the given authentication method, or + /// if none is registered. + /// + internal SqlAuthenticationProvider? GetProvider(SqlAuthenticationMethod authenticationMethod) + { + return _providers.TryGetValue(authenticationMethod, out ProviderEntry entry) + ? entry.Provider + : null; + } + + /// + /// Registers an overridable provider for the given authentication method. + /// + /// + /// if the provider was registered; if a + /// permanent provider is already registered for the authentication method. + /// + /// + /// The provider does not support the given authentication method. + /// + /// + /// is . + /// + internal bool SetProvider(SqlAuthenticationMethod authenticationMethod, SqlAuthenticationProvider provider) + { + if (!provider.IsSupported(authenticationMethod)) + { + throw new NotSupportedException( + string.Format( + AbstractionsStrings.SQL_UnsupportedAuthenticationByProvider, + provider.GetType().Name, + authenticationMethod.ToString())); + } + + ProviderEntry result = _providers.AddOrUpdate( + authenticationMethod, + // addValueFactory: no provider is registered for this method yet. + (SqlAuthenticationMethod key) => + { + InvokeProviderCallback(provider, provider.BeforeLoad, key, nameof(SqlAuthenticationProvider.BeforeLoad)); + + SqlClientEventSource.Log.TryTraceEvent( + "AuthenticationProviderRegistry.SetProvider | Added auth provider {0} for authentication {1}.", + GetProviderType(provider), + key); + + return new ProviderEntry(provider, IsPermanent: false); + }, + // updateValueFactory: a provider is already registered for this method. + (SqlAuthenticationMethod key, ProviderEntry existing) => + { + // Permanent providers cannot be replaced. Return the existing entry unchanged so + // AddOrUpdate keeps it; SetProvider detects this from the returned entry below. + if (existing.IsPermanent) + { + SqlClientEventSource.Log.TryTraceEvent( + "AuthenticationProviderRegistry.SetProvider | Failed to add provider {0} because a " + + "permanent provider with type {1} already existed for authentication {2}.", + GetProviderType(provider), + GetProviderType(existing.Provider), + key); + + return existing; + } + + InvokeProviderCallback(existing.Provider, existing.Provider.BeforeUnload, key, nameof(SqlAuthenticationProvider.BeforeUnload)); + InvokeProviderCallback(provider, provider.BeforeLoad, key, nameof(SqlAuthenticationProvider.BeforeLoad)); + + SqlClientEventSource.Log.TryTraceEvent( + "AuthenticationProviderRegistry.SetProvider | Added auth provider {0}, overriding " + + "existing provider {1} for authentication {2}.", + GetProviderType(provider), + GetProviderType(existing.Provider), + key); + + return new ProviderEntry(provider, IsPermanent: false); + }); + + // The new provider is always stored non-permanent; if a permanent provider blocked the + // update, AddOrUpdate returned that (permanent) entry instead. + return !result.IsPermanent; + } + + /// + /// Registers a permanent provider for the given authentication method. Permanent providers + /// take precedence and cannot be overridden by . + /// + /// + /// Callers are responsible for verifying that the provider supports the authentication method + /// before registering it. + /// + /// This is a last-in-wins operation: a later call for the + /// same authentication method unconditionally replaces any previously registered provider + /// (permanent or not). Only is blocked by an existing permanent + /// provider; itself always overwrites. + /// + /// + internal void SetPermanentProvider(SqlAuthenticationMethod authenticationMethod, SqlAuthenticationProvider provider) + { + _providers[authenticationMethod] = new ProviderEntry(provider, IsPermanent: true); + } + + #endregion + + #region Private Helpers + + /// + /// Returns a human-readable type name for the given provider, for use in trace messages. + /// + /// The provider to describe, or . + /// + /// The provider's full type name; "null" if is + /// ; or "unknown" if the type name is unavailable. + /// + private static string GetProviderType(SqlAuthenticationProvider? provider) + { + if (provider is null) + { + return "null"; + } + return provider.GetType().FullName ?? "unknown"; + } + + /// + /// Invokes a provider lifecycle callback ( or + /// ), isolating the registry from a + /// misbehaving provider: any exception the callback throws is logged and swallowed so it + /// cannot corrupt registration. + /// + /// The provider whose callback is being invoked (used for logging). + /// The callback to invoke. + /// The authentication method passed to the callback. + /// The callback name, used in trace messages. + private static void InvokeProviderCallback( + SqlAuthenticationProvider provider, + Action callback, + SqlAuthenticationMethod authenticationMethod, + string callbackName) + { + try + { + callback(authenticationMethod); + } + catch (Exception ex) + { + SqlClientEventSource.Log.TryTraceEvent( + "AuthenticationProviderRegistry.SetProvider | {0} threw for provider {1} with " + + "authentication {2}; ignoring: {3}", + callbackName, + GetProviderType(provider), + authenticationMethod, + ex); + } + } + + #endregion +} diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/IsExternalInit.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/IsExternalInit.cs new file mode 100644 index 0000000000..225b0e3860 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/IsExternalInit.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if !NET + +namespace System.Runtime.CompilerServices; + +/// +/// Polyfill for the marker type the C# compiler requires to emit init-only setters +/// (used by records and init-only properties). It is provided by the BCL on .NET, but not on +/// the netstandard2.0 / .NET Framework targets this assembly supports, so we define it here. +/// +internal static class IsExternalInit +{ +} + +#endif 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..595106a294 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 AuthenticationProviderRegistry.Instance.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 AuthenticationProviderRegistry.Instance.SetProvider(authenticationMethod, provider); } } diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.Designer.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.Designer.cs new file mode 100644 index 0000000000..c07b958067 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.Designer.cs @@ -0,0 +1,80 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.Data.SqlClient +{ + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class AbstractionsStrings + { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal AbstractionsStrings() + { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager + { + get + { + if (object.ReferenceEquals(resourceMan, null)) + { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Data.SqlClient.Strings", typeof(AbstractionsStrings).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture + { + get + { + return resourceCulture; + } + set + { + resourceCulture = value; + } + } + + /// + /// 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); + } + } + } +} diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.cs.resx b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.cs.resx new file mode 100644 index 0000000000..ed789746b9 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.cs.resx @@ -0,0 +1,18 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Zprostředkovatel {0} nepodporuje ověřování {1}. + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.de.resx b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.de.resx new file mode 100644 index 0000000000..bebed6aad9 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.de.resx @@ -0,0 +1,18 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Die Authentifizierung "{1}" wird vom Anbieter "{0}" nicht unterstützt. + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.es.resx b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.es.resx new file mode 100644 index 0000000000..eb9a12c8d1 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.es.resx @@ -0,0 +1,18 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + El proveedor "{0}" no admite la autenticación "{1}". + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.fr.resx b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.fr.resx new file mode 100644 index 0000000000..499e11b9e7 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.fr.resx @@ -0,0 +1,18 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Le fournisseur '{0}' ne prend pas en charge l'authentification '{1}'. + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.it.resx b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.it.resx new file mode 100644 index 0000000000..d15ff92cd4 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.it.resx @@ -0,0 +1,18 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Il provider '{0}' non supporta l'autenticazione '{1}'. + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.ja.resx b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.ja.resx new file mode 100644 index 0000000000..6db9db6080 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.ja.resx @@ -0,0 +1,18 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + プロバイダー '{0}' では認証 '{1}' はサポートされていません。 + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.ko.resx b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.ko.resx new file mode 100644 index 0000000000..c153160993 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.ko.resx @@ -0,0 +1,18 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + '{0}' 공급자에서 '{1}' 인증을 지원하지 않습니다. + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.pl.resx b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.pl.resx new file mode 100644 index 0000000000..58aa1ddc94 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.pl.resx @@ -0,0 +1,18 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Dostawca „{0}” nie obsługuje uwierzytelniania „{1}”. + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.pt-BR.resx b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.pt-BR.resx new file mode 100644 index 0000000000..cc0343939e --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.pt-BR.resx @@ -0,0 +1,18 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + O provedor '{0}' não dá suporte à autenticação '{1}'. + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.resx b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.resx new file mode 100644 index 0000000000..44856c63c5 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The provider '{0}' does not support authentication '{1}'. + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.ru.resx b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.ru.resx new file mode 100644 index 0000000000..30c30793d9 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.ru.resx @@ -0,0 +1,18 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Поставщик "{0}" не поддерживает проверку подлинности "{1}". + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.tr.resx b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.tr.resx new file mode 100644 index 0000000000..359aced0bd --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.tr.resx @@ -0,0 +1,18 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + '{0}' adlı sağlayıcı, '{1}' kimlik doğrulamasını desteklemiyor. + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.zh-Hans.resx b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.zh-Hans.resx new file mode 100644 index 0000000000..26cfda9a6e --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.zh-Hans.resx @@ -0,0 +1,18 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 提供程序“{0}”不支持身份验证“{1}”。 + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.zh-Hant.resx b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.zh-Hant.resx new file mode 100644 index 0000000000..8505a12ec6 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.zh-Hant.resx @@ -0,0 +1,18 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 提供者 '{0}' 不支援驗證 '{1}'。 + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/Abstractions.Test.csproj b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/Abstractions.Test.csproj index 427a7aaf9f..8a80c24a51 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/Abstractions.Test.csproj +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/Abstractions.Test.csproj @@ -2,12 +2,26 @@ Microsoft.Data.SqlClient.Extensions.Abstractions.Test - net462;net8.0;net9.0;net10.0 + net8.0;net9.0;net10.0 + + + $(TargetFrameworks);net462 false true + + + + true + $(TestSigningKeyPath) + + enable diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/AuthenticationProviderRegistryTest.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/AuthenticationProviderRegistryTest.cs new file mode 100644 index 0000000000..742ba2b5d8 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/AuthenticationProviderRegistryTest.cs @@ -0,0 +1,463 @@ +// 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.Extensions.Abstractions.Test; + +/// +/// Tests for the AuthenticationProviderRegistry class. +/// +/// Each test exercises an isolated registry instance (constructed via the internal constructor) +/// so there is no shared global state and the tests are safe to run in parallel. +/// +public class AuthenticationProviderRegistryTest +{ + #region GetProvider + + /// + /// GetProvider returns null when no provider has been registered for the specified + /// authentication method. + /// + [Fact] + public void GetProvider_ReturnsNull_WhenNoProviderRegistered() + { + AuthenticationProviderRegistry registry = new(); + + Assert.Null(registry.GetProvider(SqlAuthenticationMethod.SqlPassword)); + } + + /// + /// GetProvider returns null for NotSpecified, which is never a valid registration target. + /// + [Fact] + public void GetProvider_ReturnsNull_ForNotSpecified() + { + AuthenticationProviderRegistry registry = new(); + + Assert.Null(registry.GetProvider(SqlAuthenticationMethod.NotSpecified)); + } + + /// + /// Getting an existing provider works. + /// + [Fact] + public void GetProvider_ReturnsSameInstance_AfterSetProvider() + { + AuthenticationProviderRegistry registry = new(); + DeviceCodeProvider provider = new(); + + Assert.True( + registry.SetProvider( + SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, + provider)); + + Assert.Same( + provider, + registry.GetProvider( + SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow)); + } + + #endregion + + #region SetProvider - Basic + + /// + /// SetProvider throws NullReferenceException when a null provider is passed (current behavior, + /// not a validated argument). + /// + [Fact] + public void SetProvider_ThrowsOnNullProvider() + { + AuthenticationProviderRegistry registry = new(); + + Assert.Throws(() => + registry.SetProvider( + SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, + null!)); + } + + /// + /// SetProvider throws NotSupportedException when the provider does not support the specified + /// authentication method, and the message names both the provider type and the method. + /// + [Fact] + public void SetProvider_ThrowsOnUnsupportedMethod() + { + AuthenticationProviderRegistry registry = new(); + + // DeviceCodeProvider only supports DeviceCodeFlow. + DeviceCodeProvider provider = new(); + + NotSupportedException ex = Assert.Throws(() => + registry.SetProvider( + SqlAuthenticationMethod.ActiveDirectoryInteractive, + provider)); + + Assert.Contains(nameof(DeviceCodeProvider), ex.Message); + Assert.Contains( + SqlAuthenticationMethod.ActiveDirectoryInteractive.ToString(), + ex.Message); + } + + /// + /// SetProvider replaces a previously registered provider for the same authentication method. + /// + [Fact] + public void SetProvider_ReplacesExistingProvider() + { + AuthenticationProviderRegistry registry = new(); + DeviceCodeProvider provider1 = new(); + DeviceCodeProvider provider2 = new(); + + Assert.True( + registry.SetProvider( + SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, + provider1)); + + Assert.Same( + provider1, + registry.GetProvider( + SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow)); + + // Replace with provider2. + Assert.True( + registry.SetProvider( + SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, + provider2)); + + Assert.Same( + provider2, + registry.GetProvider( + SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow)); + } + + /// + /// Distinct providers registered for distinct methods are keyed independently: each method + /// returns its own provider, and a method that was never registered returns null. This guards + /// against mis-keying or cross-method bleed in the backing store. + /// + [Fact] + public void SetProvider_DistinctProvidersPerMethod_AreKeyedIndependently() + { + AuthenticationProviderRegistry registry = new(); + + AllMethodsProvider integrated = new(); + AllMethodsProvider interactive = new(); + AllMethodsProvider deviceCode = new(); + + Assert.True(registry.SetProvider(SqlAuthenticationMethod.ActiveDirectoryIntegrated, integrated)); + Assert.True(registry.SetProvider(SqlAuthenticationMethod.ActiveDirectoryInteractive, interactive)); + Assert.True(registry.SetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, deviceCode)); + + // Each method returns its own provider -- no cross-talk. + Assert.Same(integrated, registry.GetProvider(SqlAuthenticationMethod.ActiveDirectoryIntegrated)); + Assert.Same(interactive, registry.GetProvider(SqlAuthenticationMethod.ActiveDirectoryInteractive)); + Assert.Same(deviceCode, registry.GetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow)); + + // A method that was never registered returns null. + Assert.Null(registry.GetProvider(SqlAuthenticationMethod.ActiveDirectoryServicePrincipal)); + } + + #endregion + + + #region SetProvider - Lifecycle callbacks + + /// + /// The first registration for a method invokes BeforeLoad (immediately before the provider is + /// added to the registry) but not BeforeUnload (there is no prior provider to unload). + /// + [Fact] + public void SetProvider_FirstRegistration_InvokesBeforeLoad_NotBeforeUnload() + { + AuthenticationProviderRegistry registry = new(); + const SqlAuthenticationMethod method = + SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow; + + RecordingProvider provider = new(); + + Assert.True(registry.SetProvider(method, provider)); + Assert.Same(provider, registry.GetProvider(method)); + + Assert.Equal([method], provider.BeforeLoadCalls); + Assert.Empty(provider.BeforeUnloadCalls); + } + + /// + /// An exception thrown by BeforeLoad during the first registration (the add path, with no prior + /// provider to override) is swallowed; the provider is still registered and SetProvider + /// succeeds. + /// + [Fact] + public void SetProvider_FirstRegistration_BeforeLoadThrows_IsSwallowed_AndRegistrationSucceeds() + { + AuthenticationProviderRegistry registry = new(); + const SqlAuthenticationMethod method = + SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow; + + RecordingProvider provider = new(throwFromCallbacks: true); + + // No prior provider exists, so this takes the add path; BeforeLoad throws but is swallowed. + Assert.True(registry.SetProvider(method, provider)); + Assert.Same(provider, registry.GetProvider(method)); + + // The throwing BeforeLoad was actually invoked (before it threw); BeforeUnload was not + // (there was nothing to unload). + Assert.Equal([method], provider.BeforeLoadCalls); + Assert.Empty(provider.BeforeUnloadCalls); + } + + /// + /// Replacing an existing provider invokes BeforeUnload on the old provider and BeforeLoad on + /// the new provider, each for the affected method. + /// + [Fact] + public void SetProvider_Replace_InvokesBeforeUnloadOnOld_AndBeforeLoadOnNew() + { + AuthenticationProviderRegistry registry = new(); + const SqlAuthenticationMethod method = + SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow; + + RecordingProvider oldProvider = new(); + RecordingProvider newProvider = new(); + + Assert.True(registry.SetProvider(method, oldProvider)); + + // We see BeforeLoad called on oldProvider. + Assert.Equal([method], oldProvider.BeforeLoadCalls); + Assert.Empty(oldProvider.BeforeUnloadCalls); + + Assert.True(registry.SetProvider(method, newProvider)); + + // We see BeforeUnload called on oldProvider, and its BeforeLoad calls are not repeated. + Assert.Equal([method], oldProvider.BeforeLoadCalls); + Assert.Equal([method], oldProvider.BeforeUnloadCalls); + + // We see BeforeLoad called on newProvider. + Assert.Equal([method], newProvider.BeforeLoadCalls); + Assert.Empty(newProvider.BeforeUnloadCalls); + + Assert.Same(newProvider, registry.GetProvider(method)); + } + + /// + /// An exception thrown by the old provider's BeforeUnload callback is swallowed; the new + /// provider is still registered and SetProvider succeeds. + /// + [Fact] + public void SetProvider_Replace_BeforeUnloadThrows_IsSwallowed_AndRegistrationSucceeds() + { + AuthenticationProviderRegistry registry = new(); + const SqlAuthenticationMethod method = + SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow; + + RecordingProvider oldProvider = new(throwFromCallbacks: true); + RecordingProvider newProvider = new(); + + Assert.True(registry.SetProvider(method, oldProvider)); + + // We see BeforeLoad called on oldProvider. + Assert.Equal([method], oldProvider.BeforeLoadCalls); + Assert.Empty(oldProvider.BeforeUnloadCalls); + + // oldProvider.BeforeUnload throws, but the override still succeeds. + Assert.True(registry.SetProvider(method, newProvider)); + Assert.Same(newProvider, registry.GetProvider(method)); + + // The throwing BeforeUnload was actually invoked (before it threw). + Assert.Equal([method], oldProvider.BeforeLoadCalls); + Assert.Equal([method], oldProvider.BeforeUnloadCalls); + + // The new provider's BeforeLoad still ran. + Assert.Equal([method], newProvider.BeforeLoadCalls); + Assert.Empty(newProvider.BeforeUnloadCalls); + } + + /// + /// An exception thrown by the new provider's BeforeLoad callback is swallowed; the new provider + /// is still registered and SetProvider succeeds. + /// + [Fact] + public void SetProvider_Replace_BeforeLoadThrows_IsSwallowed_AndRegistrationSucceeds() + { + AuthenticationProviderRegistry registry = new(); + const SqlAuthenticationMethod method = + SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow; + + RecordingProvider oldProvider = new(); + RecordingProvider newProvider = new(throwFromCallbacks: true); + + Assert.True(registry.SetProvider(method, oldProvider)); + + // We see BeforeLoad called on oldProvider. + Assert.Equal([method], oldProvider.BeforeLoadCalls); + Assert.Empty(oldProvider.BeforeUnloadCalls); + + // newProvider.BeforeLoad throws, but the override still succeeds. + Assert.True(registry.SetProvider(method, newProvider)); + Assert.Same(newProvider, registry.GetProvider(method)); + + // The old provider's BeforeUnload still ran. + Assert.Equal([method], oldProvider.BeforeLoadCalls); + Assert.Equal([method], oldProvider.BeforeUnloadCalls); + + // The throwing BeforeLoad was actually invoked (before it threw). + Assert.Equal([method], newProvider.BeforeLoadCalls); + Assert.Empty(newProvider.BeforeUnloadCalls); + } + + #endregion + + #region SetPermanentProvider + + /// + /// SetPermanentProvider registers the provider, and GetProvider then returns + /// that same instance. + /// + [Fact] + public void SetPermanentProvider_ThenGetProvider_ReturnsSameInstance() + { + AuthenticationProviderRegistry registry = new(); + const SqlAuthenticationMethod method = + SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity; + + AllMethodsProvider provider = new(); + registry.SetPermanentProvider(method, provider); + + Assert.Same(provider, registry.GetProvider(method)); + } + + /// + /// A permanently registered provider takes precedence: a subsequent user + /// SetProvider call for the same method returns false and does not replace + /// it. + /// + [Fact] + public void SetPermanentProvider_TakesPrecedence_OverUserSetProvider() + { + AuthenticationProviderRegistry registry = new(); + const SqlAuthenticationMethod method = + SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity; + + AllMethodsProvider permanent = new(); + registry.SetPermanentProvider(method, permanent); + + Assert.Same(permanent, registry.GetProvider(method)); + + // A user attempt to override the permanent provider fails. + AllMethodsProvider userProvider = new(); + Assert.False(registry.SetProvider(method, userProvider)); + + // The permanent provider is still in place. + Assert.Same(permanent, registry.GetProvider(method)); + } + + /// + /// SetPermanentProvider is last-in-wins: a later call for the same method + /// unconditionally replaces the previously registered permanent provider, + /// and the replacement remains non-overridable by SetProvider. + /// + [Fact] + public void SetPermanentProvider_LastInWins() + { + AuthenticationProviderRegistry registry = new(); + const SqlAuthenticationMethod method = + SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity; + + AllMethodsProvider first = new(); + AllMethodsProvider second = new(); + + registry.SetPermanentProvider(method, first); + registry.SetPermanentProvider(method, second); + + // The second permanent registration replaced the first. + Assert.Same(second, registry.GetProvider(method)); + + // The replacement is still permanent: a user SetProvider is refused. + Assert.False(registry.SetProvider(method, new AllMethodsProvider())); + Assert.Same(second, registry.GetProvider(method)); + } + + #endregion + + #region Helpers + + /// + /// A dummy provider that supports all authentication methods. + /// + private sealed class AllMethodsProvider : SqlAuthenticationProvider + { + /// + public override bool IsSupported(SqlAuthenticationMethod authenticationMethod) => true; + + /// + public override Task AcquireTokenAsync( + SqlAuthenticationParameters parameters) + => throw new NotImplementedException(); + } + + /// + /// A dummy provider that only supports ActiveDirectoryDeviceCodeFlow. + /// + private sealed class DeviceCodeProvider : SqlAuthenticationProvider + { + /// + public override bool IsSupported(SqlAuthenticationMethod authenticationMethod) + => authenticationMethod == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow; + + /// + public override Task AcquireTokenAsync( + SqlAuthenticationParameters parameters) + => Task.FromResult( + new SqlAuthenticationToken( + "SampleAccessToken", DateTimeOffset.UtcNow.AddMinutes(5))); + } + + /// + /// A provider that supports all methods and records every BeforeLoad and + /// BeforeUnload invocation. When constructed with throwFromCallbacks, + /// each callback throws after recording, so tests can verify the registry + /// both invokes the callback and isolates its failure. + /// + private sealed class RecordingProvider : SqlAuthenticationProvider + { + private readonly bool _throwFromCallbacks; + + public RecordingProvider(bool throwFromCallbacks = false) + => _throwFromCallbacks = throwFromCallbacks; + + public List BeforeLoadCalls { get; } = new(); + + public List BeforeUnloadCalls { get; } = new(); + + /// + public override bool IsSupported(SqlAuthenticationMethod authenticationMethod) => true; + + /// + public override void BeforeLoad(SqlAuthenticationMethod authenticationMethod) + { + BeforeLoadCalls.Add(authenticationMethod); + if (_throwFromCallbacks) + { + throw new InvalidOperationException("BeforeLoad failed."); + } + } + + /// + public override void BeforeUnload(SqlAuthenticationMethod authenticationMethod) + { + BeforeUnloadCalls.Add(authenticationMethod); + if (_throwFromCallbacks) + { + throw new InvalidOperationException("BeforeUnload failed."); + } + } + + /// + public override Task AcquireTokenAsync( + SqlAuthenticationParameters parameters) + => throw new NotImplementedException(); + } + + #endregion +} diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/SqlAuthenticationProviderTest.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/SqlAuthenticationProviderTest.cs index c5872a8391..362a707bce 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/SqlAuthenticationProviderTest.cs +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/SqlAuthenticationProviderTest.cs @@ -6,6 +6,11 @@ namespace Microsoft.Data.SqlClient.Extensions.Abstractions.Test; +/// +/// Tests for the public static API, which delegates to +/// the shared AuthenticationProviderRegistry.Instance within the Abstractions assembly. +/// Registry behavior in isolation is covered by AuthenticationProviderRegistryTest. +/// public class SqlAuthenticationProviderTest { #region Test Setup @@ -15,7 +20,8 @@ public class SqlAuthenticationProviderTest /// public SqlAuthenticationProviderTest() { - // Confirm that the MDS assembly is indeed not present. + // Confirm that the MDS assembly is indeed not present. This proves the + // registry operates purely within the Abstractions assembly. Assert.Throws( () => Assembly.Load("Microsoft.Data.SqlClient")); } @@ -25,50 +31,35 @@ public SqlAuthenticationProviderTest() #region Tests /// - /// Test that GetProvider fails predictably when the MDS assembly can't be - /// found. + /// The public static API delegates to the shared + /// , so reads and writes through the + /// public API and the shared registry instance observe the same backing store. /// - [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 GetProvider_NoMdsAssembly(SqlAuthenticationMethod method) + [Fact] + public void PublicApi_DelegatesToSharedInstance() { - // GetProvider() should return null when the MDS assembly can't be - // found. - Assert.Null(SqlAuthenticationProvider.GetProvider(method)); - } + // Use a method that no other test registers on the shared instance, so this cannot + // interfere with other tests running in the same class. + const SqlAuthenticationMethod method = + SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow; - /// - /// 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())); + DeviceCodeProvider provider = new(); + + Assert.True(SqlAuthenticationProvider.SetProvider(method, provider)); + + Assert.Same(provider, SqlAuthenticationProvider.GetProvider(method)); + + // The public API and the shared registry instance agree. + Assert.Same(provider, AuthenticationProviderRegistry.Instance.GetProvider(method)); + + // Replacing via the internal API is reflected through both the public API and the shared + // registry instance, confirming they observe the same backing store. + DeviceCodeProvider replacement = new(); + + Assert.True(AuthenticationProviderRegistry.Instance.SetProvider(method, replacement)); + + Assert.Same(replacement, SqlAuthenticationProvider.GetProvider(method)); + Assert.Same(replacement, AuthenticationProviderRegistry.Instance.GetProvider(method)); } #endregion @@ -76,22 +67,25 @@ public void SetProvider_NoMdsAssembly(SqlAuthenticationMethod method) #region Helpers /// - /// A dummy provider that supports all authentication methods. + /// A dummy provider that only supports ActiveDirectoryDeviceCodeFlow. /// - private sealed class Provider : SqlAuthenticationProvider + private sealed class DeviceCodeProvider : SqlAuthenticationProvider { /// public override bool IsSupported( SqlAuthenticationMethod authenticationMethod) { - return true; + return authenticationMethod == + SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow; } /// public override Task AcquireTokenAsync( SqlAuthenticationParameters parameters) { - throw new NotImplementedException(); + return Task.FromResult( + new SqlAuthenticationToken( + "SampleAccessToken", DateTimeOffset.UtcNow.AddMinutes(5))); } } diff --git a/src/Microsoft.Data.SqlClient.Extensions/Azure/src/Azure.csproj b/src/Microsoft.Data.SqlClient.Extensions/Azure/src/Azure.csproj index da0a4cb4c7..8734cc2ccb 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Azure/src/Azure.csproj +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/src/Azure.csproj @@ -33,10 +33,22 @@ + + + + + + + $(RepoRoot)artifacts/ diff --git a/src/Microsoft.Data.SqlClient.Extensions/Azure/test/AADAuthenticationTests.cs b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/AADAuthenticationTests.cs index e90dcce506..e24b7ceaab 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Azure/test/AADAuthenticationTests.cs +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/AADAuthenticationTests.cs @@ -10,7 +10,7 @@ namespace Microsoft.Data.SqlClient.Extensions.Azure.Test; // These tests were moved from MDS FunctionalTests AADAuthenticationTests.cs. -[Collection("SqlAuthenticationProvider")] +[Collection("SqlAuthenticationProviderGlobal")] public class AADAuthenticationTests { [Fact] diff --git a/src/Microsoft.Data.SqlClient.Extensions/Azure/test/AuthenticationBootstrapperGlobalTests.cs b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/AuthenticationBootstrapperGlobalTests.cs new file mode 100644 index 0000000000..e77fc711c7 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/AuthenticationBootstrapperGlobalTests.cs @@ -0,0 +1,115 @@ +// 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.Azure.Test; + +/// +/// Tests for the SqlClient-internal AuthenticationBootstrapper that run the full bootstrap +/// and so mutate the process-wide AuthenticationProviderRegistry.Instance. They are +/// serialized via the shared SqlAuthenticationProvider collection. +/// +/// +/// Like , these require the Azure extension assembly +/// to be present (guaranteed only by this test project). The non-global, side-effect-free tests +/// live in . +/// +[Collection("SqlAuthenticationProviderGlobal")] +public class AuthenticationBootstrapperGlobalTests +{ + public AuthenticationBootstrapperGlobalTests() + { + // Precondition: the Azure extension assembly must be present for these tests to be + // meaningful. This is what distinguishes this project from the core UnitTests. + Assert.NotNull(Assembly.Load("Microsoft.Data.SqlClient.Extensions.Azure")); + } + + // Verify that the bootstrapper installs the Azure auth provider for all AAD/Entra + // authentication methods, and not for any other methods. + // + // This project configures neither applicationClientId nor useWamBroker (it has no app.config + // overrides), so the bootstrapper constructs the Azure extension's + // ActiveDirectoryAuthenticationProvider via its parameterless constructor and registers that + // single instance for every Active Directory method. + [Fact] + public void Bootstrap_InstallsAzureProvider_ForAllActiveDirectoryMethods() + { + // Under the lazy-bootstrap model the SqlClient bootstrapper only runs on first federated + // authentication. Force it to run so the Azure extension provider is discovered and + // registered. + // + // GOTCHA: This modifies global state. + Bootstrap(); + + // Iterate over all authentication methods rather than specifying them via Theory data so + // that we detect any new methods that don't meet our expectations. + #if NET + var methods = Enum.GetValues(); + #else + var methods = Enum.GetValues(typeof(SqlAuthenticationMethod)).Cast(); + #endif + + foreach (var method in methods) + { + SqlAuthenticationProvider? provider = SqlAuthenticationProvider.GetProvider(method); + + switch (method) + { + #pragma warning disable 0618 // Type or member is obsolete + case SqlAuthenticationMethod.ActiveDirectoryPassword: + #pragma warning restore 0618 // Type or member is obsolete + case SqlAuthenticationMethod.ActiveDirectoryIntegrated: + case SqlAuthenticationMethod.ActiveDirectoryInteractive: + case SqlAuthenticationMethod.ActiveDirectoryServicePrincipal: + case SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow: + case SqlAuthenticationMethod.ActiveDirectoryManagedIdentity: + case SqlAuthenticationMethod.ActiveDirectoryMSI: + case SqlAuthenticationMethod.ActiveDirectoryDefault: + case SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity: + { + Assert.NotNull(provider); + Assert.IsType(provider); + break; + } + default: + { + // There is either no provider installed, or it is not ours. + if (provider is not null) + { + Assert.IsNotType(provider); + } + break; + } + } + } + } + + // Forces the MDS bootstrapper to run by invoking its internal static Bootstrap() method via + // reflection. This project does not have InternalsVisibleTo from Microsoft.Data.SqlClient, so + // the method cannot be called directly. + // + // NOTE: This perturbs GLOBAL state -- Bootstrap() seeds the process-wide + // AuthenticationProviderRegistry.Instance (installing the Azure provider for the AD methods). + // That is why this class lives in the [Collection("SqlAuthenticationProviderGlobal")] collection, + // which serializes it with the other tests that mutate the shared registry. + // + // TODO(https://sqlclientdrivers.visualstudio.com/ADO.Net/_workitems/edit/41888): + // Once PR #4385 completes (signing Azure/Azure.Test for internal Package-mode CI builds), grant + // this project InternalsVisibleTo from Microsoft.Data.SqlClient and replace this reflection + // with a direct call to AuthenticationBootstrapper.Bootstrap(). + private static void Bootstrap() + { + Type? bootstrapper = Type.GetType( + "Microsoft.Data.SqlClient.AuthenticationBootstrapper, Microsoft.Data.SqlClient"); + Assert.NotNull(bootstrapper); + + MethodInfo? bootstrap = bootstrapper!.GetMethod( + "Bootstrap", + BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + Assert.NotNull(bootstrap); + + bootstrap!.Invoke(null, null); + } +} diff --git a/src/Microsoft.Data.SqlClient.Extensions/Azure/test/AuthenticationBootstrapperTests.cs b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/AuthenticationBootstrapperTests.cs new file mode 100644 index 0000000000..cbeecaf66c --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/AuthenticationBootstrapperTests.cs @@ -0,0 +1,148 @@ +// 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.Azure.Test; + +/// +/// Tests for the MDS-internal AuthenticationBootstrapper that require the Azure extension +/// assembly to be present but do NOT mutate global state. These exercise +/// CreateAzureAuthenticationProvider's constructor selection against the real Azure provider, +/// so they need no [Collection] serialization. +/// +/// +/// This is the only test project that references — and therefore guarantees the presence of — +/// Microsoft.Data.SqlClient.Extensions.Azure, so the bootstrapper's Azure-extension +/// discovery can be exercised for real here. The core UnitTests project, where the Azure extension +/// is absent, covers the bootstrapper's Azure-absent paths instead. +/// +/// The global-state-mutating tests (which run the full bootstrap) live in +/// (AuthenticationBootstrapperGlobalTests.cs). +/// +public class AuthenticationBootstrapperTests +{ + public AuthenticationBootstrapperTests() + { + // Precondition: the Azure extension assembly must be present for these tests to be + // meaningful. This is what distinguishes this project from the core UnitTests. + Assert.NotNull(Assembly.Load("Microsoft.Data.SqlClient.Extensions.Azure")); + } + + // CreateAzureAuthenticationProvider -- constructor selection against the REAL Azure extension. + // + // These tests drive the same logic the bootstrapper runs inside LoadAzureExtensionProvider, + // using the applicationClientId / useWamBroker values that the + // app.config section would produce. Because the real Azure extension always exposes + // ActiveDirectoryAuthenticationProviderOptions, the legacy-(string) fallback, the + // "no compatible ctor -> null", and the "useWamBroker but Options missing -> throw" paths are + // NOT reachable here; those are covered with stubs in the core UnitTests. + + // The SqlClient first-party application client id hard-coded in the provider (always enables + // WAM broker). + private const string SqlClientApplicationId = "2fd908ad-0664-4344-b9be-cd3e8b574c38"; + + // A fixed stand-in for a caller-/config-supplied application id, distinct from the first-party id. + private const string TestCustomAppId = "11111111-2222-3333-4444-555555555555"; + + // No config (applicationClientId and useWamBroker both unset) -> parameterless ctor -> the + // first-party id, which enables WAM broker. + [Fact] + public void CreateAzureProvider_NoConfig_UsesParameterlessCtor() + { + var provider = Assert.IsType( + CreateAzureAuthenticationProvider(applicationClientId: null, useWamBroker: null)); + + Assert.Equal(SqlClientApplicationId, provider.ApplicationClientId); + Assert.True(provider.UseWamBroker); + } + + // applicationClientId only -> Options ctor; UseWamBroker stays at its default (false) for a + // caller-supplied id. + [Fact] + public void CreateAzureProvider_AppClientIdOnly_UsesOptionsCtor_WamDisabled() + { + var provider = Assert.IsType( + CreateAzureAuthenticationProvider(applicationClientId: TestCustomAppId, useWamBroker: null)); + + Assert.Equal(TestCustomAppId, provider.ApplicationClientId); + Assert.False(provider.UseWamBroker); + } + + // applicationClientId + useWamBroker=true -> Options ctor; both are forwarded. + [Fact] + public void CreateAzureProvider_AppClientIdAndUseWamBrokerTrue_UsesOptionsCtor_WamEnabled() + { + var provider = Assert.IsType( + CreateAzureAuthenticationProvider(applicationClientId: TestCustomAppId, useWamBroker: true)); + + Assert.Equal(TestCustomAppId, provider.ApplicationClientId); + Assert.True(provider.UseWamBroker); + } + + // applicationClientId + useWamBroker=false -> Options ctor; the explicit opt-out is honored. + [Fact] + public void CreateAzureProvider_AppClientIdAndUseWamBrokerFalse_UsesOptionsCtor_WamDisabled() + { + var provider = Assert.IsType( + CreateAzureAuthenticationProvider(applicationClientId: TestCustomAppId, useWamBroker: false)); + + Assert.Equal(TestCustomAppId, provider.ApplicationClientId); + Assert.False(provider.UseWamBroker); + } + + // useWamBroker=true with no applicationClientId -> Options ctor; the id falls back to the + // first-party id, which enables WAM broker. + [Fact] + public void CreateAzureProvider_UseWamBrokerOnly_UsesOptionsCtor_WamEnabled() + { + var provider = Assert.IsType( + CreateAzureAuthenticationProvider(applicationClientId: null, useWamBroker: true)); + + Assert.Equal(SqlClientApplicationId, provider.ApplicationClientId); + Assert.True(provider.UseWamBroker); + } + + // Invokes the MDS-internal AuthenticationBootstrapper.CreateAzureAuthenticationProvider via + // reflection, passing the REAL Azure provider and options types. Unwraps the reflection wrapper + // so a test observes the real exception type, if any. + // + // NOTE: This reflection is only needed because this project does not have InternalsVisibleTo + // from Microsoft.Data.SqlClient. This call has no global side effects -- it just returns a new + // provider instance. + // + // TODO(https://sqlclientdrivers.visualstudio.com/ADO.Net/_workitems/edit/41888): + // Once PR #4385 completes (signing Azure/Azure.Test for internal Package-mode CI builds), grant + // this project InternalsVisibleTo from Microsoft.Data.SqlClient and replace this reflection + // with a direct call to AuthenticationBootstrapper.CreateAzureAuthenticationProvider. + private static SqlAuthenticationProvider? CreateAzureAuthenticationProvider( + string? applicationClientId, + bool? useWamBroker) + { + Type? bootstrapper = Type.GetType( + "Microsoft.Data.SqlClient.AuthenticationBootstrapper, Microsoft.Data.SqlClient"); + Assert.NotNull(bootstrapper); + + MethodInfo? method = bootstrapper!.GetMethod( + "CreateAzureAuthenticationProvider", + BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + Assert.NotNull(method); + + try + { + return (SqlAuthenticationProvider?)method!.Invoke( + null, + [ + typeof(ActiveDirectoryAuthenticationProvider), + typeof(ActiveDirectoryAuthenticationProviderOptions), + applicationClientId, + useWamBroker, + ]); + } + catch (TargetInvocationException ex) when (ex.InnerException is not null) + { + throw ex.InnerException; + } + } +} diff --git a/src/Microsoft.Data.SqlClient.Extensions/Azure/test/Azure.Test.csproj b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/Azure.Test.csproj index 79372ca2d8..9b294a4f98 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Azure/test/Azure.Test.csproj +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/Azure.Test.csproj @@ -13,6 +13,13 @@ net462;net8.0;net9.0;net10.0 + + + + true + $(TestSigningKeyPath) + + enable diff --git a/src/Microsoft.Data.SqlClient.Extensions/Azure/test/DefaultAuthProviderTests.cs b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/DefaultAuthProviderTests.cs deleted file mode 100644 index fa426141c9..0000000000 --- a/src/Microsoft.Data.SqlClient.Extensions/Azure/test/DefaultAuthProviderTests.cs +++ /dev/null @@ -1,67 +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. - -namespace Microsoft.Data.SqlClient.Extensions.Azure.Test; - -[Collection("SqlAuthenticationProvider")] -public class DefaultAuthProviderTests -{ - // Verify that our auth provider has been installed for all AAD/Entra - // 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. - // - // TODO(https://sqlclientdrivers.visualstudio.com/ADO.Net/_workitems/edit/41888): - // Move this test to a more appropriate location once we have one. - // - [Fact] - public void AuthProviderInstalled() - { - // Iterate over all authentication methods rather than specifying them - // via Theory data so that we detect any new methods that don't meet - // our expectations. - #if NET - var methods = Enum.GetValues(); - #else - var methods = Enum.GetValues(typeof(SqlAuthenticationMethod)).Cast(); - #endif - - foreach (var method in methods) - { - SqlAuthenticationProvider? provider = - SqlAuthenticationProvider.GetProvider(method); - - switch (method) - { - #pragma warning disable 0618 // Type or member is obsolete - case SqlAuthenticationMethod.ActiveDirectoryPassword: - #pragma warning restore 0618 // Type or member is obsolete - case SqlAuthenticationMethod.ActiveDirectoryIntegrated: - case SqlAuthenticationMethod.ActiveDirectoryInteractive: - case SqlAuthenticationMethod.ActiveDirectoryServicePrincipal: - case SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow: - case SqlAuthenticationMethod.ActiveDirectoryManagedIdentity: - case SqlAuthenticationMethod.ActiveDirectoryMSI: - case SqlAuthenticationMethod.ActiveDirectoryDefault: - case SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity: - Assert.NotNull(provider); - Assert.IsType(provider); - break; - - default: - // There is either no provider installed, or it is not ours. - if (provider is not null) - { - Assert.IsNotType(provider); - } - break; - } - } - } -} diff --git a/src/Microsoft.Data.SqlClient.Extensions/Azure/test/SqlAuthenticationProviderCollection.cs b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/SqlAuthenticationProviderGlobalCollection.cs similarity index 79% rename from src/Microsoft.Data.SqlClient.Extensions/Azure/test/SqlAuthenticationProviderCollection.cs rename to src/Microsoft.Data.SqlClient.Extensions/Azure/test/SqlAuthenticationProviderGlobalCollection.cs index 2bf23f7873..892239d990 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Azure/test/SqlAuthenticationProviderCollection.cs +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/SqlAuthenticationProviderGlobalCollection.cs @@ -8,7 +8,7 @@ namespace Microsoft.Data.SqlClient.Extensions.Azure.Test; /// Defines a test collection that serializes execution of test classes /// which mutate the global registry. /// -[CollectionDefinition("SqlAuthenticationProvider")] -public class SqlAuthenticationProviderCollection +[CollectionDefinition("SqlAuthenticationProviderGlobal")] +public class SqlAuthenticationProviderGlobalCollection { } diff --git a/src/Microsoft.Data.SqlClient.Extensions/Azure/test/WamBrokerTests.cs b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/WamBrokerTests.cs index 9b2aa5dcf7..ad9a625d1b 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Azure/test/WamBrokerTests.cs +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/WamBrokerTests.cs @@ -6,7 +6,6 @@ namespace Microsoft.Data.SqlClient.Extensions.Azure.Test; -[Collection("SqlAuthenticationProvider")] public class WamBrokerTests { // The SqlClient first-party application client id that is hard-coded in the provider. @@ -88,7 +87,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 AuthenticationBootstrapper relies on when reflecting onto the /// Options ctor and only forwarding the properties that were explicitly configured. /// [Fact] @@ -272,45 +271,4 @@ public void Ctor_Options_Null_ThrowsArgumentNullException() Assert.Throws( () => new ActiveDirectoryAuthenticationProvider((ActiveDirectoryAuthenticationProviderOptions)null!)); } - - /// - /// Registering an instance via must not - /// wrap or replace the instance, so its WAM broker setting survives registration. - /// - /// - /// Provider registration mutates global state shared across this test class collection - /// (and any other test that depends on the default provider being installed). Save and - /// restore the original provider in a finally block to keep cross-test isolation. - /// - [Fact] - public void Ctor_RegisteredAsProvider_PreservesUseWamBrokerSetting() - { - var provider = new ActiveDirectoryAuthenticationProvider( - new ActiveDirectoryAuthenticationProviderOptions - { - ApplicationClientId = TestCustomAppId, - UseWamBroker = true, - }); - - SqlAuthenticationProvider? original = - SqlAuthenticationProvider.GetProvider(SqlAuthenticationMethod.ActiveDirectoryInteractive); - try - { - SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryInteractive, provider); - - var retrieved = SqlAuthenticationProvider.GetProvider(SqlAuthenticationMethod.ActiveDirectoryInteractive) - as ActiveDirectoryAuthenticationProvider; - Assert.NotNull(retrieved); - Assert.Same(provider, retrieved); - Assert.Equal(TestCustomAppId, retrieved!.ApplicationClientId); - Assert.True(retrieved.UseWamBroker); - } - finally - { - if (original is not null) - { - SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryInteractive, original); - } - } - } } diff --git a/src/Microsoft.Data.SqlClient/ref/Microsoft.Data.SqlClient.cs b/src/Microsoft.Data.SqlClient/ref/Microsoft.Data.SqlClient.cs index 266d0b9a1d..3b9a7a6989 100644 --- a/src/Microsoft.Data.SqlClient/ref/Microsoft.Data.SqlClient.cs +++ b/src/Microsoft.Data.SqlClient/ref/Microsoft.Data.SqlClient.cs @@ -2,6 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +// NOTE: This ref assembly intentionally does not use #nullable annotations. +// The implementation source uses nullable (e.g. string?, SqlAuthenticationProvider?) +// but the ref/notsupported projects omit them for consistency with the existing +// codebase convention and to avoid GenAPI nullable attribute complications. + namespace Microsoft.Data.SqlClient; /// diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AuthenticationBootstrapper.cs similarity index 60% rename from src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs rename to src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AuthenticationBootstrapper.cs index 082a5599ce..ad8fdf7475 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AuthenticationBootstrapper.cs @@ -3,8 +3,6 @@ // 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; @@ -14,50 +12,219 @@ namespace Microsoft.Data.SqlClient { - internal sealed class SqlAuthenticationProviderManager + /// + /// Seeds an with the authentication providers + /// discovered from application configuration and from the optional Azure extension assembly. + /// + /// + /// + /// Production code uses a lazily-created singleton that seeds the shared + /// . Because the singleton is only + /// created when a federated/Active Directory connection first authenticates, the (reflection + /// based) config and Azure-extension discovery is deferred until it is actually needed. + /// + /// + /// Call to force that one-time initialization. It accesses the lazy + /// singleton, whose factory runs exactly once in a thread-safe manner. Constructing a + /// bootstrapper directly does not run that factory, so it has no effect on the shared registry. + /// + /// + /// Tests can instead construct an isolated bootstrapper (which seeds a fresh registry) to + /// inspect discovered providers without mutating global state. + /// + /// + internal sealed class AuthenticationBootstrapper { - [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() + // The production singleton. Its factory seeds the shared AuthenticationProviderRegistry. + // Instance, and runs exactly once, only when Value is first accessed (via Bootstrap()). + // Constructing a bootstrapper directly does not touch this field, so it has no effect on + // the shared registry - keeping isolated-registry callers (e.g. tests) free of global state. + private static readonly Lazy s_instance = + new(static () => new AuthenticationBootstrapper(AuthenticationProviderRegistry.Instance)); + + // Our logging instance. + private readonly SqlClientLogger _sqlAuthLogger = new(); + + /// + /// Gets the registry this bootstrapper seeds. Production uses the shared singleton registry; + /// tests can inject an isolated registry to avoid mutating global state. + /// + internal AuthenticationProviderRegistry Registry { get; } + + /// + /// Gets the application client ID read from the app.config configuration section, + /// or if none was configured. + /// + internal string? ApplicationClientId { get; private set; } + + /// + /// Gets the optional override for ActiveDirectoryAuthenticationProviderOptions.UseWamBroker + /// read from the app.config <SqlClientAuthenticationProviders useWamBroker="..."/> + /// attribute. 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. + /// + internal bool? UseWamBroker { get; private set; } + + /// + /// Creates a bootstrapper that seeds the supplied registry, running config-driven and + /// Azure extension provider discovery. + /// + internal AuthenticationBootstrapper(AuthenticationProviderRegistry registry) { - SqlAuthenticationProviderConfigurationSection? configurationSection = null; + Registry = registry; + + LoadConfiguration(); + LoadAzureExtensionProvider(); + } + + /// + /// Forces the one-time initialization that seeds the shared authentication provider + /// registry. Accessing the lazy singleton's value runs its factory exactly once, in a + /// thread-safe manner; subsequent calls are a cheap no-op. + /// + internal static void Bootstrap() + { + _ = s_instance.Value; + } + + /// + /// Reads the app.config configuration section and registers config-driven initializers and + /// authentication providers. Uses reflection (Type.GetType / Activator.CreateInstance). + /// + private void LoadConfiguration() + { + SqlClientEventSource.Log.TryTraceEvent("AuthenticationBootstrapper | Loading authentication provider configuration from app.config."); + + SqlAuthenticationProviderConfigurationSection? configSection = null; try { // New configuration section "SqlClientAuthenticationProviders" for Microsoft.Data.SqlClient accepted to avoid conflicts with older one. - configurationSection = FetchConfigurationSection(SqlClientAuthenticationProviderConfigurationSection.Name); - if (configurationSection == null) + configSection = FetchConfigurationSection(SqlClientAuthenticationProviderConfigurationSection.Name); + if (configSection == null) { // If configuration section is not yet found, try with old Configuration Section name for backwards compatibility - configurationSection = FetchConfigurationSection(SqlAuthenticationProviderConfigurationSection.Name); + configSection = 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); + SqlClientEventSource.Log.TryTraceEvent("static AuthenticationBootstrapper: Unable to load custom SqlAuthenticationProviders or SqlClientAuthenticationProviders. ConfigurationManager failed to load due to configuration errors: {0}", e); + } + + var methodName = "Ctor"; + + if (configSection == null) + { + _sqlAuthLogger.LogInfo(nameof(AuthenticationBootstrapper), methodName, "Neither SqlClientAuthenticationProviders nor SqlAuthenticationProviders configuration section found."); + return; + } + + if (!string.IsNullOrEmpty(configSection.ApplicationClientId)) + { + ApplicationClientId = configSection.ApplicationClientId; + _sqlAuthLogger.LogInfo(nameof(AuthenticationBootstrapper), methodName, "Received user-defined Application Client Id"); + } + else + { + _sqlAuthLogger.LogInfo(nameof(AuthenticationBootstrapper), 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(AuthenticationBootstrapper), methodName, $"Received user-defined UseWamBroker={useWamBroker}."); + } + else + { + _sqlAuthLogger.LogError(nameof(AuthenticationBootstrapper), methodName, $"Ignoring user-defined UseWamBroker='{configSection.UseWamBroker}': not a valid boolean."); + } + } + else + { + _sqlAuthLogger.LogInfo(nameof(AuthenticationBootstrapper), 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(AuthenticationBootstrapper), methodName, "Created user-defined SqlAuthenticationInitializer."); + } + else + { + _sqlAuthLogger.LogInfo(nameof(AuthenticationBootstrapper), methodName, "No user-defined SqlAuthenticationInitializer found."); } - Instance = new SqlAuthenticationProviderManager(configurationSection); + // 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); + } + + // Register as a permanent (application-specified) provider so it cannot be + // overridden by the Azure extension default or by a later SetProvider call. + Registry.SetPermanentProvider(authentication, provider); + _sqlAuthLogger.LogInfo(nameof(AuthenticationBootstrapper), methodName, string.Format("Added user-defined auth provider: {0} for authentication {1}.", providerSettings?.Type, authentication)); + } + } + else + { + _sqlAuthLogger.LogInfo(nameof(AuthenticationBootstrapper), methodName, "No user-defined auth providers."); + } + } + + /// + /// Attempts to load the Azure extension authentication provider via + /// reflection. This method uses Assembly.Load and Activator.CreateInstance. + /// + private void LoadAzureExtensionProvider() + { + // The name of our Azure extension assembly. + const string azureAssemblyName = "Microsoft.Data.SqlClient.Extensions.Azure"; - // If our Azure extensions package is present, use its authentication provider as our - // default. try { // Try to load our Azure extension. @@ -66,15 +233,18 @@ static SqlAuthenticationProviderManager() // When strong-name signing is enabled, build a fully-qualified AssemblyName // that includes the expected public key token. + // The public key token of our Azure extension assembly, used to avoid loading + // imposter assemblies. + byte[] azurePublicKeyToken = [ 0x23, 0xec, 0x7f, 0xc2, 0xd6, 0xea, 0xa4, 0xa5 ]; + SqlClientEventSource.Log.TryTraceEvent( - nameof(SqlAuthenticationProviderManager) + - ": Attempting to load Azure extension assembly={0} with " + - "expected public key token={1}", - azureAssemblyName, - BitConverter.ToString(s_azurePublicKeyToken).Replace("-", "")); + nameof(AuthenticationBootstrapper) + + $": Attempting to load Azure extension assembly={azureAssemblyName} with " + + "expected public key token=" + + BitConverter.ToString(azurePublicKeyToken).Replace("-", "")); var qualifiedName = new AssemblyName(azureAssemblyName); - qualifiedName.SetPublicKeyToken(s_azurePublicKeyToken); + qualifiedName.SetPublicKeyToken(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 @@ -93,14 +263,13 @@ static SqlAuthenticationProviderManager() { byte[]? actualToken = assembly.GetName().GetPublicKeyToken(); - if (actualToken is null || !actualToken.AsSpan().SequenceEqual(s_azurePublicKeyToken)) + if (actualToken is null || !actualToken.AsSpan().SequenceEqual(azurePublicKeyToken)) { SqlClientEventSource.Log.TryTraceEvent( - nameof(SqlAuthenticationProviderManager) + - ": Azure extension assembly={0} has an " + + nameof(AuthenticationBootstrapper) + + $": Azure extension assembly={assembly.GetName()} has an " + "unexpected public key token; " + - "no default Active Directory provider installed", - assembly.GetName()); + "no default Active Directory provider installed"); return; } } @@ -109,10 +278,9 @@ static SqlAuthenticationProviderManager() #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); + nameof(AuthenticationBootstrapper) + + $": Attempting to load Azure extension assembly={azureAssemblyName} without " + + "strong name verification; ensure this assembly is from a trusted source"); var assembly = Assembly.Load(azureAssemblyName); @@ -121,19 +289,17 @@ static SqlAuthenticationProviderManager() if (assembly is null) { SqlClientEventSource.Log.TryTraceEvent( - nameof(SqlAuthenticationProviderManager) + - ": Azure extension assembly={0} not found; " + - "no default Active Directory provider installed", - azureAssemblyName); + nameof(AuthenticationBootstrapper) + + $": Azure extension assembly={azureAssemblyName} not found; " + + "no default Active Directory provider installed"); return; } SqlClientEventSource.Log.TryTraceEvent( - nameof(SqlAuthenticationProviderManager) + - ": Azure extension assembly={0} found; " + + nameof(AuthenticationBootstrapper) + + $": Azure extension assembly={assembly.GetName()} found; " + "attempting to set as default provider for all Active " + - "Directory authentication methods", - assembly.GetName()); + "Directory authentication methods"); // Look for the authentication provider class. const string className = "Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProvider"; @@ -142,10 +308,9 @@ static SqlAuthenticationProviderManager() if (type is null) { SqlClientEventSource.Log.TryTraceEvent( - nameof(SqlAuthenticationProviderManager) + - ": Azure extension does not contain class={0}; " + - "no default Active Directory provider installed", - className); + nameof(AuthenticationBootstrapper) + + $": Azure extension does not contain class={className}; " + + "no default Active Directory provider installed"); return; } @@ -170,16 +335,15 @@ static SqlAuthenticationProviderManager() SqlAuthenticationProvider? instance = CreateAzureAuthenticationProvider( type, optionsType, - Instance._applicationClientId, - Instance._useWamBroker); + ApplicationClientId, + UseWamBroker); if (instance is null) { SqlClientEventSource.Log.TryTraceEvent( - nameof(SqlAuthenticationProviderManager) + - ": Failed to instantiate Azure extension class={0}; " + - "no default Active Directory provider installed", - className); + nameof(AuthenticationBootstrapper) + + $": Failed to instantiate Azure extension class={className}; " + + "no default Active Directory provider installed"); return; } @@ -190,23 +354,22 @@ static SqlAuthenticationProviderManager() // 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); + Registry.SetProvider(SqlAuthenticationMethod.ActiveDirectoryIntegrated, instance); #pragma warning disable 0618 // Type or member is obsolete - SetProvider(SqlAuthenticationMethod.ActiveDirectoryPassword, instance); + Registry.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); + Registry.SetProvider(SqlAuthenticationMethod.ActiveDirectoryInteractive, instance); + Registry.SetProvider(SqlAuthenticationMethod.ActiveDirectoryServicePrincipal, instance); + Registry.SetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, instance); + Registry.SetProvider(SqlAuthenticationMethod.ActiveDirectoryManagedIdentity, instance); + Registry.SetProvider(SqlAuthenticationMethod.ActiveDirectoryMSI, instance); + Registry.SetProvider(SqlAuthenticationMethod.ActiveDirectoryDefault, instance); + Registry.SetProvider(SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity, instance); SqlClientEventSource.Log.TryTraceEvent( - nameof(SqlAuthenticationProviderManager) + - ": Azure extension class={0} installed as " + - "provider for all Active Directory authentication methods", - className); + nameof(AuthenticationBootstrapper) + + $": Azure extension class={className} installed as " + + "provider for all Active Directory authentication methods"); } // All of these exceptions mean we couldn't find or instantiate the // Azure extension's authentication provider, in which case we @@ -228,147 +391,14 @@ TypeInitializationException or TypeLoadException) { SqlClientEventSource.Log.TryTraceEvent( - nameof(SqlAuthenticationProviderManager) + - ": Azure extension assembly={0} not found or " + + nameof(AuthenticationBootstrapper) + + $": Azure extension assembly={azureAssemblyName} not found or " + "not usable; no default provider installed; " + - "{1}: {2}", - azureAssemblyName, - ex.GetType().Name, - ex.Message); + $"{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. @@ -443,55 +473,10 @@ private SqlAuthenticationProviderManager(SqlAuthenticationProviderConfigurationS 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; - } - /// /// 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); @@ -516,39 +501,30 @@ private static SqlAuthenticationMethod AuthenticationEnumFromString(string authe { switch (authentication.ToLowerInvariant()) { - case ActiveDirectoryIntegrated: + case "active directory integrated": return SqlAuthenticationMethod.ActiveDirectoryIntegrated; #pragma warning disable 0618 // Type or member is obsolete - case ActiveDirectoryPassword: + case "active directory password": return SqlAuthenticationMethod.ActiveDirectoryPassword; #pragma warning restore 0618 // Type or member is obsolete - case ActiveDirectoryInteractive: + case "active directory interactive": return SqlAuthenticationMethod.ActiveDirectoryInteractive; - case ActiveDirectoryServicePrincipal: + case "active directory service principal": return SqlAuthenticationMethod.ActiveDirectoryServicePrincipal; - case ActiveDirectoryDeviceCodeFlow: + case "active directory device code flow": return SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow; - case ActiveDirectoryManagedIdentity: + case "active directory managed identity": return SqlAuthenticationMethod.ActiveDirectoryManagedIdentity; - case ActiveDirectoryMSI: + case "active directory msi": return SqlAuthenticationMethod.ActiveDirectoryMSI; - case ActiveDirectoryDefault: + case "active directory default": return SqlAuthenticationMethod.ActiveDirectoryDefault; - case ActiveDirectoryWorkloadIdentity: + case "active directory workload identity": 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"; - } } /// 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 ec210e407a..e20af8f170 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 @@ -2786,7 +2786,12 @@ private SqlFedAuthToken GetFedAuthToken(SqlFedAuthInfo fedAuthInfo) // Username to use in error messages. string? username = null; - SqlAuthenticationProvider? authProvider = SqlAuthenticationProviderManager.GetProvider(ConnectionOptions.Authentication); + // Ensure config-driven and Azure extension providers have been discovered and + // registered before we look one up. This is a one-time, lazy initialization that + // only runs the first time a federated/Active Directory connection authenticates. + AuthenticationBootstrapper.Bootstrap(); + + SqlAuthenticationProvider? authProvider = SqlAuthenticationProvider.GetProvider(ConnectionOptions.Authentication); if (authProvider == null && _accessTokenCallback == null) { throw SQL.CannotFindAuthProvider(ConnectionOptions.Authentication); diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AADAuthenticationTests.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AADAuthenticationTests.cs index dc6d5b3d7a..13e41e2d90 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AADAuthenticationTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AADAuthenticationTests.cs @@ -4,8 +4,6 @@ using System; using System.Security; -using System.Threading.Tasks; -using Microsoft.Data.SqlClient.FunctionalTests.DataCommon; using Xunit; namespace Microsoft.Data.SqlClient.Tests @@ -49,29 +47,5 @@ private void InvalidCombinationCheck(SqlCredential credential) Assert.Throws(() => connection.AccessToken = "SampleAccessToken"); } } - - /// - /// 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. - /// - /// 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. - /// - [ConditionalFact(typeof(TestUtility), nameof(TestUtility.IsNetFramework))] - public async Task IsDummySqlAuthenticationProviderSetByDefault() - { - var provider = SqlAuthenticationProvider.GetProvider(SqlAuthenticationMethod.ActiveDirectoryInteractive); - - Assert.NotNull(provider); - Assert.IsType(provider); - - var token = await provider.AcquireTokenAsync(null); - Assert.Equal(token.AccessToken, DummySqlAuthenticationProvider.DUMMY_TOKEN_STR); - } } } diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/DataCommon/DummySqlAuthenticationProvider.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/DataCommon/DummySqlAuthenticationProvider.cs deleted file mode 100644 index c68baf63eb..0000000000 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/DataCommon/DummySqlAuthenticationProvider.cs +++ /dev/null @@ -1,28 +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.Threading.Tasks; - -namespace Microsoft.Data.SqlClient.FunctionalTests.DataCommon -{ - /// - /// Dummy class to override default Sql Authentication provider in functional tests. - /// This type returns a dummy access token and is only used for registration test from app.config file. - /// Since no actual connections are intended to be made in Functional tests, - /// this type is added by default to validate config file registration scenario. - /// - public class DummySqlAuthenticationProvider : SqlAuthenticationProvider - { - public static string DUMMY_TOKEN_STR = "dummy_access_token"; - - public override Task AcquireTokenAsync(SqlAuthenticationParameters parameters) - => Task.FromResult(new SqlAuthenticationToken(DUMMY_TOKEN_STR, new DateTimeOffset(DateTime.Now.AddHours(2)))); - - public override bool IsSupported(SqlAuthenticationMethod authenticationMethod) - { - return authenticationMethod == SqlAuthenticationMethod.ActiveDirectoryInteractive; - } - } -} diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.FunctionalTests.csproj b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.FunctionalTests.csproj index e22d6430d9..eb107e6ec7 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.FunctionalTests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.FunctionalTests.csproj @@ -26,11 +26,6 @@ - - - Always - - PreserveNewest xunit.runner.json diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlAuthenticationProviderManagerTests.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlAuthenticationProviderManagerTests.cs deleted file mode 100644 index 0d276624c9..0000000000 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlAuthenticationProviderManagerTests.cs +++ /dev/null @@ -1,75 +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.Threading.Tasks; -using Microsoft.Data.SqlClient.FunctionalTests.DataCommon; -using Xunit; - -namespace Microsoft.Data.SqlClient.Tests -{ - public class SqlAuthenticationProviderManagerTests - { - // The FunctionalTests project employs a .NET Framework app.config file - // that configures a dummy authentication provider for - // ActiveDirectoryInteractive authentication. Verify that this is - // respected. - [ConditionalFact(typeof(TestUtility), nameof(TestUtility.IsNetFramework))] - public void DefaultAuthenticationProviders_AppConfig() - { - // The provider for ActiveDirectoryInteractive should be our dummy - // provider. - Assert.IsType( - SqlAuthenticationProvider.GetProvider( - SqlAuthenticationMethod.ActiveDirectoryInteractive)); - - // There should be no provider for other methods. Spot-check a few. - Assert.Null(SqlAuthenticationProvider.GetProvider( - #pragma warning disable CS0618 // Type or member is obsolete - SqlAuthenticationMethod.ActiveDirectoryPassword)); - #pragma warning restore CS0618 // Type or member is obsolete - - Assert.Null(SqlAuthenticationProvider.GetProvider( - SqlAuthenticationMethod.ActiveDirectoryManagedIdentity)); - } - - // Verify that the dummy provider installed via app.config cannot be replaced. - [ConditionalFact(typeof(TestUtility), nameof(TestUtility.IsNetFramework))] - public void DefaultAuthenticationProviders_NoReplace() - { - // The provider for ActiveDirectoryInteractive should be our dummy - // provider. - Assert.IsType( - SqlAuthenticationProvider.GetProvider( - SqlAuthenticationMethod.ActiveDirectoryInteractive)); - - // Try to add another provider for ActiveDirectoryInteractive. - bool setResult = SqlAuthenticationProvider.SetProvider( - SqlAuthenticationMethod.ActiveDirectoryInteractive, - new TestProvider()); - - // The set should have failed. - Assert.False(setResult); - - // The dummy provider is still installed. - Assert.IsType( - SqlAuthenticationProvider.GetProvider( - SqlAuthenticationMethod.ActiveDirectoryInteractive)); - } - - private class TestProvider : SqlAuthenticationProvider - { - public override async Task AcquireTokenAsync( - SqlAuthenticationParameters parameters) - { - throw new NotImplementedException(); - } - - public override bool IsSupported(SqlAuthenticationMethod authenticationMethod) - { - return authenticationMethod == SqlAuthenticationMethod.ActiveDirectoryInteractive; - } - } - } -} diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/app.config b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/app.config deleted file mode 100644 index 9fc08c65a7..0000000000 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/app.config +++ /dev/null @@ -1,14 +0,0 @@ - - - - -
- - - - - - - - - diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft.Data.SqlClient.UnitTests.csproj b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft.Data.SqlClient.UnitTests.csproj index 4111f701f4..b67112447c 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft.Data.SqlClient.UnitTests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft.Data.SqlClient.UnitTests.csproj @@ -24,6 +24,9 @@ True Resources.resx + + Always + PreserveNewest xunit.runner.json diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/AuthenticationBootstrapperTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/AuthenticationBootstrapperTests.cs new file mode 100644 index 0000000000..d4119e51be --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/AuthenticationBootstrapperTests.cs @@ -0,0 +1,350 @@ +// 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.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Microsoft.DotNet.XUnitExtensions; +using Xunit; + +namespace Microsoft.Data.SqlClient.UnitTests; + +/// +/// Tests for , the core-side +/// component that discovers config-driven and Azure extension authentication +/// providers and seeds them into the Abstractions registry. +/// +public class AuthenticationBootstrapperTests +{ + // The Azure extension assembly is intentionally NOT referenced by this project (nor by the + // core driver), so these tests exercise the bootstrapper's Azure-ABSENT behavior: stub-based + // constructor selection (CreateAzureAuthenticationProvider_*) and config-driven providers. + // The Azure-PRESENT behavior is covered by + // Microsoft.Data.SqlClient.Extensions.Azure.Test.AuthenticationBootstrapperTests, the only + // test project that references (and therefore guarantees the presence of) the Azure extension. + public AuthenticationBootstrapperTests() + { + // Precondition: confirm the Azure extension is not present in this test context, so the + // Azure-absent assumptions in these tests hold. + Assert.Throws( + () => Assembly.Load("Microsoft.Data.SqlClient.Extensions.Azure")); + } + + // CreateAzureAuthenticationProvider tests ---------------------------------------------- + // + // Each Stub* container mimics one shape the real Azure extension might expose: + // * StubModern - both a (string) ctor and an (Options) ctor. + // * StubLegacy - only the (string) ctor; no Options type at all. + // * StubMinimal - only a parameterless ctor. + // + // The helper takes a Type directly, so these stubs do not need any particular full name. + + public class StubProviderBase : SqlAuthenticationProvider + { + public string? CapturedApplicationClientId; + public bool? CapturedUseWamBroker; + public bool ParameterlessCtorUsed; + public bool StringCtorUsed; + public bool OptionsCtorUsed; + + public override Task AcquireTokenAsync(SqlAuthenticationParameters parameters) + => Task.FromResult(new SqlAuthenticationToken("stub", DateTimeOffset.UtcNow.AddMinutes(5))); + + public override bool IsSupported(SqlAuthenticationMethod authenticationMethod) => true; + } + + public static class StubModern + { + public sealed class ActiveDirectoryAuthenticationProviderOptions + { + public string? ApplicationClientId { get; set; } + public bool UseWamBroker { get; set; } + } + + public sealed class ActiveDirectoryAuthenticationProvider : StubProviderBase + { + public ActiveDirectoryAuthenticationProvider() { ParameterlessCtorUsed = true; } + + public ActiveDirectoryAuthenticationProvider(string applicationClientId) + { + StringCtorUsed = true; + CapturedApplicationClientId = applicationClientId; + } + + public ActiveDirectoryAuthenticationProvider(ActiveDirectoryAuthenticationProviderOptions options) + { + OptionsCtorUsed = true; + CapturedApplicationClientId = options.ApplicationClientId; + CapturedUseWamBroker = options.UseWamBroker; + } + } + } + + public static class StubLegacy + { + // No Options type defined -- mimics older Azure extension versions. + public sealed class ActiveDirectoryAuthenticationProvider : StubProviderBase + { + public ActiveDirectoryAuthenticationProvider() { ParameterlessCtorUsed = true; } + + public ActiveDirectoryAuthenticationProvider(string applicationClientId) + { + StringCtorUsed = true; + CapturedApplicationClientId = applicationClientId; + } + } + } + + public static class StubMinimal + { + // Parameterless only -- mimics a hypothetical extension with no 1-arg ctors at all. + public sealed class ActiveDirectoryAuthenticationProvider : StubProviderBase + { + public ActiveDirectoryAuthenticationProvider() { ParameterlessCtorUsed = true; } + } + } + + [Fact] + public void CreateAzureAuthenticationProvider_NeitherConfigured_UsesParameterlessCtor() + { + var instance = AuthenticationBootstrapper.CreateAzureAuthenticationProvider( + typeof(StubModern.ActiveDirectoryAuthenticationProvider), + typeof(StubModern.ActiveDirectoryAuthenticationProviderOptions), + applicationClientId: null, + useWamBroker: null); + + var stub = Assert.IsType(instance); + Assert.True(stub.ParameterlessCtorUsed); + Assert.False(stub.StringCtorUsed); + Assert.False(stub.OptionsCtorUsed); + Assert.Null(stub.CapturedApplicationClientId); + Assert.Null(stub.CapturedUseWamBroker); + } + + [Fact] + public void CreateAzureAuthenticationProvider_AppIdOnly_OptionsAvailable_UsesOptionsCtor() + { + var instance = AuthenticationBootstrapper.CreateAzureAuthenticationProvider( + typeof(StubModern.ActiveDirectoryAuthenticationProvider), + typeof(StubModern.ActiveDirectoryAuthenticationProviderOptions), + applicationClientId: "app-123", + useWamBroker: null); + + var stub = Assert.IsType(instance); + Assert.True(stub.OptionsCtorUsed); + Assert.False(stub.StringCtorUsed); + Assert.Equal("app-123", stub.CapturedApplicationClientId); + Assert.Equal(false, stub.CapturedUseWamBroker); + } + + [Fact] + public void CreateAzureAuthenticationProvider_AppIdOnly_OptionsMissing_FallsBackToStringCtor() + { + var instance = AuthenticationBootstrapper.CreateAzureAuthenticationProvider( + typeof(StubLegacy.ActiveDirectoryAuthenticationProvider), + optionsType: null, + applicationClientId: "legacy-456", + useWamBroker: null); + + var stub = Assert.IsType(instance); + Assert.True(stub.StringCtorUsed); + Assert.False(stub.OptionsCtorUsed); + Assert.False(stub.ParameterlessCtorUsed); + Assert.Equal("legacy-456", stub.CapturedApplicationClientId); + Assert.Null(stub.CapturedUseWamBroker); + } + + [Fact] + public void CreateAzureAuthenticationProvider_AppIdOnly_NoCompatibleCtor_ReturnsNull() + { + var instance = AuthenticationBootstrapper.CreateAzureAuthenticationProvider( + typeof(StubMinimal.ActiveDirectoryAuthenticationProvider), + optionsType: null, + applicationClientId: "no-ctor", + useWamBroker: null); + + Assert.Null(instance); + } + + [Fact] + public void CreateAzureAuthenticationProvider_UseWamBroker_OptionsMissing_Throws() + { + InvalidOperationException ex = Assert.Throws(() => + AuthenticationBootstrapper.CreateAzureAuthenticationProvider( + typeof(StubLegacy.ActiveDirectoryAuthenticationProvider), + optionsType: null, + applicationClientId: null, + useWamBroker: true)); + + Assert.Contains("ActiveDirectoryAuthenticationProviderOptions", ex.Message); + Assert.Contains("Microsoft.Data.SqlClient.Extensions.Azure", ex.Message); + } + + [Fact] + public void CreateAzureAuthenticationProvider_UseWamBroker_OptionsAvailable_UsesOptionsCtor() + { + var instance = AuthenticationBootstrapper.CreateAzureAuthenticationProvider( + typeof(StubModern.ActiveDirectoryAuthenticationProvider), + typeof(StubModern.ActiveDirectoryAuthenticationProviderOptions), + applicationClientId: "app-789", + useWamBroker: true); + + var stub = Assert.IsType(instance); + Assert.True(stub.OptionsCtorUsed); + Assert.Equal("app-789", stub.CapturedApplicationClientId); + Assert.Equal(true, stub.CapturedUseWamBroker); + } + + // ApplicationClientId tests ------------------------------------------------------------ + + /// + /// Verifies that ApplicationClientId is accessible and returns null when no app.config + /// section is present (non-Framework targets), and that the bootstrapper exposes the + /// registry it was constructed with. + /// + [ConditionalFact(nameof(IsNotNetFramework))] + public void ApplicationClientId_IsNull_WhenNoConfig() + { + // On non-Framework targets there is no app.config, so ApplicationClientId should be null. + AuthenticationProviderRegistry registry = new(); + AuthenticationBootstrapper bootstrapper = new(registry); + + // The bootstrapper exposes the registry it was given. + Assert.Same(registry, bootstrapper.Registry); + + Assert.Null(bootstrapper.ApplicationClientId); + } + + // The UnitTests project has an app.config that configures a dummy + // authentication provider for ActiveDirectoryInteractive and sets + // applicationClientId. The following tests verify this on .NET Framework. + + /// + /// Verifies that ApplicationClientId is read from the app.config section and that the + /// bootstrapper exposes the registry it was constructed with. + /// + [ConditionalFact(nameof(IsNetFramework))] + public void ApplicationClientId_ReadFromAppConfig() + { + // The app.config sets applicationClientId="f3e3a0a0-1234-5678-9abc-def012345678". + AuthenticationProviderRegistry registry = new(); + AuthenticationBootstrapper bootstrapper = new(registry); + + Assert.Same(registry, bootstrapper.Registry); + Assert.Equal( + "f3e3a0a0-1234-5678-9abc-def012345678", + bootstrapper.ApplicationClientId); + } + + // UseWamBroker tests ------------------------------------------------------------------- + + /// + /// Verifies that UseWamBroker is accessible and returns null when no app.config + /// section is present (non-Framework targets). + /// + [ConditionalFact(nameof(IsNotNetFramework))] + public void UseWamBroker_IsNull_WhenNoConfig() + { + // On non-Framework targets there is no app.config, so the property should be null. + AuthenticationBootstrapper bootstrapper = new(new AuthenticationProviderRegistry()); + Assert.Null(bootstrapper.UseWamBroker); + } + + /// + /// Verifies that UseWamBroker is read from the app.config section. + /// + [ConditionalFact(nameof(IsNetFramework))] + public void UseWamBroker_ReadFromAppConfig() + { + // The app.config sets useWamBroker="true". + AuthenticationBootstrapper bootstrapper = new(new AuthenticationProviderRegistry()); + Assert.True(bootstrapper.UseWamBroker); + } + + /// + /// Verifies that the dummy provider from app.config is registered for + /// ActiveDirectoryInteractive and that no other methods have providers. + /// + [ConditionalFact(nameof(IsNetFramework))] + public void DefaultAuthenticationProviders_AppConfig() + { + AuthenticationProviderRegistry registry = new(); + _ = new AuthenticationBootstrapper(registry); + + foreach (SqlAuthenticationMethod method in Enum.GetValues(typeof(SqlAuthenticationMethod))) + { + var provider = registry.GetProvider(method); + + if (method == SqlAuthenticationMethod.ActiveDirectoryInteractive) + { + Assert.IsType(provider); + } + else + { + Assert.Null(provider); + } + } + } + + /// + /// Verifies that the app.config-registered dummy provider can acquire a token. + /// + [ConditionalFact(nameof(IsNetFramework))] + public async Task DefaultAuthenticationProvider_AcquiresToken() + { + AuthenticationProviderRegistry registry = new(); + _ = new AuthenticationBootstrapper(registry); + + var provider = registry.GetProvider( + SqlAuthenticationMethod.ActiveDirectoryInteractive); + Assert.NotNull(provider); + var token = await provider.AcquireTokenAsync(null!); + Assert.Equal(DummySqlAuthenticationProvider.DummyAccessToken, token.AccessToken); + } + + /// + /// Verifies that the app.config-registered provider cannot be replaced. + /// + [ConditionalFact(nameof(IsNetFramework))] + public void DefaultAuthenticationProviders_NoReplace() + { + AuthenticationProviderRegistry registry = new(); + _ = new AuthenticationBootstrapper(registry); + + Assert.IsType( + registry.GetProvider( + SqlAuthenticationMethod.ActiveDirectoryInteractive)); + + bool setResult = registry.SetProvider( + SqlAuthenticationMethod.ActiveDirectoryInteractive, + new InteractiveProvider()); + + Assert.False(setResult); + + Assert.IsType( + registry.GetProvider( + SqlAuthenticationMethod.ActiveDirectoryInteractive)); + } + + private static bool IsNetFramework => + RuntimeInformation.FrameworkDescription.StartsWith(".NET Framework"); + + private static bool IsNotNetFramework => !IsNetFramework; + + private class InteractiveProvider : SqlAuthenticationProvider + { + public override Task AcquireTokenAsync( + SqlAuthenticationParameters parameters) + { + throw new NotImplementedException(); + } + + public override bool IsSupported(SqlAuthenticationMethod authenticationMethod) + { + return authenticationMethod == SqlAuthenticationMethod.ActiveDirectoryInteractive; + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/DummySqlAuthenticationProvider.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/DummySqlAuthenticationProvider.cs new file mode 100644 index 0000000000..2367a04630 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/DummySqlAuthenticationProvider.cs @@ -0,0 +1,23 @@ +// 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.Threading.Tasks; + +namespace Microsoft.Data.SqlClient.UnitTests; + +/// +/// Dummy authentication provider registered via app.config for .NET Framework +/// unit tests. Returns a dummy token and only supports ActiveDirectoryInteractive. +/// +public class DummySqlAuthenticationProvider : SqlAuthenticationProvider +{ + public const string DummyAccessToken = "dummy_access_token"; + + public override Task AcquireTokenAsync(SqlAuthenticationParameters parameters) + => Task.FromResult(new SqlAuthenticationToken(DummyAccessToken, new DateTimeOffset(DateTime.Now.AddHours(2)))); + + public override bool IsSupported(SqlAuthenticationMethod authenticationMethod) + => authenticationMethod == SqlAuthenticationMethod.ActiveDirectoryInteractive; +} diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlAuthenticationProviderManagerTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlAuthenticationProviderManagerTests.cs deleted file mode 100644 index 08089abe98..0000000000 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlAuthenticationProviderManagerTests.cs +++ /dev/null @@ -1,272 +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.Threading.Tasks; -using Xunit; - -namespace Microsoft.Data.SqlClient.UnitTests; - -public class SqlAuthenticationProviderManagerTests -{ - 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: - // * StubModern - both a (string) ctor and an (Options) ctor. - // * StubLegacy - only the (string) ctor; no Options type at all. - // * StubMinimal - only a parameterless ctor. - // - // The helper takes a Type directly, so these stubs do not need any particular full name. - - public class StubProviderBase : SqlAuthenticationProvider - { - public string? CapturedApplicationClientId; - public bool? CapturedUseWamBroker; - public bool ParameterlessCtorUsed; - public bool StringCtorUsed; - public bool OptionsCtorUsed; - - public override Task AcquireTokenAsync(SqlAuthenticationParameters parameters) - => Task.FromResult(new SqlAuthenticationToken("stub", DateTimeOffset.UtcNow.AddMinutes(5))); - - public override bool IsSupported(SqlAuthenticationMethod authenticationMethod) => true; - } - - public static class StubModern - { - public sealed class ActiveDirectoryAuthenticationProviderOptions - { - public string? ApplicationClientId { get; set; } - public bool UseWamBroker { get; set; } - } - - public sealed class ActiveDirectoryAuthenticationProvider : StubProviderBase - { - public ActiveDirectoryAuthenticationProvider() { ParameterlessCtorUsed = true; } - - public ActiveDirectoryAuthenticationProvider(string applicationClientId) - { - StringCtorUsed = true; - CapturedApplicationClientId = applicationClientId; - } - - public ActiveDirectoryAuthenticationProvider(ActiveDirectoryAuthenticationProviderOptions options) - { - OptionsCtorUsed = true; - CapturedApplicationClientId = options.ApplicationClientId; - CapturedUseWamBroker = options.UseWamBroker; - } - } - } - - public static class StubLegacy - { - // No Options type defined -- mimics older Azure extension versions. - public sealed class ActiveDirectoryAuthenticationProvider : StubProviderBase - { - public ActiveDirectoryAuthenticationProvider() { ParameterlessCtorUsed = true; } - - public ActiveDirectoryAuthenticationProvider(string applicationClientId) - { - StringCtorUsed = true; - CapturedApplicationClientId = applicationClientId; - } - } - } - - public static class StubMinimal - { - // Parameterless only -- mimics a hypothetical extension with no 1-arg ctors at all. - public sealed class ActiveDirectoryAuthenticationProvider : StubProviderBase - { - public ActiveDirectoryAuthenticationProvider() { ParameterlessCtorUsed = true; } - } - } - - [Fact] - public void CreateAzureAuthenticationProvider_NeitherConfigured_UsesParameterlessCtor() - { - var instance = SqlAuthenticationProviderManager.CreateAzureAuthenticationProvider( - typeof(StubModern.ActiveDirectoryAuthenticationProvider), - typeof(StubModern.ActiveDirectoryAuthenticationProviderOptions), - applicationClientId: null, - useWamBroker: null); - - var stub = Assert.IsType(instance); - Assert.True(stub.ParameterlessCtorUsed); - Assert.False(stub.StringCtorUsed); - Assert.False(stub.OptionsCtorUsed); - Assert.Null(stub.CapturedApplicationClientId); - Assert.Null(stub.CapturedUseWamBroker); - } - - [Fact] - public void CreateAzureAuthenticationProvider_AppIdOnly_OptionsAvailable_UsesOptionsCtor() - { - var instance = SqlAuthenticationProviderManager.CreateAzureAuthenticationProvider( - typeof(StubModern.ActiveDirectoryAuthenticationProvider), - typeof(StubModern.ActiveDirectoryAuthenticationProviderOptions), - applicationClientId: "app-123", - useWamBroker: null); - - var stub = Assert.IsType(instance); - Assert.True(stub.OptionsCtorUsed); - Assert.False(stub.StringCtorUsed); - Assert.Equal("app-123", stub.CapturedApplicationClientId); - Assert.Equal(false, stub.CapturedUseWamBroker); - } - - [Fact] - public void CreateAzureAuthenticationProvider_AppIdOnly_OptionsMissing_FallsBackToStringCtor() - { - var instance = SqlAuthenticationProviderManager.CreateAzureAuthenticationProvider( - typeof(StubLegacy.ActiveDirectoryAuthenticationProvider), - optionsType: null, - applicationClientId: "legacy-456", - useWamBroker: null); - - var stub = Assert.IsType(instance); - Assert.True(stub.StringCtorUsed); - Assert.False(stub.OptionsCtorUsed); - Assert.False(stub.ParameterlessCtorUsed); - Assert.Equal("legacy-456", stub.CapturedApplicationClientId); - Assert.Null(stub.CapturedUseWamBroker); - } - - [Fact] - public void CreateAzureAuthenticationProvider_AppIdOnly_NoCompatibleCtor_ReturnsNull() - { - var instance = SqlAuthenticationProviderManager.CreateAzureAuthenticationProvider( - typeof(StubMinimal.ActiveDirectoryAuthenticationProvider), - optionsType: null, - applicationClientId: "no-ctor", - useWamBroker: null); - - Assert.Null(instance); - } - - [Fact] - public void CreateAzureAuthenticationProvider_UseWamBroker_OptionsMissing_Throws() - { - InvalidOperationException ex = Assert.Throws(() => - SqlAuthenticationProviderManager.CreateAzureAuthenticationProvider( - typeof(StubLegacy.ActiveDirectoryAuthenticationProvider), - optionsType: null, - applicationClientId: null, - useWamBroker: true)); - - Assert.Contains("ActiveDirectoryAuthenticationProviderOptions", ex.Message); - Assert.Contains("Microsoft.Data.SqlClient.Extensions.Azure", ex.Message); - } - - [Fact] - public void CreateAzureAuthenticationProvider_UseWamBroker_OptionsAvailable_UsesOptionsCtor() - { - var instance = SqlAuthenticationProviderManager.CreateAzureAuthenticationProvider( - typeof(StubModern.ActiveDirectoryAuthenticationProvider), - typeof(StubModern.ActiveDirectoryAuthenticationProviderOptions), - applicationClientId: "app-789", - useWamBroker: true); - - var stub = Assert.IsType(instance); - Assert.True(stub.OptionsCtorUsed); - Assert.Equal("app-789", stub.CapturedApplicationClientId); - Assert.Equal(true, stub.CapturedUseWamBroker); - } -} diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/app.config b/src/Microsoft.Data.SqlClient/tests/UnitTests/app.config new file mode 100644 index 0000000000..d238ea0106 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/app.config @@ -0,0 +1,11 @@ + + + +
+ + + + + + +