diff --git a/BUILDGUIDE.md b/BUILDGUIDE.md index 7984813fb1..1c2fb5dbbd 100644 --- a/BUILDGUIDE.md +++ b/BUILDGUIDE.md @@ -192,19 +192,51 @@ dotnet test "src\Microsoft.Data.SqlClient\tests\ManualTests\Microsoft.Data.SqlCl ## Testing with Custom ReferenceType -Tests can be built and run with custom "Reference Type" property that enables different styles of testing: +The MDS driver consists of several components, each of which produces its own +NuGet package. During development, components reference each other via +`` properties by default. This means that building +and testing one component will implicitly build its project referenced +dependencies. + +Alternatively, the `ReferenceType` build property property may be specified with +a value of `Package`. This will change inter-component dependencies to use +`` dependencies, and require that dependent components be +built and packaged before building the depending component. In this scenario, +the root `NuGet.config` file must be updated to include the following entry +under the `` element: + +```xml + + + ... + + + +``` + +As a convenience, a `NuGet.config.local` file is supplied with the above +package source already present. You may simply copy it over `NuGet.config` +when using `Package` references. + +Then, you can specify `Package` references be used, for example: -- "Project" => Build and run tests with Microsoft.Data.SqlClient as a Project Reference -- "Package" => Build and run tests with Microsoft.Data.SqlClient as a Package Reference with configured "TestMicrosoftDataSqlClientVersion" in "Versions.props" file. +```bash +dotnet build -t:BuildAbstractions +dotnet build -t:BuildAzure -p:ReferenceType=Package +dotnet build -t:BuildAll -p:ReferenceType=Package +dotnet build -t:BuildAKVNetCore -p:ReferenceType=Package +dotnet build -t:GenerateMdsPackage +dotnet build -t:GenerateAkvPackage +dotnet build -t:BuildTestsNetCore -p:ReferenceType=Package +``` -> ************** IMPORTANT NOTE BEFORE PROCEEDING WITH "PACKAGE" REFERENCE TYPE *************** -> CREATE A NUGET PACKAGE WITH BELOW COMMAND AND ADD TO LOCAL FOLDER + UPDATE NUGET CONFIG FILE TO READ FROM THAT LOCATION -> -> ```bash -> msbuild -p:Configuration=Release -> ``` +The above will build the Abstractions, Azure, MDS, and AKV components, place +their NuGet packages into the `packages/` directory, and then build the tests +using those packages. -A non-AnyCPU platform reference can only be used with package reference type. Otherwise, the specified platform will be replaced with AnyCPU in the build process. +A non-AnyCPU platform reference can only be used with package reference type. +Otherwise, the specified platform will be replaced with AnyCPU in the build +process. ### Building Tests with Reference Type diff --git a/NuGet.config.local b/NuGet.config.local new file mode 100644 index 0000000000..3e23ac372c --- /dev/null +++ b/NuGet.config.local @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + diff --git a/build.proj b/build.proj index 2a41791844..d5cdcf73ef 100644 --- a/build.proj +++ b/build.proj @@ -28,10 +28,13 @@ $(TF) $(TF) true - Configuration=$(Configuration);AssemblyVersion=$(SqlServerAssemblyVersion);AssemblyFileVersion=$(SqlServerAssemblyFileVersion);Version=$(SqlServerPackageVersion); - Configuration=$(Configuration);AssemblyFileVersion=$(AssemblyFileVersion);TargetsWindows=$(TargetsWindows);TargetsUnix=$(TargetsUnix); + + Configuration=$(Configuration);ReferenceType=$(ReferenceType) + $(CommonProperties);AssemblyVersion=$(SqlServerAssemblyVersion);AssemblyFileVersion=$(SqlServerAssemblyFileVersion);Version=$(SqlServerPackageVersion); + $(CommonProperties);AssemblyFileVersion=$(AssemblyFileVersion);TargetsWindows=$(TargetsWindows);TargetsUnix=$(TargetsUnix); $(ProjectProperties);BuildForRelease=false;TargetNetCoreVersion=$(TargetNetCoreVersion);TargetNetFxVersion=$(TargetNetFxVersion) TestResults + + + + @@ -101,6 +107,8 @@ + $(CommonProperties) + - AbstractionsPackageVersion=$(AbstractionsPackageVersion) + $(AbstractionsProperties);AbstractionsPackageVersion=$(AbstractionsPackageVersion) @@ -133,7 +141,7 @@ Properties="$(AbstractionsProperties)" /> - + - + + + + $(CommonProperties) + + + + $(AzureProperties);AzurePackageVersion=$(AzurePackageVersion) + + + + + $(AzureProperties);AzureAssemblyFileVersion=$(AzureAssemblyFileVersion) + + + + + + + + + + + + + + + + + + + - + - + @@ -175,7 +239,7 @@ Name="RestoreNetFx" DependsOnTargets="RestoreSqlServerLib;RestoreAbstractions" Condition="'$(IsEnabledWindows)' == 'true'"> - + - - - dotnet build -c Release -p:ReferenceType=$(ReferenceType) - - + + + + + + - + - + @@ -238,29 +303,29 @@ Name="BuildUnitTestsNetCore" DependsOnTargets="RestoreTestsNetCore;BuildNetCore" Condition="$(ReferenceType.Contains('Project'))"> - + - + - + - + - + diff --git a/eng/pipelines/common/templates/jobs/build-signed-package-job.yml b/eng/pipelines/common/templates/jobs/build-signed-package-job.yml index b90fe026a8..a77ee79146 100644 --- a/eng/pipelines/common/templates/jobs/build-signed-package-job.yml +++ b/eng/pipelines/common/templates/jobs/build-signed-package-job.yml @@ -49,7 +49,7 @@ jobs: configuration: $(Configuration) msbuildArguments: -t:BuildTools - # Perform analysis before building, since this step will clobber build output + # Perform analysis before building, since this step will clobber build output. - template: ../steps/code-analyze-step.yml@self # Update the root NuGet.config to use the packages/ directory as a source. 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 06bc31911b..ceab63eb61 100644 --- a/eng/pipelines/common/templates/jobs/ci-run-tests-job.yml +++ b/eng/pipelines/common/templates/jobs/ci-run-tests-job.yml @@ -10,6 +10,12 @@ parameters: - name: abstractionsPackageVersion type: string + - name: azureArtifactName + type: string + + - name: azurePackageVersion + type: string + - name: configProperties type: object default: {} # - key: 'value' @@ -121,6 +127,12 @@ jobs: artifactName: ${{ parameters.mdsArtifactName }} targetPath: $(Build.SourcesDirectory)/packages + - task: DownloadPipelineArtifact@2 + displayName: Download Azure Package Artifact + inputs: + artifactName: ${{ parameters.azureArtifactName }} + targetPath: $(Build.SourcesDirectory)/packages + - ${{ if ne(parameters.prebuildSteps, '') }}: - ${{ parameters.prebuildSteps }} # extra steps to run before the build like downloading sni and the required configuration @@ -253,6 +265,7 @@ jobs: referenceType: ${{ parameters.referenceType }} testSet: ${{ parameters.testSet }} abstractionsPackageVersion: ${{ parameters.abstractionsPackageVersion }} + azurePackageVersion: ${{ parameters.azurePackageVersion }} mdsPackageVersion: ${{ parameters.mdsPackageVersion }} ${{ if ne(parameters.operatingSystem, 'Windows') }}: OSGroup: Unix diff --git a/eng/pipelines/common/templates/jobs/validate-signed-package-job.yml b/eng/pipelines/common/templates/jobs/validate-signed-package-job.yml index 42000da697..030199e72b 100644 --- a/eng/pipelines/common/templates/jobs/validate-signed-package-job.yml +++ b/eng/pipelines/common/templates/jobs/validate-signed-package-job.yml @@ -305,17 +305,6 @@ jobs: Get-ChildItem *.dll -Path $(extractedNugetPath) -Recurse | ForEach-Object VersionInfo | Format-List displayName: 'Verify "File Version" matches expected values for DLLs' - - powershell: | - # Change TestMicrosoftDataSqlClientVersion - - [Xml] $versionprops = Get-Content -Path "tools/props/Versions.props" - $versionpropspath = "tools\props\Versions.props" - $versionprops.Project.PropertyGroup[$versionprops.Project.PropertyGroup.Count-1].TestMicrosoftDataSqlClientVersion ="$(mdsPackageVersion)" - Write-Host "Saving Test nuget version at $rootfolder\props ...." -ForegroundColor Green - $versionprops.Save($versionpropspath) - - displayName: 'Modify TestMicrosoftDataSqlClientVersion' - - powershell: | # Check assembly versions. # 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 46318f8e06..d9c7148c82 100644 --- a/eng/pipelines/common/templates/stages/ci-run-tests-stage.yml +++ b/eng/pipelines/common/templates/stages/ci-run-tests-stage.yml @@ -10,6 +10,12 @@ parameters: - name: abstractionsPackageVersion type: string + - name: azureArtifactName + type: string + + - name: azurePackageVersion + type: string + - name: debug type: boolean default: false @@ -68,7 +74,9 @@ stages: jobDisplayName: ${{ format('{0}_{1}_{2}', replace(targetFramework, '.', '_'), platform, testSet) }} configProperties: ${{ config.value.configProperties }} abstractionsArtifactName: ${{ parameters.abstractionsArtifactName }} - abstractionsPackageVersion: ${{parameters.abstractionsPackageVersion}} + abstractionsPackageVersion: ${{ parameters.abstractionsPackageVersion }} + azureArtifactName: ${{ parameters.azureArtifactName }} + azurePackageVersion: ${{ parameters.azurePackageVersion }} mdsArtifactName: ${{ parameters.mdsArtifactName }} mdsPackageVersion: ${{ parameters.mdsPackageVersion }} prebuildSteps: ${{ parameters.prebuildSteps }} @@ -102,7 +110,9 @@ stages: configProperties: ${{ config.value.configProperties }} useManagedSNI: ${{ useManagedSNI }} abstractionsArtifactName: ${{ parameters.abstractionsArtifactName }} - abstractionsPackageVersion: ${{parameters.abstractionsPackageVersion}} + abstractionsPackageVersion: ${{ parameters.abstractionsPackageVersion }} + azureArtifactName: ${{ parameters.azureArtifactName }} + azurePackageVersion: ${{ parameters.azurePackageVersion }} mdsArtifactName: ${{ parameters.mdsArtifactName }} mdsPackageVersion: ${{ parameters.mdsPackageVersion }} prebuildSteps: ${{ parameters.prebuildSteps }} diff --git a/eng/pipelines/common/templates/steps/build-all-tests-step.yml b/eng/pipelines/common/templates/steps/build-all-tests-step.yml index 43ecb6aafa..36aa703f28 100644 --- a/eng/pipelines/common/templates/steps/build-all-tests-step.yml +++ b/eng/pipelines/common/templates/steps/build-all-tests-step.yml @@ -7,6 +7,9 @@ parameters: - name: abstractionsPackageVersion type: string + - name: azurePackageVersion + type: string + - name: configuration type: string default: '$(Configuration)' @@ -42,7 +45,7 @@ steps: solution: build.proj platform: '${{parameters.platform }}' configuration: '${{parameters.configuration }}' - msbuildArguments: '-t:BuildTestsNetFx -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.mdsPackageVersion }} -p:AbstractionsPackageVersion=${{ parameters.abstractionsPackageVersion }}' + msbuildArguments: '-t:BuildTestsNetFx -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} -p:AbstractionsPackageVersion=${{ parameters.abstractionsPackageVersion }} -p:AzurePackageVersion=${{ parameters.azurePackageVersion }}' - ${{elseif eq(parameters.osGroup, '')}}: # .NET on Windows - task: MSBuild@1 @@ -51,7 +54,7 @@ steps: solution: build.proj platform: '${{parameters.platform }}' configuration: '${{parameters.configuration }}' - msbuildArguments: '-t:BuildTestsNetCore -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.mdsPackageVersion }} -p:AbstractionsPackageVersion=${{ parameters.abstractionsPackageVersion }}' + msbuildArguments: '-t:BuildTestsNetCore -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} -p:AbstractionsPackageVersion=${{ parameters.abstractionsPackageVersion }} -p:AzurePackageVersion=${{ parameters.azurePackageVersion }}' condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) - ${{ else }}: # .NET on Unix @@ -61,7 +64,7 @@ steps: command: custom projects: build.proj custom: msbuild - arguments: '-t:BuildTestsNetCore -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.mdsPackageVersion }} -p:OSGroup=${{parameters.osGroup }} -p:platform=${{parameters.platform }} -p:Configuration=${{parameters.configuration }} -p:AbstractionsPackageVersion=${{ parameters.abstractionsPackageVersion }}' + arguments: '-t:BuildTestsNetCore -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} -p:OSGroup=${{parameters.osGroup }} -p:platform=${{parameters.platform }} -p:Configuration=${{parameters.configuration }} -p:AbstractionsPackageVersion=${{ parameters.abstractionsPackageVersion }} -p:AzurePackageVersion=${{ parameters.azurePackageVersion }}' verbosityRestore: Detailed verbosityPack: Detailed condition: and(succeeded(), ne(variables['Agent.OS'], 'Windows_NT')) diff --git a/eng/pipelines/common/templates/steps/build-and-run-tests-netcore-step.yml b/eng/pipelines/common/templates/steps/build-and-run-tests-netcore-step.yml index 43eea4449d..c23e7d310a 100644 --- a/eng/pipelines/common/templates/steps/build-and-run-tests-netcore-step.yml +++ b/eng/pipelines/common/templates/steps/build-and-run-tests-netcore-step.yml @@ -55,14 +55,14 @@ steps: inputs: solution: build.proj msbuildArchitecture: x64 - msbuildArguments: '-p:Configuration=${{parameters.configuration }} -t:BuildAKVNetCore -p:ReferenceType=${{parameters.referenceType }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.mdsPackageVersion }}' + msbuildArguments: '-p:Configuration=${{parameters.configuration }} -t:BuildAKVNetCore -p:ReferenceType=${{parameters.referenceType }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }}' - task: MSBuild@1 displayName: 'MSBuild Build Tests for ${{parameters.TargetNetCoreVersion }}' inputs: solution: build.proj msbuildArchitecture: x64 - msbuildArguments: '-t:BuildTestsNetCore -p:ReferenceType=${{parameters.referenceType }} -p:TargetNetCoreVersion=${{parameters.TargetNetCoreVersion }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.mdsPackageVersion }} -p:Configuration=${{parameters.configuration }}' + msbuildArguments: '-t:BuildTestsNetCore -p:ReferenceType=${{parameters.referenceType }} -p:TargetNetCoreVersion=${{parameters.TargetNetCoreVersion }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} -p:Configuration=${{parameters.configuration }}' # Don't run unit tests using package reference. Unit tests are only run using project reference. @@ -71,12 +71,12 @@ steps: inputs: command: test projects: 'src\Microsoft.Data.SqlClient\tests\FunctionalTests\Microsoft.Data.SqlClient.FunctionalTests.csproj' - arguments: '-p:Platform=${{parameters.platform }} -p:TestTargetOS="${{parameters.TestTargetOS }}" -p:TargetNetCoreVersion=${{parameters.TargetNetCoreVersion }} -p:ReferenceType=${{parameters.referenceType }} -p:Configuration=${{parameters.configuration }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.mdsPackageVersion }} --no-build -v n --filter "category!=nonnetcoreapptests&category!=failing&category!=nonwindowstests"' + arguments: '-p:Platform=${{parameters.platform }} -p:TestTargetOS="${{parameters.TestTargetOS }}" -p:TargetNetCoreVersion=${{parameters.TargetNetCoreVersion }} -p:ReferenceType=${{parameters.referenceType }} -p:Configuration=${{parameters.configuration }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} --no-build -v n --filter "category!=nonnetcoreapptests&category!=failing&category!=nonwindowstests"' - task: DotNetCoreCLI@2 displayName: 'Run Manual Tests for ${{parameters.TargetNetCoreVersion }}' inputs: command: test projects: 'src\Microsoft.Data.SqlClient\tests\ManualTests\Microsoft.Data.SqlClient.ManualTesting.Tests.csproj' - arguments: '-p:Platform=${{parameters.platform }} -p:TestTargetOS="${{parameters.TestTargetOS }}" -p:TargetNetCoreVersion=${{parameters.TargetNetCoreVersion }} -p:ReferenceType=${{parameters.referenceType }} -p:Configuration=${{parameters.configuration }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.mdsPackageVersion }} --no-build -v n --filter category!=nonnetcoreapptests&category!=failing&category!=nonwindowstests --collect "Code Coverage"' + arguments: '-p:Platform=${{parameters.platform }} -p:TestTargetOS="${{parameters.TestTargetOS }}" -p:TargetNetCoreVersion=${{parameters.TargetNetCoreVersion }} -p:ReferenceType=${{parameters.referenceType }} -p:Configuration=${{parameters.configuration }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} --no-build -v n --filter category!=nonnetcoreapptests&category!=failing&category!=nonwindowstests --collect "Code Coverage"' retryCountOnTaskFailure: ${{parameters.retryCountOnManualTests }} diff --git a/eng/pipelines/common/templates/steps/build-and-run-tests-netfx-step.yml b/eng/pipelines/common/templates/steps/build-and-run-tests-netfx-step.yml index e7b35f653e..8d2a57872b 100644 --- a/eng/pipelines/common/templates/steps/build-and-run-tests-netfx-step.yml +++ b/eng/pipelines/common/templates/steps/build-and-run-tests-netfx-step.yml @@ -55,13 +55,13 @@ steps: inputs: solution: build.proj msbuildArchitecture: x64 - msbuildArguments: '-p:Configuration=${{parameters.configuration }} -t:BuildAKVNetFx -p:ReferenceType=${{parameters.referenceType }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.mdsPackageVersion }}' + msbuildArguments: '-p:Configuration=${{parameters.configuration }} -t:BuildAKVNetFx -p:ReferenceType=${{parameters.referenceType }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }}' - task: MSBuild@1 displayName: 'MSBuild Build Tests for ${{parameters.TargetNetFxVersion }}' inputs: solution: build.proj - msbuildArguments: ' -t:BuildTestsNetFx -p:ReferenceType=${{parameters.referenceType }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.mdsPackageVersion }} -p:TargetNetFxVersion=${{parameters.TargetNetFxVersion }} -p:Configuration=${{parameters.configuration }} -p:Platform=${{parameters.platform }}' + msbuildArguments: ' -t:BuildTestsNetFx -p:ReferenceType=${{parameters.referenceType }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} -p:TargetNetFxVersion=${{parameters.TargetNetFxVersion }} -p:Configuration=${{parameters.configuration }} -p:Platform=${{parameters.platform }}' # Don't run unit tests using package reference. Unit tests are only run using project reference. @@ -70,12 +70,12 @@ steps: inputs: command: test projects: 'src\Microsoft.Data.SqlClient\tests\FunctionalTests\Microsoft.Data.SqlClient.FunctionalTests.csproj' - arguments: '-p:Platform=${{parameters.platform }} -p:TestTargetOS="${{parameters.TestTargetOS }}" -p:TargetNetFxVersion=${{parameters.TargetNetFxVersion }} -p:ReferenceType=${{parameters.referenceType }} -p:Configuration=${{parameters.configuration }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.mdsPackageVersion }} --no-build -v n --filter "category!=nonnetfxtests&category!=failing&category!=nonwindowstests" --collect "Code Coverage"' + arguments: '-p:Platform=${{parameters.platform }} -p:TestTargetOS="${{parameters.TestTargetOS }}" -p:TargetNetFxVersion=${{parameters.TargetNetFxVersion }} -p:ReferenceType=${{parameters.referenceType }} -p:Configuration=${{parameters.configuration }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} --no-build -v n --filter "category!=nonnetfxtests&category!=failing&category!=nonwindowstests" --collect "Code Coverage"' - task: DotNetCoreCLI@2 displayName: 'Run Manual Tests for ${{parameters.TargetNetFxVersion }}' inputs: command: test projects: 'src\Microsoft.Data.SqlClient\tests\ManualTests\Microsoft.Data.SqlClient.ManualTesting.Tests.csproj' - arguments: '-p:Platform=${{parameters.platform }} -p:TestTargetOS="${{parameters.TestTargetOS }}" -p:TargetNetFxVersion=${{parameters.TargetNetFxVersion }} -p:ReferenceType=${{parameters.referenceType }} -p:Configuration=${{parameters.configuration }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.mdsPackageVersion }} --no-build -v n --filter "category!=nonnetfxtests&category!=failing&category!=nonwindowstests" --collect "Code Coverage"' + arguments: '-p:Platform=${{parameters.platform }} -p:TestTargetOS="${{parameters.TestTargetOS }}" -p:TargetNetFxVersion=${{parameters.TargetNetFxVersion }} -p:ReferenceType=${{parameters.referenceType }} -p:Configuration=${{parameters.configuration }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} --no-build -v n --filter "category!=nonnetfxtests&category!=failing&category!=nonwindowstests" --collect "Code Coverage"' retryCountOnTaskFailure: ${{parameters.retryCountOnManualTests }} 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 27da9e4e73..9b3289e300 100644 --- a/eng/pipelines/common/templates/steps/run-all-tests-step.yml +++ b/eng/pipelines/common/templates/steps/run-all-tests-step.yml @@ -62,9 +62,9 @@ steps: platform: '${{parameters.platform }}' configuration: '${{parameters.configuration }}' ${{ if eq(parameters.msbuildArchitecture, 'x64') }}: - msbuildArguments: '-t:RunUnitTests -p:TF=${{parameters.targetFramework }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.mdsPackageVersion }}' + msbuildArguments: '-t:RunUnitTests -p:TF=${{parameters.targetFramework }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }}' ${{ else }}: # x86 - msbuildArguments: '-t:RunUnitTests -p:TF=${{parameters.targetFramework }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.mdsPackageVersion }} -p:DotnetPath=${{parameters.dotnetx86RootPath }}' + msbuildArguments: '-t:RunUnitTests -p:TF=${{parameters.targetFramework }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} -p:DotnetPath=${{parameters.dotnetx86RootPath }}' condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) retryCountOnTaskFailure: 1 @@ -76,9 +76,9 @@ steps: platform: '${{parameters.platform }}' configuration: '${{parameters.configuration }}' ${{ if eq(parameters.msbuildArchitecture, 'x64') }}: - msbuildArguments: '-t:RunFunctionalTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.mdsPackageVersion }}' + msbuildArguments: '-t:RunFunctionalTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }}' ${{ else }}: # x86 - msbuildArguments: '-t:RunFunctionalTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.mdsPackageVersion }} -p:DotnetPath=${{parameters.dotnetx86RootPath }}' + msbuildArguments: '-t:RunFunctionalTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} -p:DotnetPath=${{parameters.dotnetx86RootPath }}' condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) retryCountOnTaskFailure: 1 @@ -90,9 +90,9 @@ steps: platform: '${{parameters.platform }}' configuration: '${{parameters.configuration }}' ${{ if eq(parameters.msbuildArchitecture, 'x64') }}: - msbuildArguments: '-t:RunManualTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.mdsPackageVersion }}' + msbuildArguments: '-t:RunManualTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }}' ${{ else }}: # x86 - msbuildArguments: '-t:RunManualTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.mdsPackageVersion }} -p:DotnetPath=${{parameters.dotnetx86RootPath }}' + msbuildArguments: '-t:RunManualTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} -p:DotnetPath=${{parameters.dotnetx86RootPath }}' condition: eq(variables['Agent.OS'], 'Windows_NT') retryCountOnTaskFailure: 2 @@ -104,7 +104,7 @@ steps: command: custom projects: build.proj custom: msbuild - arguments: '-t:RunUnitTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.mdsPackageVersion }} -p:platform=${{parameters.platform }} -p:Configuration=${{parameters.configuration }}' + arguments: '-t:RunUnitTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} -p:platform=${{parameters.platform }} -p:Configuration=${{parameters.configuration }}' verbosityRestore: Detailed verbosityPack: Detailed retryCountOnTaskFailure: 1 @@ -116,7 +116,7 @@ steps: command: custom projects: build.proj custom: msbuild - arguments: '-t:RunFunctionalTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.mdsPackageVersion }} -p:platform=${{parameters.platform }} -p:Configuration=${{parameters.configuration }}' + arguments: '-t:RunFunctionalTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} -p:platform=${{parameters.platform }} -p:Configuration=${{parameters.configuration }}' verbosityRestore: Detailed verbosityPack: Detailed retryCountOnTaskFailure: 1 @@ -128,7 +128,7 @@ steps: command: custom projects: build.proj custom: msbuild - arguments: '-t:RunManualTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.mdsPackageVersion }} -p:platform=${{parameters.platform }} -p:Configuration=${{parameters.configuration }}' + arguments: '-t:RunManualTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} -p:platform=${{parameters.platform }} -p:Configuration=${{parameters.configuration }}' verbosityRestore: Detailed verbosityPack: Detailed retryCountOnTaskFailure: 2 diff --git a/eng/pipelines/dotnet-sqlclient-ci-core.yml b/eng/pipelines/dotnet-sqlclient-ci-core.yml index 86987f63e2..9c13159a14 100644 --- a/eng/pipelines/dotnet-sqlclient-ci-core.yml +++ b/eng/pipelines/dotnet-sqlclient-ci-core.yml @@ -94,6 +94,9 @@ variables: - name: abstractionsArtifactName value: Abstractions.Artifact + + - name: azureArtifactName + value: Azure.Artifact - name: mdsArtifactName value: MDS.Artifact @@ -107,7 +110,7 @@ stages: # under the given artifact name. - template: stages/build-abstractions-package-ci-stage.yml@self parameters: - buildConfiguration: Release + buildConfiguration: ${{ parameters.buildConfiguration }} abstractionsPackageVersion: $(abstractionsPackageVersion) artifactName: $(abstractionsArtifactName) ${{if eq(parameters.debug, 'true')}}: @@ -141,6 +144,24 @@ stages: SNIVersion: ${{parameters.SNIVersion}} SNIValidationFeed: ${{parameters.SNIValidationFeed}} + # Build the Azure package, and publish it to the pipeline artifacts under the + # given artifact name. + - template: stages/build-azure-package-ci-stage.yml@self + parameters: + abstractionsArtifactName: $(abstractionsArtifactName) + abstractionsPackageVersion: $(abstractionsPackageVersion) + azureArtifactName: $(azureArtifactName) + azurePackageVersion: $(azurePackageVersion) + buildConfiguration: ${{ parameters.buildConfiguration }} + # When building via package references, we must depend on the Abstractions + # package. + ${{ if eq(parameters.referenceType, 'Package') }}: + dependsOn: + - build_abstractions_package_stage + referenceType: ${{ parameters.referenceType }} + ${{if eq(parameters.debug, 'true')}}: + verbosity: diagnostic + # Run the stress tests, if desired. - ${{ if eq(parameters.enableStressTests, true) }}: - template: stages/stress-tests-ci-stage.yml@self @@ -160,14 +181,17 @@ stages: testsTimeout: ${{ parameters.testsTimeout }} abstractionsArtifactName: $(abstractionsArtifactName) abstractionsPackageVersion: $(abstractionsPackageVersion) + azureArtifactName: $(azureArtifactName) + azurePackageVersion: $(azurePackageVersion) mdsArtifactName: $(mdsArtifactName) mdsPackageVersion: $(mdsPackageVersion) - # When testing MDS via packages, we must depend on the Abstractions and - # MDS packages. + # When testing MDS via packages, we must depend on the Abstractions, + # Azure, and MDS packages. ${{ if eq(parameters.referenceType, 'Package') }}: dependsOn: - build_abstractions_package_stage + - build_azure_package_stage - build_mds_akv_packages_stage prebuildSteps: # steps to run prior to building and running tests on each job diff --git a/eng/pipelines/jobs/build-akv-official-job.yml b/eng/pipelines/jobs/build-akv-official-job.yml index bad8758f6e..55ebff9f79 100644 --- a/eng/pipelines/jobs/build-akv-official-job.yml +++ b/eng/pipelines/jobs/build-akv-official-job.yml @@ -87,7 +87,7 @@ jobs: displayName: 'Output Job Parameters' # Perform analysis before building, since this step will clobber build - # output + # output. - template: ../steps/roslyn-analyzers-akv-step.yml@self parameters: akvPackageVersion: '${{ parameters.akvPackageVersion }}' diff --git a/eng/pipelines/jobs/pack-azure-package-ci-job.yml b/eng/pipelines/jobs/pack-azure-package-ci-job.yml new file mode 100644 index 0000000000..9cc33eadb1 --- /dev/null +++ b/eng/pipelines/jobs/pack-azure-package-ci-job.yml @@ -0,0 +1,188 @@ +################################################################################ +# 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 job packs the Azure package into NuGet and symbols packages and publishes +# them as a named pipeline artifact. +# +# This template defines a job named 'pack_azure_package_job' that can be +# depended on by downstream jobs. + +parameters: + + # The name of the Abstractions pipeline artifact to download. + # + # This is used when the referenceType is 'Package'. + - name: abstractionsArtifactName + type: string + default: Abstractions.Artifact + + # The Abstractions package verion to depend on. + # + # This is used when the referenceType is 'Package'. + - name: abstractionsPackageVersion + type: string + + # The name of the pipeline artifact to publish. + - name: azureArtifactName + type: string + default: Azure.Artifact + + # The version to apply to the NuGet package and DLLs. + - name: azurePackageVersion + type: string + + # The type of build to test (Release or Debug) + - name: buildConfiguration + type: string + values: + - Release + - Debug + + # The list of upstream jobs to depend on. + - name: dependsOn + type: object + default: [] + + # The reference type to use: + # + # Project - dependent projects are referenced directly. + # Package - dependent projects are referenced via NuGet packages. + # + - name: referenceType + type: string + values: + - Package + - Project + + # The verbosity level for the dotnet CLI commands. + - name: verbosity + type: string + default: normal + values: + - quiet + - minimal + - normal + - detailed + - diagnostic + +jobs: + + - job: pack_azure_package_job + displayName: 'Pack Azure Package' + + dependsOn: ${{ parameters.dependsOn }} + + pool: + name: Azure Pipelines + vmImage: ubuntu-latest + + variables: + + # The Azure project file to use for all dotnet CLI commands. + - name: project + value: src/Microsoft.Data.SqlClient.Extensions/Azure/src/Azure.csproj + + # The directory where the NuGet packages will be staged before being + # published as pipeline artifacts. + - name: dotnetPackagesDir + value: $(Build.StagingDirectory)/dotnetPackages + + # dotnet CLI arguments common to all commands. + - name: commonArguments + value: >- + --verbosity ${{ parameters.verbosity }} + -p:ReferenceType=${{ parameters.referenceType }} + -p:AbstractionsPackageVersion=${{ parameters.abstractionsPackageVersion }} + + # dotnet CLI arguments for build/test/pack commands + - name: buildArguments + value: >- + $(commonArguments) + --configuration ${{ parameters.buildConfiguration }} + -p:AzurePackageVersion=${{ parameters.azurePackageVersion }} + + # 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 the builds if left defined. The stress tests solution does + # not require any specific Platform, and so its solution file doesn't + # support any non-standard platforms. + # + # Note that Azure Pipelines will inject this variable as PLATFORM into the + # environment of all tasks in this job. + # + # See: + # https://learn.microsoft.com/en-us/azure/devops/pipelines/process/variables?view=azure-devops&tabs=yaml%2Cbatch + # + - name: Platform + value: '' + + # Do the same for $CONFIGURATION since we explicitly set it using our + # 'buildConfiguration' parameter, and we don't want the environment to + # override us. + - name: Configuration + value: '' + + steps: + + # We have a few extra steps for Package reference builds. + - ${{ if eq(parameters.referenceType, 'Package') }}: + + # Download the Abstractions package artifacts into packages/. + - task: DownloadPipelineArtifact@2 + displayName: Download Abstractions Package Artifact + inputs: + artifactName: ${{ parameters.abstractionsArtifactName }} + targetPath: $(Build.SourcesDirectory)/packages + + # Use the local NuGet.config that references the packages/ directory. + - pwsh: cp $(Build.SourcesDirectory)/NuGet.config.local $(Build.SourcesDirectory)/NuGet.config + displayName: Use local NuGet.config + + # Install the .NET 9.0 SDK. + - task: UseDotNet@2 + displayName: Install .NET 9.0 SDK + inputs: + packageType: sdk + version: 9.x + + # We use the 'custom' command because the DotNetCoreCLI@2 task doesn't + # support all of our argument combinations for the different build steps. + + # Restore the solution. + - task: DotNetCoreCLI@2 + displayName: Restore Solution + inputs: + command: custom + custom: restore + projects: $(project) + arguments: $(commonArguments) + + # Build the solution. + - task: DotNetCoreCLI@2 + displayName: Build Solution + inputs: + command: custom + custom: build + projects: $(project) + arguments: $(buildArguments) --no-restore + + # Create the NuGet packages. + - task: DotNetCoreCLI@2 + displayName: Create NuGet Package + inputs: + command: custom + custom: pack + projects: $(project) + arguments: $(buildArguments) --no-build -o $(dotnetPackagesDir) + + # Publish the NuGet packages as a named pipeline artifact. + - task: PublishPipelineArtifact@1 + displayName: Publish Pipeline Artifact + inputs: + targetPath: $(dotnetPackagesDir) + artifactName: ${{ parameters.azureArtifactName }} + publishLocation: pipeline diff --git a/eng/pipelines/jobs/test-azure-package-ci-job.yml b/eng/pipelines/jobs/test-azure-package-ci-job.yml new file mode 100644 index 0000000000..8e1b8fc6db --- /dev/null +++ b/eng/pipelines/jobs/test-azure-package-ci-job.yml @@ -0,0 +1,214 @@ +################################################################################ +# 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 job builds the Azure package and runs its tests for a set of .NET +# runtimes. +# +# This template defines a job named 'test_azure_package_job_' that can +# be depended on by downstream jobs. + +parameters: + + # The name of the Abstractions pipeline artifact to download. + # + # This is used when the referenceType is 'Package'. + - name: abstractionsArtifactName + type: string + default: Abstractions.Artifact + + # The Abstractions package verion to depend on. + # + # This is used when the referenceType is 'Package'. + - name: abstractionsPackageVersion + type: string + + # The type of build to test (Release or Debug) + - name: buildConfiguration + type: string + values: + - Release + - Debug + + # The prefix to prepend to the job's display name: + # + # [] Run Stress Tests + # + - name: displayNamePrefix + type: string + + # The suffix to append to the job name. + - name: jobNameSuffix + type: string + + # The list of .NET Framework runtimes to test against. + - name: netFrameworkRuntimes + type: object + default: [] + + # The list of .NET runtimes to test against. + - name: netRuntimes + type: object + default: [] + + # The name of the Azure Pipelines pool to use. + - name: poolName + type: string + + # The reference type to use: + # + # Project - dependent projects are referenced directly. + # Package - dependent projects are referenced via NuGet packages. + # + - name: referenceType + type: string + values: + - Package + - Project + + # The verbosity level for the dotnet CLI commands. + - name: verbosity + type: string + default: normal + values: + - quiet + - minimal + - normal + - detailed + - diagnostic + + # The pool VM image to use. + - name: vmImage + type: string + +jobs: + + - job: test_azure_package_job_${{ parameters.jobNameSuffix }} + displayName: '[${{ parameters.displayNamePrefix }}] Test Azure Package' + pool: + name: ${{ parameters.poolName }} + + # Images provided by Azure Pipelines must be selected using 'vmImage'. + ${{ if eq(parameters.poolName, 'Azure Pipelines') }}: + vmImage: ${{ parameters.vmImage }} + # Images provided by 1ES must be selected using a demand. + ${{ else }}: + demands: + - imageOverride -equals ${{ parameters.vmImage }} + + variables: + + # The Azure test project file to use for all dotnet CLI commands. + - name: project + value: src/Microsoft.Data.SqlClient.Extensions/Azure/test/Azure.Test.csproj + + # dotnet CLI arguments common to all commands. + - name: commonArguments + value: >- + --verbosity ${{ parameters.verbosity }} + -p:ReferenceType=${{ parameters.referenceType }} + -p:AbstractionsPackageVersion=${{ parameters.abstractionsPackageVersion }} + + # dotnet CLI arguments for build/test/pack commands + - name: buildArguments + value: >- + $(commonArguments) + --configuration ${{ parameters.buildConfiguration }} + + # 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 the builds if left defined. The stress tests solution does + # not require any specific Platform, and so its solution file doesn't + # support any non-standard platforms. + # + # Note that Azure Pipelines will inject this variable as PLATFORM into the + # environment of all tasks in this job. + # + # See: + # https://learn.microsoft.com/en-us/azure/devops/pipelines/process/variables?view=azure-devops&tabs=yaml%2Cbatch + # + - name: Platform + value: '' + + # Do the same for $CONFIGURATION since we explicitly set it using our + # 'buildConfiguration' parameter, and we don't want the environment to + # override us. + - name: Configuration + value: '' + + steps: + + # We have a few extra steps for Package reference builds. + - ${{ if eq(parameters.referenceType, 'Package') }}: + + # Download the Abstractions package artifacts into packages/. + - task: DownloadPipelineArtifact@2 + displayName: Download Abstractions Package Artifact + inputs: + artifactName: ${{ parameters.abstractionsArtifactName }} + targetPath: $(Build.SourcesDirectory)/packages + + # Use the local NuGet.config that references the packages/ directory. + - pwsh: cp $(Build.SourcesDirectory)/NuGet.config.local $(Build.SourcesDirectory)/NuGet.config + displayName: Use local NuGet.config + + # Install the .NET 9.0 SDK. + - task: UseDotNet@2 + displayName: Install .NET 9.0 SDK + inputs: + packageType: sdk + version: 9.x + + # Install the .NET 8.0 runtime. + - task: UseDotNet@2 + displayName: Install .NET 8.0 Runtime + inputs: + packageType: runtime + version: 8.x + + # The Windows agent images include a suitable .NET Framework runtime, so + # we don't have to install one explicitly. + + # We use the 'custom' command because the DotNetCoreCLI@2 task doesn't + # support all of our argument combinations for the different build steps. + + # Restore the solution. + - task: DotNetCoreCLI@2 + displayName: Restore Solution + inputs: + command: custom + custom: restore + projects: $(project) + arguments: $(commonArguments) + + # Build the solution. + - task: DotNetCoreCLI@2 + displayName: Build Solution + inputs: + command: custom + custom: build + projects: $(project) + arguments: $(buildArguments) --no-restore + + # Run the tests for each .NET runtime. + - ${{ each runtime in parameters.netRuntimes }}: + - task: DotNetCoreCLI@2 + displayName: Test [${{ runtime }}] + inputs: + command: custom + custom: test + projects: $(project) + arguments: $(buildArguments) --no-build -f ${{ runtime }} + + # Run the tests for each .NET Framework runtime. + - ${{ each runtime in parameters.netFrameworkRuntimes }}: + - task: DotNetCoreCLI@2 + displayName: Test [${{ runtime }}] + inputs: + command: custom + custom: test + projects: $(project) + arguments: $(buildArguments) --no-build -f ${{ runtime }} diff --git a/eng/pipelines/libraries/ci-build-variables.yml b/eng/pipelines/libraries/ci-build-variables.yml index 6b6ac2a825..79ec983027 100644 --- a/eng/pipelines/libraries/ci-build-variables.yml +++ b/eng/pipelines/libraries/ci-build-variables.yml @@ -18,6 +18,8 @@ variables: value: 1.0.0.$(buildNumber)-ci - name: akvPackageVersion value: 7.0.0.$(buildNumber)-ci + - name: azurePackageVersion + value: 1.0.0.$(buildNumber)-ci - name: mdsPackageVersion value: 7.0.0.$(buildNumber)-ci - name: skipComponentGovernanceDetection diff --git a/eng/pipelines/stages/build-abstractions-package-ci-stage.yml b/eng/pipelines/stages/build-abstractions-package-ci-stage.yml index 2a9268e737..0d84dc6ccb 100644 --- a/eng/pipelines/stages/build-abstractions-package-ci-stage.yml +++ b/eng/pipelines/stages/build-abstractions-package-ci-stage.yml @@ -85,7 +85,7 @@ stages: vmImage: windows-latest buildConfiguration: ${{ parameters.buildConfiguration }} netRuntimes: [net8.0, net9.0] - netFrameworkRuntimes: [net462, net47, net471, net472, net48, net481] + netFrameworkRuntimes: [net462] verbosity: ${{ parameters.verbosity }} # ------------------------------------------------------------------------ diff --git a/eng/pipelines/stages/build-azure-package-ci-stage.yml b/eng/pipelines/stages/build-azure-package-ci-stage.yml new file mode 100644 index 0000000000..90beb30caf --- /dev/null +++ b/eng/pipelines/stages/build-azure-package-ci-stage.yml @@ -0,0 +1,162 @@ +################################################################################ +# 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 stage builds the Azure package, runs tests, and publishes the resulting +# NuGet packages as pipeline artifacts. +# +# The NuGet packages have the following properties: +# +# Name: Microsoft.Data.SqlClient.Extensions.Azure +# Version: azurePackageVersion (from parameters) +# +# The following NuGet packages are published: +# +# Microsoft.Data.SqlClient.Extensions.Azure..nupkg +# Microsoft.Data.SqlClient.Extensions.Azure..snupkg (symbols) +# +# The packages are published to pipeline artifacts with the name specified by +# the azureArtifactName parameter. +# +# This template defines a stage named 'build_azure_package_stage' that +# can be depended on by downstream stages. + +parameters: + + # The name of the Abstractions pipeline artifact to download. + # + # This is used when the referenceType is 'Package'. + - name: abstractionsArtifactName + type: string + default: Abstractions.Artifact + + # The Abstractions package verion to depend on. + # + # This is used when the referenceType is 'Package'. + - name: abstractionsPackageVersion + type: string + + # The name of the pipeline artifact to publish. + - name: azureArtifactName + type: string + default: Azure.Artifact + + # The version to apply to the NuGet package and DLLs. + - name: azurePackageVersion + type: string + + # The type of build to produce (Release or Debug) + - name: buildConfiguration + type: string + default: Release + values: + - Release + - Debug + + # The stages we depend on, if any. + - name: dependsOn + type: object + default: [] + + # The reference type to use: + # + # Project - dependent projects are referenced directly. + # Package - dependent projects are referenced via NuGet packages. + # + - name: referenceType + type: string + values: + - Package + - Project + + # The dotnet CLI verbosity to use. + - name: verbosity + type: string + default: normal + values: + - quiet + - minimal + - normal + - detailed + - diagnostic + +stages: + + - stage: build_azure_package_stage + displayName: Build Azure Package + + dependsOn: ${{ parameters.dependsOn }} + + jobs: + + # ------------------------------------------------------------------------ + # Build and test on Linux. + + - template: ../jobs/test-azure-package-ci-job.yml@self + parameters: + abstractionsArtifactName: ${{ parameters.abstractionsArtifactName }} + abstractionsPackageVersion: ${{ parameters.abstractionsPackageVersion }} + buildConfiguration: ${{ parameters.buildConfiguration }} + displayNamePrefix: Linux + jobNameSuffix: linux + netFrameworkRuntimes: [] + netRuntimes: [net8.0, net9.0] + poolName: Azure Pipelines + referenceType: ${{ parameters.referenceType }} + verbosity: ${{ parameters.verbosity }} + vmImage: ubuntu-latest + + # ------------------------------------------------------------------------ + # Build and test on Windows + + - template: ../jobs/test-azure-package-ci-job.yml@self + parameters: + abstractionsArtifactName: ${{ parameters.abstractionsArtifactName }} + abstractionsPackageVersion: ${{ parameters.abstractionsPackageVersion }} + buildConfiguration: ${{ parameters.buildConfiguration }} + displayNamePrefix: Win + jobNameSuffix: windows + netFrameworkRuntimes: [net462] + netRuntimes: [net8.0, net9.0] + poolName: Azure Pipelines + referenceType: ${{ parameters.referenceType }} + verbosity: ${{ parameters.verbosity }} + vmImage: windows-latest + + # ------------------------------------------------------------------------ + # Build and test on macOS. + + - template: ../jobs/test-azure-package-ci-job.yml + parameters: + abstractionsArtifactName: ${{ parameters.abstractionsArtifactName }} + abstractionsPackageVersion: ${{ parameters.abstractionsPackageVersion }} + buildConfiguration: ${{ parameters.buildConfiguration }} + displayNamePrefix: macOS + jobNameSuffix: macos + netFrameworkRuntimes: [] + netRuntimes: [net8.0, net9.0] + poolName: Azure Pipelines + referenceType: ${{ parameters.referenceType }} + verbosity: ${{ parameters.verbosity }} + vmImage: macos-latest + + # ------------------------------------------------------------------------ + # Create and publish the NuGet package. + + - template: ../jobs/pack-azure-package-ci-job.yml@self + parameters: + abstractionsArtifactName: ${{ parameters.abstractionsArtifactName }} + abstractionsPackageVersion: ${{ parameters.abstractionsPackageVersion }} + azureArtifactName: ${{ parameters.azureArtifactName }} + azurePackageVersion: ${{ parameters.abstractionsPackageVersion }} + buildConfiguration: ${{ parameters.buildConfiguration }} + dependsOn: + # We depend on all of the test jobs to ensure the tests pass before + # producing the NuGet package. + - test_azure_package_job_linux + - test_azure_package_job_windows + - test_azure_package_job_macos + referenceType: ${{ parameters.referenceType }} + verbosity: ${{ parameters.verbosity }} diff --git a/eng/pipelines/steps/compound-build-akv-step.yml b/eng/pipelines/steps/compound-build-akv-step.yml index 75e6e00e3d..393c51362f 100644 --- a/eng/pipelines/steps/compound-build-akv-step.yml +++ b/eng/pipelines/steps/compound-build-akv-step.yml @@ -18,7 +18,7 @@ parameters: - name: buildConfiguration type: string - + - name: mdsPackageVersion type: string diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 86f3529cbb..705daf092a 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -12,12 +12,10 @@ Project diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 44c9f9d55a..fe1426fe43 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -5,22 +5,33 @@ - - - + + + + + + + + - + - + @@ -30,7 +41,7 @@ - + @@ -52,7 +63,7 @@ - + @@ -67,12 +78,16 @@ + + + + + - @@ -110,16 +125,22 @@ - + - + + + + + + + diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationMethod.xml b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationMethod.xml similarity index 95% rename from doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationMethod.xml rename to src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationMethod.xml index cd15a65ec2..c804a8fb3a 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationMethod.xml +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationMethod.xml @@ -1,3 +1,8 @@ + diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationParameters.xml b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationParameters.xml similarity index 62% rename from doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationParameters.xml rename to src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationParameters.xml index 7c409cdc5c..087045b2c9 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationParameters.xml +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationParameters.xml @@ -1,4 +1,9 @@ - + + @@ -6,23 +11,34 @@ - One of the enumeration values that specifies the authentication method. + + Construct with values for all properties. + + The authentication method. The server name. The database name. The resource URI. The authority URI. - The user login name/ID. - The user password. + The user login name/ID, or null if not applicable. + The user password, or null if not applicable. The connection ID. - The connection timeout value in seconds. - - Initializes a new instance of the class using the specified authentication method, server name, database name, resource URI, authority URI, user login name/ID, user password, connection ID and connection timeout value. - + + The authentication timeout, in seconds. The overall connection timeout + is managed by the driver; this timeout only applies to authentication. + Gets the authentication method. The authentication method. + + Gets the server name. + The server name. + + + Gets the database name. + The database name. + The resource URIs. The resource URI. @@ -33,27 +49,18 @@ Gets the user login name/ID. - The user login name/ID. + The user login name/ID, or null if not applicable. Gets the user password. - The user password. + The user password, or null if not applicable. Gets the connection ID. The connection ID. - - Gets the server name. - The server name. - - - Gets the database name. - The database name. - - Gets the connection timeout value. - The connection timeout value to be passed to Cancellation Token Source. + Gets the authentication timeout value, in seconds. diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationProvider.xml b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationProvider.xml similarity index 92% rename from doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationProvider.xml rename to src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationProvider.xml index 75cc752d76..7848aaec1a 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationProvider.xml +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationProvider.xml @@ -1,4 +1,9 @@ - + + Defines the core behavior of authentication providers and provides a base class for derived classes. @@ -89,49 +94,50 @@ - - - Called from constructors in derived classes to initialize the class. - - - - The authentication method. - Gets an authentication provider by method. - The authentication provider or if not found. - - - The authentication method. - The authentication provider. - Sets an authentication provider by method. - - if the operation succeeded; otherwise, (for example, the existing provider disallows overriding). - - - The authentication method. This method is called immediately before the provider is added to the SQL authentication provider registry. Avoid performing long-waiting tasks in this method, since it can block other threads from accessing the provider registry. + + This method must not throw. + The authentication method. - The authentication method. This method is called immediately before the provider is removed from the SQL authentication provider registry. 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 not throw. + The authentication method. - The authentication method. Indicates whether the specified authentication method is supported. + This method must not throw. if the specified authentication method is supported; otherwise, . + The authentication method. - The Active Directory authentication parameters passed by the driver to authentication providers. - Acquires a security token from the authority. + Acquires an access token from the authority. + The parameters passed to the provider by the driver. + If any errors occur. Represents an asynchronous operation that returns the AD authentication token. + + Gets an authentication provider by method. + The authentication method. + The authentication provider or if not found. + + + Sets an authentication provider by method. + The authentication method. + The authentication provider. + + if the operation succeeded; otherwise, (for example, the existing provider disallows overriding). + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationProviderException.xml b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationProviderException.xml new file mode 100644 index 0000000000..648e4a7e2f --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationProviderException.xml @@ -0,0 +1,82 @@ + + + + + + This exception is thrown for any errors that occur during the + authentication process. + + + + + Protected construction for derived classes to supply a minimal set of + values. + + Method will be NotSpecified. + FailureCode will be "Unknown". + ShouldRetry will be false. + RetryPeriod will be 0. + + The error message. + The exception that caused this exception, if any. + + + + Protected construction for derived classes to supply values for all + public properties. + + + The authentication method that failed, or NotSpecified if not known. + + + The failure code, or "Unknown" if not known. + + + True if the action should be retried, false otherwise. + + + The period of time, in milliseconds, to wait before retrying the action. + Specify 0 if no retry period is suggested. Ignored if negative. Not + used when ShouldRetry is false, in which cases 0 is assumed. + + + The error message. + + + The exception that caused this exception, if any. + + + + + The authentication method that failed, or NotSpecified if not known. + + + + + The failure code, or "Unknown" if not known. + + + + + True if the action should be retried, false otherwise. + + + + + The period of time, in milliseconds, to wait before retrying the action. + 0 if no retry period is suggested. Never negative. Always 0 when + ShouldRetry is false. + + + + + A string that includes the base exception information along with all + property values. + + + + diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationToken.xml b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationToken.xml similarity index 55% rename from doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationToken.xml rename to src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationToken.xml index 52f79dd535..4eb9a78f52 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationToken.xml +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationToken.xml @@ -1,25 +1,30 @@ - + + Represents an authentication token. - The access token. - The token expiration time. - Initializes a new instance of the class. + Construct with values for all properties. - - The parameter is or empty. - + The token string. + The token expiration time. + + is null or empty. + - - Gets the token expiration time. - The token expiration time. - Gets the token string. The token string. + + Gets the token expiration time. + The token expiration time. + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Abstractions.csproj b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Abstractions.csproj index 485a3de83a..c1725ee867 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Abstractions.csproj +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Abstractions.csproj @@ -23,26 +23,35 @@ Microsoft.Data.SqlClient.Extensions.Abstractions - Microsoft.Data.SqlClient.Extensions.Abstractions + $(AssemblyName) - $(_DefaultMajorVersion).0.0.0 + $(AbstractionsDefaultMajorVersion).0.0.0 $(AbstractionsAssemblyFileVersion) $(AbstractionsAssemblyFileVersion) $(AbstractionsPackageVersion) + + $(Artifacts)/doc/$(TargetFramework)/$(AssemblyName).xml <_Parameter1>true + + - + CDP_BUILD_TYPE is a OneBranch governed pipeline variable. + --> + + - <_DefaultMajorVersion>1 + 1 <_OurPackageVersion Condition="'$(AbstractionsPackageVersion)' != ''">$(AbstractionsPackageVersion) - <_OurPackageVersion Condition="'$(AbstractionsPackageVersion)' == ''">$(_DefaultMajorVersion).0.0.$(BuildNumber)-dev + <_OurPackageVersion Condition="'$(AbstractionsPackageVersion)' == ''">$(AbstractionsDefaultMajorVersion).0.0.$(BuildNumber)-dev @@ -56,7 +56,7 @@ <_OurAssemblyFileVersion Condition="'$(AbstractionsAssemblyFileVersion)' == '' and '$(AbstractionsPackageVersion)' != ''">$(AbstractionsPackageVersion.Split('-')[0]) - <_OurAssemblyFileVersion Condition="'$(AbstractionsAssemblyFileVersion)' == '' and '$(AbstractionsPackageVersion)' == ''">$(_DefaultMajorVersion).0.0.$(BuildNumber) + <_OurAssemblyFileVersion Condition="'$(AbstractionsAssemblyFileVersion)' == '' and '$(AbstractionsPackageVersion)' == ''">$(AbstractionsDefaultMajorVersion).0.0.$(BuildNumber) + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Azure/src/ActiveDirectoryAuthenticationProvider.cs b/src/Microsoft.Data.SqlClient.Extensions/Azure/src/ActiveDirectoryAuthenticationProvider.cs new file mode 100644 index 0000000000..ac40d8fdea --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/src/ActiveDirectoryAuthenticationProvider.cs @@ -0,0 +1,896 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Security.Cryptography; +using System.Text; +using Azure.Core; +using Azure.Identity; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Identity.Client; +using Microsoft.Identity.Client.Extensibility; + +namespace Microsoft.Data.SqlClient; + +/// +public sealed class ActiveDirectoryAuthenticationProvider : SqlAuthenticationProvider +{ + /// + /// This is a static cache instance meant to hold instances of "PublicClientApplication" mapping to information available in PublicClientAppKey. + /// The purpose of this cache is to allow re-use of Access Tokens fetched for a user interactively or with any other mode + /// to avoid interactive authentication request every-time, within application scope making use of MSAL's userTokenCache. + /// + private static readonly ConcurrentDictionary s_pcaMap = new(); + private static readonly ConcurrentDictionary s_tokenCredentialMap = new(); + private static SemaphoreSlim s_pcaMapModifierSemaphore = new(1, 1); + private static SemaphoreSlim s_tokenCredentialMapModifierSemaphore = new(1, 1); + private static readonly MemoryCache s_accountPwCache = new MemoryCache(new MemoryCacheOptions()); + private const int s_accountPwCacheTtlInHours = 2; + private const string s_nativeClientRedirectUri = "https://login.microsoftonline.com/common/oauth2/nativeclient"; + private const string s_defaultScopeSuffix = "/.default"; + private readonly string _type = typeof(ActiveDirectoryAuthenticationProvider).Name; + private readonly SqlClientLogger _logger = new(); + private Func _deviceCodeFlowCallback; + private ICustomWebUi? _customWebUI = null; + private readonly string _applicationClientId = "2fd908ad-0664-4344-b9be-cd3e8b574c38"; + + // The MSAL error code that indicates the action should be retried. + // + // https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/retry-after#simple-retry-for-errors-with-http-error-codes-500-600 + private const int MsalRetryStatusCode = 429; + + /// + public ActiveDirectoryAuthenticationProvider() + : this(DefaultDeviceFlowCallback) + { + } + + /// + public ActiveDirectoryAuthenticationProvider(string applicationClientId) + : this(DefaultDeviceFlowCallback, applicationClientId) + { + } + + /// + public ActiveDirectoryAuthenticationProvider(Func deviceCodeFlowCallbackMethod, string? applicationClientId = null) + { + _deviceCodeFlowCallback = deviceCodeFlowCallbackMethod; + if (applicationClientId is not null) + { + _applicationClientId = applicationClientId; + } + } + + /// + public static void ClearUserTokenCache() + { + if (!s_pcaMap.IsEmpty) + { + s_pcaMap.Clear(); + } + + if (!s_tokenCredentialMap.IsEmpty) + { + s_tokenCredentialMap.Clear(); + } + } + + /// + public void SetDeviceCodeFlowCallback(Func deviceCodeFlowCallbackMethod) => _deviceCodeFlowCallback = deviceCodeFlowCallbackMethod; + + /// + public void SetAcquireAuthorizationCodeAsyncCallback(Func> acquireAuthorizationCodeAsyncCallback) => _customWebUI = new CustomWebUi(acquireAuthorizationCodeAsyncCallback); + + /// + public override bool IsSupported(SqlAuthenticationMethod authentication) + { + return authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated + #pragma warning disable CS0618 // Type or member is obsolete + || authentication == SqlAuthenticationMethod.ActiveDirectoryPassword + #pragma warning restore CS0618 // Type or member is obsolete + || authentication == SqlAuthenticationMethod.ActiveDirectoryInteractive + || authentication == SqlAuthenticationMethod.ActiveDirectoryServicePrincipal + || authentication == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow + || authentication == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity + || authentication == SqlAuthenticationMethod.ActiveDirectoryMSI + || authentication == SqlAuthenticationMethod.ActiveDirectoryDefault + || authentication == SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity; + } + + /// + public override void BeforeLoad(SqlAuthenticationMethod authentication) + { + _logger.LogInfo(_type, "BeforeLoad", $"being loaded into SqlAuthProviders for {authentication}."); + } + + /// + public override void BeforeUnload(SqlAuthenticationMethod authentication) + { + _logger.LogInfo(_type, "BeforeUnload", $"being unloaded from SqlAuthProviders for {authentication}."); + } + + #if NETFRAMEWORK + private Func _iWin32WindowFunc = null; + + /// + public void SetIWin32WindowFunc(Func iWin32WindowFunc) => this._iWin32WindowFunc = iWin32WindowFunc; + #endif + + /// + public override async Task AcquireTokenAsync(SqlAuthenticationParameters parameters) + { + try + { + using CancellationTokenSource cts = new(); + + // Use the authentication timeout value to cancel token acquire + // request after certain period of time. + if (parameters.ConnectionTimeout > 0) + { + // Safely convert to milliseconds. + if (int.MaxValue / 1000 > parameters.ConnectionTimeout) + { + cts.CancelAfter(int.MaxValue); + } + else + { + cts.CancelAfter(parameters.ConnectionTimeout * 1000); + } + } + + string scope = parameters.Resource.EndsWith(s_defaultScopeSuffix, StringComparison.Ordinal) ? parameters.Resource : parameters.Resource + s_defaultScopeSuffix; + string[] scopes = [scope]; + TokenRequestContext tokenRequestContext = new(scopes); + + // We split audience from Authority URL here. Audience can be one of + // the following: + // + // - The Azure AD authority audience enumeration + // - The tenant ID, which can be: + // - A GUID (the ID of your Azure AD instance), for + // single-tenant applications + // - A domain name associated with your Azure AD instance (also + // for single-tenant applications) + // - One of these placeholders as a tenant ID in place of the + // Azure AD authority audience enumeration: + // - `organizations` for a multitenant application + // - `consumers` to sign in users only with their personal + // accounts + // - `common` to sign in users with their work and school + // accounts or their personal Microsoft accounts + // + // MSAL will throw a meaningful exception if you specify both the + // Azure AD authority audience and the tenant ID. + // + // If you don't specify an audience, your app will target Azure AD + // and personal Microsoft accounts as an audience. (That is, it + // will behave as though `common` were specified.) + // + // More information: + // + // https://docs.microsoft.com/azure/active-directory/develop/msal-client-application-configuration + + int separatorIndex = parameters.Authority.LastIndexOf('/'); + string authority = parameters.Authority.Remove(separatorIndex + 1); + string audience = parameters.Authority.Substring(separatorIndex + 1); + string? clientId = string.IsNullOrWhiteSpace(parameters.UserId) ? null : parameters.UserId; + + if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryDefault) + { + // Cache DefaultAzureCredenial based on scope, authority, audience, and clientId + TokenCredentialKey tokenCredentialKey = new(typeof(DefaultAzureCredential), authority, scope, audience, clientId); + AccessToken accessToken = await GetTokenAsync(tokenCredentialKey, string.Empty, tokenRequestContext, cts.Token).ConfigureAwait(false); + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token for Default auth mode. Expiry Time: {0}", accessToken.ExpiresOn); + return new SqlAuthenticationToken(accessToken.Token, accessToken.ExpiresOn); + } + + TokenCredentialOptions tokenCredentialOptions = new() { AuthorityHost = new Uri(authority) }; + + if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity || parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryMSI) + { + // Cache ManagedIdentityCredential based on scope, authority, and clientId + TokenCredentialKey tokenCredentialKey = new(typeof(ManagedIdentityCredential), authority, scope, string.Empty, clientId); + AccessToken accessToken = await GetTokenAsync(tokenCredentialKey, string.Empty, tokenRequestContext, cts.Token).ConfigureAwait(false); + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token for Managed Identity auth mode. Expiry Time: {0}", accessToken.ExpiresOn); + return new SqlAuthenticationToken(accessToken.Token, accessToken.ExpiresOn); + } + + if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryServicePrincipal) + { + // Cache ClientSecretCredential based on scope, authority, audience, and clientId + TokenCredentialKey tokenCredentialKey = new(typeof(ClientSecretCredential), authority, scope, audience, clientId); + string password = parameters.Password is null ? string.Empty : parameters.Password; + AccessToken accessToken = await GetTokenAsync(tokenCredentialKey, password, tokenRequestContext, cts.Token).ConfigureAwait(false); + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token for Active Directory Service Principal auth mode. Expiry Time: {0}", accessToken.ExpiresOn); + return new SqlAuthenticationToken(accessToken.Token, accessToken.ExpiresOn); + } + + if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity) + { + // Cache WorkloadIdentityCredential based on authority and clientId + TokenCredentialKey tokenCredentialKey = new(typeof(WorkloadIdentityCredential), authority, string.Empty, string.Empty, clientId); + // If either tenant id, client id, or the token file path are not specified when fetching the token, + // a CredentialUnavailableException will be thrown instead + AccessToken accessToken = await GetTokenAsync(tokenCredentialKey, string.Empty, tokenRequestContext, cts.Token).ConfigureAwait(false); + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token for Workload Identity auth mode. Expiry Time: {0}", accessToken.ExpiresOn); + return new SqlAuthenticationToken(accessToken.Token, accessToken.ExpiresOn); + } + + /* + * Today, MSAL.NET uses another redirect URI by default in desktop applications that run on Windows + * (urn:ietf:wg:oauth:2.0:oob). In the future, we'll want to change this default, so we recommend + * that you use https://login.microsoftonline.com/common/oauth2/nativeclient. + * + * https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-desktop-app-registration#redirect-uris + */ + string redirectUri = s_nativeClientRedirectUri; + + #if NET + if (parameters.AuthenticationMethod != SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow) + { + redirectUri = "http://localhost"; + } + #endif + + PublicClientAppKey pcaKey = + #if NETFRAMEWORK + new(parameters.Authority, redirectUri, _applicationClientId, _iWin32WindowFunc); + #else + new(parameters.Authority, redirectUri, _applicationClientId); + #endif + + AuthenticationResult? result = null; + IPublicClientApplication app = await GetPublicClientAppInstanceAsync(pcaKey, cts.Token).ConfigureAwait(false); + + if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryIntegrated) + { + result = await TryAcquireTokenSilent(app, parameters, scopes, cts).ConfigureAwait(false); + + if (result == null) + { + // The AcquireTokenByIntegratedWindowsAuth method is marked + // as obsolete in MSAL.NET but it is still a supported way + // to acquire tokens for Active Directory Integrated + // authentication. + var builder = + #pragma warning disable CS0618 // Type or member is obsolete + app.AcquireTokenByIntegratedWindowsAuth(scopes) + #pragma warning restore CS0618 // Type or member is obsolete + .WithCorrelationId(parameters.ConnectionId); + + if (!string.IsNullOrEmpty(parameters.UserId)) + { + builder = builder.WithUsername(parameters.UserId); + } + + result = await builder + .ExecuteAsync(cancellationToken: cts.Token) + .ConfigureAwait(false); + + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token for Active Directory Integrated auth mode. Expiry Time: {0}", result?.ExpiresOn); + } + } + #pragma warning disable CS0618 // Type or member is obsolete + else if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryPassword) + #pragma warning restore CS0618 // Type or member is obsolete + { + string pwCacheKey = GetAccountPwCacheKey(parameters); + object? previousPw = s_accountPwCache.Get(pwCacheKey); + string password = parameters.Password is null ? string.Empty : parameters.Password; + byte[] currPwHash = GetHash(password); + + if (previousPw != null && + previousPw is byte[] previousPwBytes && + // Only get the cached token if the current password hash matches the previously used password hash + AreEqual(currPwHash, previousPwBytes)) + { + result = await TryAcquireTokenSilent(app, parameters, scopes, cts).ConfigureAwait(false); + } + + if (result == null) + { + #pragma warning disable CS0618 // Type or member is obsolete + result = await app.AcquireTokenByUsernamePassword(scopes, parameters.UserId, parameters.Password) + #pragma warning disable CS0618 // Type or member is obsolete + .WithCorrelationId(parameters.ConnectionId) + .ExecuteAsync(cancellationToken: cts.Token) + .ConfigureAwait(false); + + // We cache the password hash to ensure future connection requests include a validated password + // when we check for a cached MSAL account. Otherwise, a connection request with the same username + // against the same tenant could succeed with an invalid password when we re-use the cached token. + using (ICacheEntry entry = s_accountPwCache.CreateEntry(pwCacheKey)) + { + entry.Value = currPwHash; + entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(s_accountPwCacheTtlInHours); + } + + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token for Active Directory Password auth mode. Expiry Time: {0}", result?.ExpiresOn); + } + } + else if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryInteractive || + parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow) + { + try + { + result = await TryAcquireTokenSilent(app, parameters, scopes, cts).ConfigureAwait(false); + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token (silent) for {0} auth mode. Expiry Time: {1}", parameters.AuthenticationMethod, result?.ExpiresOn); + } + catch (MsalUiRequiredException) + { + // An 'MsalUiRequiredException' is thrown in the case where an interaction is required with the end user of the application, + // for instance, if no refresh token was in the cache, or the user needs to consent, or re-sign-in (for instance if the password expired), + // or the user needs to perform two factor authentication. + // + // result should be null here, but we make sure of that. + Debug.Assert(result is null); + result = null; + } + + if (result == null) + { + // If no existing 'account' is found, we request user to sign in interactively. + result = await AcquireTokenInteractiveDeviceFlowAsync(app, scopes, parameters.ConnectionId, parameters.UserId, parameters.AuthenticationMethod, cts, _customWebUI, _deviceCodeFlowCallback).ConfigureAwait(false); + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token (interactive) for {0} auth mode. Expiry Time: {1}", parameters.AuthenticationMethod, result?.ExpiresOn); + } + } + else + { + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | {0} authentication mode not supported by ActiveDirectoryAuthenticationProvider class.", parameters.AuthenticationMethod); + + throw new Extensions.Azure.AuthenticationException( + parameters.AuthenticationMethod, + $"Authentication method {parameters.AuthenticationMethod} not supported."); + } + + // TODO: Existing bug? result may be null here. + if (result is null) + { + throw new Extensions.Azure.AuthenticationException( + parameters.AuthenticationMethod, + "Internal error - authentication result is null"); + } + + return new SqlAuthenticationToken(result.AccessToken, result.ExpiresOn); + } + catch (MsalException ex) + { + // Check for an explicitly retryable error. + if (ex is MsalServiceException svcEx && + svcEx.StatusCode == MsalRetryStatusCode) + { + int retryPeriod = 0; + + var retryAfter = svcEx.Headers.RetryAfter; + if (retryAfter is not null) + { + if (retryAfter.Delta.HasValue) + { + retryPeriod = retryAfter.Delta.Value.Milliseconds; + } + else if (retryAfter.Date.HasValue) + { + retryPeriod = Convert.ToInt32(retryAfter.Date.Value.Offset.TotalMilliseconds); + } + + throw new Extensions.Azure.AuthenticationException( + parameters.AuthenticationMethod, + ex.ErrorCode, + true, + retryPeriod, + ex.Message, + ex); + } + + // Fall through to check the ErrorCode... + } + + // Check for an unknown error, which we will treat as implicitly + // retryable, but without a suggested period. + if (ex.ErrorCode == MsalError.UnknownError) + { + throw new Extensions.Azure.AuthenticationException( + parameters.AuthenticationMethod, + ex.ErrorCode, + true, + // Don't suggest a retry period. + 0, + ex.Message, + ex); + } + + // The error isn't retryable. + throw new Extensions.Azure.AuthenticationException( + parameters.AuthenticationMethod, + ex.ErrorCode, + false, + 0, + ex.Message, + ex); + } + catch (Exception ex) + when (ex is + AuthenticationFailedException or + AuthenticationRequiredException or + CredentialUnavailableException) + { + // These errors aren't retryable. + throw new Extensions.Azure.AuthenticationException( + parameters.AuthenticationMethod, + "Unknown", + false, + 0, + $"Azure.Identity error: {ex.Message}", + ex); + } + catch (Exception ex) + { + // These errors aren't retryable. + throw new Extensions.Azure.AuthenticationException( + parameters.AuthenticationMethod, + "Unknown", + false, + 0, + $"Unexpected error: {ex.Message}", + ex); + } + } + + private static async Task TryAcquireTokenSilent(IPublicClientApplication app, SqlAuthenticationParameters parameters, + string[] scopes, CancellationTokenSource cts) + { + AuthenticationResult? result = null; + + // Fetch available accounts from 'app' instance + System.Collections.Generic.IEnumerator accounts = (await app.GetAccountsAsync().ConfigureAwait(false)).GetEnumerator(); + + IAccount? account = default; + if (accounts.MoveNext()) + { + if (!string.IsNullOrEmpty(parameters.UserId)) + { + do + { + IAccount currentVal = accounts.Current; + if (string.Compare(parameters.UserId, currentVal.Username, StringComparison.InvariantCultureIgnoreCase) == 0) + { + account = currentVal; + break; + } + } + while (accounts.MoveNext()); + } + else + { + account = accounts.Current; + } + } + + if (account != null) + { + // If 'account' is available in 'app', we use the same to acquire token silently. + // Read More on API docs: https://docs.microsoft.com/dotnet/api/microsoft.identity.client.clientapplicationbase.acquiretokensilent + result = await app.AcquireTokenSilent(scopes, account).ExecuteAsync(cancellationToken: cts.Token).ConfigureAwait(false); + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token (silent) for {0} auth mode. Expiry Time: {1}", parameters.AuthenticationMethod, result?.ExpiresOn); + } + + return result; + } + + private static async Task AcquireTokenInteractiveDeviceFlowAsync(IPublicClientApplication app, string[] scopes, Guid connectionId, string? userId, + SqlAuthenticationMethod authenticationMethod, CancellationTokenSource cts, ICustomWebUi? customWebUI, Func deviceCodeFlowCallback) + { + try + { + if (authenticationMethod == SqlAuthenticationMethod.ActiveDirectoryInteractive) + { + CancellationTokenSource ctsInteractive = new(); + #if NET + // On .NET Core, MSAL will start the system browser as a + // separate process. MSAL does not have control over this + // browser, but once the user finishes authentication, the web + // page is redirected in such a way that MSAL can intercept the + // Uri. MSAL cannot detect if the user navigates away or simply + // closes the browser. Apps using this technique are encouraged + // to define a timeout (via CancellationToken). We recommend a + // timeout of at least a few minutes, to take into account cases + // where the user is prompted to change password or perform 2FA. + // + // https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/System-Browser-on-.Net-Core#system-browser-experience + // + // Wait up to 3 minutes. + ctsInteractive.CancelAfter(180000); + #endif + if (customWebUI != null) + { + return await app.AcquireTokenInteractive(scopes) + .WithCorrelationId(connectionId) + .WithCustomWebUi(customWebUI) + .WithLoginHint(userId) + .ExecuteAsync(ctsInteractive.Token) + .ConfigureAwait(false); + } + else + { + /* + * We will use the MSAL Embedded or System web browser which changes by Default in MSAL according to this table: + * + * Framework Embedded System Default + * ------------------------------------------- + * .NET Classic Yes Yes^ Embedded + * .NET Core No Yes^ System + * .NET Standard No No NONE + * UWP Yes No Embedded + * Xamarin.Android Yes Yes System + * Xamarin.iOS Yes Yes System + * Xamarin.Mac Yes No Embedded + * + * ^ Requires "http://localhost" redirect URI + * + * https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/MSAL.NET-uses-web-browser#at-a-glance + */ + return await app.AcquireTokenInteractive(scopes) + .WithCorrelationId(connectionId) + .WithLoginHint(userId) + .ExecuteAsync(ctsInteractive.Token) + .ConfigureAwait(false); + } + } + else + { + return await app.AcquireTokenWithDeviceCode(scopes, + deviceCodeResult => deviceCodeFlowCallback(deviceCodeResult)) + .WithCorrelationId(connectionId) + .ExecuteAsync(cancellationToken: cts.Token) + .ConfigureAwait(false); + } + } + catch (OperationCanceledException ex) + { + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenInteractiveDeviceFlowAsync | Operation timed out while acquiring access token."); + + throw new Extensions.Azure.AuthenticationException( + authenticationMethod, + "OperationCanceled", + false, + 0, + // TODO: This used to use the following localized strings + // depending on the method: + // + // Strings.SQL_Timeout_Active_Directory_Interactive_Authentication + // Strings.SQL_Timeout_Active_Directory_DeviceFlow_Authentication + ex.Message, + ex); + } + } + + private static Task DefaultDeviceFlowCallback(DeviceCodeResult result) + { + // This will print the message on the console which tells the user where to go sign-in using + // a separate browser and the code to enter once they sign in. + // The AcquireTokenWithDeviceCode() method will poll the server after firing this + // device code callback to look for the successful login of the user via that browser. + // This background polling (whose interval and timeout data is also provided as fields in the + // deviceCodeCallback class) will occur until: + // * The user has successfully logged in via browser and entered the proper code + // * The timeout specified by the server for the lifetime of this code (typically ~15 minutes) has been reached + // * The developing application calls the Cancel() method on a CancellationToken sent into the method. + // If this occurs, an OperationCanceledException will be thrown (see catch below for more details). + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenInteractiveDeviceFlowAsync | Callback triggered with Device Code Result: {0}", result.Message); + Console.WriteLine(result.Message); + return Task.FromResult(0); + } + + private class CustomWebUi : ICustomWebUi + { + private readonly Func> _acquireAuthorizationCodeAsyncCallback; + + internal CustomWebUi(Func> acquireAuthorizationCodeAsyncCallback) => _acquireAuthorizationCodeAsyncCallback = acquireAuthorizationCodeAsyncCallback; + + public Task AcquireAuthorizationCodeAsync(Uri authorizationUri, Uri redirectUri, CancellationToken cancellationToken) + => _acquireAuthorizationCodeAsyncCallback.Invoke(authorizationUri, redirectUri, cancellationToken); + } + + private async Task GetPublicClientAppInstanceAsync(PublicClientAppKey publicClientAppKey, CancellationToken cancellationToken) + { + if (!s_pcaMap.TryGetValue(publicClientAppKey, out IPublicClientApplication clientApplicationInstance)) + { + await s_pcaMapModifierSemaphore.WaitAsync(cancellationToken); + try + { + // Double-check in case another thread added it while we waited for the semaphore + if (!s_pcaMap.TryGetValue(publicClientAppKey, out clientApplicationInstance)) + { + clientApplicationInstance = CreateClientAppInstance(publicClientAppKey); + s_pcaMap.TryAdd(publicClientAppKey, clientApplicationInstance); + } + } + finally + { + s_pcaMapModifierSemaphore.Release(); + } + } + + return clientApplicationInstance; + } + + private static async Task GetTokenAsync(TokenCredentialKey tokenCredentialKey, string secret, + TokenRequestContext tokenRequestContext, CancellationToken cancellationToken) + { + if (!s_tokenCredentialMap.TryGetValue(tokenCredentialKey, out TokenCredentialData tokenCredentialInstance)) + { + await s_tokenCredentialMapModifierSemaphore.WaitAsync(cancellationToken); + try + { + // Double-check in case another thread added it while we waited for the semaphore + if (!s_tokenCredentialMap.TryGetValue(tokenCredentialKey, out tokenCredentialInstance)) + { + tokenCredentialInstance = CreateTokenCredentialInstance(tokenCredentialKey, secret); + s_tokenCredentialMap.TryAdd(tokenCredentialKey, tokenCredentialInstance); + } + } + finally + { + s_tokenCredentialMapModifierSemaphore.Release(); + } + } + + if (!AreEqual(tokenCredentialInstance._secretHash, GetHash(secret))) + { + // If the secret hash has changed, we need to remove the old token credential instance and create a new one. + await s_tokenCredentialMapModifierSemaphore.WaitAsync(cancellationToken); + try + { + s_tokenCredentialMap.TryRemove(tokenCredentialKey, out _); + tokenCredentialInstance = CreateTokenCredentialInstance(tokenCredentialKey, secret); + s_tokenCredentialMap.TryAdd(tokenCredentialKey, tokenCredentialInstance); + } + finally + { + s_tokenCredentialMapModifierSemaphore.Release(); + } + } + + return await tokenCredentialInstance._tokenCredential.GetTokenAsync(tokenRequestContext, cancellationToken); + } + + private static string GetAccountPwCacheKey(SqlAuthenticationParameters parameters) + { + return parameters.Authority + "+" + parameters.UserId; + } + + private static byte[] GetHash(string input) + { + byte[] unhashedBytes = Encoding.Unicode.GetBytes(input); + SHA256 sha256 = SHA256.Create(); + byte[] hashedBytes = sha256.ComputeHash(unhashedBytes); + return hashedBytes; + } + + private static bool AreEqual(byte[] a1, byte[] a2) + { + if (ReferenceEquals(a1, a2)) + { + return true; + } + else if (a1 is null || a2 is null) + { + return false; + } + else if (a1.Length != a2.Length) + { + return false; + } + + return a1.AsSpan().SequenceEqual(a2.AsSpan()); + } + + private IPublicClientApplication CreateClientAppInstance(PublicClientAppKey publicClientAppKey) + { + PublicClientApplicationBuilder builder = PublicClientApplicationBuilder + .CreateWithApplicationOptions(new PublicClientApplicationOptions + { + ClientId = publicClientAppKey._applicationClientId, + ClientName = typeof(ActiveDirectoryAuthenticationProvider).FullName, + ClientVersion = Extensions.Azure.ThisAssembly.InformationalVersion, + RedirectUri = publicClientAppKey._redirectUri, + }) + .WithAuthority(publicClientAppKey._authority); + + #if NETFRAMEWORK + if (_iWin32WindowFunc is not null) + { + builder.WithParentActivityOrWindow(_iWin32WindowFunc); + } + #endif + + return builder.Build(); + } + + private static TokenCredentialData CreateTokenCredentialInstance(TokenCredentialKey tokenCredentialKey, string secret) + { + if (tokenCredentialKey._tokenCredentialType == typeof(DefaultAzureCredential)) + { + DefaultAzureCredentialOptions defaultAzureCredentialOptions = new() + { + AuthorityHost = new Uri(tokenCredentialKey._authority), + TenantId = tokenCredentialKey._audience, + ExcludeInteractiveBrowserCredential = true // Force disabled, even though it's disabled by default to respect driver specifications. + }; + + // Optionally set clientId when available + if (tokenCredentialKey._clientId is not null) + { + defaultAzureCredentialOptions.ManagedIdentityClientId = tokenCredentialKey._clientId; + defaultAzureCredentialOptions.SharedTokenCacheUsername = tokenCredentialKey._clientId; + defaultAzureCredentialOptions.WorkloadIdentityClientId = tokenCredentialKey._clientId; + } + + // SqlClient is a library and provides support to acquire access + // token using 'DefaultAzureCredential' on user demand when they + // specify 'Authentication = Active Directory Default' in + // connection string. + // + // Default Azure Credential is instantiated by the calling + // application when using "Active Directory Default" + // authentication code to connect to Azure SQL instance. + // SqlClient is a library, doesn't instantiate the credential + // without running application instructions. + // + // Note that CodeQL suppression support can only detect + // suppression comments that appear immediately above the + // flagged statement, or appended to the end of the statement. + // Multi-line justifications are not supported. + // + // https://eng.ms/docs/cloud-ai-platform/devdiv/one-engineering-system-1es/1es-docs/codeql/codeql-semmle#guidance-on-suppressions + // + // CodeQL [SM05137] See above for justification. + DefaultAzureCredential cred = new(defaultAzureCredentialOptions); + + return new TokenCredentialData(cred, GetHash(secret)); + } + + TokenCredentialOptions tokenCredentialOptions = new() { AuthorityHost = new Uri(tokenCredentialKey._authority) }; + + if (tokenCredentialKey._tokenCredentialType == typeof(ManagedIdentityCredential)) + { + return new TokenCredentialData(new ManagedIdentityCredential(tokenCredentialKey._clientId, tokenCredentialOptions), GetHash(secret)); + } + else if (tokenCredentialKey._tokenCredentialType == typeof(ClientSecretCredential)) + { + return new TokenCredentialData(new ClientSecretCredential(tokenCredentialKey._audience, tokenCredentialKey._clientId, secret, tokenCredentialOptions), GetHash(secret)); + } + else if (tokenCredentialKey._tokenCredentialType == typeof(WorkloadIdentityCredential)) + { + // The WorkloadIdentityCredentialOptions object initialization populates its instance members + // from the environment variables AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_FEDERATED_TOKEN_FILE, + // and AZURE_ADDITIONALLY_ALLOWED_TENANTS. AZURE_CLIENT_ID may be overridden by the User Id. + WorkloadIdentityCredentialOptions options = new() { AuthorityHost = new Uri(tokenCredentialKey._authority) }; + + if (tokenCredentialKey._clientId is not null) + { + options.ClientId = tokenCredentialKey._clientId; + } + + return new TokenCredentialData(new WorkloadIdentityCredential(options), GetHash(secret)); + } + + // This should never be reached, but if it is, throw an exception that will be noticed during development + throw new ArgumentException(nameof(ActiveDirectoryAuthenticationProvider)); + } + + internal class PublicClientAppKey + { + public readonly string _authority; + public readonly string _redirectUri; + public readonly string _applicationClientId; + #if NETFRAMEWORK + public readonly Func _iWin32WindowFunc; + #endif + + public PublicClientAppKey(string authority, string redirectUri, string applicationClientId + #if NETFRAMEWORK + , Func iWin32WindowFunc + #endif + ) + { + _authority = authority; + _redirectUri = redirectUri; + _applicationClientId = applicationClientId; + #if NETFRAMEWORK + _iWin32WindowFunc = iWin32WindowFunc; + #endif + } + + public override bool Equals(object obj) + { + if (obj != null && obj is PublicClientAppKey pcaKey) + { + return (string.CompareOrdinal(_authority, pcaKey._authority) == 0 + && string.CompareOrdinal(_redirectUri, pcaKey._redirectUri) == 0 + && string.CompareOrdinal(_applicationClientId, pcaKey._applicationClientId) == 0 + #if NETFRAMEWORK + && pcaKey._iWin32WindowFunc == _iWin32WindowFunc + #endif + ); + } + return false; + } + + public override int GetHashCode() => Tuple.Create(_authority, _redirectUri, _applicationClientId + #if NETFRAMEWORK + , _iWin32WindowFunc + #endif + ).GetHashCode(); + } + + internal class TokenCredentialData + { + public TokenCredential _tokenCredential; + public byte[] _secretHash; + + public TokenCredentialData(TokenCredential tokenCredential, byte[] secretHash) + { + _tokenCredential = tokenCredential; + _secretHash = secretHash; + } + } + + internal class TokenCredentialKey + { + public readonly Type _tokenCredentialType; + public readonly string _authority; + public readonly string _scope; + public readonly string _audience; + public readonly string? _clientId; + + public TokenCredentialKey(Type tokenCredentialType, string authority, string scope, string audience, string? clientId) + { + _tokenCredentialType = tokenCredentialType; + _authority = authority; + _scope = scope; + _audience = audience; + _clientId = clientId; + } + + public override bool Equals(object obj) + { + if (obj != null && obj is TokenCredentialKey tcKey) + { + return string.CompareOrdinal(nameof(_tokenCredentialType), nameof(tcKey._tokenCredentialType)) == 0 + && string.CompareOrdinal(_authority, tcKey._authority) == 0 + && string.CompareOrdinal(_scope, tcKey._scope) == 0 + && string.CompareOrdinal(_audience, tcKey._audience) == 0 + && string.CompareOrdinal(_clientId, tcKey._clientId) == 0 + ; + } + return false; + } + + public override int GetHashCode() => Tuple.Create(_tokenCredentialType, _authority, _scope, _audience, _clientId).GetHashCode(); + } + +} + +internal class SqlClientLogger +{ + public void LogInfo(string type, string method, string message) + { + SqlClientEventSource.Log.TryTraceEvent( + "{3}", type, method, LogLevel.Info, message); + } +} + +internal class SqlClientEventSource +{ + internal class Logger + { + public void TryTraceEvent(string message, params object?[] args) + { + } + } + + public static readonly Logger Log = new(); +} diff --git a/src/Microsoft.Data.SqlClient.Extensions/Azure/src/AuthenticationException.cs b/src/Microsoft.Data.SqlClient.Extensions/Azure/src/AuthenticationException.cs new file mode 100644 index 0000000000..b8d1eb5b5c --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/src/AuthenticationException.cs @@ -0,0 +1,37 @@ +// 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; + +// This exception is used internally by authentication providers to signal +// authentication failures. It is not exposed publicly. +internal class AuthenticationException : SqlAuthenticationProviderException +{ + // Construct with just a method and message. Other properties are set to + // defaults per the base class. + internal AuthenticationException( + SqlAuthenticationMethod method, + string message) + : base($"Failed to acquire access token for {method}: {message}", null) + { + } + + // Construct with all properties specified. See the base class for details. + internal AuthenticationException( + SqlAuthenticationMethod method, + string failureCode, + bool shouldRetry, + int retryPeriod, + string message, + Exception? causedBy = null) + : base( + method, + failureCode, + shouldRetry, + retryPeriod, + $"Failed to acquire access token for {method}: {message}", + causedBy) + { + } +} diff --git a/src/Microsoft.Data.SqlClient.Extensions/Azure/src/Azure.csproj b/src/Microsoft.Data.SqlClient.Extensions/Azure/src/Azure.csproj new file mode 100644 index 0000000000..50d70d3228 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/src/Azure.csproj @@ -0,0 +1,120 @@ + + + + + + + + netstandard2.0 + + + + + enable + enable + + + + + Microsoft.Data.SqlClient.Extensions.Azure + $(AssemblyName) + + + $(AzureDefaultMajorVersion).0.0.0 + + $(AzureAssemblyFileVersion) + $(AzureAssemblyFileVersion) + $(AzurePackageVersion) + + $(Artifacts)/doc/$(TargetFramework)/$(AssemblyName).xml + + + + + <_Parameter1>true + + + + + + + + + + $(AssemblyName) + $(AbstractionsPackageVersion) + $(PackagesDir) + true + snupkg + + Microsoft Corporation + Microsoft Corporation + Microsoft.Data.SqlClient Extensions Azure + https://github.com/dotnet/SqlClient + MIT + dotnet.png + + + + + + + + + + + + + + + + + + + + + + false + + + $(AssemblyName) + + + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Azure/src/AzureVersions.props b/src/Microsoft.Data.SqlClient.Extensions/Azure/src/AzureVersions.props new file mode 100644 index 0000000000..8c9d16969e --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/src/AzureVersions.props @@ -0,0 +1,69 @@ + + + + + + + + + + + + + 1 + + + <_OurPackageVersion Condition="'$(AzurePackageVersion)' != ''">$(AzurePackageVersion) + <_OurPackageVersion Condition="'$(AzurePackageVersion)' == ''">$(AzureDefaultMajorVersion).0.0.$(BuildNumber)-dev + + + + <_OurAssemblyFileVersion Condition="'$(AzureAssemblyFileVersion)' != ''">$(AzureAssemblyFileVersion) + + <_OurAssemblyFileVersion Condition="'$(AzureAssemblyFileVersion)' == '' and '$(AzurePackageVersion)' != ''">$(AzurePackageVersion.Split('-')[0]) + + <_OurAssemblyFileVersion Condition="'$(AzureAssemblyFileVersion)' == '' and '$(AzurePackageVersion)' == ''">$(AzureDefaultMajorVersion).0.0.$(BuildNumber) + + + $(_OurPackageVersion) + $(_OurAssemblyFileVersion) + + + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Azure/test/Azure.Test.csproj b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/Azure.Test.csproj new file mode 100644 index 0000000000..ad94e9b4b1 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/Azure.Test.csproj @@ -0,0 +1,27 @@ + + + + net462;net8.0;net9.0 + enable + enable + false + true + Microsoft.Data.SqlClient.Extensions.Azure.Test + + + + + + + + + + + + + + + + + + diff --git a/src/Microsoft.Data.SqlClient.sln b/src/Microsoft.Data.SqlClient.sln index d7a2f70689..d5efe9e4b9 100644 --- a/src/Microsoft.Data.SqlClient.sln +++ b/src/Microsoft.Data.SqlClient.sln @@ -327,6 +327,16 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{59667E4C-0 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Abstractions.Test", "Microsoft.Data.SqlClient.Extensions\Abstractions\test\Abstractions.Test.csproj", "{04ACBF75-CFF2-41AB-B652-776BC0533490}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Azure", "Azure", "{A20114E1-82D8-903A-C389-726EB4FD943F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0D2F834B-6D91-18D0-3F09-672D448751BD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure", "Microsoft.Data.SqlClient.Extensions\Azure\src\Azure.csproj", "{20C16035-7293-45AC-8217-9B86A389E571}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{5AF52CDD-DF78-3712-7516-5B49F94F9491}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure.Test", "Microsoft.Data.SqlClient.Extensions\Azure\test\Azure.Test.csproj", "{A7C0B6C7-A4B2-43CA-921B-D4FAEE86ACBC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -679,6 +689,30 @@ Global {04ACBF75-CFF2-41AB-B652-776BC0533490}.Release|x64.Build.0 = Release|Any CPU {04ACBF75-CFF2-41AB-B652-776BC0533490}.Release|x86.ActiveCfg = Release|Any CPU {04ACBF75-CFF2-41AB-B652-776BC0533490}.Release|x86.Build.0 = Release|Any CPU + {20C16035-7293-45AC-8217-9B86A389E571}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {20C16035-7293-45AC-8217-9B86A389E571}.Debug|Any CPU.Build.0 = Debug|Any CPU + {20C16035-7293-45AC-8217-9B86A389E571}.Debug|x64.ActiveCfg = Debug|Any CPU + {20C16035-7293-45AC-8217-9B86A389E571}.Debug|x64.Build.0 = Debug|Any CPU + {20C16035-7293-45AC-8217-9B86A389E571}.Debug|x86.ActiveCfg = Debug|Any CPU + {20C16035-7293-45AC-8217-9B86A389E571}.Debug|x86.Build.0 = Debug|Any CPU + {20C16035-7293-45AC-8217-9B86A389E571}.Release|Any CPU.ActiveCfg = Release|Any CPU + {20C16035-7293-45AC-8217-9B86A389E571}.Release|Any CPU.Build.0 = Release|Any CPU + {20C16035-7293-45AC-8217-9B86A389E571}.Release|x64.ActiveCfg = Release|Any CPU + {20C16035-7293-45AC-8217-9B86A389E571}.Release|x64.Build.0 = Release|Any CPU + {20C16035-7293-45AC-8217-9B86A389E571}.Release|x86.ActiveCfg = Release|Any CPU + {20C16035-7293-45AC-8217-9B86A389E571}.Release|x86.Build.0 = Release|Any CPU + {A7C0B6C7-A4B2-43CA-921B-D4FAEE86ACBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A7C0B6C7-A4B2-43CA-921B-D4FAEE86ACBC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A7C0B6C7-A4B2-43CA-921B-D4FAEE86ACBC}.Debug|x64.ActiveCfg = Debug|Any CPU + {A7C0B6C7-A4B2-43CA-921B-D4FAEE86ACBC}.Debug|x64.Build.0 = Debug|Any CPU + {A7C0B6C7-A4B2-43CA-921B-D4FAEE86ACBC}.Debug|x86.ActiveCfg = Debug|Any CPU + {A7C0B6C7-A4B2-43CA-921B-D4FAEE86ACBC}.Debug|x86.Build.0 = Debug|Any CPU + {A7C0B6C7-A4B2-43CA-921B-D4FAEE86ACBC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A7C0B6C7-A4B2-43CA-921B-D4FAEE86ACBC}.Release|Any CPU.Build.0 = Release|Any CPU + {A7C0B6C7-A4B2-43CA-921B-D4FAEE86ACBC}.Release|x64.ActiveCfg = Release|Any CPU + {A7C0B6C7-A4B2-43CA-921B-D4FAEE86ACBC}.Release|x64.Build.0 = Release|Any CPU + {A7C0B6C7-A4B2-43CA-921B-D4FAEE86ACBC}.Release|x86.ActiveCfg = Release|Any CPU + {A7C0B6C7-A4B2-43CA-921B-D4FAEE86ACBC}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -740,6 +774,11 @@ Global {B21E7C94-D805-427E-928A-8DE8EA2F08CC} = {210228A5-979A-DE06-EE1F-B35C65E1583C} {59667E4C-0BD2-9F48-FB50-9E55DD8B1011} = {556B486E-F9B0-7EA9-6A25-DA560C312761} {04ACBF75-CFF2-41AB-B652-776BC0533490} = {59667E4C-0BD2-9F48-FB50-9E55DD8B1011} + {A20114E1-82D8-903A-C389-726EB4FD943F} = {19F1F1E5-3013-7660-661A-2A15F7D606C1} + {0D2F834B-6D91-18D0-3F09-672D448751BD} = {A20114E1-82D8-903A-C389-726EB4FD943F} + {20C16035-7293-45AC-8217-9B86A389E571} = {0D2F834B-6D91-18D0-3F09-672D448751BD} + {5AF52CDD-DF78-3712-7516-5B49F94F9491} = {A20114E1-82D8-903A-C389-726EB4FD943F} + {A7C0B6C7-A4B2-43CA-921B-D4FAEE86ACBC} = {5AF52CDD-DF78-3712-7516-5B49F94F9491} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {01D48116-37A2-4D33-B9EC-94793C702431} diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.csproj b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.csproj index 51af5632e3..b3e100b36c 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.csproj +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.csproj @@ -33,15 +33,22 @@ - - - - + + + + + + + + + + + - + diff --git a/src/Microsoft.Data.SqlClient/add-ons/Directory.Build.props b/src/Microsoft.Data.SqlClient/add-ons/Directory.Build.props index dfeb60b38c..d5bd53f012 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/Directory.Build.props +++ b/src/Microsoft.Data.SqlClient/add-ons/Directory.Build.props @@ -8,7 +8,6 @@ $(OS) true true - Project true $([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(TargetFramework)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)')) diff --git a/src/Microsoft.Data.SqlClient/add-ons/Directory.Packages.props b/src/Microsoft.Data.SqlClient/add-ons/Directory.Packages.props deleted file mode 100644 index 4a24bb25ae..0000000000 --- a/src/Microsoft.Data.SqlClient/add-ons/Directory.Packages.props +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs index 0a04d047f8..6c74d2e85f 100644 --- a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs +++ b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs @@ -142,30 +142,6 @@ public SqlVector(System.ReadOnlyMemory memory) { } } namespace Microsoft.Data.SqlClient { - /// - public sealed partial class ActiveDirectoryAuthenticationProvider : SqlAuthenticationProvider - { - /// - public ActiveDirectoryAuthenticationProvider() { } - /// - public ActiveDirectoryAuthenticationProvider(string applicationClientId) { } - /// - public static void ClearUserTokenCache() { } - /// - public ActiveDirectoryAuthenticationProvider(System.Func deviceCodeFlowCallbackMethod, string applicationClientId = null) { } - /// - public override System.Threading.Tasks.Task AcquireTokenAsync(SqlAuthenticationParameters parameters) { throw null; } - /// - public void SetDeviceCodeFlowCallback(System.Func deviceCodeFlowCallbackMethod) { } - /// - public void SetAcquireAuthorizationCodeAsyncCallback(System.Func> acquireAuthorizationCodeAsyncCallback) { } - /// - public override bool IsSupported(SqlAuthenticationMethod authentication) { throw null; } - /// - public override void BeforeLoad(SqlAuthenticationMethod authentication) { } - /// - public override void BeforeUnload(SqlAuthenticationMethod authentication) { } - } /// public enum ApplicationIntent { @@ -204,85 +180,6 @@ protected SqlAuthenticationInitializer() { } /// public abstract void Initialize(); } - /// - public enum SqlAuthenticationMethod - { - /// - NotSpecified = 0, - /// - SqlPassword = 1, - /// - [System.Obsolete("ActiveDirectoryPassword is deprecated, use a more secure authentication method. See https://aka.ms/SqlClientEntraIDAuthentication for more details.")] - ActiveDirectoryPassword = 2, - /// - ActiveDirectoryIntegrated = 3, - /// - ActiveDirectoryInteractive = 4, - /// - ActiveDirectoryServicePrincipal = 5, - /// - ActiveDirectoryDeviceCodeFlow = 6, - /// - ActiveDirectoryManagedIdentity = 7, - /// - ActiveDirectoryMSI = 8, - /// - ActiveDirectoryDefault = 9, - /// - ActiveDirectoryWorkloadIdentity = 10 - } - /// - public class SqlAuthenticationParameters - { - /// - protected SqlAuthenticationParameters(Microsoft.Data.SqlClient.SqlAuthenticationMethod authenticationMethod, string serverName, string databaseName, string resource, string authority, string userId, string password, System.Guid connectionId, int connectionTimeout) { } - /// - public Microsoft.Data.SqlClient.SqlAuthenticationMethod AuthenticationMethod { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } - /// - public string Authority { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } - /// - public System.Guid ConnectionId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } - /// - public string DatabaseName { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } - /// - public string Password { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } - /// - public string Resource { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } - /// - public string ServerName { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } - /// - public string UserId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } - /// - public int ConnectionTimeout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } - } - /// - public abstract partial class SqlAuthenticationProvider - { - /// - protected SqlAuthenticationProvider() { } - /// - public abstract System.Threading.Tasks.Task AcquireTokenAsync(Microsoft.Data.SqlClient.SqlAuthenticationParameters parameters); - /// - public virtual void BeforeLoad(Microsoft.Data.SqlClient.SqlAuthenticationMethod authenticationMethod) { } - /// - public virtual void BeforeUnload(Microsoft.Data.SqlClient.SqlAuthenticationMethod authenticationMethod) { } - /// - public static Microsoft.Data.SqlClient.SqlAuthenticationProvider GetProvider(Microsoft.Data.SqlClient.SqlAuthenticationMethod authenticationMethod) { throw null; } - /// - public abstract bool IsSupported(Microsoft.Data.SqlClient.SqlAuthenticationMethod authenticationMethod); - /// - public static bool SetProvider(Microsoft.Data.SqlClient.SqlAuthenticationMethod authenticationMethod, Microsoft.Data.SqlClient.SqlAuthenticationProvider provider) { throw null; } - } - /// - public partial class SqlAuthenticationToken - { - /// - public SqlAuthenticationToken(string accessToken, System.DateTimeOffset expiresOn) { } - /// - public string AccessToken { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } - /// - public System.DateTimeOffset ExpiresOn { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } - } /// public sealed partial class SqlBulkCopy : System.IDisposable { diff --git a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.csproj index 42e6f4ca89..52812d835b 100644 --- a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.csproj @@ -32,8 +32,6 @@ - - diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj index 3d602ffe45..d426e18541 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -180,9 +180,6 @@ Microsoft\Data\SqlClient\AAsyncCallContext.cs - - Microsoft\Data\SqlClient\ActiveDirectoryAuthenticationProvider.cs - Microsoft\Data\SqlClient\ActiveDirectoryAuthenticationTimeoutRetryHelper.cs @@ -492,18 +489,12 @@ Microsoft\Data\SqlClient\SqlAppContextSwitchManager.netcore.cs - - Microsoft\Data\SqlClient\SqlAuthenticationParameters.cs - - - Microsoft\Data\SqlClient\SqlAuthenticationProvider.cs + + Microsoft\Data\SqlClient\SqlAuthenticationParametersBuilder.cs Microsoft\Data\SqlClient\SqlAuthenticationProviderManager.cs - - Microsoft\Data\SqlClient\SqlAuthenticationToken.cs - Microsoft\Data\SqlClient\SqlBatch.cs @@ -1071,8 +1062,6 @@ - - diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 9b240cae82..3f8eed9dea 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -18,7 +18,6 @@ using Microsoft.Data.Common.ConnectionString; using Microsoft.Data.ProviderBase; using Microsoft.Data.SqlClient.ConnectionPool; -using Microsoft.Identity.Client; namespace Microsoft.Data.SqlClient { @@ -106,8 +105,6 @@ public void AssertUnrecoverableStateCountIsCorrect() internal sealed class SqlInternalConnectionTds : SqlInternalConnection, IDisposable { - // https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/retry-after#simple-retry-for-errors-with-http-error-codes-500-600 - internal const int MsalHttpRetryStatusCode = 429; // Connection re-route limit internal const int MaxNumberOfRedirectRoute = 10; @@ -134,7 +131,8 @@ internal sealed class SqlInternalConnectionTds : SqlInternalConnection, IDisposa internal bool _federatedAuthenticationInfoReceived; // The Federated Authentication returned by TryGetFedAuthTokenLocked or GetFedAuthToken. - SqlFedAuthToken _fedAuthToken = null; + private SqlFedAuthToken _fedAuthToken = null; + internal byte[] _accessTokenInBytes; internal readonly Func> _accessTokenCallback; internal readonly SspiContextProvider _sspiContextProvider; @@ -229,17 +227,6 @@ internal bool IsDNSCachingBeforeRedirectSupported private DbConnectionPoolAuthenticationContextKey _dbConnectionPoolAuthenticationContextKey; #if DEBUG - // This is a test hook to enable testing of the retry paths for MSAL get access token. - // Sample code to enable: - // - // Type type = typeof(SqlConnection).Assembly.GetType("Microsoft.Data.SqlClient.SQLInternalConnectionTds"); - // System.Reflection.FieldInfo field = type.GetField("_forceMsalRetry", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); - // if (field != null) { - // field.SetValue(null, true); - // } - // - internal static bool _forceMsalRetry = false; - // This is a test hook to simulate a token expiring within the next 45 minutes. private static bool _forceExpiryLocked = false; @@ -760,7 +747,7 @@ protected override bool UnbindOnTransactionCompletion /// /// Validates if federated authentication is used, Access Token used by this connection is active for the value of 'accessTokenExpirationBufferTime'. /// - internal override bool IsAccessTokenExpired => _federatedAuthenticationInfoRequested && DateTime.FromFileTimeUtc(_fedAuthToken.expirationFileTime) < DateTime.UtcNow.AddSeconds(accessTokenExpirationBufferTime); + internal override bool IsAccessTokenExpired => _federatedAuthenticationInfoRequested && DateTime.FromFileTimeUtc(_fedAuthToken.ExpirationFileTime) < DateTime.UtcNow.AddSeconds(accessTokenExpirationBufferTime); //////////////////////////////////////////////////////////////////////////////////////// // GENERAL METHODS @@ -2332,7 +2319,7 @@ internal void OnFedAuthInfo(SqlFedAuthInfo fedAuthInfo) Debug.Assert(_dbConnectionPool.AuthenticationContexts != null); // Construct the dbAuthenticationContextKey with information from FedAuthInfo and store for later use, when inserting in to the token cache. - _dbConnectionPoolAuthenticationContextKey = new DbConnectionPoolAuthenticationContextKey(fedAuthInfo.stsurl, fedAuthInfo.spn); + _dbConnectionPoolAuthenticationContextKey = new DbConnectionPoolAuthenticationContextKey(fedAuthInfo.StsUrl, fedAuthInfo.Spn); // Try to retrieve the authentication context from the pool, if one does exist for this key. if (_dbConnectionPool.AuthenticationContexts.TryGetValue(_dbConnectionPoolAuthenticationContextKey, out dbConnectionPoolAuthenticationContext)) @@ -2421,15 +2408,14 @@ internal void OnFedAuthInfo(SqlFedAuthInfo fedAuthInfo) Debug.Assert(_fedAuthToken == null, "_fedAuthToken should be null in this case."); Debug.Assert(_newDbConnectionPoolAuthenticationContext == null, "_newDbConnectionPoolAuthenticationContext should be null."); - _fedAuthToken = new SqlFedAuthToken(); - // If the code flow is here, then we are re-using the context from the cache for this connection attempt and not // generating a new access token on this thread. - _fedAuthToken.accessToken = dbConnectionPoolAuthenticationContext.AccessToken; - _fedAuthToken.expirationFileTime = dbConnectionPoolAuthenticationContext.ExpirationTime.ToFileTime(); + _fedAuthToken = new( + dbConnectionPoolAuthenticationContext.AccessToken, + dbConnectionPoolAuthenticationContext.ExpirationTime.ToFileTime()); } - Debug.Assert(_fedAuthToken != null && _fedAuthToken.accessToken != null, "_fedAuthToken and _fedAuthToken.accessToken cannot be null."); + Debug.Assert(_fedAuthToken != null && _fedAuthToken.AccessToken != null, "_fedAuthToken and _fedAuthToken.AccessToken cannot be null."); _parser.SendFedAuthToken(_fedAuthToken); } @@ -2489,6 +2475,8 @@ internal bool TryGetFedAuthTokenLocked(SqlFedAuthInfo fedAuthInfo, DbConnectionP return authenticationContextLocked; } + #nullable enable + /// /// Get the Federated Authentication Token. /// @@ -2496,41 +2484,36 @@ internal bool TryGetFedAuthTokenLocked(SqlFedAuthInfo fedAuthInfo, DbConnectionP /// SqlFedAuthToken internal SqlFedAuthToken GetFedAuthToken(SqlFedAuthInfo fedAuthInfo) { - Debug.Assert(fedAuthInfo != null, "fedAuthInfo should not be null."); - - // No:of milliseconds to sleep for the inital back off. - int sleepInterval = 100; - - // No:of attempts, for tracing purposes, if we underwent retries. - int numberOfAttempts = 0; + // Number of milliseconds to sleep for the initial back off, if a + // retry period is not specified by the provider. + const int defaultRetryPeriod = 100; - // Object that will be returned to the caller, containing all required data about the token. - _fedAuthToken = new SqlFedAuthToken(); + // Number of attempts we are willing to perform. + const int maxAttempts = 1; // Username to use in error messages. - string username = null; + string? username = null; - SqlAuthenticationProvider authProvider = SqlAuthenticationProvider.GetProvider(ConnectionOptions.Authentication); + SqlAuthenticationProvider? authProvider = SqlAuthenticationProviderManager.GetProvider(ConnectionOptions.Authentication); if (authProvider == null && _accessTokenCallback == null) { throw SQL.CannotFindAuthProvider(ConnectionOptions.Authentication.ToString()); } - // retry getting access token once if MsalException.error_code is unknown_error. - // extra logic to deal with HTTP 429 (Retry after). - while (numberOfAttempts <= 1) + // We will perform retries if the provider indicates an error that + // is retryable. + for (int attempt = 0; attempt <= maxAttempts; ++attempt) { - numberOfAttempts++; try { - var authParamsBuilder = new SqlAuthenticationParameters.Builder( + var authParamsBuilder = new SqlAuthenticationParametersBuilder( authenticationMethod: ConnectionOptions.Authentication, - resource: fedAuthInfo.spn, - authority: fedAuthInfo.stsurl, + resource: fedAuthInfo.Spn, + authority: fedAuthInfo.StsUrl, serverName: ConnectionOptions.DataSource, databaseName: ConnectionOptions.InitialCatalog) .WithConnectionId(_clientConnectionId) - .WithConnectionTimeout(ConnectionOptions.ConnectTimeout); + .WithAuthenticationTimeout(ConnectionOptions.ConnectTimeout); switch (ConnectionOptions.Authentication) { case SqlAuthenticationMethod.ActiveDirectoryIntegrated: @@ -2554,7 +2537,8 @@ internal SqlFedAuthToken GetFedAuthToken(SqlFedAuthInfo fedAuthInfo) { // We use Task.Run here in all places to execute task synchronously in the same context. // Fixes block-over-async deadlock possibilities https://github.com/dotnet/SqlClient/issues/1209 - _fedAuthToken = Task.Run(async () => await authProvider.AcquireTokenAsync(authParamsBuilder)).GetAwaiter().GetResult().ToSqlFedAuthToken(); + _fedAuthToken = new(Task.Run(async () => await authProvider!.AcquireTokenAsync(authParamsBuilder)).GetAwaiter().GetResult()); + _activeDirectoryAuthTimeoutRetryHelper.CachedToken = _fedAuthToken; } break; @@ -2571,7 +2555,7 @@ internal SqlFedAuthToken GetFedAuthToken(SqlFedAuthInfo fedAuthInfo) else { authParamsBuilder.WithUserId(ConnectionOptions.UserID); - _fedAuthToken = Task.Run(async () => await authProvider.AcquireTokenAsync(authParamsBuilder)).GetAwaiter().GetResult().ToSqlFedAuthToken(); + _fedAuthToken = new(Task.Run(async () => await authProvider!.AcquireTokenAsync(authParamsBuilder)).GetAwaiter().GetResult()); _activeDirectoryAuthTimeoutRetryHelper.CachedToken = _fedAuthToken; } break; @@ -2589,13 +2573,13 @@ internal SqlFedAuthToken GetFedAuthToken(SqlFedAuthInfo fedAuthInfo) { username = _credential.UserId; authParamsBuilder.WithUserId(username).WithPassword(_credential.Password); - _fedAuthToken = Task.Run(async () => await authProvider.AcquireTokenAsync(authParamsBuilder)).GetAwaiter().GetResult().ToSqlFedAuthToken(); + _fedAuthToken = new(Task.Run(async () => await authProvider!.AcquireTokenAsync(authParamsBuilder)).GetAwaiter().GetResult()); } else { username = ConnectionOptions.UserID; authParamsBuilder.WithUserId(username).WithPassword(ConnectionOptions.Password); - _fedAuthToken = Task.Run(async () => await authProvider.AcquireTokenAsync(authParamsBuilder)).GetAwaiter().GetResult().ToSqlFedAuthToken(); + _fedAuthToken = new(Task.Run(async () => await authProvider!.AcquireTokenAsync(authParamsBuilder)).GetAwaiter().GetResult()); } _activeDirectoryAuthTimeoutRetryHelper.CachedToken = _fedAuthToken; } @@ -2629,105 +2613,80 @@ internal SqlFedAuthToken GetFedAuthToken(SqlFedAuthInfo fedAuthInfo) { cts.CancelAfter((int)_timeout.MillisecondsRemaining); } - _fedAuthToken = Task.Run(async () => await _accessTokenCallback(parameters, cts.Token)).GetAwaiter().GetResult().ToSqlFedAuthToken(); + _fedAuthToken = new(Task.Run(async () => await _accessTokenCallback(parameters, cts.Token)).GetAwaiter().GetResult()); _activeDirectoryAuthTimeoutRetryHelper.CachedToken = _fedAuthToken; } break; } - Debug.Assert(_fedAuthToken.accessToken != null, "AccessToken should not be null."); -#if DEBUG - if (_forceMsalRetry) - { - // 3399614468 is 0xCAA20004L just for testing. - throw new MsalServiceException(MsalError.UnknownError, "Force retry in GetFedAuthToken"); - } -#endif + Debug.Assert(_fedAuthToken.AccessToken != null, "AccessToken should not be null."); + // Break out of the retry loop in successful case. break; } - // Deal with Msal service exceptions first, retry if 429 received. - catch (MsalServiceException serviceException) + catch (SqlAuthenticationProviderException ex) { - if (serviceException.StatusCode == MsalHttpRetryStatusCode) + // Is the error fatal or retryable? + if (!ex.ShouldRetry) { - RetryConditionHeaderValue retryAfter = serviceException.Headers.RetryAfter; - if (retryAfter.Delta.HasValue) - { - sleepInterval = retryAfter.Delta.Value.Milliseconds; - } - else if (retryAfter.Date.HasValue) - { - sleepInterval = Convert.ToInt32(retryAfter.Date.Value.Offset.TotalMilliseconds); - } - - // if there's enough time to retry before timeout, then retry, otherwise break out the retry loop. - if (sleepInterval < _timeout.MillisecondsRemaining) - { - Thread.Sleep(sleepInterval); - } - else - { - SqlClientEventSource.Log.TryTraceEvent(" Timeout: {0}", serviceException.ErrorCode); - throw SQL.ActiveDirectoryTokenRetrievingTimeout(Enum.GetName(typeof(SqlAuthenticationMethod), ConnectionOptions.Authentication), serviceException.ErrorCode, serviceException); - } + // It's fatal, so translate into a SqlException. + throw ADP.CreateSqlException(ex, ConnectionOptions, this, username); } - else + + // We should retry. + + // Could we retry if we wanted to? + if (_timeout.IsExpired || _timeout.MillisecondsRemaining <= 0) { - SqlClientEventSource.Log.TryTraceEvent(" {0}", serviceException.ErrorCode); - throw ADP.CreateSqlException(serviceException, ConnectionOptions, this, username); + // No, so we throw. + SqlClientEventSource.Log.TryTraceEvent(" Attempt: {0}, Timeout: {1}", attempt, ex.FailureCode); + throw SQL.ActiveDirectoryTokenRetrievingTimeout(Enum.GetName(typeof(SqlAuthenticationMethod), ConnectionOptions.Authentication), ex.FailureCode, ex); } - } - // Deal with normal MsalExceptions. - catch (MsalException msalException) - { - if (MsalError.UnknownError != msalException.ErrorCode || _timeout.IsExpired || _timeout.MillisecondsRemaining <= sleepInterval) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}", msalException.ErrorCode); - throw ADP.CreateSqlException(msalException, ConnectionOptions, this, username); - } + // We use a doubling backoff if the provider didn't provide + // a retry period. + int retryPeriod = + ex.RetryPeriod > 0 + ? ex.RetryPeriod + : defaultRetryPeriod * (2 ^ attempt); - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, sleeping {1}[Milliseconds]", ObjectID, sleepInterval); - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, remaining {1}[Milliseconds]", ObjectID, _timeout.MillisecondsRemaining); + SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Attempt: {1}, sleeping {2}[Milliseconds]", ObjectID, attempt, retryPeriod); + SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Attempt: {1}, remaining {2}[Milliseconds]", ObjectID, attempt, _timeout.MillisecondsRemaining); - Thread.Sleep(sleepInterval); - sleepInterval *= 2; - } - // All other exceptions from MSAL/Azure Identity APIs - catch (Exception e) - { - throw SqlException.CreateException( - new() - { - new( - 0, - (byte)0x00, - (byte)TdsEnums.FATAL_ERROR_CLASS, - ConnectionOptions.DataSource, - e.Message, - ActiveDirectoryAuthentication.MSALGetAccessTokenFunctionName, - 0) - }, - "", - this, - e); + // Sleep for the desired period. + Thread.Sleep(retryPeriod); + + // Fall through to retry... } } - Debug.Assert(_fedAuthToken != null, "fedAuthToken should not be null."); - Debug.Assert(_fedAuthToken.accessToken != null && _fedAuthToken.accessToken.Length > 0, "fedAuthToken.accessToken should not be null or empty."); + // Nullable context has exposed that _fedAuthToken may be null here, + // which the existing code didn't handle. + if (_fedAuthToken is null) + { + // We failed to acquire a token, so use a default one. + // + // TODO: The old code actually allowed the AccessToken byte + // array to be null, and then had Debug.Assert()s to verify it + // wasn't null. We never test in debug, so those were never + // firing, and we were happily using a _fedAuthToken with a null + // AccessToken. Now that SqlFedAuthToken doesn't allow a null + // AccessToken, we just create an empty one instead. + _fedAuthToken = new SqlFedAuthToken([], 0); + } // Store the newly generated token in _newDbConnectionPoolAuthenticationContext, only if using pooling. if (_dbConnectionPool != null) { - DateTime expirationTime = DateTime.FromFileTimeUtc(_fedAuthToken.expirationFileTime); - _newDbConnectionPoolAuthenticationContext = new DbConnectionPoolAuthenticationContext(_fedAuthToken.accessToken, expirationTime); + DateTime expirationTime = DateTime.FromFileTimeUtc(_fedAuthToken.ExpirationFileTime); + _newDbConnectionPoolAuthenticationContext = new DbConnectionPoolAuthenticationContext(_fedAuthToken.AccessToken, expirationTime); } SqlClientEventSource.Log.TryTraceEvent(" {0}, Finished generating federated authentication token.", ObjectID); return _fedAuthToken; } + #nullable disable + internal void OnFeatureExtAck(int featureId, byte[] data) { if (RoutingInfo != null && featureId != TdsEnums.FEATUREEXT_SQLDNSCACHING) diff --git a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs index e662fc0560..30816757eb 100644 --- a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs +++ b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs @@ -57,32 +57,6 @@ public sealed class SqlDataSourceEnumerator : System.Data.Common.DbDataSourceEnu namespace Microsoft.Data.SqlClient { - /// - public sealed class ActiveDirectoryAuthenticationProvider : SqlAuthenticationProvider - { - /// - public ActiveDirectoryAuthenticationProvider() { } - /// - public ActiveDirectoryAuthenticationProvider(string applicationClientId) { } - /// - public static void ClearUserTokenCache() { } - /// - public ActiveDirectoryAuthenticationProvider(System.Func deviceCodeFlowCallbackMethod, string applicationClientId = null) { } - /// - public override System.Threading.Tasks.Task AcquireTokenAsync(SqlAuthenticationParameters parameters) { throw null; } - /// - public void SetDeviceCodeFlowCallback(System.Func deviceCodeFlowCallbackMethod) { } - /// - public void SetAcquireAuthorizationCodeAsyncCallback(System.Func> acquireAuthorizationCodeAsyncCallback) { } - /// - public void SetIWin32WindowFunc(System.Func iWin32WindowFunc) { } - /// - public override bool IsSupported(SqlAuthenticationMethod authentication) { throw null; } - /// - public override void BeforeLoad(SqlAuthenticationMethod authentication) { } - /// - public override void BeforeUnload(SqlAuthenticationMethod authentication) { } - } /// public enum ApplicationIntent { @@ -122,85 +96,6 @@ protected SqlAuthenticationInitializer() { } /// public abstract void Initialize(); } - /// - public enum SqlAuthenticationMethod - { - /// - NotSpecified = 0, - /// - SqlPassword = 1, - /// - [System.ObsoleteAttribute("ActiveDirectoryPassword is deprecated, use a more secure authentication method. See https://aka.ms/SqlClientEntraIDAuthentication for more details.")] - ActiveDirectoryPassword = 2, - /// - ActiveDirectoryIntegrated = 3, - /// - ActiveDirectoryInteractive = 4, - /// - ActiveDirectoryServicePrincipal = 5, - /// - ActiveDirectoryDeviceCodeFlow = 6, - /// - ActiveDirectoryManagedIdentity = 7, - /// - ActiveDirectoryMSI = 8, - /// - ActiveDirectoryDefault = 9, - /// - ActiveDirectoryWorkloadIdentity = 10 - } - /// - public class SqlAuthenticationParameters - { - /// - protected SqlAuthenticationParameters(Microsoft.Data.SqlClient.SqlAuthenticationMethod authenticationMethod, string serverName, string databaseName, string resource, string authority, string userId, string password, System.Guid connectionId, int connectionTimeout) { } - /// - public Microsoft.Data.SqlClient.SqlAuthenticationMethod AuthenticationMethod { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - /// - public string Authority { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - /// - public System.Guid ConnectionId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - /// - public string DatabaseName { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - /// - public string Password { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - /// - public string Resource { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - /// - public string ServerName { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - /// - public string UserId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - /// - public int ConnectionTimeout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - } - /// - public abstract partial class SqlAuthenticationProvider - { - /// - protected SqlAuthenticationProvider() { } - /// - public abstract System.Threading.Tasks.Task AcquireTokenAsync(Microsoft.Data.SqlClient.SqlAuthenticationParameters parameters); - /// - public virtual void BeforeLoad(Microsoft.Data.SqlClient.SqlAuthenticationMethod authenticationMethod) { } - /// - public virtual void BeforeUnload(Microsoft.Data.SqlClient.SqlAuthenticationMethod authenticationMethod) { } - /// - public static Microsoft.Data.SqlClient.SqlAuthenticationProvider GetProvider(Microsoft.Data.SqlClient.SqlAuthenticationMethod authenticationMethod) { throw null; } - /// - public abstract bool IsSupported(Microsoft.Data.SqlClient.SqlAuthenticationMethod authenticationMethod); - /// - public static bool SetProvider(Microsoft.Data.SqlClient.SqlAuthenticationMethod authenticationMethod, Microsoft.Data.SqlClient.SqlAuthenticationProvider provider) { throw null; } - } - /// - public partial class SqlAuthenticationToken - { - /// - public SqlAuthenticationToken(string accessToken, System.DateTimeOffset expiresOn) { } - /// - public string AccessToken { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - /// - public System.DateTimeOffset ExpiresOn { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - } /// public sealed partial class SqlBulkCopy : System.IDisposable { diff --git a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.csproj index f73d833d7f..e8131c3022 100644 --- a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.csproj @@ -32,8 +32,6 @@ - - All diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj index a84e34adeb..67887402f1 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -462,9 +462,6 @@ Microsoft\Data\SqlClient\AssemblyRef.cs - - Microsoft\Data\SqlClient\ActiveDirectoryAuthenticationProvider.cs - Microsoft\Data\SqlClient\AlwaysEncryptedEnclaveProviderUtils.cs @@ -663,11 +660,8 @@ Microsoft\Data\SqlClient\SqlAeadAes256CbcHmac256Factory.cs - - Microsoft\Data\SqlClient\SqlAuthenticationParameters.cs - - - Microsoft\Data\SqlClient\SqlAuthenticationProvider.cs + + Microsoft\Data\SqlClient\SqlAuthenticationParametersBuilder.cs Microsoft\Data\SqlClient\SqlAuthenticationProviderManager.cs @@ -678,9 +672,6 @@ Microsoft\Data\SqlClient\SqlBulkCopy.cs - - Microsoft\Data\SqlClient\SqlAuthenticationToken.cs - Microsoft\Data\SqlClient\SqlBatchCommand.cs @@ -1044,8 +1035,6 @@ - - All diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 037ab671bb..8b8e7e4f88 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -19,7 +19,6 @@ using Microsoft.Data.Common.ConnectionString; using Microsoft.Data.ProviderBase; using Microsoft.Data.SqlClient.ConnectionPool; -using Microsoft.Identity.Client; namespace Microsoft.Data.SqlClient { @@ -107,8 +106,6 @@ public void AssertUnrecoverableStateCountIsCorrect() internal sealed class SqlInternalConnectionTds : SqlInternalConnection, IDisposable { - // https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/retry-after#simple-retry-for-errors-with-http-error-codes-500-600 - internal const int MsalHttpRetryStatusCode = 429; // Connection re-route limit internal const int MaxNumberOfRedirectRoute = 10; @@ -230,17 +227,6 @@ internal bool IsDNSCachingBeforeRedirectSupported private DbConnectionPoolAuthenticationContextKey _dbConnectionPoolAuthenticationContextKey; #if DEBUG - // This is a test hook to enable testing of the retry paths for MSAL get access token. - // Sample code to enable: - // - // Type type = typeof(SqlConnection).Assembly.GetType("Microsoft.Data.SqlClient.SQLInternalConnectionTds"); - // System.Reflection.FieldInfo field = type.GetField("_forceMsalRetry", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); - // if (field != null) { - // field.SetValue(null, true); - // } - // - internal static bool _forceMsalRetry = false; - // This is a test hook to simulate a token expiring within the next 45 minutes. private static bool _forceExpiryLocked = false; @@ -770,7 +756,7 @@ protected override bool UnbindOnTransactionCompletion /// /// Validates if federated authentication is used, Access Token used by this connection is active for the value of 'accessTokenExpirationBufferTime'. /// - internal override bool IsAccessTokenExpired => _federatedAuthenticationInfoRequested && DateTime.FromFileTimeUtc(_fedAuthToken.expirationFileTime) < DateTime.UtcNow.AddSeconds(accessTokenExpirationBufferTime); + internal override bool IsAccessTokenExpired => _federatedAuthenticationInfoRequested && DateTime.FromFileTimeUtc(_fedAuthToken.ExpirationFileTime) < DateTime.UtcNow.AddSeconds(accessTokenExpirationBufferTime); //////////////////////////////////////////////////////////////////////////////////////// // GENERAL METHODS @@ -2389,7 +2375,7 @@ internal void OnFedAuthInfo(SqlFedAuthInfo fedAuthInfo) Debug.Assert(_dbConnectionPool.AuthenticationContexts != null); // Construct the dbAuthenticationContextKey with information from FedAuthInfo and store for later use, when inserting in to the token cache. - _dbConnectionPoolAuthenticationContextKey = new DbConnectionPoolAuthenticationContextKey(fedAuthInfo.stsurl, fedAuthInfo.spn); + _dbConnectionPoolAuthenticationContextKey = new DbConnectionPoolAuthenticationContextKey(fedAuthInfo.StsUrl, fedAuthInfo.Spn); // Try to retrieve the authentication context from the pool, if one does exist for this key. if (_dbConnectionPool.AuthenticationContexts.TryGetValue(_dbConnectionPoolAuthenticationContextKey, out dbConnectionPoolAuthenticationContext)) @@ -2478,15 +2464,14 @@ internal void OnFedAuthInfo(SqlFedAuthInfo fedAuthInfo) Debug.Assert(_fedAuthToken == null, "_fedAuthToken should be null in this case."); Debug.Assert(_newDbConnectionPoolAuthenticationContext == null, "_newDbConnectionPoolAuthenticationContext should be null."); - _fedAuthToken = new SqlFedAuthToken(); - // If the code flow is here, then we are re-using the context from the cache for this connection attempt and not // generating a new access token on this thread. - _fedAuthToken.accessToken = dbConnectionPoolAuthenticationContext.AccessToken; - _fedAuthToken.expirationFileTime = dbConnectionPoolAuthenticationContext.ExpirationTime.ToFileTime(); + _fedAuthToken = new( + dbConnectionPoolAuthenticationContext.AccessToken, + dbConnectionPoolAuthenticationContext.ExpirationTime.ToFileTime()); } - Debug.Assert(_fedAuthToken != null && _fedAuthToken.accessToken != null, "_fedAuthToken and _fedAuthToken.accessToken cannot be null."); + Debug.Assert(_fedAuthToken != null && _fedAuthToken.AccessToken != null, "_fedAuthToken and _fedAuthToken.AccessToken cannot be null."); _parser.SendFedAuthToken(_fedAuthToken); } @@ -2546,6 +2531,8 @@ internal bool TryGetFedAuthTokenLocked(SqlFedAuthInfo fedAuthInfo, DbConnectionP return authenticationContextLocked; } + #nullable enable + /// /// Get the Federated Authentication Token. /// @@ -2553,41 +2540,36 @@ internal bool TryGetFedAuthTokenLocked(SqlFedAuthInfo fedAuthInfo, DbConnectionP /// SqlFedAuthToken internal SqlFedAuthToken GetFedAuthToken(SqlFedAuthInfo fedAuthInfo) { - Debug.Assert(fedAuthInfo != null, "fedAuthInfo should not be null."); - - // No:of milliseconds to sleep for the inital back off. - int sleepInterval = 100; - - // No:of attempts, for tracing purposes, if we underwent retries. - int numberOfAttempts = 0; + // Number of milliseconds to sleep for the initial back off, if a + // retry period is not specified by the provider. + const int defaultRetryPeriod = 100; - // Object that will be returned to the caller, containing all required data about the token. - _fedAuthToken = new SqlFedAuthToken(); + // Number of retry attempts we are willing to perform. + const int maxAttempts = 1; // Username to use in error messages. - string username = null; + string? username = null; - SqlAuthenticationProvider authProvider = SqlAuthenticationProvider.GetProvider(ConnectionOptions.Authentication); + SqlAuthenticationProvider? authProvider = SqlAuthenticationProviderManager.GetProvider(ConnectionOptions.Authentication); if (authProvider == null && _accessTokenCallback == null) { throw SQL.CannotFindAuthProvider(ConnectionOptions.Authentication.ToString()); } - // retry getting access token once if MsalException.error_code is unknown_error. - // extra logic to deal with HTTP 429 (Retry after). - while (numberOfAttempts <= 1 && sleepInterval <= _timeout.MillisecondsRemaining) + // We will perform retries if the provider indicates an error that + // is retryable. + for (int attempt = 0; attempt < maxAttempts; ++attempt) { - numberOfAttempts++; try { - var authParamsBuilder = new SqlAuthenticationParameters.Builder( + var authParamsBuilder = new SqlAuthenticationParametersBuilder( authenticationMethod: ConnectionOptions.Authentication, - resource: fedAuthInfo.spn, - authority: fedAuthInfo.stsurl, + resource: fedAuthInfo.Spn, + authority: fedAuthInfo.StsUrl, serverName: ConnectionOptions.DataSource, databaseName: ConnectionOptions.InitialCatalog) .WithConnectionId(_clientConnectionId) - .WithConnectionTimeout(ConnectionOptions.ConnectTimeout); + .WithAuthenticationTimeout(ConnectionOptions.ConnectTimeout); switch (ConnectionOptions.Authentication) { case SqlAuthenticationMethod.ActiveDirectoryIntegrated: @@ -2600,7 +2582,10 @@ internal SqlFedAuthToken GetFedAuthToken(SqlFedAuthInfo fedAuthInfo) { // We use Task.Run here in all places to execute task synchronously in the same context. // Fixes block-over-async deadlock possibilities https://github.com/dotnet/SqlClient/issues/1209 - _fedAuthToken = Task.Run(async () => await authProvider.AcquireTokenAsync(authParamsBuilder)).GetAwaiter().GetResult().ToSqlFedAuthToken(); + // + // TODO: Remove this async stuff and the '!' + // when we have a sync API on the auth provider. + _fedAuthToken = new(Task.Run(async () => await authProvider!.AcquireTokenAsync(authParamsBuilder)).GetAwaiter().GetResult()); _activeDirectoryAuthTimeoutRetryHelper.CachedToken = _fedAuthToken; } break; @@ -2617,7 +2602,7 @@ internal SqlFedAuthToken GetFedAuthToken(SqlFedAuthInfo fedAuthInfo) else { authParamsBuilder.WithUserId(ConnectionOptions.UserID); - _fedAuthToken = Task.Run(async () => await authProvider.AcquireTokenAsync(authParamsBuilder)).GetAwaiter().GetResult().ToSqlFedAuthToken(); + _fedAuthToken = new(Task.Run(async () => await authProvider!.AcquireTokenAsync(authParamsBuilder)).GetAwaiter().GetResult()); _activeDirectoryAuthTimeoutRetryHelper.CachedToken = _fedAuthToken; } break; @@ -2635,13 +2620,13 @@ internal SqlFedAuthToken GetFedAuthToken(SqlFedAuthInfo fedAuthInfo) { username = _credential.UserId; authParamsBuilder.WithUserId(username).WithPassword(_credential.Password); - _fedAuthToken = Task.Run(async () => await authProvider.AcquireTokenAsync(authParamsBuilder)).GetAwaiter().GetResult().ToSqlFedAuthToken(); + _fedAuthToken = new(Task.Run(async () => await authProvider!.AcquireTokenAsync(authParamsBuilder)).GetAwaiter().GetResult()); } else { username = ConnectionOptions.UserID; authParamsBuilder.WithUserId(username).WithPassword(ConnectionOptions.Password); - _fedAuthToken = Task.Run(async () => await authProvider.AcquireTokenAsync(authParamsBuilder)).GetAwaiter().GetResult().ToSqlFedAuthToken(); + _fedAuthToken = new(Task.Run(async () => await authProvider!.AcquireTokenAsync(authParamsBuilder)).GetAwaiter().GetResult()); } _activeDirectoryAuthTimeoutRetryHelper.CachedToken = _fedAuthToken; } @@ -2675,105 +2660,80 @@ internal SqlFedAuthToken GetFedAuthToken(SqlFedAuthInfo fedAuthInfo) { cts.CancelAfter((int)_timeout.MillisecondsRemaining); } - _fedAuthToken = Task.Run(async () => await _accessTokenCallback(parameters, cts.Token)).GetAwaiter().GetResult().ToSqlFedAuthToken(); + _fedAuthToken = new(Task.Run(async () => await _accessTokenCallback(parameters, cts.Token)).GetAwaiter().GetResult()); _activeDirectoryAuthTimeoutRetryHelper.CachedToken = _fedAuthToken; } break; } - Debug.Assert(_fedAuthToken.accessToken != null, "AccessToken should not be null."); -#if DEBUG - if (_forceMsalRetry) - { - // 3399614468 is 0xCAA20004L just for testing. - throw new MsalServiceException(MsalError.UnknownError, "Force retry in GetFedAuthToken"); - } -#endif + Debug.Assert(_fedAuthToken.AccessToken != null, "AccessToken should not be null."); + // Break out of the retry loop in successful case. break; } - // Deal with Msal service exceptions first, retry if 429 received. - catch (MsalServiceException serviceException) + catch (SqlAuthenticationProviderException ex) { - if (serviceException.StatusCode == MsalHttpRetryStatusCode) + // Is the error fatal or retryable? + if (!ex.ShouldRetry) { - RetryConditionHeaderValue retryAfter = serviceException.Headers.RetryAfter; - if (retryAfter.Delta.HasValue) - { - sleepInterval = retryAfter.Delta.Value.Milliseconds; - } - else if (retryAfter.Date.HasValue) - { - sleepInterval = Convert.ToInt32(retryAfter.Date.Value.Offset.TotalMilliseconds); - } - - // if there's enough time to retry before timeout, then retry, otherwise break out the retry loop. - if (sleepInterval < _timeout.MillisecondsRemaining) - { - Thread.Sleep(sleepInterval); - } - else - { - SqlClientEventSource.Log.TryTraceEvent(" Timeout: {0}", serviceException.ErrorCode); - throw SQL.ActiveDirectoryTokenRetrievingTimeout(Enum.GetName(typeof(SqlAuthenticationMethod), ConnectionOptions.Authentication), serviceException.ErrorCode, serviceException); - } + // It's fatal, so translate into a SqlException. + throw ADP.CreateSqlException(ex, ConnectionOptions, this, username); } - else + + // We should retry. + + // Could we retry if we wanted to? + if (_timeout.IsExpired || _timeout.MillisecondsRemaining <= 0) { - SqlClientEventSource.Log.TryTraceEvent(" {0}", serviceException.ErrorCode); - throw ADP.CreateSqlException(serviceException, ConnectionOptions, this, username); + // No, so we throw. + SqlClientEventSource.Log.TryTraceEvent(" Attempt: {0}, Timeout: {1}", attempt, ex.FailureCode); + throw SQL.ActiveDirectoryTokenRetrievingTimeout(Enum.GetName(typeof(SqlAuthenticationMethod), ConnectionOptions.Authentication), ex.FailureCode, ex); } - } - // Deal with normal MsalExceptions. - catch (MsalException msalException) - { - if (MsalError.UnknownError != msalException.ErrorCode || _timeout.IsExpired || _timeout.MillisecondsRemaining <= sleepInterval) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}", msalException.ErrorCode); - throw ADP.CreateSqlException(msalException, ConnectionOptions, this, username); - } + // We use a doubling backoff if the provider didn't provide + // a retry period. + int retryPeriod = + ex.RetryPeriod > 0 + ? ex.RetryPeriod + : defaultRetryPeriod * (2 ^ attempt); - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, sleeping {1}[Milliseconds]", ObjectID, sleepInterval); - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, remaining {1}[Milliseconds]", ObjectID, _timeout.MillisecondsRemaining); + SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Attempt: {1}, sleeping {2}[Milliseconds]", ObjectID, attempt, retryPeriod); + SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Attempt: {1}, remaining {2}[Milliseconds]", ObjectID, attempt, _timeout.MillisecondsRemaining); - Thread.Sleep(sleepInterval); - sleepInterval *= 2; - } - // All other exceptions from MSAL/Azure Identity APIs - catch (Exception e) - { - throw SqlException.CreateException( - new() - { - new( - 0, - (byte)0x00, - (byte)TdsEnums.FATAL_ERROR_CLASS, - ConnectionOptions.DataSource, - e.Message, - ActiveDirectoryAuthentication.MSALGetAccessTokenFunctionName, - 0) - }, - "", - this, - e); + // Sleep for the desired period. + Thread.Sleep(retryPeriod); + + // Fall through to retry... } } - Debug.Assert(_fedAuthToken != null, "fedAuthToken should not be null."); - Debug.Assert(_fedAuthToken.accessToken != null && _fedAuthToken.accessToken.Length > 0, "fedAuthToken.accessToken should not be null or empty."); + // Nullable context has exposed that _fedAuthToken may be null here, + // which the existing code didn't handle. + if (_fedAuthToken is null) + { + // We failed to acquire a token, so use a default one. + // + // TODO: The old code actually allowed the AccessToken byte + // array to be null, and then had Debug.Assert()s to verify it + // wasn't null. We never test in debug, so those were never + // firing, and we were happily using a _fedAuthToken with a null + // AccessToken. Now that SqlFedAuthToken doesn't allow a null + // AccessToken, we just create an empty one instead. + _fedAuthToken = new SqlFedAuthToken([], 0); + } // Store the newly generated token in _newDbConnectionPoolAuthenticationContext, only if using pooling. if (_dbConnectionPool != null) { - DateTime expirationTime = DateTime.FromFileTimeUtc(_fedAuthToken.expirationFileTime); - _newDbConnectionPoolAuthenticationContext = new DbConnectionPoolAuthenticationContext(_fedAuthToken.accessToken, expirationTime); + DateTime expirationTime = DateTime.FromFileTimeUtc(_fedAuthToken.ExpirationFileTime); + _newDbConnectionPoolAuthenticationContext = new DbConnectionPoolAuthenticationContext(_fedAuthToken.AccessToken, expirationTime); } SqlClientEventSource.Log.TryTraceEvent(" {0}, Finished generating federated authentication token.", ObjectID); return _fedAuthToken; } + #nullable disable + internal void OnFeatureExtAck(int featureId, byte[] data) { if (RoutingInfo != null && featureId != TdsEnums.FEATUREEXT_SQLDNSCACHING) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj index 75b9377df2..a2e8d482bf 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj @@ -8,8 +8,6 @@ - - diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs index b37f97ac17..961c50e9ed 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs @@ -21,7 +21,6 @@ using Microsoft.Data.Common.ConnectionString; using Microsoft.Data.SqlClient; using IsolationLevel = System.Data.IsolationLevel; -using Microsoft.Identity.Client; using Microsoft.SqlServer.Server; using System.Security.Authentication; using System.Collections.Generic; @@ -465,7 +464,7 @@ internal static ArgumentException InvalidArgumentLength(string argumentName, int internal static ArgumentException MustBeReadOnly(string argumentName) => Argument(StringsHelper.GetString(Strings.ADP_MustBeReadOnly, argumentName)); - internal static Exception CreateSqlException(MsalException msalException, SqlConnectionString connectionOptions, SqlInternalConnectionTds sender, string username) + internal static Exception CreateSqlException(SqlAuthenticationProviderException authException, SqlConnectionString connectionOptions, SqlInternalConnectionTds sender, string username) { // Error[0] SqlErrorCollection sqlErs = new(); @@ -473,20 +472,20 @@ internal static Exception CreateSqlException(MsalException msalException, SqlCon sqlErs.Add(new SqlError(0, (byte)0x00, (byte)TdsEnums.MIN_ERROR_CLASS, connectionOptions.DataSource, StringsHelper.GetString(Strings.SQL_MSALFailure, username, connectionOptions.Authentication.ToString("G")), - ActiveDirectoryAuthentication.MSALGetAccessTokenFunctionName, 0)); + authException.Method.ToString(), 0)); // Error[1] - string errorMessage1 = StringsHelper.GetString(Strings.SQL_MSALInnerException, msalException.ErrorCode); + string errorMessage1 = StringsHelper.GetString(Strings.SQL_MSALInnerException, authException.FailureCode); sqlErs.Add(new SqlError(0, (byte)0x00, (byte)TdsEnums.MIN_ERROR_CLASS, - connectionOptions.DataSource, errorMessage1, - ActiveDirectoryAuthentication.MSALGetAccessTokenFunctionName, 0)); + connectionOptions.DataSource, errorMessage1, + authException.Method.ToString(), 0)); // Error[2] - if (!string.IsNullOrEmpty(msalException.Message)) + if (!string.IsNullOrEmpty(authException.Message)) { sqlErs.Add(new SqlError(0, (byte)0x00, (byte)TdsEnums.MIN_ERROR_CLASS, - connectionOptions.DataSource, msalException.Message, - ActiveDirectoryAuthentication.MSALGetAccessTokenFunctionName, 0)); + connectionOptions.DataSource, authException.Message, + authException.Method.ToString(), 0)); } return SqlException.CreateException(sqlErs, "", sender, innerException: null, batchCommand: null); } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ActiveDirectoryAuthenticationProvider.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ActiveDirectoryAuthenticationProvider.cs deleted file mode 100644 index 0393de9beb..0000000000 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ActiveDirectoryAuthenticationProvider.cs +++ /dev/null @@ -1,743 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Concurrent; -using System.Security.Cryptography; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Azure.Core; -using Azure.Identity; -using Microsoft.Data.Common.ConnectionString; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Identity.Client; -using Microsoft.Identity.Client.Extensibility; - -namespace Microsoft.Data.SqlClient -{ - /// - public sealed class ActiveDirectoryAuthenticationProvider : SqlAuthenticationProvider - { - /// - /// This is a static cache instance meant to hold instances of "PublicClientApplication" mapping to information available in PublicClientAppKey. - /// The purpose of this cache is to allow re-use of Access Tokens fetched for a user interactively or with any other mode - /// to avoid interactive authentication request every-time, within application scope making use of MSAL's userTokenCache. - /// - private static readonly ConcurrentDictionary s_pcaMap = new(); - private static readonly ConcurrentDictionary s_tokenCredentialMap = new(); - private static SemaphoreSlim s_pcaMapModifierSemaphore = new(1, 1); - private static SemaphoreSlim s_tokenCredentialMapModifierSemaphore = new(1, 1); - private static readonly MemoryCache s_accountPwCache = new MemoryCache(new MemoryCacheOptions()); - private static readonly int s_accountPwCacheTtlInHours = 2; - private static readonly string s_nativeClientRedirectUri = "https://login.microsoftonline.com/common/oauth2/nativeclient"; - private static readonly string s_defaultScopeSuffix = "/.default"; - private readonly string _type = typeof(ActiveDirectoryAuthenticationProvider).Name; - private readonly SqlClientLogger _logger = new(); - private Func _deviceCodeFlowCallback; - private ICustomWebUi _customWebUI = null; - private readonly string _applicationClientId = ActiveDirectoryAuthentication.AdoClientId; - - /// - public ActiveDirectoryAuthenticationProvider() - : this(DefaultDeviceFlowCallback) - { - } - - /// - public ActiveDirectoryAuthenticationProvider(string applicationClientId) - : this(DefaultDeviceFlowCallback, applicationClientId) - { - } - - /// - public ActiveDirectoryAuthenticationProvider(Func deviceCodeFlowCallbackMethod, string applicationClientId = null) - { - if (applicationClientId != null) - { - _applicationClientId = applicationClientId; - } - SetDeviceCodeFlowCallback(deviceCodeFlowCallbackMethod); - } - - /// - public static void ClearUserTokenCache() - { - if (!s_pcaMap.IsEmpty) - { - s_pcaMap.Clear(); - } - - if (!s_tokenCredentialMap.IsEmpty) - { - s_tokenCredentialMap.Clear(); - } - } - - /// - public void SetDeviceCodeFlowCallback(Func deviceCodeFlowCallbackMethod) => _deviceCodeFlowCallback = deviceCodeFlowCallbackMethod; - - /// - public void SetAcquireAuthorizationCodeAsyncCallback(Func> acquireAuthorizationCodeAsyncCallback) => _customWebUI = new CustomWebUi(acquireAuthorizationCodeAsyncCallback); - - /// - public override bool IsSupported(SqlAuthenticationMethod authentication) - { - return authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated - #pragma warning disable 0618 // Type or member is obsolete - || authentication == SqlAuthenticationMethod.ActiveDirectoryPassword - #pragma warning restore 0618 // Type or member is obsolete - || authentication == SqlAuthenticationMethod.ActiveDirectoryInteractive - || authentication == SqlAuthenticationMethod.ActiveDirectoryServicePrincipal - || authentication == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow - || authentication == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity - || authentication == SqlAuthenticationMethod.ActiveDirectoryMSI - || authentication == SqlAuthenticationMethod.ActiveDirectoryDefault - || authentication == SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity; - } - - /// - public override void BeforeLoad(SqlAuthenticationMethod authentication) - { - _logger.LogInfo(_type, "BeforeLoad", $"being loaded into SqlAuthProviders for {authentication}."); - } - - /// - public override void BeforeUnload(SqlAuthenticationMethod authentication) - { - _logger.LogInfo(_type, "BeforeUnload", $"being unloaded from SqlAuthProviders for {authentication}."); - } - -#if NETFRAMEWORK - private Func _iWin32WindowFunc = null; - - /// - public void SetIWin32WindowFunc(Func iWin32WindowFunc) => this._iWin32WindowFunc = iWin32WindowFunc; -#endif - - /// - - public override async Task AcquireTokenAsync(SqlAuthenticationParameters parameters) - { - using CancellationTokenSource cts = new(); - - // Use Connection timeout value to cancel token acquire request after certain period of time. - int timeout = parameters.ConnectionTimeout * 1000; // Convert to milliseconds - if (timeout > 0) // if ConnectionTimeout is 0 or the millis overflows an int, no need to set CancelAfter - { - cts.CancelAfter(timeout); - } - - string scope = parameters.Resource.EndsWith(s_defaultScopeSuffix, StringComparison.Ordinal) ? parameters.Resource : parameters.Resource + s_defaultScopeSuffix; - string[] scopes = new string[] { scope }; - TokenRequestContext tokenRequestContext = new(scopes); - - /* We split audience from Authority URL here. Audience can be one of the following: - * The Azure AD authority audience enumeration - * The tenant ID, which can be: - * - A GUID (the ID of your Azure AD instance), for single-tenant applications - * - A domain name associated with your Azure AD instance (also for single-tenant applications) - * One of these placeholders as a tenant ID in place of the Azure AD authority audience enumeration: - * - `organizations` for a multitenant application - * - `consumers` to sign in users only with their personal accounts - * - `common` to sign in users with their work and school accounts or their personal Microsoft accounts - * - * MSAL will throw a meaningful exception if you specify both the Azure AD authority audience and the tenant ID. - * If you don't specify an audience, your app will target Azure AD and personal Microsoft accounts as an audience. (That is, it will behave as though `common` were specified.) - * More information: https://docs.microsoft.com/azure/active-directory/develop/msal-client-application-configuration - **/ - - int separatorIndex = parameters.Authority.LastIndexOf('/'); - string authority = parameters.Authority.Remove(separatorIndex + 1); - string audience = parameters.Authority.Substring(separatorIndex + 1); - string clientId = string.IsNullOrWhiteSpace(parameters.UserId) ? null : parameters.UserId; - - if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryDefault) - { - // Cache DefaultAzureCredenial based on scope, authority, audience, and clientId - TokenCredentialKey tokenCredentialKey = new(typeof(DefaultAzureCredential), authority, scope, audience, clientId); - AccessToken accessToken = await GetTokenAsync(tokenCredentialKey, string.Empty, tokenRequestContext, cts.Token).ConfigureAwait(false); - SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token for Default auth mode. Expiry Time: {0}", accessToken.ExpiresOn); - return new SqlAuthenticationToken(accessToken.Token, accessToken.ExpiresOn); - } - - TokenCredentialOptions tokenCredentialOptions = new() { AuthorityHost = new Uri(authority) }; - - if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity || parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryMSI) - { - // Cache ManagedIdentityCredential based on scope, authority, and clientId - TokenCredentialKey tokenCredentialKey = new(typeof(ManagedIdentityCredential), authority, scope, string.Empty, clientId); - AccessToken accessToken = await GetTokenAsync(tokenCredentialKey, string.Empty, tokenRequestContext, cts.Token).ConfigureAwait(false); - SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token for Managed Identity auth mode. Expiry Time: {0}", accessToken.ExpiresOn); - return new SqlAuthenticationToken(accessToken.Token, accessToken.ExpiresOn); - } - - if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryServicePrincipal) - { - // Cache ClientSecretCredential based on scope, authority, audience, and clientId - TokenCredentialKey tokenCredentialKey = new(typeof(ClientSecretCredential), authority, scope, audience, clientId); - AccessToken accessToken = await GetTokenAsync(tokenCredentialKey, parameters.Password, tokenRequestContext, cts.Token).ConfigureAwait(false); - SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token for Active Directory Service Principal auth mode. Expiry Time: {0}", accessToken.ExpiresOn); - return new SqlAuthenticationToken(accessToken.Token, accessToken.ExpiresOn); - } - - if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity) - { - // Cache WorkloadIdentityCredential based on authority and clientId - TokenCredentialKey tokenCredentialKey = new(typeof(WorkloadIdentityCredential), authority, string.Empty, string.Empty, clientId); - // If either tenant id, client id, or the token file path are not specified when fetching the token, - // a CredentialUnavailableException will be thrown instead - AccessToken accessToken = await GetTokenAsync(tokenCredentialKey, string.Empty, tokenRequestContext, cts.Token).ConfigureAwait(false); - SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token for Workload Identity auth mode. Expiry Time: {0}", accessToken.ExpiresOn); - return new SqlAuthenticationToken(accessToken.Token, accessToken.ExpiresOn); - } - - /* - * Today, MSAL.NET uses another redirect URI by default in desktop applications that run on Windows - * (urn:ietf:wg:oauth:2.0:oob). In the future, we'll want to change this default, so we recommend - * that you use https://login.microsoftonline.com/common/oauth2/nativeclient. - * - * https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-desktop-app-registration#redirect-uris - */ - string redirectUri = s_nativeClientRedirectUri; - -#if NET - if (parameters.AuthenticationMethod != SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow) - { - redirectUri = "http://localhost"; - } -#endif - PublicClientAppKey pcaKey = new(parameters.Authority, redirectUri, _applicationClientId -#if NETFRAMEWORK - , _iWin32WindowFunc -#endif - ); - - AuthenticationResult result = null; - IPublicClientApplication app = await GetPublicClientAppInstanceAsync(pcaKey, cts.Token).ConfigureAwait(false); - - if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryIntegrated) - { - result = await TryAcquireTokenSilent(app, parameters, scopes, cts).ConfigureAwait(false); - - if (result == null) - { - if (!string.IsNullOrEmpty(parameters.UserId)) - { - // The AcquireTokenByIntegratedWindowsAuth method is marked as obsolete in MSAL.NET - // but it is still a supported way to acquire tokens for Active Directory Integrated authentication. -#pragma warning disable CS0618 // Type or member is obsolete - result = await app.AcquireTokenByIntegratedWindowsAuth(scopes) -#pragma warning restore CS0618 // Type or member is obsolete - .WithCorrelationId(parameters.ConnectionId) - .WithUsername(parameters.UserId) - .ExecuteAsync(cancellationToken: cts.Token) - .ConfigureAwait(false); - } - else - { -#pragma warning disable CS0618 // Type or member is obsolete - result = await app.AcquireTokenByIntegratedWindowsAuth(scopes) -#pragma warning restore CS0618 // Type or member is obsolete - .WithCorrelationId(parameters.ConnectionId) - .ExecuteAsync(cancellationToken: cts.Token) - .ConfigureAwait(false); - } - SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token for Active Directory Integrated auth mode. Expiry Time: {0}", result?.ExpiresOn); - } - } - #pragma warning disable 0618 // Type or member is obsolete - else if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryPassword) - #pragma warning restore 0618 // Type or member is obsolete - { - string pwCacheKey = GetAccountPwCacheKey(parameters); - object previousPw = s_accountPwCache.Get(pwCacheKey); - byte[] currPwHash = GetHash(parameters.Password); - - if (previousPw != null && - previousPw is byte[] previousPwBytes && - // Only get the cached token if the current password hash matches the previously used password hash - AreEqual(currPwHash, previousPwBytes)) - { - result = await TryAcquireTokenSilent(app, parameters, scopes, cts).ConfigureAwait(false); - } - - if (result == null) - { -#pragma warning disable CS0618 // Type or member is obsolete - result = await app.AcquireTokenByUsernamePassword(scopes, parameters.UserId, parameters.Password) -#pragma warning restore CS0618 // Type or member is obsolete - .WithCorrelationId(parameters.ConnectionId) - .ExecuteAsync(cancellationToken: cts.Token) - .ConfigureAwait(false); - - // We cache the password hash to ensure future connection requests include a validated password - // when we check for a cached MSAL account. Otherwise, a connection request with the same username - // against the same tenant could succeed with an invalid password when we re-use the cached token. - using (ICacheEntry entry = s_accountPwCache.CreateEntry(pwCacheKey)) - { - entry.Value = GetHash(parameters.Password); - entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(s_accountPwCacheTtlInHours); - } - SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token for Active Directory Password auth mode. Expiry Time: {0}", result?.ExpiresOn); - } - } - else if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryInteractive || - parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow) - { - try - { - result = await TryAcquireTokenSilent(app, parameters, scopes, cts).ConfigureAwait(false); - SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token (silent) for {0} auth mode. Expiry Time: {1}", parameters.AuthenticationMethod, result?.ExpiresOn); - } - catch (MsalUiRequiredException) - { - // An 'MsalUiRequiredException' is thrown in the case where an interaction is required with the end user of the application, - // for instance, if no refresh token was in the cache, or the user needs to consent, or re-sign-in (for instance if the password expired), - // or the user needs to perform two factor authentication. - result = await AcquireTokenInteractiveDeviceFlowAsync(app, scopes, parameters.ConnectionId, parameters.UserId, parameters.AuthenticationMethod, cts, _customWebUI, _deviceCodeFlowCallback).ConfigureAwait(false); - SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token (interactive) for {0} auth mode. Expiry Time: {1}", parameters.AuthenticationMethod, result?.ExpiresOn); - } - - if (result == null) - { - // If no existing 'account' is found, we request user to sign in interactively. - result = await AcquireTokenInteractiveDeviceFlowAsync(app, scopes, parameters.ConnectionId, parameters.UserId, parameters.AuthenticationMethod, cts, _customWebUI, _deviceCodeFlowCallback).ConfigureAwait(false); - SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token (interactive) for {0} auth mode. Expiry Time: {1}", parameters.AuthenticationMethod, result?.ExpiresOn); - } - } - else - { - SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | {0} authentication mode not supported by ActiveDirectoryAuthenticationProvider class.", parameters.AuthenticationMethod); - throw SQL.UnsupportedAuthenticationSpecified(parameters.AuthenticationMethod); - } - - return new SqlAuthenticationToken(result.AccessToken, result.ExpiresOn); - } - - private static async Task TryAcquireTokenSilent(IPublicClientApplication app, SqlAuthenticationParameters parameters, - string[] scopes, CancellationTokenSource cts) - { - AuthenticationResult result = null; - - // Fetch available accounts from 'app' instance - System.Collections.Generic.IEnumerator accounts = (await app.GetAccountsAsync().ConfigureAwait(false)).GetEnumerator(); - - IAccount account = default; - if (accounts.MoveNext()) - { - if (!string.IsNullOrEmpty(parameters.UserId)) - { - do - { - IAccount currentVal = accounts.Current; - if (string.Compare(parameters.UserId, currentVal.Username, StringComparison.InvariantCultureIgnoreCase) == 0) - { - account = currentVal; - break; - } - } - while (accounts.MoveNext()); - } - else - { - account = accounts.Current; - } - } - - if (account != null) - { - // If 'account' is available in 'app', we use the same to acquire token silently. - // Read More on API docs: https://docs.microsoft.com/dotnet/api/microsoft.identity.client.clientapplicationbase.acquiretokensilent - result = await app.AcquireTokenSilent(scopes, account).ExecuteAsync(cancellationToken: cts.Token).ConfigureAwait(false); - SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token (silent) for {0} auth mode. Expiry Time: {1}", parameters.AuthenticationMethod, result?.ExpiresOn); - } - - return result; - } - - private static async Task AcquireTokenInteractiveDeviceFlowAsync(IPublicClientApplication app, string[] scopes, Guid connectionId, string userId, - SqlAuthenticationMethod authenticationMethod, CancellationTokenSource cts, ICustomWebUi customWebUI, Func deviceCodeFlowCallback) - { - try - { - if (authenticationMethod == SqlAuthenticationMethod.ActiveDirectoryInteractive) - { - CancellationTokenSource ctsInteractive = new(); -#if NET - /* - * On .NET Core, MSAL will start the system browser as a separate process. MSAL does not have control over this browser, - * but once the user finishes authentication, the web page is redirected in such a way that MSAL can intercept the Uri. - * MSAL cannot detect if the user navigates away or simply closes the browser. Apps using this technique are encouraged - * to define a timeout (via CancellationToken). We recommend a timeout of at least a few minutes, to take into account - * cases where the user is prompted to change password or perform 2FA. - * - * https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/System-Browser-on-.Net-Core#system-browser-experience - */ - ctsInteractive.CancelAfter(180000); -#endif - if (customWebUI != null) - { - return await app.AcquireTokenInteractive(scopes) - .WithCorrelationId(connectionId) - .WithCustomWebUi(customWebUI) - .WithLoginHint(userId) - .ExecuteAsync(ctsInteractive.Token) - .ConfigureAwait(false); - } - else - { - /* - * We will use the MSAL Embedded or System web browser which changes by Default in MSAL according to this table: - * - * Framework Embedded System Default - * ------------------------------------------- - * .NET Classic Yes Yes^ Embedded - * .NET Core No Yes^ System - * .NET Standard No No NONE - * UWP Yes No Embedded - * Xamarin.Android Yes Yes System - * Xamarin.iOS Yes Yes System - * Xamarin.Mac Yes No Embedded - * - * ^ Requires "http://localhost" redirect URI - * - * https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/MSAL.NET-uses-web-browser#at-a-glance - */ - return await app.AcquireTokenInteractive(scopes) - .WithCorrelationId(connectionId) - .WithLoginHint(userId) - .ExecuteAsync(ctsInteractive.Token) - .ConfigureAwait(false); - } - } - else - { - AuthenticationResult result = await app.AcquireTokenWithDeviceCode(scopes, - deviceCodeResult => deviceCodeFlowCallback(deviceCodeResult)) - .WithCorrelationId(connectionId) - .ExecuteAsync(cancellationToken: cts.Token) - .ConfigureAwait(false); - return result; - } - } - catch (OperationCanceledException) - { - SqlClientEventSource.Log.TryTraceEvent("AcquireTokenInteractiveDeviceFlowAsync | Operation timed out while acquiring access token."); - throw (authenticationMethod == SqlAuthenticationMethod.ActiveDirectoryInteractive) ? - SQL.ActiveDirectoryInteractiveTimeout() : - SQL.ActiveDirectoryDeviceFlowTimeout(); - } - } - - private static Task DefaultDeviceFlowCallback(DeviceCodeResult result) - { - // This will print the message on the console which tells the user where to go sign-in using - // a separate browser and the code to enter once they sign in. - // The AcquireTokenWithDeviceCode() method will poll the server after firing this - // device code callback to look for the successful login of the user via that browser. - // This background polling (whose interval and timeout data is also provided as fields in the - // deviceCodeCallback class) will occur until: - // * The user has successfully logged in via browser and entered the proper code - // * The timeout specified by the server for the lifetime of this code (typically ~15 minutes) has been reached - // * The developing application calls the Cancel() method on a CancellationToken sent into the method. - // If this occurs, an OperationCanceledException will be thrown (see catch below for more details). - SqlClientEventSource.Log.TryTraceEvent("AcquireTokenInteractiveDeviceFlowAsync | Callback triggered with Device Code Result: {0}", result.Message); - Console.WriteLine(result.Message); - return Task.FromResult(0); - } - - private class CustomWebUi : ICustomWebUi - { - private readonly Func> _acquireAuthorizationCodeAsyncCallback; - - internal CustomWebUi(Func> acquireAuthorizationCodeAsyncCallback) => _acquireAuthorizationCodeAsyncCallback = acquireAuthorizationCodeAsyncCallback; - - public Task AcquireAuthorizationCodeAsync(Uri authorizationUri, Uri redirectUri, CancellationToken cancellationToken) - => _acquireAuthorizationCodeAsyncCallback.Invoke(authorizationUri, redirectUri, cancellationToken); - } - - private async Task GetPublicClientAppInstanceAsync(PublicClientAppKey publicClientAppKey, CancellationToken cancellationToken) - { - if (!s_pcaMap.TryGetValue(publicClientAppKey, out IPublicClientApplication clientApplicationInstance)) - { - await s_pcaMapModifierSemaphore.WaitAsync(cancellationToken); - try - { - // Double-check in case another thread added it while we waited for the semaphore - if (!s_pcaMap.TryGetValue(publicClientAppKey, out clientApplicationInstance)) - { - clientApplicationInstance = CreateClientAppInstance(publicClientAppKey); - s_pcaMap.TryAdd(publicClientAppKey, clientApplicationInstance); - } - } - finally - { - s_pcaMapModifierSemaphore.Release(); - } - } - - return clientApplicationInstance; - } - - private static async Task GetTokenAsync(TokenCredentialKey tokenCredentialKey, string secret, - TokenRequestContext tokenRequestContext, CancellationToken cancellationToken) - { - if (!s_tokenCredentialMap.TryGetValue(tokenCredentialKey, out TokenCredentialData tokenCredentialInstance)) - { - await s_tokenCredentialMapModifierSemaphore.WaitAsync(cancellationToken); - try - { - // Double-check in case another thread added it while we waited for the semaphore - if (!s_tokenCredentialMap.TryGetValue(tokenCredentialKey, out tokenCredentialInstance)) - { - tokenCredentialInstance = CreateTokenCredentialInstance(tokenCredentialKey, secret); - s_tokenCredentialMap.TryAdd(tokenCredentialKey, tokenCredentialInstance); - } - } - finally - { - s_tokenCredentialMapModifierSemaphore.Release(); - } - } - - if (!AreEqual(tokenCredentialInstance._secretHash, GetHash(secret))) - { - // If the secret hash has changed, we need to remove the old token credential instance and create a new one. - await s_tokenCredentialMapModifierSemaphore.WaitAsync(cancellationToken); - try - { - s_tokenCredentialMap.TryRemove(tokenCredentialKey, out _); - tokenCredentialInstance = CreateTokenCredentialInstance(tokenCredentialKey, secret); - s_tokenCredentialMap.TryAdd(tokenCredentialKey, tokenCredentialInstance); - } - finally - { - s_tokenCredentialMapModifierSemaphore.Release(); - } - } - - return await tokenCredentialInstance._tokenCredential.GetTokenAsync(tokenRequestContext, cancellationToken); - } - - private static string GetAccountPwCacheKey(SqlAuthenticationParameters parameters) - { - return parameters.Authority + "+" + parameters.UserId; - } - - private static byte[] GetHash(string input) - { - byte[] unhashedBytes = Encoding.Unicode.GetBytes(input); - SHA256 sha256 = SHA256.Create(); - byte[] hashedBytes = sha256.ComputeHash(unhashedBytes); - return hashedBytes; - } - - private static bool AreEqual(byte[] a1, byte[] a2) - { - if (ReferenceEquals(a1, a2)) - { - return true; - } - else if (a1 is null || a2 is null) - { - return false; - } - else if (a1.Length != a2.Length) - { - return false; - } - - return a1.AsSpan().SequenceEqual(a2.AsSpan()); - } - - private IPublicClientApplication CreateClientAppInstance(PublicClientAppKey publicClientAppKey) - { - PublicClientApplicationBuilder builder = PublicClientApplicationBuilder - .CreateWithApplicationOptions(new PublicClientApplicationOptions - { - ClientId = publicClientAppKey._applicationClientId, - ClientName = DbConnectionStringDefaults.ApplicationName, - ClientVersion = Common.ADP.GetAssemblyVersion().ToString(), - RedirectUri = publicClientAppKey._redirectUri, - }) - .WithAuthority(publicClientAppKey._authority); - - #if NETFRAMEWORK - if (_iWin32WindowFunc is not null) - { - builder.WithParentActivityOrWindow(_iWin32WindowFunc); - } - #endif - - return builder.Build(); - } - - private static TokenCredentialData CreateTokenCredentialInstance(TokenCredentialKey tokenCredentialKey, string secret) - { - if (tokenCredentialKey._tokenCredentialType == typeof(DefaultAzureCredential)) - { - DefaultAzureCredentialOptions defaultAzureCredentialOptions = new() - { - AuthorityHost = new Uri(tokenCredentialKey._authority), - TenantId = tokenCredentialKey._audience, - ExcludeInteractiveBrowserCredential = true // Force disabled, even though it's disabled by default to respect driver specifications. - }; - - // Optionally set clientId when available - if (tokenCredentialKey._clientId is not null) - { - defaultAzureCredentialOptions.ManagedIdentityClientId = tokenCredentialKey._clientId; -#pragma warning disable CS0618 // Type or member is obsolete - defaultAzureCredentialOptions.SharedTokenCacheUsername = tokenCredentialKey._clientId; -#pragma warning restore CS0618 // Type or member is obsolete - defaultAzureCredentialOptions.WorkloadIdentityClientId = tokenCredentialKey._clientId; - } - - // SqlClient is a library and provides support to acquire access - // token using 'DefaultAzureCredential' on user demand when they - // specify 'Authentication = Active Directory Default' in - // connection string. - // - // Default Azure Credential is instantiated by the calling - // application when using "Active Directory Default" - // authentication code to connect to Azure SQL instance. - // SqlClient is a library, doesn't instantiate the credential - // without running application instructions. - // - // Note that CodeQL suppression support can only detect - // suppression comments that appear immediately above the - // flagged statement, or appended to the end of the statement. - // Multi-line justifications are not supported. - // - // https://eng.ms/docs/cloud-ai-platform/devdiv/one-engineering-system-1es/1es-docs/codeql/codeql-semmle#guidance-on-suppressions - // - // CodeQL [SM05137] See above for justification. - DefaultAzureCredential cred = new(defaultAzureCredentialOptions); - - return new TokenCredentialData(cred, GetHash(secret)); - } - - TokenCredentialOptions tokenCredentialOptions = new() { AuthorityHost = new Uri(tokenCredentialKey._authority) }; - - if (tokenCredentialKey._tokenCredentialType == typeof(ManagedIdentityCredential)) - { - return new TokenCredentialData(new ManagedIdentityCredential(tokenCredentialKey._clientId, tokenCredentialOptions), GetHash(secret)); - } - else if (tokenCredentialKey._tokenCredentialType == typeof(ClientSecretCredential)) - { - return new TokenCredentialData(new ClientSecretCredential(tokenCredentialKey._audience, tokenCredentialKey._clientId, secret, tokenCredentialOptions), GetHash(secret)); - } - else if (tokenCredentialKey._tokenCredentialType == typeof(WorkloadIdentityCredential)) - { - // The WorkloadIdentityCredentialOptions object initialization populates its instance members - // from the environment variables AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_FEDERATED_TOKEN_FILE, - // and AZURE_ADDITIONALLY_ALLOWED_TENANTS. AZURE_CLIENT_ID may be overridden by the User Id. - WorkloadIdentityCredentialOptions options = new() { AuthorityHost = new Uri(tokenCredentialKey._authority) }; - - if (tokenCredentialKey._clientId is not null) - { - options.ClientId = tokenCredentialKey._clientId; - } - - return new TokenCredentialData(new WorkloadIdentityCredential(options), GetHash(secret)); - } - - // This should never be reached, but if it is, throw an exception that will be noticed during development - throw new ArgumentException(nameof(ActiveDirectoryAuthenticationProvider)); - } - - internal class PublicClientAppKey - { - public readonly string _authority; - public readonly string _redirectUri; - public readonly string _applicationClientId; -#if NETFRAMEWORK - public readonly Func _iWin32WindowFunc; -#endif - - public PublicClientAppKey(string authority, string redirectUri, string applicationClientId -#if NETFRAMEWORK - , Func iWin32WindowFunc -#endif - ) - { - _authority = authority; - _redirectUri = redirectUri; - _applicationClientId = applicationClientId; -#if NETFRAMEWORK - _iWin32WindowFunc = iWin32WindowFunc; -#endif - } - - public override bool Equals(object obj) - { - if (obj != null && obj is PublicClientAppKey pcaKey) - { - return (string.CompareOrdinal(_authority, pcaKey._authority) == 0 - && string.CompareOrdinal(_redirectUri, pcaKey._redirectUri) == 0 - && string.CompareOrdinal(_applicationClientId, pcaKey._applicationClientId) == 0 -#if NETFRAMEWORK - && pcaKey._iWin32WindowFunc == _iWin32WindowFunc -#endif - ); - } - return false; - } - - public override int GetHashCode() => Tuple.Create(_authority, _redirectUri, _applicationClientId -#if NETFRAMEWORK - , _iWin32WindowFunc -#endif - ).GetHashCode(); - } - - internal class TokenCredentialData - { - public TokenCredential _tokenCredential; - public byte[] _secretHash; - - public TokenCredentialData(TokenCredential tokenCredential, byte[] secretHash) - { - _tokenCredential = tokenCredential; - _secretHash = secretHash; - } - } - - internal class TokenCredentialKey - { - public readonly Type _tokenCredentialType; - public readonly string _authority; - public readonly string _scope; - public readonly string _audience; - public readonly string _clientId; - - public TokenCredentialKey(Type tokenCredentialType, string authority, string scope, string audience, string clientId) - { - _tokenCredentialType = tokenCredentialType; - _authority = authority; - _scope = scope; - _audience = audience; - _clientId = clientId; - } - - public override bool Equals(object obj) - { - if (obj != null && obj is TokenCredentialKey tcKey) - { - return string.CompareOrdinal(nameof(_tokenCredentialType), nameof(tcKey._tokenCredentialType)) == 0 - && string.CompareOrdinal(_authority, tcKey._authority) == 0 - && string.CompareOrdinal(_scope, tcKey._scope) == 0 - && string.CompareOrdinal(_audience, tcKey._audience) == 0 - && string.CompareOrdinal(_clientId, tcKey._clientId) == 0 - ; - } - return false; - } - - public override int GetHashCode() => Tuple.Create(_tokenCredentialType, _authority, _scope, _audience, _clientId).GetHashCode(); - } - - } -} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ActiveDirectoryAuthenticationTimeoutRetryHelper.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ActiveDirectoryAuthenticationTimeoutRetryHelper.cs index e972102260..721e31ca6f 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ActiveDirectoryAuthenticationTimeoutRetryHelper.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ActiveDirectoryAuthenticationTimeoutRetryHelper.cs @@ -132,7 +132,8 @@ private static string GetTokenHash(SqlFedAuthToken token) } // Here we mimic how ADAL calculates hash for token. They use UTF8 instead of Unicode. - var originalTokenString = SqlAuthenticationToken.AccessTokenStringFromBytes(token.accessToken); + var originalTokenString = Encoding.Unicode.GetString(token.AccessToken); + var bytesInUtf8 = Encoding.UTF8.GetBytes(originalTokenString); using (var sha256 = SHA256.Create()) { diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationParameters.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationParameters.cs deleted file mode 100644 index 9c74b937b8..0000000000 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationParameters.cs +++ /dev/null @@ -1,162 +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.Runtime.InteropServices; -using System.Security; -using Microsoft.Data.Common; - -namespace Microsoft.Data.SqlClient -{ - - /// - public class SqlAuthenticationParameters - { - /// - public SqlAuthenticationMethod AuthenticationMethod { get; } - - /// - public string Resource { get; } - - /// - public string Authority { get; } - - /// - public string UserId { get; } - - /// - public string Password { get; } - - /// - public Guid ConnectionId { get; } - - /// - public string ServerName { get; } - - /// - public string DatabaseName { get; } - - /// - public int ConnectionTimeout { get; } = ADP.DefaultConnectionTimeout; - - /// - protected SqlAuthenticationParameters( - SqlAuthenticationMethod authenticationMethod, - string serverName, - string databaseName, - string resource, - string authority, - string userId, - string password, - Guid connectionId, - int connectionTimeout) - { - AuthenticationMethod = authenticationMethod; - ServerName = serverName; - DatabaseName = databaseName; - Resource = resource; - Authority = authority; - UserId = userId; - Password = password; - ConnectionId = connectionId; - ConnectionTimeout = connectionTimeout; - } - - /// - /// AD authentication parameter builder. - /// - internal class Builder - { - private readonly SqlAuthenticationMethod _authenticationMethod; - private readonly string _serverName; - private readonly string _databaseName; - private readonly string _resource; - private readonly string _authority; - private string _userId; - private string _password; - private Guid _connectionId = Guid.NewGuid(); - private int _connectionTimeout = ADP.DefaultConnectionTimeout; - - /// - /// Implicitly converts to . - /// - public static implicit operator SqlAuthenticationParameters(Builder builder) - { - return new SqlAuthenticationParameters( - authenticationMethod: builder._authenticationMethod, - serverName: builder._serverName, - databaseName: builder._databaseName, - resource: builder._resource, - authority: builder._authority, - userId: builder._userId, - password: builder._password, - connectionId: builder._connectionId, - connectionTimeout: builder._connectionTimeout); - } - - /// - /// Set user id. - /// - public Builder WithUserId(string userId) - { - _userId = userId; - return this; - } - - /// - /// Set password. - /// - public Builder WithPassword(string password) - { - _password = password; - return this; - } - - /// - /// Set password. - /// - public Builder WithPassword(SecureString password) - { - IntPtr valuePtr = IntPtr.Zero; - try - { - valuePtr = Marshal.SecureStringToGlobalAllocUnicode(password); - _password = Marshal.PtrToStringUni(valuePtr); - } - finally - { - Marshal.ZeroFreeGlobalAllocUnicode(valuePtr); - } - return this; - } - - /// - /// Set a specific connection id instead of using a random one. - /// - public Builder WithConnectionId(Guid connectionId) - { - _connectionId = connectionId; - return this; - } - - /// - /// Set connection timeout. - /// - public Builder WithConnectionTimeout(int timeout) - { - _connectionTimeout = timeout; - return this; - } - - internal Builder(SqlAuthenticationMethod authenticationMethod, string resource, string authority, string serverName, string databaseName) - { - _authenticationMethod = authenticationMethod; - _serverName = serverName; - _databaseName = databaseName; - _resource = resource; - _authority = authority; - } - } - } -} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationParametersBuilder.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationParametersBuilder.cs new file mode 100644 index 0000000000..a9863ee2ac --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationParametersBuilder.cs @@ -0,0 +1,104 @@ +// 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.Runtime.InteropServices; +using System.Security; +using Microsoft.Data.Common; + +namespace Microsoft.Data.SqlClient +{ + internal sealed class SqlAuthenticationParametersBuilder + { + private readonly SqlAuthenticationMethod _authenticationMethod; + private readonly string _serverName; + private readonly string _databaseName; + private readonly string _resource; + private readonly string _authority; + private string _userId; + private string _password; + private Guid _connectionId = Guid.NewGuid(); + private int _authenticationTimeout = ADP.DefaultConnectionTimeout; + + /// + /// Implicitly converts to . + /// + public static implicit operator SqlAuthenticationParameters(SqlAuthenticationParametersBuilder builder) + { + return new SqlAuthenticationParameters( + authenticationMethod: builder._authenticationMethod, + serverName: builder._serverName, + databaseName: builder._databaseName, + resource: builder._resource, + authority: builder._authority, + userId: builder._userId, + password: builder._password, + connectionId: builder._connectionId, + connectionTimeout: builder._authenticationTimeout); + } + + /// + /// Set user id. + /// + public SqlAuthenticationParametersBuilder WithUserId(string userId) + { + _userId = userId; + return this; + } + + /// + /// Set password. + /// + public SqlAuthenticationParametersBuilder WithPassword(string password) + { + _password = password; + return this; + } + + /// + /// Set password. + /// + public SqlAuthenticationParametersBuilder WithPassword(SecureString password) + { + IntPtr valuePtr = IntPtr.Zero; + try + { + valuePtr = Marshal.SecureStringToGlobalAllocUnicode(password); + _password = Marshal.PtrToStringUni(valuePtr); + } + finally + { + Marshal.ZeroFreeGlobalAllocUnicode(valuePtr); + } + return this; + } + + /// + /// Set a specific connection id instead of using a random one. + /// + public SqlAuthenticationParametersBuilder WithConnectionId(Guid connectionId) + { + _connectionId = connectionId; + return this; + } + + /// + /// Set authentication timeout. + /// + public SqlAuthenticationParametersBuilder WithAuthenticationTimeout(int timeout) + { + _authenticationTimeout = timeout; + return this; + } + + internal SqlAuthenticationParametersBuilder(SqlAuthenticationMethod authenticationMethod, string resource, string authority, string serverName, string databaseName) + { + _authenticationMethod = authenticationMethod; + _serverName = serverName; + _databaseName = databaseName; + _resource = resource; + _authority = authority; + } + } +} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProvider.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProvider.cs deleted file mode 100644 index 25e1cf006e..0000000000 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProvider.cs +++ /dev/null @@ -1,37 +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.Threading.Tasks; - -namespace Microsoft.Data.SqlClient -{ - - /// - public abstract class SqlAuthenticationProvider - { - /// - public static SqlAuthenticationProvider GetProvider(SqlAuthenticationMethod authenticationMethod) - { - return SqlAuthenticationProviderManager.Instance.GetProvider(authenticationMethod); - } - - /// - public static bool SetProvider(SqlAuthenticationMethod authenticationMethod, SqlAuthenticationProvider provider) - { - return SqlAuthenticationProviderManager.Instance.SetProvider(authenticationMethod, provider); - } - - /// - public virtual void BeforeLoad(SqlAuthenticationMethod authenticationMethod) { } - - /// - public virtual void BeforeUnload(SqlAuthenticationMethod authenticationMethod) { } - - /// - public abstract bool IsSupported(SqlAuthenticationMethod authenticationMethod); - - /// - public abstract Task AcquireTokenAsync(SqlAuthenticationParameters parameters); - } -} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs index 447ea0e9c5..06d7423028 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs @@ -6,12 +6,13 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Configuration; +using System.IO; +using System.Reflection; + +#nullable enable namespace Microsoft.Data.SqlClient { - /// - /// Authentication provider manager. - /// internal sealed class SqlAuthenticationProviderManager { [Obsolete("ActiveDirectoryPassword is deprecated, use a more secure authentication method. See https://aka.ms/SqlClientEntraIDAuthentication for more details.")] @@ -27,7 +28,7 @@ internal sealed class SqlAuthenticationProviderManager static SqlAuthenticationProviderManager() { - SqlAuthenticationProviderConfigurationSection configurationSection = null; + SqlAuthenticationProviderConfigurationSection? configurationSection = null; try { @@ -46,49 +47,125 @@ static SqlAuthenticationProviderManager() } Instance = new SqlAuthenticationProviderManager(configurationSection); - SetDefaultAuthProviders(Instance); - } - /// - /// Sets default supported Active Directory Authentication providers by the driver - /// on the SqlAuthenticationProviderManager instance. - /// - private static void SetDefaultAuthProviders(SqlAuthenticationProviderManager instance) - { - if (instance != null) + // If our Azure extensions package is present, use its + // authentication provider as our default. + const string assemblyName = "Microsoft.Data.SqlClient.Extensions.Azure"; + + try { - var activeDirectoryAuthProvider = new ActiveDirectoryAuthenticationProvider(instance._applicationClientId); - instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryIntegrated, activeDirectoryAuthProvider); + // Try to load our Azure extension. + var assembly = Assembly.Load(assemblyName); + + if (assembly is null) + { + SqlClientEventSource.Log.TryTraceEvent( + nameof(SqlAuthenticationProviderManager) + + $": Azure extension assembly={assemblyName} not found; " + + "no default Active Directory provider installed"); + return; + } + + // TODO(ADO-39845): Verify the assembly is signed by us? + + SqlClientEventSource.Log.TryTraceEvent( + nameof(SqlAuthenticationProviderManager) + + $": Azure extension assembly={assemblyName} found; " + + "attempting to set as default provider for all Active " + + "Directory authentication methods"); + + // Look for the authentication provider class. + const string className = "Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProvider"; + var type = assembly.GetType(className); + + if (type is null) + { + SqlClientEventSource.Log.TryTraceEvent( + nameof(SqlAuthenticationProviderManager) + + $": Azure extension does not contain class={className}; " + + "no default Active Directory provider installed"); + + return; + } + + // Try to instantiate it. + var instance = Activator.CreateInstance( + type, + [Instance._applicationClientId]) + as SqlAuthenticationProvider; + + if (instance is null) + { + SqlClientEventSource.Log.TryTraceEvent( + nameof(SqlAuthenticationProviderManager) + + $": Failed to instantiate Azure extension class={className}; " + + "no default Active Directory provider installed"); + + return; + } + + // We successfully instantiated the provider, so set it as the + // default for all Active Directory authentication methods. + // + // Note that SetProvider() will refuse to clobber an application + // specified provider, so these defaults will only be applied + // for methods that do not already have a provider. + SetProvider(SqlAuthenticationMethod.ActiveDirectoryIntegrated, instance); #pragma warning disable 0618 // Type or member is obsolete - instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryPassword, activeDirectoryAuthProvider); + SetProvider(SqlAuthenticationMethod.ActiveDirectoryPassword, instance); #pragma warning restore 0618 // Type or member is obsolete - instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryInteractive, activeDirectoryAuthProvider); - instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryServicePrincipal, activeDirectoryAuthProvider); - instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, activeDirectoryAuthProvider); - instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryManagedIdentity, activeDirectoryAuthProvider); - instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryMSI, activeDirectoryAuthProvider); - instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryDefault, activeDirectoryAuthProvider); - instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity, activeDirectoryAuthProvider); + SetProvider(SqlAuthenticationMethod.ActiveDirectoryInteractive, instance); + SetProvider(SqlAuthenticationMethod.ActiveDirectoryServicePrincipal, instance); + SetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, instance); + SetProvider(SqlAuthenticationMethod.ActiveDirectoryManagedIdentity, instance); + SetProvider(SqlAuthenticationMethod.ActiveDirectoryMSI, instance); + SetProvider(SqlAuthenticationMethod.ActiveDirectoryDefault, instance); + SetProvider(SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity, instance); + + SqlClientEventSource.Log.TryTraceEvent( + nameof(SqlAuthenticationProviderManager) + + $": Azure extension class={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 + // simply have no default and the app must provide one if they + // attempt to use Active Directory authentication. + catch (Exception ex) + when (ex is ArgumentNullException || + ex is ArgumentException || + ex is BadImageFormatException || + ex is FileLoadException || + ex is FileNotFoundException || + ex is MemberAccessException || + ex is MethodAccessException || + ex is MissingMethodException || + ex is NotSupportedException || + ex is TargetInvocationException || + ex is TypeLoadException) + { + SqlClientEventSource.Log.TryTraceEvent( + nameof(SqlAuthenticationProviderManager) + + $": Azure extension assembly={assemblyName} not found or " + + "not usable; no default provider installed; " + + $"{ex.GetType().Name}: {ex.Message}"); + } + // Any other exceptions are fatal. } - public static readonly SqlAuthenticationProviderManager Instance; + private static readonly SqlAuthenticationProviderManager Instance; - private readonly SqlAuthenticationInitializer _initializer; - private readonly IReadOnlyCollection _authenticationsWithAppSpecifiedProvider; - private readonly ConcurrentDictionary _providers; + private readonly HashSet _authenticationsWithAppSpecifiedProvider = new(); + private readonly ConcurrentDictionary _providers = new(); private readonly SqlClientLogger _sqlAuthLogger = new SqlClientLogger(); - private readonly string _applicationClientId = ActiveDirectoryAuthentication.AdoClientId; + private readonly string? _applicationClientId = null; /// /// Constructor. /// - public SqlAuthenticationProviderManager(SqlAuthenticationProviderConfigurationSection configSection = null) + private SqlAuthenticationProviderManager(SqlAuthenticationProviderConfigurationSection? configSection) { var methodName = "Ctor"; - _providers = new ConcurrentDictionary(); - var authenticationsWithAppSpecifiedProvider = new HashSet(); - _authenticationsWithAppSpecifiedProvider = authenticationsWithAppSpecifiedProvider; if (configSection == null) { @@ -112,8 +189,14 @@ public SqlAuthenticationProviderManager(SqlAuthenticationProviderConfigurationSe try { var initializerType = Type.GetType(configSection.InitializerType, true); - _initializer = (SqlAuthenticationInitializer)Activator.CreateInstance(initializerType); - _initializer.Initialize(); + if (initializerType is not null) + { + var initializer = (SqlAuthenticationInitializer?)Activator.CreateInstance(initializerType); + if (initializer is not null) + { + initializer.Initialize(); + } + } } catch (Exception e) { @@ -132,23 +215,31 @@ public SqlAuthenticationProviderManager(SqlAuthenticationProviderConfigurationSe foreach (ProviderSettings providerSettings in configSection.Providers) { SqlAuthenticationMethod authentication = AuthenticationEnumFromString(providerSettings.Name); - SqlAuthenticationProvider provider; + SqlAuthenticationProvider? provider; try { var providerType = Type.GetType(providerSettings.Type, true); - provider = (SqlAuthenticationProvider)Activator.CreateInstance(providerType); + 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); + _authenticationsWithAppSpecifiedProvider.Add(authentication); _sqlAuthLogger.LogInfo(nameof(SqlAuthenticationProviderManager), methodName, string.Format("Added user-defined auth provider: {0} for authentication {1}.", providerSettings?.Type, authentication)); } } @@ -158,54 +249,48 @@ public SqlAuthenticationProviderManager(SqlAuthenticationProviderConfigurationSe } } - /// - /// Get an authentication provider by method. - /// - /// Authentication method. - /// Authentication provider or null if not found. - public SqlAuthenticationProvider GetProvider(SqlAuthenticationMethod authenticationMethod) + internal static SqlAuthenticationProvider? GetProvider(SqlAuthenticationMethod authenticationMethod) { - SqlAuthenticationProvider value; - return _providers.TryGetValue(authenticationMethod, out value) ? value : null; + SqlAuthenticationProvider? value; + return Instance._providers.TryGetValue(authenticationMethod, out value) ? value : null; } - /// - /// Set an authentication provider by method. - /// - /// Authentication method. - /// Authentication provider. - /// True if succeeded, false otherwise, e.g., the existing provider disallows overriding. - public bool SetProvider(SqlAuthenticationMethod authenticationMethod, SqlAuthenticationProvider provider) + internal static bool SetProvider(SqlAuthenticationMethod authenticationMethod, SqlAuthenticationProvider provider) { if (!provider.IsSupported(authenticationMethod)) { throw SQL.UnsupportedAuthenticationByProvider(authenticationMethod.ToString(), provider.GetType().Name); } var methodName = "SetProvider"; - if (_authenticationsWithAppSpecifiedProvider.Count > 0) + if (Instance._authenticationsWithAppSpecifiedProvider.Count > 0) { - foreach (SqlAuthenticationMethod candidateMethod in _authenticationsWithAppSpecifiedProvider) + foreach (SqlAuthenticationMethod candidateMethod in Instance._authenticationsWithAppSpecifiedProvider) { if (candidateMethod == authenticationMethod) { - _sqlAuthLogger.LogError(nameof(SqlAuthenticationProviderManager), methodName, $"Failed to add provider {GetProviderType(provider)} because a user-defined provider with type {GetProviderType(_providers[authenticationMethod])} already existed for authentication {authenticationMethod}."); - return false; // return here to avoid replacing user-defined provider + 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; } } } - _providers.AddOrUpdate(authenticationMethod, provider, (key, oldProvider) => - { - if (oldProvider != null) - { - oldProvider.BeforeUnload(authenticationMethod); - } - if (provider != null) + Instance._providers.AddOrUpdate( + authenticationMethod, + provider, + (SqlAuthenticationMethod key, SqlAuthenticationProvider oldProvider) => { + if (oldProvider != null) + { + oldProvider.BeforeUnload(authenticationMethod); + } + provider.BeforeLoad(authenticationMethod); - } - _sqlAuthLogger.LogInfo(nameof(SqlAuthenticationProviderManager), methodName, $"Added auth provider {GetProviderType(provider)}, overriding existed provider {GetProviderType(oldProvider)} for authentication {authenticationMethod}."); - return provider; - }); + + Instance._sqlAuthLogger.LogInfo(nameof(SqlAuthenticationProviderManager), methodName, $"Added auth provider {GetProviderType(provider)}, overriding existed provider {GetProviderType(oldProvider)} for authentication {authenticationMethod}."); + return provider; + }); return true; } @@ -216,7 +301,7 @@ public bool SetProvider(SqlAuthenticationMethod authenticationMethod, SqlAuthent /// /// /// - private static T FetchConfigurationSection(string name) + private static T? FetchConfigurationSection(string name) where T : class { Type t = typeof(T); @@ -265,14 +350,13 @@ private static SqlAuthenticationMethod AuthenticationEnumFromString(string authe } } - private static string GetProviderType(SqlAuthenticationProvider provider) + private static string GetProviderType(SqlAuthenticationProvider? provider) { - if (provider == null) + if (provider is null) { return "null"; } - - return provider.GetType().FullName; + return provider.GetType().FullName ?? "unknown"; } } @@ -293,13 +377,13 @@ internal class SqlAuthenticationProviderConfigurationSection : ConfigurationSect /// User-defined initializer. /// [ConfigurationProperty("initializerType")] - public string InitializerType => this["initializerType"] as string; + public string InitializerType => this["initializerType"] as string ?? string.Empty; /// /// Application Client Id /// [ConfigurationProperty("applicationClientId", IsRequired = false)] - public string ApplicationClientId => this["applicationClientId"] as string; + public string ApplicationClientId => this["applicationClientId"] as string ?? string.Empty; } /// diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationToken.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationToken.cs deleted file mode 100644 index c405cac2cb..0000000000 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationToken.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. - -using System; -using System.Text; - -namespace Microsoft.Data.SqlClient -{ - /// - public class SqlAuthenticationToken - { - /// - public DateTimeOffset ExpiresOn { get; } - - /// - public string AccessToken { get; } - - /// - public SqlAuthenticationToken(string accessToken, DateTimeOffset expiresOn) - { - if (string.IsNullOrEmpty(accessToken)) - { - throw SQL.ParameterCannotBeEmpty("AccessToken"); - } - - AccessToken = accessToken; - ExpiresOn = expiresOn; - } - - /// - /// Constructor. - /// - internal SqlAuthenticationToken(byte[] accessToken, DateTimeOffset expiresOn) - : this(AccessTokenStringFromBytes(accessToken), expiresOn) { } - - /// - /// Convert to driver's internal token class. - /// - internal SqlFedAuthToken ToSqlFedAuthToken() - { - var tokenBytes = AccessTokenBytesFromString(AccessToken); - return new SqlFedAuthToken - { - accessToken = tokenBytes, - dataLen = (uint)tokenBytes.Length, - expirationFileTime = ExpiresOn.ToFileTime() - }; - } - - /// - /// Convert token bytes to string. - /// - internal static string AccessTokenStringFromBytes(byte[] bytes) - { - return Encoding.Unicode.GetString(bytes); - } - - /// - /// Convert token string to bytes. - /// - internal static byte[] AccessTokenBytesFromString(string token) - { - return Encoding.Unicode.GetBytes(token); - } - } -} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlErrorCollection.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlErrorCollection.cs index 4684747627..fe20d0d2e5 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlErrorCollection.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlErrorCollection.cs @@ -45,6 +45,11 @@ internal SqlErrorCollection() { } /// public IEnumerator GetEnumerator() => _errors.GetEnumerator(); - internal void Add(SqlError error) => _errors.Add(error); + // Append the error to our list, and return ourselves for chaining. + internal SqlErrorCollection Add(SqlError error) + { + _errors.Add(error); + return this; + } } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlException.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlException.cs index 80c810e748..dcaa8b55dd 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlException.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlException.cs @@ -205,8 +205,6 @@ internal static SqlException CreateException(SqlErrorCollection errorCollection, internal static SqlException CreateException(SqlErrorCollection errorCollection, string serverVersion, Guid conId, Exception innerException = null, SqlBatchCommand batchCommand = null) { - Debug.Assert(errorCollection != null && errorCollection.Count > 0, "no errorCollection?"); - StringBuilder message = new(); for (int i = 0; i < errorCollection.Count; i++) { @@ -217,7 +215,11 @@ internal static SqlException CreateException(SqlErrorCollection errorCollection, message.Append(errorCollection[i].Message); } - if (innerException == null && errorCollection[0].Win32ErrorCode != 0 && errorCollection[0].Win32ErrorCode != -1) + if (innerException is null && + errorCollection is not null && + errorCollection.Count > 0 && + errorCollection[0].Win32ErrorCode != 0 && + errorCollection[0].Win32ErrorCode != -1) { innerException = new Win32Exception(errorCollection[0].Win32ErrorCode); } @@ -230,7 +232,10 @@ internal static SqlException CreateException(SqlErrorCollection errorCollection, exception.Data.Add("HelpLink.ProdVer", serverVersion); } exception.Data.Add("HelpLink.EvtSrc", "MSSQLServer"); - exception.Data.Add("HelpLink.EvtID", errorCollection[0].Number.ToString(CultureInfo.InvariantCulture)); + if (errorCollection is not null && errorCollection.Count > 0) + { + exception.Data.Add("HelpLink.EvtID", errorCollection[0].Number.ToString(CultureInfo.InvariantCulture)); + } exception.Data.Add("HelpLink.BaseHelpUrl", "https://go.microsoft.com/fwlink"); exception.Data.Add("HelpLink.LinkId", "20476"); diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs index e13a77bd73..9b0361a008 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs @@ -1382,17 +1382,6 @@ internal static Exception UnsupportedFeatureAndToken(SqlInternalConnectionTds in return exc; } - internal static Exception Azure_ManagedIdentityException(string msg) - { - SqlErrorCollection errors = new SqlErrorCollection - { - new SqlError(0, (byte)0x00, TdsEnums.FATAL_ERROR_CLASS, null, msg, "", 0) - }; - SqlException exc = SqlException.CreateException(errors, null); - exc._doNotReconnect = true; // disable open retry logic on this error - return exc; - } - #region Always Encrypted Errors #region Always Encrypted - Certificate Store Provider Errors diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs index f6df0a82fe..27a87d29e4 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs @@ -1132,44 +1132,6 @@ public enum SqlCommandColumnEncryptionSetting /// Disabled, } - - /// - public enum SqlAuthenticationMethod - { - /// - NotSpecified = 0, - - /// - SqlPassword, - - /// - [Obsolete("ActiveDirectoryPassword is deprecated, use a more secure authentication method. See https://aka.ms/SqlClientEntraIDAuthentication for more details.")] - ActiveDirectoryPassword, - - /// - ActiveDirectoryIntegrated, - - /// - ActiveDirectoryInteractive, - - /// - ActiveDirectoryServicePrincipal, - - /// - ActiveDirectoryDeviceCodeFlow, - - /// - ActiveDirectoryManagedIdentity, - - /// - ActiveDirectoryMSI, - - /// - ActiveDirectoryDefault, - - /// - ActiveDirectoryWorkloadIdentity - } // This enum indicates the state of TransparentNetworkIPResolution // The first attempt when TNIR is on should be sequential. If the first attempt fails next attempts should be parallel. internal enum TransparentNetworkResolutionState @@ -1179,12 +1141,6 @@ internal enum TransparentNetworkResolutionState ParallelMode }; - internal class ActiveDirectoryAuthentication - { - internal const string AdoClientId = "2fd908ad-0664-4344-b9be-cd3e8b574c38"; - internal const string MSALGetAccessTokenFunctionName = "AcquireToken"; - } - // Fields in the first resultset of "sp_describe_parameter_encryption". // We expect the server to return the fields in the resultset in the same order as mentioned below. // If the server changes the below order, then transparent parameter encryption will break. diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs index eb72b8d6b1..f07ac1a138 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -4307,7 +4307,8 @@ private TdsOperationStatus TryProcessLoginAck(TdsParserStateObject stateObj, out private TdsOperationStatus TryProcessFedAuthInfo(TdsParserStateObject stateObj, int tokenLen, out SqlFedAuthInfo sqlFedAuthInfo) { sqlFedAuthInfo = null; - SqlFedAuthInfo tempFedAuthInfo = new SqlFedAuthInfo(); + string spn = null; + string stsUrl = null; // Skip reading token length, since it has already been read in caller SqlClientEventSource.Log.TryAdvancedTraceEvent(" FEDAUTHINFO token stream length = {0}", tokenLen); @@ -4400,15 +4401,15 @@ private TdsOperationStatus TryProcessFedAuthInfo(TdsParserStateObject stateObj, } SqlClientEventSource.Log.TryAdvancedTraceEvent(" FedAuthInfoData: {0}", data); - // store data in tempFedAuthInfo + // Store data in temporaries. switch ((TdsEnums.FedAuthInfoId)id) { case TdsEnums.FedAuthInfoId.Spn: - tempFedAuthInfo.spn = data; + spn = data; break; case TdsEnums.FedAuthInfoId.Stsurl: - tempFedAuthInfo.stsurl = data; + stsUrl = data; break; default: @@ -4423,15 +4424,16 @@ private TdsOperationStatus TryProcessFedAuthInfo(TdsParserStateObject stateObj, throw SQL.ParsingErrorLength(ParsingErrorState.FedAuthInfoLengthTooShortForData, tokenLen); } - SqlClientEventSource.Log.TryTraceEvent(" Processed FEDAUTHINFO token stream: {0}", tempFedAuthInfo); - if (string.IsNullOrWhiteSpace(tempFedAuthInfo.stsurl) || string.IsNullOrWhiteSpace(tempFedAuthInfo.spn)) + if (string.IsNullOrWhiteSpace(spn) || string.IsNullOrWhiteSpace(stsUrl)) { // We should be receiving both stsurl and spn SqlClientEventSource.Log.TryTraceEvent(" FEDAUTHINFO token stream does not contain both STSURL and SPN."); throw SQL.ParsingError(ParsingErrorState.FedAuthInfoDoesNotContainStsurlAndSpn); } - sqlFedAuthInfo = tempFedAuthInfo; + sqlFedAuthInfo = new(spn, stsUrl); + SqlClientEventSource.Log.TryTraceEvent(" Processed FEDAUTHINFO token stream: {0}", sqlFedAuthInfo); + return TdsOperationStatus.Done; } @@ -9563,11 +9565,11 @@ private int ApplyFeatureExData(TdsEnums.FeatureExtension requestedFeatures, internal void SendFedAuthToken(SqlFedAuthToken fedAuthToken) { Debug.Assert(fedAuthToken != null, "fedAuthToken cannot be null"); - Debug.Assert(fedAuthToken.accessToken != null, "fedAuthToken.accessToken cannot be null"); + Debug.Assert(fedAuthToken.AccessToken != null, "fedAuthToken.AccessToken cannot be null"); SqlClientEventSource.Log.TryTraceEvent(" Sending federated authentication token"); _physicalStateObj._outputMessageType = TdsEnums.MT_FEDAUTH; - byte[] accessToken = fedAuthToken.accessToken; + byte[] accessToken = fedAuthToken.AccessToken; // Send total length (length of token plus 4 bytes for the token length field) // If we were sending a nonce, this would include that length as well diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserHelperClasses.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserHelperClasses.cs index 8f0bb915c0..16386e1d9d 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserHelperClasses.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserHelperClasses.cs @@ -13,6 +13,7 @@ using System.Security; using System.Security.Authentication; using System.Text; +using System.Text.Encodings; using Microsoft.Data.Common; using Microsoft.Data.Common.ConnectionString; @@ -128,23 +129,53 @@ internal sealed class SqlLoginAck internal uint tdsVersion; } + #nullable enable + internal sealed class SqlFedAuthInfo { - internal string spn; - internal string stsurl; + internal string Spn { get; } + internal string StsUrl { get; } + + internal SqlFedAuthInfo(string spn, string stsurl) + { + Spn = spn; + StsUrl = stsurl; + } + public override string ToString() { - return $"STSURL: {stsurl}, SPN: {spn}"; + return $"SPN: {Spn}, STSURL: {StsUrl}"; } } internal sealed class SqlFedAuthToken { - internal uint dataLen; - internal byte[] accessToken; - internal long expirationFileTime; + internal byte[] AccessToken { get; } + internal uint DataLen { get; } + internal long ExpirationFileTime { get; } + + internal SqlFedAuthToken( + byte[] accessToken, + long expirationFileTime) + { + AccessToken = accessToken; + DataLen = (uint)AccessToken.Length; + ExpirationFileTime = expirationFileTime; + } + + /// + /// Convert from a SqlAuthenticationToken. + /// + internal SqlFedAuthToken(SqlAuthenticationToken token) + { + AccessToken = Encoding.Unicode.GetBytes(token.AccessToken); + DataLen = (uint)AccessToken.Length; + ExpirationFileTime = token.ExpiresOn.ToFileTime(); + } } + #nullable disable + internal sealed class _SqlMetaData : SqlMetaDataPriv { [Flags] diff --git a/src/Microsoft.Data.SqlClient/tests/CustomConfigurableRetryLogic/CustomRetryLogicProvider.csproj b/src/Microsoft.Data.SqlClient/tests/CustomConfigurableRetryLogic/CustomRetryLogicProvider.csproj index 1b35e8f80d..f420579f9e 100644 --- a/src/Microsoft.Data.SqlClient/tests/CustomConfigurableRetryLogic/CustomRetryLogicProvider.csproj +++ b/src/Microsoft.Data.SqlClient/tests/CustomConfigurableRetryLogic/CustomRetryLogicProvider.csproj @@ -10,9 +10,15 @@ - - - - + + + + + + + + + + diff --git a/src/Microsoft.Data.SqlClient/tests/Directory.Build.props b/src/Microsoft.Data.SqlClient/tests/Directory.Build.props index b470f899e1..a23f3f03a4 100644 --- a/src/Microsoft.Data.SqlClient/tests/Directory.Build.props +++ b/src/Microsoft.Data.SqlClient/tests/Directory.Build.props @@ -11,7 +11,6 @@ true Debug;Release; AnyCPU;x86;x64 - Project diff --git a/src/Microsoft.Data.SqlClient/tests/Directory.Packages.props b/src/Microsoft.Data.SqlClient/tests/Directory.Packages.props index db17cb1e48..f03010f23a 100644 --- a/src/Microsoft.Data.SqlClient/tests/Directory.Packages.props +++ b/src/Microsoft.Data.SqlClient/tests/Directory.Packages.props @@ -14,9 +14,4 @@ - - - - - diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AADAuthenticationTests.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AADAuthenticationTests.cs index f354e6f806..32050a9716 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AADAuthenticationTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AADAuthenticationTests.cs @@ -49,16 +49,20 @@ private void InvalidCombinationCheck(SqlCredential credential) Assert.Throws(() => connection.AccessToken = "SampleAccessToken"); } } - - #if NETFRAMEWORK - // This test is only valid for .NET Framework /// - /// Tests whether SQL Auth provider is overridden using app.config file. - /// This use case is only supported for .NET Framework applications, as driver doesn't support reading configuration from appsettings.json file. - /// In future if need be, appsettings.json support can be added. + /// 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. /// - [Fact] + [ConditionalFact(typeof(TestUtility), nameof(TestUtility.IsNetFramework))] public async Task IsDummySqlAuthenticationProviderSetByDefault() { var provider = SqlAuthenticationProvider.GetProvider(SqlAuthenticationMethod.ActiveDirectoryInteractive); @@ -69,14 +73,13 @@ public async Task IsDummySqlAuthenticationProviderSetByDefault() var token = await provider.AcquireTokenAsync(null); Assert.Equal(token.AccessToken, DummySqlAuthenticationProvider.DUMMY_TOKEN_STR); } - #endif [Fact] public void CustomActiveDirectoryProviderTest() { SqlAuthenticationProvider authProvider = new ActiveDirectoryAuthenticationProvider(static (result) => Task.CompletedTask); SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, authProvider); - Assert.Equal(authProvider, SqlAuthenticationProvider.GetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow)); + Assert.Same(authProvider, SqlAuthenticationProvider.GetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow)); } [Fact] @@ -84,7 +87,7 @@ public void CustomActiveDirectoryProviderTest_AppClientId() { SqlAuthenticationProvider authProvider = new ActiveDirectoryAuthenticationProvider(Guid.NewGuid().ToString()); SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, authProvider); - Assert.Equal(authProvider, SqlAuthenticationProvider.GetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow)); + Assert.Same(authProvider, SqlAuthenticationProvider.GetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow)); } [Fact] @@ -92,7 +95,7 @@ public void CustomActiveDirectoryProviderTest_AppClientId_DeviceFlowCallback() { SqlAuthenticationProvider authProvider = new ActiveDirectoryAuthenticationProvider(static (result) => Task.CompletedTask, Guid.NewGuid().ToString()); SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, authProvider); - Assert.Equal(authProvider, SqlAuthenticationProvider.GetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow)); + Assert.Same(authProvider, SqlAuthenticationProvider.GetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow)); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AssertExtensions.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AssertExtensions.cs index 1a37bc6f12..7a2acc1160 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AssertExtensions.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AssertExtensions.cs @@ -13,7 +13,7 @@ namespace System { public static class AssertExtensions { - private static bool IsFullFramework => TestUtility.IsFullFramework; + private static bool IsFullFramework => TestUtility.IsNetFramework; public static void Throws(Action action, string message) where T : Exception @@ -36,7 +36,7 @@ public static void Throws(string netCoreParamName, string netFxParamName, Act IsFullFramework ? netFxParamName : netCoreParamName; - if (!TestUtility.NetNative) + if (!TestUtility.IsNetNative) { Assert.Equal(expectedParamName, exception.ParamName); } @@ -57,7 +57,7 @@ public static void Throws(string netCoreParamName, string netFxParamName, Fun IsFullFramework ? netFxParamName : netCoreParamName; - if (!TestUtility.NetNative) + if (!TestUtility.IsNetNative) { Assert.Equal(expectedParamName, exception.ParamName); } @@ -68,7 +68,7 @@ public static T Throws(string paramName, Action action) { T exception = Assert.Throws(action); - if (!TestUtility.NetNative) + if (!TestUtility.IsNetNative) { Assert.Equal(paramName, exception.ParamName); } @@ -89,7 +89,7 @@ public static T Throws(string paramName, Func testCode) { T exception = Assert.Throws(testCode); - if (!TestUtility.NetNative) + if (!TestUtility.IsNetNative) { Assert.Equal(paramName, exception.ParamName); } @@ -102,7 +102,7 @@ public static async Task ThrowsAsync(string paramName, Func testCode { T exception = await Assert.ThrowsAsync(testCode); - if (!TestUtility.NetNative) + if (!TestUtility.IsNetNative) { Assert.Equal(paramName, exception.ParamName); } diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/DataCommon/DummySqlAuthenticationProvider.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/DataCommon/DummySqlAuthenticationProvider.cs index bb5e2e2e52..c68baf63eb 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/DataCommon/DummySqlAuthenticationProvider.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/DataCommon/DummySqlAuthenticationProvider.cs @@ -20,8 +20,9 @@ public class DummySqlAuthenticationProvider : SqlAuthenticationProvider public override Task AcquireTokenAsync(SqlAuthenticationParameters parameters) => Task.FromResult(new SqlAuthenticationToken(DUMMY_TOKEN_STR, new DateTimeOffset(DateTime.Now.AddHours(2)))); - // Supported authentication modes don't matter for dummy test, but added to demonstrate config file usage. public override bool IsSupported(SqlAuthenticationMethod authenticationMethod) - => authenticationMethod == SqlAuthenticationMethod.ActiveDirectoryInteractive; + { + return authenticationMethod == SqlAuthenticationMethod.ActiveDirectoryInteractive; + } } } diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/DataCommon/TestUtility.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/DataCommon/TestUtility.cs index 259dc8817a..bcbc68a502 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/DataCommon/TestUtility.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/DataCommon/TestUtility.cs @@ -2,7 +2,6 @@ // 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.Runtime.InteropServices; namespace Microsoft.Data.SqlClient.Tests @@ -10,7 +9,18 @@ namespace Microsoft.Data.SqlClient.Tests public static class TestUtility { public static readonly bool IsNotArmProcess = RuntimeInformation.ProcessArchitecture != Architecture.Arm; - public static bool IsFullFramework => RuntimeInformation.FrameworkDescription.StartsWith(".NET Framework"); - public static bool NetNative => RuntimeInformation.FrameworkDescription.StartsWith(".NET Native"); + public static bool IsNet + { + get + { + return + !IsNetCore && !IsNetFramework && !IsNetNative && + RuntimeInformation.FrameworkDescription.StartsWith(".NET"); + } + } + + public static bool IsNetCore => RuntimeInformation.FrameworkDescription.StartsWith(".NET Core"); + public static bool IsNetFramework => RuntimeInformation.FrameworkDescription.StartsWith(".NET Framework"); + public static bool IsNetNative => RuntimeInformation.FrameworkDescription.StartsWith(".NET Native"); } } 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 7f6d8abd2c..7c54891ff4 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 @@ -111,6 +111,8 @@ + + Common @@ -128,12 +130,31 @@ TDS + + + + + + + + - - - - + + + + + + + + PreserveNewest diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlAuthenticationProviderTest.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlAuthenticationProviderTest.cs index 8168c26f8e..c0315ce2f0 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlAuthenticationProviderTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlAuthenticationProviderTest.cs @@ -25,17 +25,12 @@ public void DefaultAuthenticationProviders(SqlAuthenticationMethod method) Assert.IsType(SqlAuthenticationProvider.GetProvider(method)); } - #if NETFRAMEWORK - // This test is only valid for .NET Framework - // Overridden by app.config in this project - [Theory] + [ConditionalTheory(typeof(TestUtility), nameof(TestUtility.IsNetFramework))] [InlineData(SqlAuthenticationMethod.ActiveDirectoryInteractive)] public void DefaultAuthenticationProviders_Interactive(SqlAuthenticationMethod method) { Assert.IsType(SqlAuthenticationProvider.GetProvider(method)); } - - #endif } } diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/app.config b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/app.config index fb7f63f65f..9fc08c65a7 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/app.config +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/app.config @@ -7,6 +7,7 @@ + diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj index fbaf55db72..3b46cce657 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj @@ -303,6 +303,8 @@ + + Common @@ -323,11 +325,32 @@ - - - + + + + + + + + + + + + + + + + + @@ -346,7 +369,6 @@ - diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/AADConnectionTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/AADConnectionTest.cs index 608c34c977..2c46c2598d 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/AADConnectionTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/AADConnectionTest.cs @@ -8,7 +8,6 @@ using System.Threading; using System.Threading.Tasks; using Azure.Core; -using Azure.Identity; using Microsoft.Identity.Client; using Xunit; diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataClassificationTest/DataClassificationTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataClassificationTest/DataClassificationTest.cs index 3e7076d52d..9a6b4c7552 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataClassificationTest/DataClassificationTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataClassificationTest/DataClassificationTest.cs @@ -25,7 +25,6 @@ public static void TestDataClassificationResultSetRank() try { sqlConnection.Open(); - Assert.True(DataTestUtility.IsSupportedDataClassification()); CreateTable(sqlCommand); AddSensitivity(sqlCommand, rankEnabled: true); InsertData(sqlCommand); @@ -48,7 +47,6 @@ public static void TestDataClassificationResultSet() try { sqlConnection.Open(); - Assert.True(DataTestUtility.IsSupportedDataClassification()); CreateTable(sqlCommand); AddSensitivity(sqlCommand); InsertData(sqlCommand); diff --git a/src/Microsoft.Data.SqlClient/tests/PerformanceTests/Microsoft.Data.SqlClient.PerformanceTests.csproj b/src/Microsoft.Data.SqlClient/tests/PerformanceTests/Microsoft.Data.SqlClient.PerformanceTests.csproj index 24bcaab242..4b9a8d99f4 100644 --- a/src/Microsoft.Data.SqlClient/tests/PerformanceTests/Microsoft.Data.SqlClient.PerformanceTests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/PerformanceTests/Microsoft.Data.SqlClient.PerformanceTests.csproj @@ -15,12 +15,22 @@ - + + + + + + + + + + + diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/Directory.Build.props b/src/Microsoft.Data.SqlClient/tests/StressTests/Directory.Build.props index 66fbacae6c..26de699227 100644 --- a/src/Microsoft.Data.SqlClient/tests/StressTests/Directory.Build.props +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/Directory.Build.props @@ -6,7 +6,7 @@ - net462;net47;net471;net472;net48;net481;net8.0;net9.0 + net462;net8.0;net9.0 + + 7.0.0 $(AkvVersionDefault).$(BuildNumber)-dev - - - $(MdsPackageVersion) diff --git a/tools/specs/Microsoft.Data.SqlClient.nuspec b/tools/specs/Microsoft.Data.SqlClient.nuspec index a6577ae694..fe8417e554 100644 --- a/tools/specs/Microsoft.Data.SqlClient.nuspec +++ b/tools/specs/Microsoft.Data.SqlClient.nuspec @@ -29,8 +29,6 @@ sqlclient microsoft.data.sqlclient - - @@ -47,8 +45,6 @@ - - @@ -62,8 +58,6 @@ - - @@ -77,8 +71,6 @@ - - diff --git a/tools/specs/Microsoft.SqlServer.Server.nuspec b/tools/specs/Microsoft.SqlServer.Server.nuspec index eec275b292..df75137b92 100644 --- a/tools/specs/Microsoft.SqlServer.Server.nuspec +++ b/tools/specs/Microsoft.SqlServer.Server.nuspec @@ -36,15 +36,20 @@ Microsoft.SqlServer.Server.Format + + - - - + + + - - - + + + diff --git a/tools/targets/GenerateSqlServerPackage.targets b/tools/targets/GenerateSqlServerPackage.targets index ea6655dcee..909c0da6e0 100644 --- a/tools/targets/GenerateSqlServerPackage.targets +++ b/tools/targets/GenerateSqlServerPackage.targets @@ -8,6 +8,6 @@ - + diff --git a/tools/targets/GenerateThisAssemblyCs.targets b/tools/targets/GenerateThisAssemblyCs.targets index 6230af8ae7..86e896013c 100644 --- a/tools/targets/GenerateThisAssemblyCs.targets +++ b/tools/targets/GenerateThisAssemblyCs.targets @@ -4,18 +4,31 @@ + + + +[assembly: System.CLSCompliant(true)] + + + + System + + -[assembly: System.CLSCompliant(true)] -namespace System +$(ThisAssemblyClsCompliantContent) +namespace $(ThisAssemblyNamespace) { -internal static class ThisAssembly -{ -internal const string InformationalVersion = "$(AssemblyFileVersion)"%3B -internal const string NuGetPackageVersion = "$(Version)"%3B -} + internal static class ThisAssembly + { + internal const string InformationalVersion = "$(AssemblyFileVersion)"%3B + internal const string NuGetPackageVersion = "$(Version)"%3B + } }