From 2d60931a2bf45fa3dd1b4f5838e35549c86f1b47 Mon Sep 17 00:00:00 2001 From: Paul Medynski <31868385+paulmedynski@users.noreply.github.com> Date: Tue, 20 Jan 2026 15:40:54 -0400 Subject: [PATCH 1/6] Task 41736: Merge feat/azure-split to main: Step 2 - New files - Added the new Abstractions and Azure source files and associated pipeline files. - Setup build.proj targets and default version numbers. - Standardized project CLS compliance. --- BUILDGUIDE.md | 4 + Directory.Packages.props | 42 +- build.proj | 98 +- .../jobs/run-tests-package-reference-job.yml | 1 - .../build-and-run-tests-netcore-step.yml | 11 - .../steps/build-and-run-tests-netfx-step.yml | 11 - .../templates/steps/ci-project-build-step.yml | 2 - eng/pipelines/dotnet-sqlclient-ci-core.yml | 47 + .../jobs/pack-abstractions-package-ci-job.yml | 158 +++ .../jobs/pack-azure-package-ci-job.yml | 198 ++++ .../jobs/test-abstractions-package-ci-job.yml | 194 ++++ .../jobs/test-azure-package-ci-job.yml | 345 +++++++ eng/pipelines/libraries/build-variables.yml | 2 + .../libraries/ci-build-variables.yml | 7 + eng/pipelines/libraries/common-variables.yml | 41 + .../libraries/mds-validation-variables.yml | 2 + eng/pipelines/libraries/mds-variables.yml | 2 + eng/pipelines/libraries/variables.yml | 2 + .../build-abstractions-package-ci-stage.yml | 128 +++ .../stages/build-azure-package-ci-stage.yml | 278 ++++++ .../variables/akv-official-variables.yml | 2 + eng/pipelines/variables/common-variables.yml | 2 + .../variables/esrp-signing-variables.yml | 2 + .../variables/onebranch-variables.yml | 2 + .../Abstractions/README.md | 285 ++++++ .../doc/SqlAuthenticationMethod.xml | 56 ++ .../doc/SqlAuthenticationParameters.xml | 66 ++ .../doc/SqlAuthenticationProvider.xml | 143 +++ .../SqlAuthenticationProviderException.xml | 82 ++ .../doc/SqlAuthenticationToken.xml | 30 + .../Abstractions/src/Abstractions.csproj | 86 ++ .../src/AbstractionsVersions.props | 70 ++ .../src/SqlAuthenticationMethod.cs | 44 + .../src/SqlAuthenticationParameters.cs | 67 ++ .../src/SqlAuthenticationProvider.cs | 49 + .../src/SqlAuthenticationProviderException.cs | 62 ++ .../src/SqlAuthenticationProviderInternal.cs | 193 ++++ .../src/SqlAuthenticationToken.cs | 44 + .../test/Abstractions.Test.csproj | 34 + .../test/SqlAuthenticationMethodTest.cs | 46 + .../test/SqlAuthenticationParametersTest.cs | 90 ++ .../SqlAuthenticationProviderExceptionTest.cs | 151 +++ .../test/SqlAuthenticationProviderTest.cs | 95 ++ .../test/SqlAuthenticationTokenTest.cs | 61 ++ .../ActiveDirectoryAuthenticationProvider.xml | 200 ++++ .../ActiveDirectoryAuthenticationProvider.cs | 896 ++++++++++++++++++ .../Azure/src/AuthenticationException.cs | 51 + .../Azure/src/Azure.csproj | 120 +++ .../Azure/src/AzureVersions.props | 69 ++ .../Azure/test/AADAuthenticationTests.cs | 33 + .../Azure/test/AADConnectionTest.cs | 396 ++++++++ .../Azure/test/Azure.Test.csproj | 62 ++ .../Azure/test/Config.cs | 274 ++++++ .../Azure/test/DefaultAuthProviderTests.cs | 68 ++ .../Azure/test/StringExtensions.cs | 15 + .../test/WorkloadIdentityFederationTests.cs | 65 ++ src/Microsoft.Data.SqlClient.sln | 104 +- .../netcore/ref/Microsoft.Data.SqlClient.cs | 1 - .../ref/Microsoft.Data.SqlClient.csproj | 8 +- .../src/Microsoft.Data.SqlClient.csproj | 7 + .../netfx/ref/Microsoft.Data.SqlClient.cs | 1 - .../netfx/ref/Microsoft.Data.SqlClient.csproj | 7 + .../netfx/src/Microsoft.Data.SqlClient.csproj | 7 + .../targets/GenerateAssemblyInfo.targets | 1 - .../src/Microsoft.Data.SqlClient.csproj | 7 + .../tools/TDS/TDS.Servers/TDS.Servers.csproj | 3 +- .../tests/tools/TDS/TDS/TDS.csproj | 3 +- tools/props/Versions.props | 6 + tools/targets/GenerateThisAssemblyCs.targets | 45 +- 69 files changed, 5705 insertions(+), 79 deletions(-) create mode 100644 eng/pipelines/jobs/pack-abstractions-package-ci-job.yml create mode 100644 eng/pipelines/jobs/pack-azure-package-ci-job.yml create mode 100644 eng/pipelines/jobs/test-abstractions-package-ci-job.yml create mode 100644 eng/pipelines/jobs/test-azure-package-ci-job.yml create mode 100644 eng/pipelines/stages/build-abstractions-package-ci-stage.yml create mode 100644 eng/pipelines/stages/build-azure-package-ci-stage.yml create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Abstractions/README.md create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationMethod.xml create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationParameters.xml create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationProvider.xml create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationProviderException.xml create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationToken.xml create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Abstractions.csproj create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/AbstractionsVersions.props create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationMethod.cs create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationParameters.cs create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProvider.cs create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProviderException.cs create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProviderInternal.cs create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationToken.cs create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/Abstractions.Test.csproj create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/SqlAuthenticationMethodTest.cs create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/SqlAuthenticationParametersTest.cs create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/SqlAuthenticationProviderExceptionTest.cs create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/SqlAuthenticationProviderTest.cs create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/SqlAuthenticationTokenTest.cs create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Azure/doc/ActiveDirectoryAuthenticationProvider.xml create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Azure/src/ActiveDirectoryAuthenticationProvider.cs create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Azure/src/AuthenticationException.cs create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Azure/src/Azure.csproj create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Azure/src/AzureVersions.props create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Azure/test/AADAuthenticationTests.cs create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Azure/test/AADConnectionTest.cs create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Azure/test/Azure.Test.csproj create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Azure/test/Config.cs create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Azure/test/DefaultAuthProviderTests.cs create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Azure/test/StringExtensions.cs create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Azure/test/WorkloadIdentityFederationTests.cs diff --git a/BUILDGUIDE.md b/BUILDGUIDE.md index 176e329438..897b56ba18 100644 --- a/BUILDGUIDE.md +++ b/BUILDGUIDE.md @@ -29,7 +29,9 @@ The following build targets are defined in `build.proj`: |Target|Description| |-|-| +|`BuildAbstractions`|Restore, build, and pack the Abstractions package, publishing the resulting NuGet into `packages/`.| |`BuildAllConfigurations`|Default target. Builds the .NET Framework and .NET drivers for all target frameworks and operating systems.| +|`BuildAzure`|Restore, build, and pack the Azure package, publishing the resulting NuGet into `packages/`.| |`BuildNetCore`|Builds the .NET driver for all target frameworks.| |`BuildNetCoreAllOS`|Builds the .NET driver for all target frameworks and operating systems.| |`BuildNetFx`|Builds the .NET Framework driver for all target frameworks.| @@ -229,6 +231,8 @@ when using `Package` references. Then, you can specify `Package` references be used, for example: ```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 diff --git a/Directory.Packages.props b/Directory.Packages.props index 33c19e1d2c..42032c2af7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,11 +6,29 @@ - + + + + + + + + + @@ -72,6 +90,16 @@ + + + + + + + + + + @@ -97,7 +125,6 @@ - @@ -112,16 +139,15 @@ + + + + + - - - - - - diff --git a/build.proj b/build.proj index 6154b7c4c3..2ea3686ec4 100644 --- a/build.proj +++ b/build.proj @@ -66,6 +66,8 @@ + + @@ -103,6 +105,91 @@ + + + $(CommonProperties) + + + + $(AbstractionsProperties);AbstractionsPackageVersion=$(AbstractionsPackageVersion) + + + + + $(AbstractionsProperties);AbstractionsAssemblyFileVersion=$(AbstractionsAssemblyFileVersion) + + + + + + + + + + + + + + $(CommonProperties) + + + + $(AzureProperties);AzurePackageVersion=$(AzurePackageVersion) + + + + + $(AzureProperties);AzureAssemblyFileVersion=$(AzureAssemblyFileVersion) + + + + + + + + + + + @@ -110,7 +197,7 @@ + DependsOnTargets="RestoreSqlServerLib;RestoreAbstractions"> @@ -120,7 +207,7 @@ @@ -142,7 +229,7 @@ @@ -162,13 +249,13 @@ + DependsOnTargets="RestoreNetCore;BuildSqlServerLib;BuildAbstractions"> + DependsOnTargets="RestoreNetCore;BuildSqlServerLib;BuildAbstractions"> @@ -348,6 +435,7 @@ + diff --git a/eng/pipelines/common/templates/jobs/run-tests-package-reference-job.yml b/eng/pipelines/common/templates/jobs/run-tests-package-reference-job.yml index e52bba27a2..70517fbd09 100644 --- a/eng/pipelines/common/templates/jobs/run-tests-package-reference-job.yml +++ b/eng/pipelines/common/templates/jobs/run-tests-package-reference-job.yml @@ -67,6 +67,5 @@ jobs: parameters: referenceType: Package buildConfiguration: Release - cleanFirst: true ${{ if parameters.isPreview }}: mdsPackageVersion: $(previewMdsPackageVersion) 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 dded524027..0fcc2f938e 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 @@ -28,10 +28,6 @@ parameters: type: string default: $(Platform) - - name: cleanFirst - type: boolean - default: false - - name: TestTargetOS type: string default: Windowsnetcoreapp @@ -45,13 +41,6 @@ parameters: default: 2 steps: -- ${{ if eq(parameters.cleanFirst, true)}}: - - task: MSBuild@1 - displayName: 'Clean artifacts folder' - inputs: - solution: build.proj - msbuildArguments: '-t:clean' - - task: MSBuild@1 displayName: 'Build AKV Provider .NET' inputs: 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 f2c719e9a3..ee42d13fc6 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 @@ -28,10 +28,6 @@ parameters: type: string default: $(Platform) - - name: cleanFirst - type: boolean - default: false - - name: TestTargetOS type: string default: Windowsnetfx @@ -45,13 +41,6 @@ parameters: default: 2 steps: -- ${{ if eq(parameters.cleanFirst, true)}}: - - task: MSBuild@1 - displayName: 'Clean artifacts folder' - inputs: - solution: build.proj - msbuildArguments: '-t:clean' - - task: MSBuild@1 displayName: 'Build AKV Provider .NET Framework' inputs: diff --git a/eng/pipelines/common/templates/steps/ci-project-build-step.yml b/eng/pipelines/common/templates/steps/ci-project-build-step.yml index fe417f2e11..b8e4de6258 100644 --- a/eng/pipelines/common/templates/steps/ci-project-build-step.yml +++ b/eng/pipelines/common/templates/steps/ci-project-build-step.yml @@ -78,7 +78,6 @@ steps: -p:GenerateDocumentationFile=false -p:BuildNumber=${{ parameters.buildNumber }} -p:MdsPackageVersion=${{ parameters.mdsPackageVersion }} - clean: true - ${{ if or(eq(parameters.build, 'MDS'), eq(parameters.build, 'all')) }}: - task: MSBuild@1 @@ -95,7 +94,6 @@ steps: -p:GenerateNuget=false -p:BuildNumber=${{ parameters.buildNumber }} -p:MdsPackageVersion=${{ parameters.mdsPackageVersion }} - clean: true - ${{ if or(eq(parameters.build, 'AKV'), eq(parameters.build, 'all'), eq(parameters.build, 'allNoDocs')) }}: - task: MSBuild@1 diff --git a/eng/pipelines/dotnet-sqlclient-ci-core.yml b/eng/pipelines/dotnet-sqlclient-ci-core.yml index 6026847c03..f27437fb74 100644 --- a/eng/pipelines/dotnet-sqlclient-ci-core.yml +++ b/eng/pipelines/dotnet-sqlclient-ci-core.yml @@ -110,16 +110,40 @@ parameters: variables: - template: /eng/pipelines/libraries/ci-build-variables.yml@self + + - name: abstractionsArtifactsName + value: Abstractions.Artifacts + + - name: azureArtifactsName + value: Azure.Artifacts - name: mdsArtifactsName value: MDS.Artifacts stages: + # Build the Abstractions package, and publish it to the pipeline artifacts + # under the given artifact name. + - template: /eng/pipelines/stages/build-abstractions-package-ci-stage.yml@self + parameters: + abstractionsArtifactsName: $(abstractionsArtifactsName) + abstractionsPackageVersion: $(abstractionsPackageVersion) + buildConfiguration: ${{ parameters.buildConfiguration }} + debug: ${{ parameters.debug }} + dotnetVerbosity: ${{ parameters.dotnetVerbosity }} + # Build MDS and its NuGet packages. - stage: build_mds_akv_packages_stage displayName: 'Build MDS & AKV Packages' + # When building MDS via packages, we must depend on the Abstractions + # package. + ${{ if eq(parameters.referenceType, 'Package') }}: + dependsOn: + - build_abstractions_package_stage + ${{ else }}: + dependsOn: [] + jobs: - template: /eng/pipelines/common/templates/jobs/ci-build-nugets-job.yml@self parameters: @@ -134,6 +158,29 @@ stages: SNIVersion: ${{parameters.SNIVersion}} SNIValidationFeed: ${{parameters.SNIValidationFeed}} + # Build the Azure package, and publish it to the pipeline artifacts under the + # given artifact name. + - template: /eng/pipelines/stages/build-azure-package-ci-stage.yml@self + parameters: + abstractionsArtifactsName: $(abstractionsArtifactsName) + abstractionsPackageVersion: $(abstractionsPackageVersion) + azureArtifactsName: $(azureArtifactsName) + azurePackageVersion: $(azurePackageVersion) + buildConfiguration: ${{ parameters.buildConfiguration }} + debug: ${{ parameters.debug }} + ${{if eq(parameters.debug, 'true')}}: + dotnetVerbosity: diagnostic + mdsArtifactsName: $(mdsArtifactsName) + mdsPackageVersion: $(mdsPackageVersion) + # When testing MDS via packages, we must depend on the Abstractions, + # MDS, and Azure packages. + ${{ if eq(parameters.referenceType, 'Package') }}: + dependsOn: + - build_abstractions_package_stage + - build_mds_akv_packages_stage + - build_azure_package_stage + referenceType: ${{ parameters.referenceType }} + # Run the stress tests, if desired. - ${{ if eq(parameters.enableStressTests, true) }}: - template: /eng/pipelines/stages/stress-tests-ci-stage.yml@self diff --git a/eng/pipelines/jobs/pack-abstractions-package-ci-job.yml b/eng/pipelines/jobs/pack-abstractions-package-ci-job.yml new file mode 100644 index 0000000000..500cf8492f --- /dev/null +++ b/eng/pipelines/jobs/pack-abstractions-package-ci-job.yml @@ -0,0 +1,158 @@ +################################################################################ +# 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 Abstractions package into NuGet and symbols packages and +# publishes them as a named pipeline artifact. +# +# This template defines a job named 'pack_abstractions_package_job' that can be +# depended on by downstream jobs. + +parameters: + + # The name to apply to the published pipeline artifacts. + - name: abstractionsArtifactsName + type: string + default: Abstractions.Artifact + + # The version to apply to the Abstractions NuGet package and its assemblies. + - name: abstractionsPackageVersion + type: string + + # The type of build to test (Release or Debug) + - name: buildConfiguration + type: string + values: + - Release + - Debug + + # True to enable extra debug steps and logging. + - name: debug + type: boolean + default: false + + # The list of upstream jobs to depend on. + - name: dependsOn + type: object + default: [] + + # The verbosity level for the dotnet CLI commands. + - name: dotnetVerbosity + type: string + default: normal + values: + - quiet + - minimal + - normal + - detailed + - diagnostic + +jobs: + + - job: pack_abstractions_package_job + displayName: 'Pack Abstractions Package' + + dependsOn: ${{ parameters.dependsOn }} + + pool: + name: Azure Pipelines + vmImage: ubuntu-latest + + variables: + + # The Abstractions project file to use for all dotnet CLI commands. + - name: project + value: src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Abstractions.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.dotnetVerbosity }} + + # dotnet CLI arguments for build/test/pack commands + - name: buildArguments + value: >- + $(commonArguments) + --configuration ${{ parameters.buildConfiguration }} + -p:AbstractionsPackageVersion=${{ parameters.abstractionsPackageVersion }} + + # 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: + + # Emit environment variables if debug is enabled. + - ${{ if eq(parameters.debug, true) }}: + - pwsh: 'Get-ChildItem Env: | Sort-Object Name' + displayName: '[Debug] Print Environment Variables' + + # Install the .NET 10.0 SDK. + - task: UseDotNet@2 + displayName: Install .NET 10.0 SDK + inputs: + packageType: sdk + version: 10.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 project. + - task: DotNetCoreCLI@2 + displayName: Restore Project + inputs: + command: custom + custom: restore + projects: $(project) + arguments: $(commonArguments) + + # Build the project. + - task: DotNetCoreCLI@2 + displayName: Build Project + 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.abstractionsArtifactsName }} + publishLocation: pipeline 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..a2e2eeddf4 --- /dev/null +++ b/eng/pipelines/jobs/pack-azure-package-ci-job.yml @@ -0,0 +1,198 @@ +################################################################################ +# 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 artifacts to download. + # + # This is used when the referenceType is 'Package'. + - name: abstractionsArtifactsName + type: string + default: Abstractions.Artifacts + + # The Abstractions package verion to depend on. + # + # This is used when the referenceType is 'Package'. + - name: abstractionsPackageVersion + type: string + + # The name of the pipeline artifacts to publish. + - name: azureArtifactsName + type: string + default: Azure.Artifacts + + # 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 + + # True to enable extra debug steps and logging. + - name: debug + type: boolean + default: false + + # The list of upstream jobs to depend on. + - name: dependsOn + type: object + default: [] + + # The verbosity level for the dotnet CLI commands. + - name: dotnetVerbosity + type: string + default: normal + values: + - quiet + - minimal + - normal + - detailed + - diagnostic + + # 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 + +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.dotnetVerbosity }} + -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: + + # Emit environment variables if debug is enabled. + - ${{ if eq(parameters.debug, true) }}: + - pwsh: 'Get-ChildItem Env: | Sort-Object Name' + displayName: '[Debug] Print Environment Variables' + + # 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 Artifacts + inputs: + artifactName: ${{ parameters.abstractionsArtifactsName }} + 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 10.0 SDK. + - task: UseDotNet@2 + displayName: Install .NET 10.0 SDK + inputs: + packageType: sdk + version: 10.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 project. + - task: DotNetCoreCLI@2 + displayName: Restore Project + inputs: + command: custom + custom: restore + projects: $(project) + arguments: $(commonArguments) + + # Build the project. + - task: DotNetCoreCLI@2 + displayName: Build Project + 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-abstractions-package-ci-job.yml b/eng/pipelines/jobs/test-abstractions-package-ci-job.yml new file mode 100644 index 0000000000..a4fea75efa --- /dev/null +++ b/eng/pipelines/jobs/test-abstractions-package-ci-job.yml @@ -0,0 +1,194 @@ +################################################################################ +# 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 Abstractions package and runs its tests for a set of .NET +# runtimes. +# +# This template defines a job named +# 'test_abstractions_package_job_' that can be depended on by +# downstream jobs. + +parameters: + + # The type of build to test (Release or Debug) + - name: buildConfiguration + type: string + values: + - Release + - Debug + + # True to enable extra debug steps and logging. + - name: debug + type: boolean + default: false + + # The prefix to prepend to the job's display name: + # + # [] Run Stress Tests + # + - name: displayNamePrefix + type: string + + # The verbosity level for the dotnet CLI commands. + - name: dotnetVerbosity + type: string + default: normal + values: + - quiet + - minimal + - normal + - detailed + - diagnostic + + # 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 pool VM image to use. + - name: vmImage + type: string + +jobs: + + - job: test_abstractions_package_job_${{ parameters.jobNameSuffix }} + displayName: '[${{ parameters.displayNamePrefix }}] Test Abstractions 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 Abstractions test project file to use for all dotnet CLI commands. + # + # Building this project implicitly builds the Abstractions project. + - name: project + value: src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/Abstractions.Test.csproj + + # dotnet CLI arguments common to all commands. + - name: commonArguments + value: >- + --verbosity ${{ parameters.dotnetVerbosity }} + + # 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: + + # Emit environment variables if debug is enabled. + - ${{ if eq(parameters.debug, true) }}: + - pwsh: 'Get-ChildItem Env: | Sort-Object Name' + displayName: '[Debug] Print Environment Variables' + + # Install the .NET 10.0 SDK. + - task: UseDotNet@2 + displayName: Install .NET 10.0 SDK + inputs: + packageType: sdk + version: 10.x + + # Install the .NET 9.0 runtime. + - task: UseDotNet@2 + displayName: Install .NET 9.0 Runtime + inputs: + packageType: runtime + 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 project. + - task: DotNetCoreCLI@2 + displayName: Restore Project + inputs: + command: custom + custom: restore + projects: $(project) + arguments: $(commonArguments) + + # Build the project. + - task: DotNetCoreCLI@2 + displayName: Build Project + 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/jobs/test-azure-package-ci-job.yml b/eng/pipelines/jobs/test-azure-package-ci-job.yml new file mode 100644 index 0000000000..4aa94f2766 --- /dev/null +++ b/eng/pipelines/jobs/test-azure-package-ci-job.yml @@ -0,0 +1,345 @@ +################################################################################ +# 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 artifacts to download. + # + # This is used when the referenceType is 'Package'. + - name: abstractionsArtifactsName + type: string + default: Abstractions.Artifacts + + # 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 + + # True to enable extra debug steps and logging. + - name: debug + type: boolean + default: false + + # The prefix to prepend to the job's display name: + # + # [] Run Stress Tests + # + - name: displayNamePrefix + type: string + + # The verbosity level for the dotnet CLI commands. + - name: dotnetVerbosity + type: string + default: normal + values: + - quiet + - minimal + - normal + - detailed + - diagnostic + + # The suffix to append to the job name. + - name: jobNameSuffix + type: string + + # The name of the MDS pipeline artifacts to download. + # + # This is used when the referenceType is 'Package'. + - name: mdsArtifactsName + type: string + default: MDS.Artifacts + + # The MDS package verion to depend on. + # + # This is used when the referenceType is 'Package'. + - name: mdsPackageVersion + 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 + + # Steps to run, if any, to configure a local SQL Server instance on the agent + # VM. + - name: sqlServerSetupSteps + type: stepList + default: [] + + # True if the VM image includes a local SQL Server that supports connections + # via integrated security. + - name: supportsIntegratedSecurity + type: boolean + default: false + + # 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. + # + # Building this project implicitly builds the Azure project. + - 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.dotnetVerbosity }} + -p:ReferenceType=${{ parameters.referenceType }} + -p:AbstractionsPackageVersion=${{ parameters.abstractionsPackageVersion }} + -p:MdsPackageVersion=${{ parameters.mdsPackageVersion }} + + # dotnet CLI arguments for build/test/pack commands. + - name: buildArguments + value: >- + $(commonArguments) + --configuration ${{ parameters.buildConfiguration }} + + # dotnet CLI arguments for test commands. + # + # Filter out tests annotated with the ActiveIssue attribute, which for + # some reason uses the category 'failing' rather than 'ActiveIssue'. + # + - name: testArguments + value: >- + --filter "category != failing" + + # 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: + + # Emit environment variables if debug is enabled. + - ${{ if eq(parameters.debug, true) }}: + - pwsh: 'Get-ChildItem Env: | Sort-Object Name' + displayName: '[Debug] Print Environment Variables' + + # 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 Artifacts + inputs: + artifactName: ${{ parameters.abstractionsArtifactsName }} + targetPath: $(Build.SourcesDirectory)/packages + + # Download the MDS package artifacts into packages/. + # + # The Azure project doesn't depend on MDS, but the test project does. + - task: DownloadPipelineArtifact@2 + displayName: Download MDS Package Artifacts + inputs: + artifactName: ${{ parameters.mdsArtifactsName }} + 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 10.0 SDK. + - task: UseDotNet@2 + displayName: Install .NET 10.0 SDK + inputs: + packageType: sdk + version: 10.x + + # Install the .NET 9.0 runtime. + - task: UseDotNet@2 + displayName: Install .NET 9.0 Runtime + inputs: + packageType: runtime + 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. + + # Setup the test config file. + # + # This must be done before building the project. This template updates + # the sample config file, which is then copied into place by the build. + # + - template: /eng/pipelines/common/templates/steps/update-config-file-step.yml@self + parameters: + debug: ${{ parameters.debug }} + + # The config.json file has many options, but only some of them are + # used by the Azure package tests. We only specify the ones that are + # necessary here. + + AADServicePrincipalId: $(AADServicePrincipalId) + AzureKeyVaultTenantId: $(AzureKeyVaultTenantId) + # macOS doesn't support managed identities. + ManagedIdentitySupported: ${{ not(eq(parameters.vmImage, 'macos-latest')) }} + SupportsIntegratedSecurity: ${{ parameters.supportsIntegratedSecurity }} + TCPConnectionString: $(AZURE_DB_TCP_CONN_STRING) + UserManagedIdentityClientId: $(UserManagedIdentityClientId) + WorkloadIdentityFederationServiceConnectionId: $(WorkloadIdentityFederationServiceConnectionId) + # Avoid exposing secrets to pipeline jobs triggered via forks. This + # prevents external contributors from creating PRs and running + # pipelines that could expose these secrets. + # + # Note that this isn't a perfect restriction since internal + # contributors may want to use forks, but this would prevent them from + # running the full test suite. We don't have a better way to detect + # external parties though. + ${{ if eq(variables['system.pullRequest.isFork'], 'False') }}: + AADPasswordConnectionString: $(AAD_PASSWORD_CONN_STR) + AADServicePrincipalSecret: $(AADServicePrincipalSecret) + + # Perform any local SQL Server setup. + - ${{ parameters.sqlServerSetupSteps }} + + # 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 project. + - task: DotNetCoreCLI@2 + displayName: Restore Project + inputs: + command: custom + custom: restore + projects: $(project) + arguments: $(commonArguments) + + # Build the project. + - task: DotNetCoreCLI@2 + displayName: Build Project + inputs: + command: custom + custom: build + projects: $(project) + arguments: $(buildArguments) --no-restore + + # List the DLLs in the output directory for debugging purposes. + - ${{ if eq(parameters.debug, true) }}: + - pwsh: > + Get-ChildItem + -Path "src/Microsoft.Data.SqlClient.Extensions/Azure/test/bin/${{ parameters.buildConfiguration }}" + -Recurse + displayName: '[Debug] List Output DLLs' + + # Run the tests for each .NET runtime. + - ${{ each runtime in parameters.netRuntimes }}: + - task: DotNetCoreCLI@2 + displayName: Test [${{ runtime }}] + env: + # Many of our tests require access to Azure resources that are + # currently only granted by agents running our custom ADO 1ES + # images in our ADO pools. + ${{ if ne(parameters.poolName, 'Azure Pipelines') }}: + ADO_POOL: 1 + # When using connectedServiceName below, the DotNetCoreCLI task + # needs the system access token to be injected as this environment + # variable. + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + ${{ if eq(parameters.debug, true) }}: + TEST_DEBUG_EMIT: 1 + inputs: + # The tests need to access Azure resources, which is achieved via + # this service connection. See: + # + # https://sqlclientdrivers.visualstudio.com/public/_settings/adminservices?resourceId=ec9623b2-829c-497f-ae1f-7461766f9a9c + connectedServiceName: dotnetMSI-managed-identity + command: custom + custom: test + projects: $(project) + arguments: $(buildArguments) $(testArguments) --no-build -f ${{ runtime }} + + # Run the tests for each .NET Framework runtime. + - ${{ each runtime in parameters.netFrameworkRuntimes }}: + - task: DotNetCoreCLI@2 + displayName: Test [${{ runtime }}] + env: + ${{ if ne(parameters.poolName, 'Azure Pipelines') }}: + ADO_POOL: 1 + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + ${{ if eq(parameters.debug, true) }}: + TEST_DEBUG_EMIT: 1 + inputs: + connectedServiceName: dotnetMSI-managed-identity + command: custom + custom: test + projects: $(project) + arguments: $(buildArguments) $(testArguments) --no-build -f ${{ runtime }} diff --git a/eng/pipelines/libraries/build-variables.yml b/eng/pipelines/libraries/build-variables.yml index a5b9fc99c4..67a374143a 100644 --- a/eng/pipelines/libraries/build-variables.yml +++ b/eng/pipelines/libraries/build-variables.yml @@ -4,6 +4,8 @@ # See the LICENSE file in the project root for more information. # ################################################################################# +# This file is only included in MDS OneBranch Official pipelines. + variables: - template: /eng/pipelines/libraries/common-variables.yml@self - template: /eng/pipelines/libraries/mds-variables.yml@self diff --git a/eng/pipelines/libraries/ci-build-variables.yml b/eng/pipelines/libraries/ci-build-variables.yml index b15de7af5b..c20a9eaa34 100644 --- a/eng/pipelines/libraries/ci-build-variables.yml +++ b/eng/pipelines/libraries/ci-build-variables.yml @@ -4,6 +4,9 @@ # See the LICENSE file in the project root for more information. # ################################################################################# +# This file is only included in PR and CI pipelines for the Abstractions, AKV, +# Azure, and MDS projects. + variables: - group: 'ADO Build properties' - group: 'ADO Test Configuration Properties' @@ -12,8 +15,12 @@ variables: value: '$(Build.BuildNumber)' - name: SQLTarget value: 'localhost' + - name: abstractionsPackageVersion + 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/libraries/common-variables.yml b/eng/pipelines/libraries/common-variables.yml index 8b02561124..25a2e6cd38 100644 --- a/eng/pipelines/libraries/common-variables.yml +++ b/eng/pipelines/libraries/common-variables.yml @@ -4,6 +4,9 @@ # See the LICENSE file in the project root for more information. # ################################################################################# +# This file is only included in Abstractions, Azure, and MDS OneBranch Official +# pipelines. + variables: - group: ESRP Federated Creds (AME) # ESRPConnectedServiceName @@ -34,6 +37,25 @@ variables: # $(Build.BuildNumber) values. - name: assemblyBuildNumber value: $[ split(variables['Build.BuildNumber'], '.')[0] ] + + # ---------------------------------------------------------------------------- + # Abstractions Package Versions + # + # These are version values that will be used by the official build. They + # should be updated after each release to reflect the next release's versions. + + # The NuGet package version for GA releases (non-preview). + - name: abstractionsPackageVersion + value: '1.0.0' + + # The NuGet package version for preview releases. + - name: abstractionsPackagePreviewVersion + value: 1.0.0-preview1.$(Build.BuildNumber) + + # The AssemblyFileVersion for all assemblies in the Abstractions package. + # + - name: abstractionsAssemblyFileVersion + value: 1.0.0.$(assemblyBuildNumber) # ---------------------------------------------------------------------------- # MDS Package Versions @@ -60,3 +82,22 @@ variables: value: $(Major).$(Minor).$(Patch).$(assemblyBuildNumber) - name: nuspecPath value: '$(REPOROOT)/tools/specs/Microsoft.Data.SqlClient.nuspec' + + # ---------------------------------------------------------------------------- + # Azure Package Versions + # + # These are version values that will be used by the official build. They + # should be updated after each release to reflect the next release's versions. + + # The NuGet package version for GA releases (non-preview). + - name: azurePackageVersion + value: '1.0.0' + + # The NuGet package version for preview releases. + - name: azurePackagePreviewVersion + value: 1.0.0-preview1.$(Build.BuildNumber) + + # The AssemblyFileVersion for all assemblies in the Azure package. + # + - name: azureAssemblyFileVersion + value: 1.0.0.$(assemblyBuildNumber) diff --git a/eng/pipelines/libraries/mds-validation-variables.yml b/eng/pipelines/libraries/mds-validation-variables.yml index f465c9c390..067faf8b88 100644 --- a/eng/pipelines/libraries/mds-validation-variables.yml +++ b/eng/pipelines/libraries/mds-validation-variables.yml @@ -4,6 +4,8 @@ # See the LICENSE file in the project root for more information. # ################################################################################# +# This file is only included in MDS OneBranch Official pipelines. + variables: - template: /eng/pipelines/libraries/common-variables.yml@self - template: /eng/pipelines/libraries/mds-variables.yml@self diff --git a/eng/pipelines/libraries/mds-variables.yml b/eng/pipelines/libraries/mds-variables.yml index e0e2cd1abf..60638988bd 100644 --- a/eng/pipelines/libraries/mds-variables.yml +++ b/eng/pipelines/libraries/mds-variables.yml @@ -4,5 +4,7 @@ # See the LICENSE file in the project root for more information. # ################################################################################# +# This file is only included in MDS OneBranch Official pipelines. + variables: - group: Release Variables diff --git a/eng/pipelines/libraries/variables.yml b/eng/pipelines/libraries/variables.yml index 03cf141318..63181ffd84 100644 --- a/eng/pipelines/libraries/variables.yml +++ b/eng/pipelines/libraries/variables.yml @@ -4,6 +4,8 @@ # See the LICENSE file in the project root for more information. # ################################################################################# +# This file is only included in MDS OneBranch Official pipelines. + variables: - template: /eng/pipelines/libraries/build-variables.yml@self # onebranch template variables diff --git a/eng/pipelines/stages/build-abstractions-package-ci-stage.yml b/eng/pipelines/stages/build-abstractions-package-ci-stage.yml new file mode 100644 index 0000000000..d9b2c52ce0 --- /dev/null +++ b/eng/pipelines/stages/build-abstractions-package-ci-stage.yml @@ -0,0 +1,128 @@ +################################################################################ +# 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 Abstractions package, runs tests, and publishes the +# resulting NuGet packages as pipeline artifacts. +# +# The NuGet packages have the following properties: +# +# Name: Microsoft.Data.SqlClient.Extensions.Abstractions +# Version: ${{ abstractionsPackageVersion }} (from parameter) +# +# The following NuGet packages are published: +# +# Microsoft.Data.SqlClient.Extensions.Abstractions..nupkg +# Microsoft.Data.SqlClient.Extensions.Abstractions..snupkg (symbols) +# +# The packages are published to pipeline artifacts with the name specified by +# the ${{ artifactName }} parameter. +# +# This template defines a stage named 'build_abstractions_package_stage' that +# can be depended on by downstream stages. + +parameters: + + # The name of the pipeline artifacts to publish. + - name: abstractionsArtifactsName + type: string + default: Abstractions.Artifacts + + # The version to apply to the NuGet package and DLLs. + - name: abstractionsPackageVersion + type: string + + # The type of build to produce (Release or Debug) + - name: buildConfiguration + type: string + default: Release + values: + - Release + - Debug + + # True to enable extra debug steps and logging. + - name: debug + type: boolean + default: false + + # The verbosity level for the dotnet CLI commands. + - name: dotnetVerbosity + type: string + default: normal + values: + - quiet + - minimal + - normal + - detailed + - diagnostic + +stages: + + - stage: build_abstractions_package_stage + displayName: Build Abstractions Package + + jobs: + + # ------------------------------------------------------------------------ + # Build and test on Linux. + + - template: /eng/pipelines/jobs/test-abstractions-package-ci-job.yml@self + parameters: + buildConfiguration: ${{ parameters.buildConfiguration }} + debug: ${{ parameters.debug }} + displayNamePrefix: Linux + dotnetVerbosity: ${{ parameters.dotnetVerbosity }} + jobNameSuffix: linux + netFrameworkRuntimes: [] + netRuntimes: [net8.0, net9.0, net10.0] + poolName: Azure Pipelines + vmImage: ubuntu-latest + + # ------------------------------------------------------------------------ + # Build and test on Windows + + - template: /eng/pipelines/jobs/test-abstractions-package-ci-job.yml@self + parameters: + buildConfiguration: ${{ parameters.buildConfiguration }} + debug: ${{ parameters.debug }} + displayNamePrefix: Win + dotnetVerbosity: ${{ parameters.dotnetVerbosity }} + jobNameSuffix: windows + netFrameworkRuntimes: [net462] + netRuntimes: [net8.0, net9.0, net10.0] + poolName: Azure Pipelines + vmImage: windows-latest + + # ------------------------------------------------------------------------ + # Build and test on macOS. + + - template: /eng/pipelines/jobs/test-abstractions-package-ci-job.yml@self + parameters: + buildConfiguration: ${{ parameters.buildConfiguration }} + debug: ${{ parameters.debug }} + displayNamePrefix: macOS + dotnetVerbosity: ${{ parameters.dotnetVerbosity }} + jobNameSuffix: macos + netFrameworkRuntimes: [] + netRuntimes: [net8.0, net9.0, net10.0] + poolName: Azure Pipelines + vmImage: macos-latest + + # ------------------------------------------------------------------------ + # Create and publish the NuGet package. + + - template: /eng/pipelines/jobs/pack-abstractions-package-ci-job.yml@self + parameters: + abstractionsArtifactsName: ${{ parameters.abstractionsArtifactsName }} + abstractionsPackageVersion: ${{ parameters.abstractionsPackageVersion }} + buildConfiguration: ${{ parameters.buildConfiguration }} + debug: ${{ parameters.debug }} + dependsOn: + # We depend on all of the test jobs to ensure the tests pass before + # producing the NuGet package. + - test_abstractions_package_job_linux + - test_abstractions_package_job_windows + - test_abstractions_package_job_macos + dotnetVerbosity: ${{ parameters.dotnetVerbosity }} 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..74bec12332 --- /dev/null +++ b/eng/pipelines/stages/build-azure-package-ci-stage.yml @@ -0,0 +1,278 @@ +################################################################################ +# 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 artifacts to download. + # + # This is used when the referenceType is 'Package'. + - name: abstractionsArtifactsName + type: string + default: Abstractions.Artifacts + + # The Abstractions package verion to depend on. + # + # This is used when the referenceType is 'Package'. + - name: abstractionsPackageVersion + type: string + + # The name of the pool to use for jobs that require customized VM images. + - name: adoPoolName + type: string + # This variable should be defined in AzureDevOps Library variable groups, + # for both the Public and ADO.Net projects. + # + # Any pool specified here must contain images with the following names: + # + # - ADO-MMS22-SQL22 + # - ADO-UB22-SQL22 + # + default: $(ci_var_defaultPoolName) + + # The name of the pipeline artifacts to publish. + - name: azureArtifactsName + type: string + default: Azure.Artifacts + + # The version to apply to the NuGet package and DLLs. + - name: azurePackageVersion + type: string + + # The name of the general Azure pool to use for jobs that don't require + # customized VM images. + - name: azurePoolName + type: string + default: Azure Pipelines + + # The type of build to produce (Release or Debug) + - name: buildConfiguration + type: string + default: Release + values: + - Release + - Debug + + # True to enable extra debug steps and logging. + - name: debug + type: boolean + default: false + + # The stages we depend on, if any. + - name: dependsOn + type: object + default: [] + + # The dotnet CLI verbosity to use. + - name: dotnetVerbosity + type: string + default: normal + values: + - quiet + - minimal + - normal + - detailed + - diagnostic + + # The name of the MDS pipeline artifacts to download. + # + # This is used when the referenceType is 'Package'. + - name: mdsArtifactsName + type: string + default: MDS.Artifacts + + # The MDS package verion to depend on. + # + # This is used when the referenceType is 'Package'. + - name: mdsPackageVersion + 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 + +stages: + + - stage: build_azure_package_stage + displayName: Build Azure Package + + dependsOn: ${{ parameters.dependsOn }} + + jobs: + + # ------------------------------------------------------------------------ + # Build and test on Linux. + + - template: /eng/pipelines/jobs/test-azure-package-ci-job.yml@self + parameters: + abstractionsArtifactsName: ${{ parameters.abstractionsArtifactsName }} + abstractionsPackageVersion: ${{ parameters.abstractionsPackageVersion }} + buildConfiguration: ${{ parameters.buildConfiguration }} + debug: ${{ parameters.debug }} + displayNamePrefix: Linux + dotnetVerbosity: ${{ parameters.dotnetVerbosity }} + jobNameSuffix: linux + mdsArtifactsName: ${{ parameters.mdsArtifactsName }} + mdsPackageVersion: ${{ parameters.mdsPackageVersion }} + netFrameworkRuntimes: [] + netRuntimes: [net8.0, net9.0, net10.0] + poolName: ${{ parameters.azurePoolName }} + referenceType: ${{ parameters.referenceType }} + vmImage: ubuntu-latest + + # Use our 1ES ADO pool for comprehensive testing. + - template: /eng/pipelines/jobs/test-azure-package-ci-job.yml@self + parameters: + abstractionsArtifactsName: ${{ parameters.abstractionsArtifactsName }} + abstractionsPackageVersion: ${{ parameters.abstractionsPackageVersion }} + buildConfiguration: ${{ parameters.buildConfiguration }} + debug: ${{ parameters.debug }} + displayNamePrefix: Linux Integration + dotnetVerbosity: ${{ parameters.dotnetVerbosity }} + jobNameSuffix: linux_integration + mdsArtifactsName: ${{ parameters.mdsArtifactsName }} + mdsPackageVersion: ${{ parameters.mdsPackageVersion }} + netFrameworkRuntimes: [] + netRuntimes: [net8.0, net9.0, net10.0] + poolName: ${{ parameters.adoPoolName }} + referenceType: ${{ parameters.referenceType }} + # The image includes a SQL Server instance that we must configure. + sqlServerSetupSteps: + - template: /eng/pipelines/common/templates/steps/configure-sql-server-linux-step.yml@self + parameters: + # Override the template's default step condition. We always + # want this step to run. + condition: true + # Use the Azure DevOps Library variable "password" for the SA + # password. + password: $(password) + vmImage: ADO-UB22-SQL22 + + # ------------------------------------------------------------------------ + # Build and test on Windows + + # Use the Azure Pipelines pool for basic testing. + - template: /eng/pipelines/jobs/test-azure-package-ci-job.yml@self + parameters: + abstractionsArtifactsName: ${{ parameters.abstractionsArtifactsName }} + abstractionsPackageVersion: ${{ parameters.abstractionsPackageVersion }} + buildConfiguration: ${{ parameters.buildConfiguration }} + debug: ${{ parameters.debug }} + displayNamePrefix: Win + dotnetVerbosity: ${{ parameters.dotnetVerbosity }} + jobNameSuffix: windows + mdsArtifactsName: ${{ parameters.mdsArtifactsName }} + mdsPackageVersion: ${{ parameters.mdsPackageVersion }} + netFrameworkRuntimes: [net462] + netRuntimes: [net8.0, net9.0, net10.0] + poolName: ${{ parameters.azurePoolName }} + referenceType: ${{ parameters.referenceType }} + vmImage: windows-latest + + # Use our 1ES ADO pool for comprehensive testing. + - template: /eng/pipelines/jobs/test-azure-package-ci-job.yml@self + parameters: + abstractionsArtifactsName: ${{ parameters.abstractionsArtifactsName }} + abstractionsPackageVersion: ${{ parameters.abstractionsPackageVersion }} + buildConfiguration: ${{ parameters.buildConfiguration }} + debug: ${{ parameters.debug }} + displayNamePrefix: Win Integration + dotnetVerbosity: ${{ parameters.dotnetVerbosity }} + jobNameSuffix: windows_integration + mdsArtifactsName: ${{ parameters.mdsArtifactsName }} + mdsPackageVersion: ${{ parameters.mdsPackageVersion }} + netFrameworkRuntimes: [net462] + netRuntimes: [net8.0, net9.0, net10.0] + poolName: ${{ parameters.adoPoolName }} + referenceType: ${{ parameters.referenceType }} + # The image includes a SQL Server instance that we must configure. + sqlServerSetupSteps: + - template: /eng/pipelines/common/templates/steps/configure-sql-server-win-step.yml@self + # Use defaults for most parameters. + parameters: + # Override the template's default step condition. We always + # want this step to run. + condition: true + enableLocalDB: true + # These variables are from an Azure DevOps Library variable + # group. + fileStreamDirectory: $(FileStreamDirectory) + password: $(password) + sqlRootPath: $(SQL22RootPath) + # The ADO-MMS22-SQL22 image includes a local SQL Server that supports + # integrated security. + supportsIntegratedSecurity: true + vmImage: ADO-MMS22-SQL22 + + # ------------------------------------------------------------------------ + # Build and test on macOS. + + # Use the Azure Pipelines pool for basic testing. + - template: /eng/pipelines/jobs/test-azure-package-ci-job.yml@self + parameters: + abstractionsArtifactsName: ${{ parameters.abstractionsArtifactsName }} + abstractionsPackageVersion: ${{ parameters.abstractionsPackageVersion }} + buildConfiguration: ${{ parameters.buildConfiguration }} + debug: ${{ parameters.debug }} + displayNamePrefix: macOS + dotnetVerbosity: ${{ parameters.dotnetVerbosity }} + jobNameSuffix: macos + mdsArtifactsName: ${{ parameters.mdsArtifactsName }} + mdsPackageVersion: ${{ parameters.mdsPackageVersion }} + netFrameworkRuntimes: [] + netRuntimes: [net8.0, net9.0, net10.0] + poolName: ${{ parameters.azurePoolName }} + referenceType: ${{ parameters.referenceType }} + vmImage: macos-latest + + # We do not currently have any images in our 1ES ADO pools for macOS. + + # ------------------------------------------------------------------------ + # Create and publish the NuGet package. + + - template: /eng/pipelines/jobs/pack-azure-package-ci-job.yml@self + parameters: + abstractionsArtifactsName: ${{ parameters.abstractionsArtifactsName }} + abstractionsPackageVersion: ${{ parameters.abstractionsPackageVersion }} + azureArtifactsName: ${{ parameters.azureArtifactsName }} + azurePackageVersion: ${{ parameters.abstractionsPackageVersion }} + buildConfiguration: ${{ parameters.buildConfiguration }} + debug: ${{ parameters.debug }} + 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_linux_integration + - test_azure_package_job_windows + - test_azure_package_job_windows_integration + - test_azure_package_job_macos + dotnetVerbosity: ${{ parameters.dotnetVerbosity }} + referenceType: ${{ parameters.referenceType }} diff --git a/eng/pipelines/variables/akv-official-variables.yml b/eng/pipelines/variables/akv-official-variables.yml index 591a562686..05f49decc1 100644 --- a/eng/pipelines/variables/akv-official-variables.yml +++ b/eng/pipelines/variables/akv-official-variables.yml @@ -4,6 +4,8 @@ # See the LICENSE file in the project root for more information. # ################################################################################# +# This file is only included in AKV OneBranch Official pipelines. + # @TODO: These seem to only really apply to official builds. Name should probably be adjusted to match. variables: diff --git a/eng/pipelines/variables/common-variables.yml b/eng/pipelines/variables/common-variables.yml index 1486c3c227..275d473aa3 100644 --- a/eng/pipelines/variables/common-variables.yml +++ b/eng/pipelines/variables/common-variables.yml @@ -4,6 +4,8 @@ # See the LICENSE file in the project root for more information. # ################################################################################# +# This file is only included in AKV OneBranch Official pipelines. + variables: # Well-known paths - name: REPO_ROOT diff --git a/eng/pipelines/variables/esrp-signing-variables.yml b/eng/pipelines/variables/esrp-signing-variables.yml index 15d920759f..a768b509c4 100644 --- a/eng/pipelines/variables/esrp-signing-variables.yml +++ b/eng/pipelines/variables/esrp-signing-variables.yml @@ -4,6 +4,8 @@ # See the LICENSE file in the project root for more information. # ################################################################################# +# This file is only included in AKV OneBranch Official pipelines. + # These variables are used for running ESRP signing tasks. All names start with "Signing" to make # it clear that these variables are used for signing (as opposed to other msc tasks). diff --git a/eng/pipelines/variables/onebranch-variables.yml b/eng/pipelines/variables/onebranch-variables.yml index 1a8ffadec5..38436d7440 100644 --- a/eng/pipelines/variables/onebranch-variables.yml +++ b/eng/pipelines/variables/onebranch-variables.yml @@ -4,6 +4,8 @@ # See the LICENSE file in the project root for more information. # ################################################################################# +# This file is only included in AKV OneBranch Official pipelines. + variables: # Variables defined by us ---------------------------------------------- - name: apiScanDllPath diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/README.md b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/README.md new file mode 100644 index 0000000000..fbb8bfb738 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/README.md @@ -0,0 +1,285 @@ +# MDS Azure Extension Design + +## Overview + +For the MDS 7.0.0 release, we are proposing the following package architecture +changes that will decouple several large dependencies from MDS and move them +into a new `Azure` extension package: + +- Create a new `Abstractions` package that all other MDS packages depend on. + - This will contain types and definitions common to the other MDS packages, + such as base classes, enums, delegates, etc. +- Create a new `Azure` package that will own the following implementations: + - Azure Authentication + - Azure Attestation + - Azure Key Vault interactions +- Move the above implementations out of MDS and into the new `Azure` package. +- Move the existing `AzureKeyVaultProvider` (AKV) implementation into the new + `Azure` extension package. + +This will reduce the main MDS package dependency tree along with a moderate +package size reduction. + +## Motivation + +Issue: [#1108](https://github.com/dotnet/SqlClient/issues/1108) + +Customers and the developer community have voiced concerns with MDS being +tightly coupled to Azure dependencies. Many customers do not use Azure and do +not want to deploy unnecessary DLLs with their applications. + +Moving the Azure dependent implementations into a separate `Azure` extension +package achieves two goals: + +- Remove Azure packages as direct dependencies of MDS and reduce the MDS + dependency tree. +- Clearly expose existing MDS extension points, prove their functionality, and + demonstrate how to use them. + +The following dependencies will be removed from the main MDS package: + +- `Azure.Identity` + - `Azure.Core` (transitive) + - `Microsoft.Identity.Client` (transitive) +- `Microsoft.IdentityModel.JsonWebTokens` + - `Microsoft.IdentityModel.Tokens` (transitive) + - `Microsoft.IdentityModel.Logging` (transitive) +- `Microsoft.IdentityModel.Protocols.OpenIdConnect` + - `Microsoft.IdentityModel.Protocols` (transitive) + +The following dependencies will be removed from the AKV Provider package: + +- `Azure.Core` +- `Azure.Security.KeyVault.Keys` + +## Package Architecture + +```mermaid +classDiagram +class MDS +class MDS.Extensions.Abstractions +class MDS.Extensions.Azure +class AKV Provider + +MDS --> MDS.Extensions.Abstractions +MDS ..> MDS.Extensions.Azure +MDS ..> AKV Provider +MDS.Extensions.Azure --> MDS.Extensions.Abstractions +AKV Provider --> MDS.Extensions.Azure + +MDS: Depend on MDS.Extensions.Abstractions +MDS: Load Azure or AKV assembly +MDS.Extensions.Abstractions: Azure Authentication Types +MDS.Extensions.Abstractions: Azure Attestation Types +MDS.Extensions.Abstractions: Azure Key Vault Types +MDS.Extensions.Azure: Depend on MDS.Extensions.Abstractions +MDS.Extensions.Azure: Authentication Implementation +MDS.Extensions.Azure: Attestation Implementation +MDS.Extensions.Azure: Key Vault Implementation +AKV Provider: Depend on MDS.Extensions.Azure +``` + +In previous MDS versions, the AKV package depended directly on the main MDS +package through a ranged version (for example [6.0.0, 7.0.0) - all 6.x +versions). With the new package architecture this is no longer the case. +Extension packages will not depend on the main MDS package, nor will the main +MDS package depend on any extension packages. All dependencies between MDS and +its extensions will occur through the `Abstractions` package. + +This new looser coupling gives applications the flexibility to depend on only +the main MDS package, or on MDS and a subset of it extension packages if +desired. + +## Consuming + +There are several ways that applications may consume MDS and its extensions: + +- MDS without Azure features +- MDS with MDS-supplied Azure features +- MDS with externally supplied Azure features + +Applications never need to directly depend on the `Abstractions` base package. +This will be transitively depended on by other MDS packages. + +### Without Azure Features + +Applications that do not use any Azure features will no longer bring in those +unwanted dependencies transitively. Simply include the main MDS package by +itself: + +```xml + + + +``` + +Calls to MDS APIs that require Azure features will throw an exception, since +no Azure feature implementation is present. + +### With MDS Azure Features + +Applications that wish to use MDS-supplied Azure features will need to include +the new `Azure` extension package as a direct dependency alongside the main MDS +package: + +```xml + + + + +``` + +MDS will automatically detect the `Azure` extension assemblies and load them. + +### With External Azure Features + +Applications that wish to use Azure features supplied by another (non-MDS) +package will need to include that package as a direct dependency alongside the +main MDS package: + +```xml + + + + +``` + +Additionally, applications will need to instruct MDS to use the external Azure +feature implementations via the appropriate APIs at runtime: + +- Authentication: [SqlAuthenticationProvider](https://learn.microsoft.com/en-us/dotnet/api/microsoft.data.sqlclient.sqlauthenticationprovider?view=sqlclient-dotnet-core-6.0) +- Attestation: _**New API will be exposed.**_ +- Key Valut: [SqlColumnEncryptionKeyStoreProvider](https://learn.microsoft.com/en-us/dotnet/api/microsoft.data.sqlclient.sqlcolumnencryptionkeystoreprovider?view=sqlclient-dotnet-core-6.0) + +## Versioning Strategy + +The MDS suite of packages will be versioned independently. This provides +flexibility to update APIs and implementations for packages as needed, avoiding +unnecessary version bumps and releases. The initial release of these packages +will have the following versions: + +|Package|Version|Comment| +|-|-|-| +|`Microsoft.Data.SqlClient.Extensions.Abstractions`|1.0.0|First version of this package.| +|`Microsoft.Data.SqlClient`|7.0.0|Major version bump due to breaking changes described in this document.| +|`Microsoft.Data.SqlClient.Extensions.Azure`|1.0.0|First version of this package.| +|`Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider`|7.0.0|_**Deprecated.**_| + +Going forward, each package will be versioned appropriately based on the nature +of the changes included with subsequent releases. + +**Note**: The `AzureKeyVaultProvider` package will remain at 7.0.0. It will be +deprecated and eventually removed, as it has been replaced by the `Azure` +extension package. + +## Intradependence + +The main MDS package and the new `Azure` package will depend on the +`Abstractions` package. When APIs are added, modified, or removed from the +`Abstractions` package, corresponding changes will be made to the dependent +packages as well. Those dependent packages will then take a strict dependency +on the appropriate `Abstractions` package version. This ensures that only +compatible extensions package versions can co-exist with the main MDS package. + +For example, imagine that a new extensible conenction pooling feature is added +to MDS. The `Abstractions` package would be updated to include any new pooling +APIs, the main MDS package would be updated to accept extensible pooling, and +the new pooling implementation would be included in a new `ConnectionPooling` +extension package. The versions of these packages would look something like +this: + +|Package|Version| +|-|-| +|`Microsoft.Data.SqlClient.Extensions.Abstractions`|1.1.0| +|`Microsoft.Data.SqlClient`|7.1.0| +|`Microsoft.Data.SqlClient.Extensions.ConnectionPooling`|1.0.0| + +Both the main MDS package and the new `ConnectionPooling` package would depend +on `Abstractions` v1.1.0. + +An application wishing to use the new `ConnectionPooling` v1.0.0 package must +also update the main MDS package to v7.1.0. The application would not be able +to use `ConnectionPooling` v1.0.0 and MDS v7.0.0. + +## Backwards Compatibility + +There are several backwards compatibility scenarios to consider for applications +that rely on MDS Azure features currently implemented in the main MDS package +and the AKV package. The new extensions package architecture aims to reduce the +friction for these apps, but not all scenarios will be seamless. + +All of the scenarios below assume that the application is upgrading to the +latest versions of MDS packages. + +### Apps using MDS Azure Authentication + +Applications currently using the MDS-supplied Azure Authentication features will +need to add a dependency on the `Azure` extension package to their project +alongside the main MDS package: + +```xml + + + + +``` + +All Azure Authentication namespaces and types will remain the same, so this +should be the only change necessary for applications. + +### Apps using MDS Azure Attestation + +Applications currently using the MDS-supplied Azure Attestation features will +need to add a dependency on the `Azure` extension package to their project +alongside the main MDS package: + +```xml + + + + +``` + +All Azure Attestation namespaces and types will remain the same, so this should +be the only change necessary for applications. + +### Apps using AKV Provider + +Applications currently using the MDS-supplied AKV provider will have two options +when upgrading to MDS v7.0.0. Both options rely on the main MDS package finding +and loading an appropriate DLL (assembly) at runtime. The absence of an +appropriate DLL will cause Azure Key Vault operations to throw an exception. + +#### Use Azure Extension + +This is the preferred approach. The application would be updated to depend +on the main MDS package and the `Azure` extension package: + +```xml + + + + +``` + +The `Azure` extension package will contain the same namespaces and types as the +current AKV provider and will be a drop-in replacement. The main MDS v7.0.0 +package will look for the `Azure` extension assembly and automatically load it. + +#### Use AKV Provider v7.0.0 + +This is a temporary solution. The AKV provider v7.0.0 will be marked as +deprecated and removed entirely at some point in the future. The applictaion +would remain dependent on the AKV provider, but must update to the v7.0.0 +package. Previous AKV package versions do not support main MDS package versions +beyond the v6.x range. + +```xml + + + + +``` + +This AKV Provider v7.0.0 package will be empty and simply depend on the `Azure` +extension package to transitively provide the Azure Key Vault features. diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationMethod.xml b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationMethod.xml new file mode 100644 index 0000000000..c804a8fb3a --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationMethod.xml @@ -0,0 +1,56 @@ + + + + + Describes the different SQL authentication methods that can be used by a client connecting to Azure SQL Database. For details, see Use Microsoft Entra Authentication. + + + The authentication method is not specified. + 0 + + + The authentication method uses Sql Password. Use Sql Password to connect to a SQL Database using SQL Server authentication. + 1 + + + The authentication method uses Active Directory Password. Use Active Directory Password to connect to a SQL Database using a Microsoft Entra principal name and password. + 2 + + + The authentication method uses Active Directory Integrated. Use Active Directory Integrated to connect to a SQL Database using integrated Windows authentication. + 3 + + + The authentication method uses Active Directory Interactive. Use Active Directory Interactive to connect to a SQL Database with an interactive authentication flow. + 4 + + + The authentication method uses Active Directory Service Principal. Use Active Directory Service Principal to connect to a SQL Database using the client ID and secret of a service principal identity. + 5 + + + The authentication method uses Active Directory Device Code Flow. Use Active Directory Device Code Flow to connect to a SQL Database from devices and operating systems that do not provide a Web browser, using another device to perform interactive authentication. + 6 + + + The authentication method uses Active Directory Managed Identity. Use System Assigned or User Assigned Managed Identity to connect to SQL Database from Azure client environments that have enabled support for Managed Identity. For User Assigned Managed Identity, 'User Id' or 'UID' is required to be set to the "client ID" of the user identity. + 7 + + + Alias for "Active Directory Managed Identity" authentication method. Use System Assigned or User Assigned Managed Identity to connect to SQL Database from Azure client environments that have enabled support for Managed Identity. For User Assigned Managed Identity, 'User Id' or 'UID' is required to be set to the "client ID" of the user identity. + 8 + + + The authentication method uses Active Directory Default. Use this mode to connect to a SQL Database using multiple non-interactive authentication methods tried sequentially to acquire an access token. This method does not fallback to the "Active Directory Interactive" authentication method. + 9 + + + The authentication method uses Active Directory Workload Identity. Use a federated User Assigned Managed Identity to connect to SQL Database from Azure client environments that have enabled support for Workload Identity. The 'User Id' or 'UID' is required to be set to the "client ID" of the user identity. + 10 + + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationParameters.xml b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationParameters.xml new file mode 100644 index 0000000000..087045b2c9 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationParameters.xml @@ -0,0 +1,66 @@ + + + + + + Represents AD authentication parameters passed by a driver to authentication providers. + + + + + 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, or null if not applicable. + The user password, or null if not applicable. + The connection ID. + + 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. + + + Gets the authority URI. + The authority URI. + + + Gets the user login name/ID. + The user login name/ID, or null if not applicable. + + + Gets the user password. + The user password, or null if not applicable. + + + Gets the connection ID. + The connection ID. + + + Gets the authentication timeout value, in seconds. + + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationProvider.xml b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationProvider.xml new file mode 100644 index 0000000000..7848aaec1a --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationProvider.xml @@ -0,0 +1,143 @@ + + + + + Defines the core behavior of authentication providers and provides a base class for derived classes. + + Derived classes must provide a parameterless constructor if they can be instantiated from the app.config file. + + + The following example demonstrates implementing a custom SqlAuthenticationProvider and providing the same to SqlClient for overriding Device Code Flow authentication mode: + + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Microsoft.Data.SqlClient; + using Microsoft.Identity.Client; + + namespace CustomAuthenticationProviderExamples + { + /// <summary> + /// Example demonstrating creating a custom device code flow authentication provider and attaching it to the driver. + /// This is helpful for applications that wish to override the Callback for the Device Code Result implemented by the SqlClient driver. + /// </summary> + public class CustomDeviceCodeFlowAzureAuthenticationProvider : SqlAuthenticationProvider + { + private const string ClientId = "my-client-id"; + private const string ClientName = "My Application Name"; + private const string DefaultScopeSuffix = "/.default"; + + // Maintain a copy of the PublicClientApplication object to cache the underlying access tokens it provides + private static IPublicClientApplication pcApplication; + + public override async Task<SqlAuthenticationToken> AcquireTokenAsync(SqlAuthenticationParameters parameters) + { + string[] scopes = [ parameters.Resource.EndsWith(DefaultScopeSuffix) ? parameters.Resource : parameters.Resource + DefaultScopeSuffix ]; + + IPublicClientApplication app = pcApplication; + if (app == null) + { + pcApplication = app = PublicClientApplicationBuilder.Create(ClientId) + .WithAuthority(parameters.Authority) + .WithClientName(ClientName) + .WithRedirectUri("https://login.microsoftonline.com/common/oauth2/nativeclient") + .Build(); + } + + AuthenticationResult result; + using CancellationTokenSource connectionTimeoutCancellation = new CancellationTokenSource(TimeSpan.FromSeconds(parameters.ConnectionTimeout)); + + try + { + IEnumerable<IAccount> accounts = await app.GetAccountsAsync(); + result = await app.AcquireTokenSilent(scopes, accounts.FirstOrDefault()) + .ExecuteAsync(connectionTimeoutCancellation.Token); + } + catch (MsalUiRequiredException) + { + result = await app.AcquireTokenWithDeviceCode(scopes, deviceCodeResult => CustomDeviceFlowCallback(deviceCodeResult)) + .ExecuteAsync(connectionTimeoutCancellation.Token); + } + + return new SqlAuthenticationToken(result.AccessToken, result.ExpiresOn); + } + + public override bool IsSupported(SqlAuthenticationMethod authenticationMethod) => + authenticationMethod.Equals(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow); + + private Task CustomDeviceFlowCallback(DeviceCodeResult result) + { + Console.WriteLine(result.Message); + return Task.CompletedTask; + } + } + + public class Program + { + public static void Main() + { + // Register our custom authentication provider class to override Active Directory Device Code Flow + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, new CustomDeviceCodeFlowAzureAuthenticationProvider()); + using (SqlConnection sqlConnection = new SqlConnection("Server=<myserver>.database.windows.net;Authentication=Active Directory Device Code Flow;Database=<db>;")) + { + sqlConnection.Open(); + Console.WriteLine("Connected successfully!"); + } + } + } + } + + + + + 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. + + + 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. + + + Indicates whether the specified authentication method is supported. + This method must not throw. + + if the specified authentication method is supported; otherwise, . + + The authentication method. + + + 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/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationToken.xml b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationToken.xml new file mode 100644 index 0000000000..4eb9a78f52 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationToken.xml @@ -0,0 +1,30 @@ + + + + + Represents an authentication token. + + + + Construct with values for all properties. + + The token string. + The token expiration time. + + is null or empty. + + + + 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 new file mode 100644 index 0000000000..ddb74f9244 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Abstractions.csproj @@ -0,0 +1,86 @@ + + + + + + + + netstandard2.0 + + + + + enable + enable + + + + + Microsoft.Data.SqlClient.Extensions.Abstractions + + + + + + $(AbstractionsDefaultMajorVersion).0.0.0 + + $(AbstractionsAssemblyFileVersion) + $(AbstractionsAssemblyFileVersion) + $(AbstractionsPackageVersion) + + $(Artifacts)/doc/$(TargetFramework)/$(AssemblyName).xml + + + + + <_Parameter1>true + + + + + + + + + + $(AssemblyName) + $(AbstractionsPackageVersion) + $(PackagesDir) + true + snupkg + + Microsoft Corporation + Microsoft Corporation + Microsoft.Data.SqlClient Extensions Abstractions + https://github.com/dotnet/SqlClient + MIT + dotnet.png + + + + + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/AbstractionsVersions.props b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/AbstractionsVersions.props new file mode 100644 index 0000000000..cf4adadeb8 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/AbstractionsVersions.props @@ -0,0 +1,70 @@ + + + + + + + + + + + + + 1 + + + <_OurPackageVersion Condition="'$(AbstractionsPackageVersion)' != ''">$(AbstractionsPackageVersion) + <_OurPackageVersion Condition="'$(AbstractionsPackageVersion)' == ''">$(AbstractionsDefaultMajorVersion).0.0.$(BuildNumber)-dev + + + + <_OurAssemblyFileVersion Condition="'$(AbstractionsAssemblyFileVersion)' != ''">$(AbstractionsAssemblyFileVersion) + + <_OurAssemblyFileVersion Condition="'$(AbstractionsAssemblyFileVersion)' == '' and '$(AbstractionsPackageVersion)' != ''">$(AbstractionsPackageVersion.Split('-')[0]) + + <_OurAssemblyFileVersion Condition="'$(AbstractionsAssemblyFileVersion)' == '' and '$(AbstractionsPackageVersion)' == ''">$(AbstractionsDefaultMajorVersion).0.0.$(BuildNumber) + + + $(_OurPackageVersion) + $(_OurAssemblyFileVersion) + + + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationMethod.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationMethod.cs new file mode 100644 index 0000000000..e5413110ae --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationMethod.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Data.SqlClient; + +/// +public enum SqlAuthenticationMethod : int +{ + /// + NotSpecified = 0, + + /// + SqlPassword, + + /// + [Obsolete("ActiveDirectoryPassword is deprecated, use a more secure authentication method. See https://aka.ms/SqlClientEntraIDAuthentication for more details.")] + // Obsoleted with MDS 7.0.0; to be removed at least 2 major versions later. + ActiveDirectoryPassword, + + /// + ActiveDirectoryIntegrated, + + /// + ActiveDirectoryInteractive, + + /// + ActiveDirectoryServicePrincipal, + + /// + ActiveDirectoryDeviceCodeFlow, + + /// + ActiveDirectoryManagedIdentity, + + /// + ActiveDirectoryMSI, + + /// + ActiveDirectoryDefault, + + /// + ActiveDirectoryWorkloadIdentity +} diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationParameters.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationParameters.cs new file mode 100644 index 0000000000..5e36f90cf0 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationParameters.cs @@ -0,0 +1,67 @@ +// 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; + +/// +public sealed 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; } + + /// + // + // We would like to deprecate this method in favour of a new + // AuthenticationTimeout property. See: + // + // https://microsoft.sharepoint-df.com/:fl:/g/contentstorage/CSP_e68c6b62-34b4-4eaa-b836-82e9cdaa0149/IQCyPmTP5HlYSpafY3DJ-8sQAbY4Ajjn2ztRZrM_eQZkyJQ?e=k1nHJd&nav=cz0lMkZjb250ZW50c3RvcmFnZSUyRkNTUF9lNjhjNmI2Mi0zNGI0LTRlYWEtYjgzNi04MmU5Y2RhYTAxNDkmZD1iJTIxWW11TTVyUTBxazY0Tm9McHphb0JTYXhVNHFkaEY5ZE9yS0ZkWTR0cDY3WU5rRUhKaHM0R1JJTjhQanNwcGliSyZmPTAxWklZTVRaNVNIWlNNN1pEWkxCRkpOSDNET0RFN1hTWVEmYz0lMkYmYT1Mb29wQXBwJnA9JTQwZmx1aWR4JTJGbG9vcC1wYWdlLWNvbnRhaW5lciZ4PSU3QiUyMnclMjIlM0ElMjJUMFJUVUh4dGFXTnliM052Wm5RdWMyaGhjbVZ3YjJsdWRDMWtaaTVqYjIxOFlpRlpiWFZOTlhKUk1IRnJOalJPYjB4d2VtRnZRbE5oZUZVMGNXUm9SamxrVDNKTFJtUlpOSFJ3TmpkWlRtdEZTRXBvY3pSSFVrbE9PRkJxYzNCd2FXSkxmREF4V2tsWlRWUmFXbE5DTlVVMFJrMVFSemRhUlROWlV6Vk9SVkZDTmxkRE1rRSUzRCUyMiUyQyUyMmklMjIlM0ElMjI1YzA2ZTE4OS03NWExLTRkNDktYjQyYi1iOTk2YmM4MDc4ZjklMjIlN0Q%3D + // + public int ConnectionTimeout { get; } + + /// + public SqlAuthenticationParameters( + SqlAuthenticationMethod authenticationMethod, + string serverName, + string databaseName, + string resource, + string authority, + string? userId, + string? password, + Guid connectionId, + // This parameter should really be named authenticationTimeout, but we + // must keep the old name for backwards compatibility. + int connectionTimeout) + { + AuthenticationMethod = authenticationMethod; + ServerName = serverName; + DatabaseName = databaseName; + Resource = resource; + Authority = authority; + UserId = userId; + Password = password; + ConnectionId = connectionId; + ConnectionTimeout = connectionTimeout; + } +} diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProvider.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProvider.cs new file mode 100644 index 0000000000..d67897da22 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProvider.cs @@ -0,0 +1,49 @@ +// 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; + +/// +public abstract partial class SqlAuthenticationProvider +{ + /// + public virtual void BeforeLoad(SqlAuthenticationMethod authenticationMethod) { } + + /// + public virtual void BeforeUnload(SqlAuthenticationMethod authenticationMethod) { } + + /// + public abstract bool IsSupported(SqlAuthenticationMethod authenticationMethod); + + /// + public abstract Task AcquireTokenAsync(SqlAuthenticationParameters parameters); + + + /// + // + // We would like to deprecate this method in favour of + // SqlAuthenticationProviderManager.GetProvider(). See: + // + // https://microsoft.sharepoint-df.com/:fl:/g/contentstorage/CSP_e68c6b62-34b4-4eaa-b836-82e9cdaa0149/IQCyPmTP5HlYSpafY3DJ-8sQAbY4Ajjn2ztRZrM_eQZkyJQ?e=k1nHJd&nav=cz0lMkZjb250ZW50c3RvcmFnZSUyRkNTUF9lNjhjNmI2Mi0zNGI0LTRlYWEtYjgzNi04MmU5Y2RhYTAxNDkmZD1iJTIxWW11TTVyUTBxazY0Tm9McHphb0JTYXhVNHFkaEY5ZE9yS0ZkWTR0cDY3WU5rRUhKaHM0R1JJTjhQanNwcGliSyZmPTAxWklZTVRaNVNIWlNNN1pEWkxCRkpOSDNET0RFN1hTWVEmYz0lMkYmYT1Mb29wQXBwJnA9JTQwZmx1aWR4JTJGbG9vcC1wYWdlLWNvbnRhaW5lciZ4PSU3QiUyMnclMjIlM0ElMjJUMFJUVUh4dGFXTnliM052Wm5RdWMyaGhjbVZ3YjJsdWRDMWtaaTVqYjIxOFlpRlpiWFZOTlhKUk1IRnJOalJPYjB4d2VtRnZRbE5oZUZVMGNXUm9SamxrVDNKTFJtUlpOSFJ3TmpkWlRtdEZTRXBvY3pSSFVrbE9PRkJxYzNCd2FXSkxmREF4V2tsWlRWUmFXbE5DTlVVMFJrMVFSemRhUlROWlV6Vk9SVkZDTmxkRE1rRSUzRCUyMiUyQyUyMmklMjIlM0ElMjI1YzA2ZTE4OS03NWExLTRkNDktYjQyYi1iOTk2YmM4MDc4ZjklMjIlN0Q%3D + // + public static SqlAuthenticationProvider? GetProvider( + SqlAuthenticationMethod authenticationMethod) + { + return Internal.GetProvider(authenticationMethod); + } + + /// + // + // We would like to deprecate this method in favour of + // SqlAuthenticationProviderManager.SetProvider(). See: + // + // https://microsoft.sharepoint-df.com/:fl:/g/contentstorage/CSP_e68c6b62-34b4-4eaa-b836-82e9cdaa0149/IQCyPmTP5HlYSpafY3DJ-8sQAbY4Ajjn2ztRZrM_eQZkyJQ?e=k1nHJd&nav=cz0lMkZjb250ZW50c3RvcmFnZSUyRkNTUF9lNjhjNmI2Mi0zNGI0LTRlYWEtYjgzNi04MmU5Y2RhYTAxNDkmZD1iJTIxWW11TTVyUTBxazY0Tm9McHphb0JTYXhVNHFkaEY5ZE9yS0ZkWTR0cDY3WU5rRUhKaHM0R1JJTjhQanNwcGliSyZmPTAxWklZTVRaNVNIWlNNN1pEWkxCRkpOSDNET0RFN1hTWVEmYz0lMkYmYT1Mb29wQXBwJnA9JTQwZmx1aWR4JTJGbG9vcC1wYWdlLWNvbnRhaW5lciZ4PSU3QiUyMnclMjIlM0ElMjJUMFJUVUh4dGFXTnliM052Wm5RdWMyaGhjbVZ3YjJsdWRDMWtaaTVqYjIxOFlpRlpiWFZOTlhKUk1IRnJOalJPYjB4d2VtRnZRbE5oZUZVMGNXUm9SamxrVDNKTFJtUlpOSFJ3TmpkWlRtdEZTRXBvY3pSSFVrbE9PRkJxYzNCd2FXSkxmREF4V2tsWlRWUmFXbE5DTlVVMFJrMVFSemRhUlROWlV6Vk9SVkZDTmxkRE1rRSUzRCUyMiUyQyUyMmklMjIlM0ElMjI1YzA2ZTE4OS03NWExLTRkNDktYjQyYi1iOTk2YmM4MDc4ZjklMjIlN0Q%3D + // + public static bool SetProvider( + SqlAuthenticationMethod authenticationMethod, + SqlAuthenticationProvider provider) + { + return Internal.SetProvider(authenticationMethod, provider); + } +} diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProviderException.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProviderException.cs new file mode 100644 index 0000000000..ebcd89fa0c --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProviderException.cs @@ -0,0 +1,62 @@ +// 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; + +/// +public abstract class SqlAuthenticationProviderException : Exception +{ + /// + /// The string value used when the failure code is not known. + /// + private const string Unknown = "Unknown"; + + /// + protected SqlAuthenticationProviderException( + string message, + Exception? causedBy = null) + : base(message, causedBy) + { + Method = SqlAuthenticationMethod.NotSpecified; + FailureCode = Unknown; + ShouldRetry = false; + RetryPeriod = 0; + } + + /// + protected SqlAuthenticationProviderException( + SqlAuthenticationMethod method, + string failureCode, + bool shouldRetry, + int retryPeriod, + string message, + Exception? causedBy = null) + : base(message, causedBy) + { + Method = method; + FailureCode = failureCode; + ShouldRetry = shouldRetry; + RetryPeriod = shouldRetry && retryPeriod > 0? retryPeriod : 0; + } + + /// + public SqlAuthenticationMethod Method { get; } + + /// + public string FailureCode { get; } + + /// + public bool ShouldRetry { get; } + + /// + public int RetryPeriod { get; } + + /// + public override string ToString() + { + return base.ToString() + + $" Method={Method} FailureCode={FailureCode}" + + $" ShouldRetry={ShouldRetry} RetryPeriod={RetryPeriod}"; + } +} diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProviderInternal.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProviderInternal.cs new file mode 100644 index 0000000000..a1903e9279 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProviderInternal.cs @@ -0,0 +1,193 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Reflection; +using System.Runtime.InteropServices; + +namespace Microsoft.Data.SqlClient; + +/// +public abstract partial class SqlAuthenticationProvider +{ + /// + /// This class implements the static GetProvider and SetProvider methods by + /// using reflection to call into the Microsoft.Data.SqlClient package's + /// SqlAuthenticationProviderManager class, if that assembly is present. + /// + private static class Internal + { + /// + /// Our handle to the reflected GetProvider() method. + /// + private static MethodInfo? _getProvider = null; + + /// + /// Our handle to the reflected SetProvider() method. + /// + private static MethodInfo? _setProvider = null; + + /// + /// Static construction performs the reflection lookups. + /// + static Internal() + { + const string assemblyName = "Microsoft.Data.SqlClient"; + + // If the MDS package is present, load its + // SqlAuthenticationProviderManager class and get/set methods. + try + { + // Try to load the MDS assembly. + var assembly = Assembly.Load(assemblyName); + + if (assembly is null) + { + Log($"MDS assembly={assemblyName} not found; " + + "Get/SetProvider() will not function"); + return; + } + + // TODO(ADO-39845): Verify the assembly is signed by us? + + // Look for the manager class. + const string className = "Microsoft.Data.SqlClient.SqlAuthenticationProviderManager"; + var manager = assembly.GetType(className); + + if (manager is null) + { + Log($"MDS auth manager manager class={className} not found; " + + "Get/SetProvider() will not function"); + return; + } + + // Get handles to the get/set static methods. + _getProvider = manager.GetMethod( + "GetProvider", + BindingFlags.NonPublic | BindingFlags.Static); + + if (_getProvider is null) + { + Log($"MDS GetProvider() method not found; " + + "GetProvider() will not function"); + } + + _setProvider = manager.GetMethod( + "SetProvider", + BindingFlags.NonPublic | BindingFlags.Static); + + if (_setProvider is null) + { + Log($"MDS SetProvider() method not found; " + + "SetProvider() will not function"); + } + } + // All of these exceptions mean we couldn't find the get/set + // methods. + catch (Exception ex) + when (ex is AmbiguousMatchException || + ex is BadImageFormatException || + ex is FileLoadException || + ex is FileNotFoundException) + { + Log($"MDS assembly={assemblyName} not found or not usable; " + + $"Get/SetProvider() will not function: {ex} "); + } + // Any other exceptions are fatal. + } + + /// + /// Call the reflected GetProvider method. + /// + /// + /// The authentication method whose provider to get. + /// + /// + /// Returns null if reflection failed or any exceptions occur. + /// Otherwise, returns as the reflected method does. + /// + internal static SqlAuthenticationProvider? GetProvider( + SqlAuthenticationMethod authenticationMethod) + { + if (_getProvider is null) + { + return null; + } + + try + { + return _getProvider.Invoke(null, [authenticationMethod]) + as SqlAuthenticationProvider; + } + catch (Exception ex) + when (ex is InvalidOperationException || + ex is MemberAccessException || + ex is MethodAccessException || + ex is NotSupportedException || + ex is TargetInvocationException) + { + Log($"GetProvider() invocation failed: " + + $"{ex.GetType().Name}: {ex.Message}"); + return null; + } + } + + /// + /// Call the reflected SetProvider method. + /// + /// + /// The authentication method whose provider to set. + /// + /// + /// The provider to set. + /// + /// + /// Returns false if reflection failed, invocation fails, or any + /// exceptions occur. Otherwise, returns as the reflected method + /// does. + /// + internal static bool SetProvider( + SqlAuthenticationMethod authenticationMethod, + SqlAuthenticationProvider provider) + { + if (_setProvider is null) + { + return false; + } + + try + { + bool? result = + _setProvider.Invoke(null, [authenticationMethod, provider]) + as bool?; + + if (!result.HasValue) + { + Log($"SetProvider() invocation returned null; " + + "translating to false"); + return false; + } + + return result.Value; + } + catch (Exception ex) + when (ex is InvalidOperationException || + ex is MemberAccessException || + ex is MethodAccessException || + ex is NotSupportedException || + ex is TargetInvocationException) + { + Log($"SetProvider() invocation failed: " + + $"{ex.GetType().Name}: {ex.Message}"); + return false; + } + } + + private static void Log(string message) + { + // TODO(https://sqlclientdrivers.visualstudio.com/ADO.Net/_workitems/edit/39080): + // Convert to proper logging. + Console.WriteLine($"SqlAuthenticationProvider.Internal(): {message}"); + } + } +} diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationToken.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationToken.cs new file mode 100644 index 0000000000..b5bdf9d3d3 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationToken.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Data.SqlClient; + +/// +public sealed class SqlAuthenticationToken +{ + /// + public string AccessToken { get; } + + /// + public DateTimeOffset ExpiresOn { get; } + + /// + public SqlAuthenticationToken( + string accessToken, + DateTimeOffset expiresOn) + { + if (string.IsNullOrEmpty(accessToken)) + { + throw new TokenException("AccessToken must not be null or empty."); + } + + AccessToken = accessToken; + ExpiresOn = expiresOn; + } + + /// + /// The exception thrown by the SqlAuthenticationToken constructor. + /// + internal sealed class TokenException : SqlAuthenticationProviderException + { + /// + /// Construct with the exception message. + /// + /// The exception message. + internal TokenException(string message) + : base(message) + { + } + } +} diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/Abstractions.Test.csproj b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/Abstractions.Test.csproj new file mode 100644 index 0000000000..aeb752d648 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/Abstractions.Test.csproj @@ -0,0 +1,34 @@ + + + + net462;net8.0;net9.0;net10.0 + enable + enable + false + true + Microsoft.Data.SqlClient.Extensions.Abstractions.Test + + + + + + + + + + + + + + + + + + + + PreserveNewest + xunit.runner.json + + + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/SqlAuthenticationMethodTest.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/SqlAuthenticationMethodTest.cs new file mode 100644 index 0000000000..dc8f95a7f2 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/SqlAuthenticationMethodTest.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Data.SqlClient.Extensions.Abstractions.Test; + +public class SqlAuthenticationMethodTest +{ + #region Tests + + /// + /// Verify the number of expected enum members. + /// + [Fact] + public void Confirm_Expected_Member_Count() + { +#if NET + Assert.Equal(11, Enum.GetNames().Length); +#else + Assert.Equal(11, Enum.GetNames(typeof(SqlAuthenticationMethod)).Length); +#endif + } + + /// + /// Verify each of the enum member numeric values. + /// + [Fact] + public void Confirm_Expected_Member_Values() + { + Assert.Equal(0, (int)SqlAuthenticationMethod.NotSpecified); + Assert.Equal(1, (int)SqlAuthenticationMethod.SqlPassword); + #pragma warning disable 0618 // Type or member is obsolete + Assert.Equal(2, (int)SqlAuthenticationMethod.ActiveDirectoryPassword); + #pragma warning restore 0618 // Type or member is obsolete + Assert.Equal(3, (int)SqlAuthenticationMethod.ActiveDirectoryIntegrated); + Assert.Equal(4, (int)SqlAuthenticationMethod.ActiveDirectoryInteractive); + Assert.Equal(5, (int)SqlAuthenticationMethod.ActiveDirectoryServicePrincipal); + Assert.Equal(6, (int)SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow); + Assert.Equal(7, (int)SqlAuthenticationMethod.ActiveDirectoryManagedIdentity); + Assert.Equal(8, (int)SqlAuthenticationMethod.ActiveDirectoryMSI); + Assert.Equal(9, (int)SqlAuthenticationMethod.ActiveDirectoryDefault); + Assert.Equal(10, (int)SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity); + } + + #endregion +} diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/SqlAuthenticationParametersTest.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/SqlAuthenticationParametersTest.cs new file mode 100644 index 0000000000..e9b8dca08c --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/SqlAuthenticationParametersTest.cs @@ -0,0 +1,90 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Data.SqlClient.Extensions.Abstractions.Test; + +public class SqlAuthenticationParametersTest +{ + #region Tests + + /// + /// Verify that the properties are set correctly when nullable arguments are + /// null. + /// + [Fact] + public void Constructor_ValidArguments_WithNulls() + { + var method = SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow; + var server = "server"; + var database = "database"; + var resource = "resource"; + var authority = "authority"; + string? user = null; + string? pass = null; + var id = Guid.NewGuid(); + var timeout = 30; + + SqlAuthenticationParameters parameters = new( + method, + server, + database, + resource, + authority, + user, + pass, + id, + timeout); + + Assert.Equal(method, parameters.AuthenticationMethod); + Assert.Equal(server, parameters.ServerName); + Assert.Equal(database, parameters.DatabaseName); + Assert.Equal(resource, parameters.Resource); + Assert.Equal(authority, parameters.Authority); + Assert.Null(parameters.UserId); + Assert.Null(parameters.Password); + Assert.Equal(id, parameters.ConnectionId); + Assert.Equal(timeout, parameters.ConnectionTimeout); + } + + /// + /// Verify that the properties are set correctly when nullable arguments are + /// non-null. + /// + [Fact] + public void Constructor_ValidArguments_WithoutNulls() + { + var method = SqlAuthenticationMethod.ActiveDirectoryIntegrated; + var server = "server"; + var database = "database"; + var resource = "resource"; + var authority = "authority"; + var user = "user"; + var pass = "pass"; + var id = Guid.NewGuid(); + var timeout = 30; + + SqlAuthenticationParameters parameters = new( + method, + server, + database, + resource, + authority, + user, + pass, + id, + timeout); + + Assert.Equal(method, parameters.AuthenticationMethod); + Assert.Equal(server, parameters.ServerName); + Assert.Equal(database, parameters.DatabaseName); + Assert.Equal(resource, parameters.Resource); + Assert.Equal(authority, parameters.Authority); + Assert.Equal(user, parameters.UserId); + Assert.Equal(pass, parameters.Password); + Assert.Equal(id, parameters.ConnectionId); + Assert.Equal(timeout, parameters.ConnectionTimeout); + } + + #endregion +} diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/SqlAuthenticationProviderExceptionTest.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/SqlAuthenticationProviderExceptionTest.cs new file mode 100644 index 0000000000..f8c625c211 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/SqlAuthenticationProviderExceptionTest.cs @@ -0,0 +1,151 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Data.SqlClient.Extensions.Abstractions.Test; + +public class SqlAuthenticationProviderExceptionTest +{ + #region Tests + + /// + /// Verify that the minimal properties are set correctly, and defaults are + /// used otherwise. The causedBy argument is null. + /// + [Fact] + public void Constructor_MinimalInfo_WithoutCausedBy() + { + var message = "message"; + + Error ex = new(message, null); + + Assert.Equal(SqlAuthenticationMethod.NotSpecified, ex.Method); + Assert.Equal("Unknown", ex.FailureCode); + Assert.False(ex.ShouldRetry); + Assert.Equal(0, ex.RetryPeriod); + Assert.Equal(message, ex.Message); + Assert.Null(ex.InnerException); + } + + /// + /// Verify that the minimal properties are set correctly, and defaults are + /// used otherwise. The causedBy argument is not null. + /// + [Fact] + public void Constructor_MinimalInfo_WithCausedBy() + { + var message = "message"; + var causedBy = new Exception("causedBy"); + + Error ex = new(message, causedBy); + + Assert.Equal(SqlAuthenticationMethod.NotSpecified, ex.Method); + Assert.Equal("Unknown", ex.FailureCode); + Assert.False(ex.ShouldRetry); + Assert.Equal(0, ex.RetryPeriod); + Assert.Equal(message, ex.Message); + Assert.Same(causedBy, ex.InnerException); + } + + /// + /// Verify that all properties are set correctly. The causedBy argument is + /// null. + /// + [Fact] + public void Constructor_AllInfo_WithoutCausedBy() + { + var method = SqlAuthenticationMethod.ActiveDirectoryIntegrated; + var failureCode = "failure"; + var shouldRetry = true; + var retryPeriod = 42; + var message = "message"; + + Error ex = new( + method, + failureCode, + shouldRetry, + retryPeriod, + message, + causedBy: null); + + Assert.Equal(method, ex.Method); + Assert.Equal(failureCode, ex.FailureCode); + Assert.Equal(shouldRetry, ex.ShouldRetry); + Assert.Equal(retryPeriod, ex.RetryPeriod); + Assert.Equal(message, ex.Message); + Assert.Null(ex.InnerException); + } + + /// + /// Verify that all properties are set correctly. The causedBy argument is + /// not null. + /// + [Fact] + public void Constructor_AllInfo_WithCausedBy() + { + var method = SqlAuthenticationMethod.ActiveDirectoryServicePrincipal; + var failureCode = "failure"; + var shouldRetry = true; + var retryPeriod = 42; + var message = "message"; + var causedBy = new Exception("causedBy"); + + Error ex = new( + method, + failureCode, + shouldRetry, + retryPeriod, + message, + causedBy); + + Assert.Equal(method, ex.Method); + Assert.Equal(failureCode, ex.FailureCode); + Assert.Equal(shouldRetry, ex.ShouldRetry); + Assert.Equal(retryPeriod, ex.RetryPeriod); + Assert.Equal(message, ex.Message); + Assert.Same(causedBy, ex.InnerException); + } + + #endregion + + #region Helpers + + /// + /// Derive from SqlAuthenticationProviderException to test the abstract + /// class' functionality. + /// + private class Error : SqlAuthenticationProviderException + { + /// + /// Construct with minimal information. + /// + /// The exception message. + /// The exception that caused this exception, or null if none. + public Error(string message, Exception? causedBy) + : base(message, causedBy) + { + } + + /// + /// Construct with all information.. + /// + /// The authentication method. + /// The failure code. + /// Whether the operation should be retried. + /// The retry period. + /// The exception message. + /// The exception that caused this exception, or null if none. + public Error( + SqlAuthenticationMethod method, + string failureCode, + bool shouldRetry, + int retryPeriod, + string message, + Exception? causedBy) + : base(method, failureCode, shouldRetry, retryPeriod, message, causedBy) + { + } + } + + #endregion +} diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/SqlAuthenticationProviderTest.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/SqlAuthenticationProviderTest.cs new file mode 100644 index 0000000000..07e2b40078 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/SqlAuthenticationProviderTest.cs @@ -0,0 +1,95 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Reflection; + +namespace Microsoft.Data.SqlClient.Extensions.Abstractions.Test; + +public class SqlAuthenticationProviderTest +{ + /// + /// Construct to confirm preconditions. + /// + public SqlAuthenticationProviderTest() + { + // Confirm that the MDS assembly is indeed not present. + Assert.Throws( + () => Assembly.Load("Microsoft.Data.SqlClient")); + } + + #region Tests + + /// + /// Test that GetProvider fails predictably when the MDS assembly can't be + /// found. + /// + [Theory] + #pragma warning disable CS0618 // Type or member is obsolete + [InlineData(SqlAuthenticationMethod.ActiveDirectoryPassword)] + #pragma warning restore CS0618 // Type or member is obsolete + [InlineData(SqlAuthenticationMethod.ActiveDirectoryIntegrated)] + [InlineData(SqlAuthenticationMethod.ActiveDirectoryInteractive)] + [InlineData(SqlAuthenticationMethod.ActiveDirectoryServicePrincipal)] + [InlineData(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow)] + [InlineData(SqlAuthenticationMethod.ActiveDirectoryManagedIdentity)] + [InlineData(SqlAuthenticationMethod.ActiveDirectoryMSI)] + [InlineData(SqlAuthenticationMethod.ActiveDirectoryDefault)] + [InlineData(SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity)] + public void GetProvider_NoMdsAssembly(SqlAuthenticationMethod method) + { + // GetProvider() should return null when the MDS assembly can't be + // found. + Assert.Null(SqlAuthenticationProvider.GetProvider(method)); + } + + /// + /// Test that SetProvider fails predictably when the MDS assembly can't be + /// found. + /// + [Theory] + #pragma warning disable CS0618 // Type or member is obsolete + [InlineData(SqlAuthenticationMethod.ActiveDirectoryPassword)] + #pragma warning restore CS0618 // Type or member is obsolete + [InlineData(SqlAuthenticationMethod.ActiveDirectoryIntegrated)] + [InlineData(SqlAuthenticationMethod.ActiveDirectoryInteractive)] + [InlineData(SqlAuthenticationMethod.ActiveDirectoryServicePrincipal)] + [InlineData(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow)] + [InlineData(SqlAuthenticationMethod.ActiveDirectoryManagedIdentity)] + [InlineData(SqlAuthenticationMethod.ActiveDirectoryMSI)] + [InlineData(SqlAuthenticationMethod.ActiveDirectoryDefault)] + [InlineData(SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity)] + public void SetProvider_NoMdsAssembly(SqlAuthenticationMethod method) + { + // SetProvider() should return false when the MDS assembly can't be + // found. + Assert.False( + SqlAuthenticationProvider.SetProvider(method, new Provider())); + } + + #endregion + + #region Helpers + + /// + /// A dummy provider that supports all authentication methods. + /// + private sealed class Provider : SqlAuthenticationProvider + { + /// + public override bool IsSupported( + SqlAuthenticationMethod authenticationMethod) + { + return true; + } + + /// + public override Task AcquireTokenAsync( + SqlAuthenticationParameters parameters) + { + throw new NotImplementedException(); + } + } + + #endregion +} diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/SqlAuthenticationTokenTest.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/SqlAuthenticationTokenTest.cs new file mode 100644 index 0000000000..06448db7a9 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/SqlAuthenticationTokenTest.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Data.SqlClient.Extensions.Abstractions.Test; + +public class SqlAuthenticationTokenTest +{ + #region Tests + + /// + /// Verify that the properties are set correctly. + /// + [Fact] + public void Constructor_ValidArguments() + { + var token = "test"; + var expiry = DateTimeOffset.UtcNow.AddHours(1); + + SqlAuthenticationToken authToken = new(token, expiry); + + Assert.Equal(token, authToken.AccessToken); + Assert.Equal(expiry, authToken.ExpiresOn); + } + + /// + /// Verify that a null token is rejected. + /// + [Fact] + public void Constructor_InvalidArguments_NullToken() + { + string? token = null; + var expiry = DateTimeOffset.UtcNow.AddHours(1); + + var ex = Assert.ThrowsAny(() => + { + new SqlAuthenticationToken(token!, expiry); + }); + + Assert.Equal("AccessToken must not be null or empty.", ex.Message); + } + + /// + /// Verify that an empty token is rejected. + /// + [Fact] + public void Constructor_InvalidArguments_EmptyToken() + { + string token = string.Empty; + var expiry = DateTimeOffset.UtcNow.AddHours(1); + + var ex = Assert.ThrowsAny(() => + { + new SqlAuthenticationToken(token, expiry); + }); + + Assert.Equal("AccessToken must not be null or empty.", ex.Message); + } + + #endregion +} diff --git a/src/Microsoft.Data.SqlClient.Extensions/Azure/doc/ActiveDirectoryAuthenticationProvider.xml b/src/Microsoft.Data.SqlClient.Extensions/Azure/doc/ActiveDirectoryAuthenticationProvider.xml new file mode 100644 index 0000000000..c680d0dd8b --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/doc/ActiveDirectoryAuthenticationProvider.xml @@ -0,0 +1,200 @@ + + + + + + This class implements and is used for active directory federated authentication mechanisms. + + + + + Initializes the class. + + + + + Client Application Id to be used for acquiring an access token for federated authentication. The driver uses its own application client id by default. + + + Initializes the class with the provided application client id. + + + The following example demonstrates providing a user-defined application client id to SqlClient for the "Active Directory Interactive" authentication method: + + using System; + using Microsoft.Data.SqlClient; + + namespace CustomAuthenticationProviderExamples + { + public class Program + { + public static void Main() + { + // Supported for all authentication modes supported by ActiveDirectoryAuthenticationProvider + ActiveDirectoryAuthenticationProvider provider = new ActiveDirectoryAuthenticationProvider("<application_client_id>"); + if (provider.IsSupported(SqlAuthenticationMethod.ActiveDirectoryInteractive)) + { + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryInteractive, provider); + } + + using (SqlConnection sqlConnection = new SqlConnection("Server=<myserver>.database.windows.net;Authentication=Active Directory Interactive;Database=>db<;")) + { + sqlConnection.Open(); + Console.WriteLine("Connected successfully!"); + } + } + } + } + + + + + + The callback method to be used with 'Active Directory Device Code Flow' authentication. + + + (Optional) Client Application Id to be used for acquiring an access token for federated authentication. The driver uses its own application client id by default. + + + Initializes the class with the provided device code flow callback method and application client id. + + + + + The Active Directory authentication parameters passed to authentication providers. + + + Acquires a security token from the authority. + + + Represents an asynchronous operation that returns the authentication token. + + + + + Clears cached user tokens from the token provider. + + + This will cause interactive authentication prompts to appear again if tokens were previously being obtained from the cache. + + + + + The callback method to be used with 'Active Directory Device Code Flow' authentication. + + + Sets the callback method, overriding the default implementation that processes the result for 'Active Directory Device Code Flow' authentication. + + + The following example demonstrates providing a custom device flow callback to SqlClient for the Device Code Flow authentication method: + + using System; + using System.Threading.Tasks; + using Microsoft.Identity.Client; + using Microsoft.Data.SqlClient; + + namespace CustomAuthenticationProviderExamples + { + public class Program + { + public static void Main() + { + SqlAuthenticationProvider authProvider = new ActiveDirectoryAuthenticationProvider(CustomDeviceFlowCallback); + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, authProvider); + using (SqlConnection sqlConnection = new SqlConnection("Server=<myserver>.database.windows.net;Authentication=Active Directory Device Code Flow;Database=<db>;")) + { + sqlConnection.Open(); + Console.WriteLine("Connected successfully!"); + } + } + + private static Task CustomDeviceFlowCallback(DeviceCodeResult result) + { + // Provide custom logic to process result information and read device code. + Console.WriteLine(result.Message); + return Task.FromResult(0); + } + } + } + + + + + + + The parent as an object, in order to be used from shared .NET Standard assemblies. + + + Sets a reference to the ViewController (if using Xamarin.iOS), Activity (if using Xamarin.Android) IWin32Window or IntPtr (if using .NET Framework). Used for invoking the browser for Active Directory Interactive authentication. + + + Mandatory to be set only on Android. See https://aka.ms/msal-net-android-activity for further documentation and details. + + + + + A function to return the current window. + + + Sets a reference to the current that triggers the browser to be shown. Used to center the browser pop-up onto this window." + + + + + The callback method to be called by MSAL.NET to delegate the Web user interface with the Secure Token Service (STS). + + + Sets a callback method which is invoked with a custom Web UI instance that will let the user sign-in with Azure Active Directory, present consent if needed, and get back the authorization code. Applicable when working with Active Directory Interactive authentication. + + + The "authorizationUri" is crafted to leverage PKCE in order to protect the token from a man in the middle attack. Only MSAL.NET can redeem the code. In the event of cancellation, the implementer should return . + + + + + The authentication method. + + + This method is called immediately before the provider is added to authentication provider registry. + + + Avoid performing long-waiting tasks in this method, since it can block other threads from accessing the provider registry. + + + + + The authentication method. + + + This method is called immediately before the provider is removed from the authentication provider registry. + + + For example, this method is called when a different provider with the same authentication method overrides this provider in the authentication provider registry. Avoid performing long-waiting task in this method, since it can block other threads from accessing the provider registry. + + + + The authentication method. + Indicates whether the specified authentication method is supported. + + if the specified authentication method is supported; otherwise, . + + + The supported authentication modes with are: + + Active Directory Password + Active Directory Integrated + Active Directory Interactive + Active Directory Service Principal + Active Directory Device Code Flow + Active Directory Managed Identity + Active Directory MSI + Active Directory Default + + + + + 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..8676cf7566 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/src/AuthenticationException.cs @@ -0,0 +1,51 @@ +// 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. + /// + /// The authentication method. + /// The error message. + 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. + /// + /// The authentication method. + /// The failure code. + /// Whether the operation should be retried. + /// The retry period. + /// The error message. + /// The exception that caused this error. + 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..b6cb0e5f99 --- /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 + + + + + + $(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 + + + + + + + + + + + + + + + + + + + + + + $(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/AADAuthenticationTests.cs b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/AADAuthenticationTests.cs new file mode 100644 index 0000000000..b7f23c1217 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/AADAuthenticationTests.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Data.SqlClient.Extensions.Azure.Test; + +// These tests were moved from MDS FunctionalTests AADAuthenticationTests.cs. +public class AADAuthenticationTests +{ + [Fact] + public void CustomActiveDirectoryProviderTest() + { + SqlAuthenticationProvider authProvider = new ActiveDirectoryAuthenticationProvider(static (result) => Task.CompletedTask); + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, authProvider); + Assert.Same(authProvider, SqlAuthenticationProvider.GetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow)); + } + + [Fact] + public void CustomActiveDirectoryProviderTest_AppClientId() + { + SqlAuthenticationProvider authProvider = new ActiveDirectoryAuthenticationProvider(Guid.NewGuid().ToString()); + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, authProvider); + Assert.Same(authProvider, SqlAuthenticationProvider.GetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow)); + } + + [Fact] + public void CustomActiveDirectoryProviderTest_AppClientId_DeviceFlowCallback() + { + SqlAuthenticationProvider authProvider = new ActiveDirectoryAuthenticationProvider(static (result) => Task.CompletedTask, Guid.NewGuid().ToString()); + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, authProvider); + Assert.Same(authProvider, SqlAuthenticationProvider.GetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow)); + } +} diff --git a/src/Microsoft.Data.SqlClient.Extensions/Azure/test/AADConnectionTest.cs b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/AADConnectionTest.cs new file mode 100644 index 0000000000..eaa9c8a481 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/AADConnectionTest.cs @@ -0,0 +1,396 @@ +// 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. + +// TODO(https://sqlclientdrivers.visualstudio.com/ADO.Net/_workitems/edit/39233): +// Enable this file once the MDS Azure files have been removed. +#if false + +using System.Text.RegularExpressions; + +namespace Microsoft.Data.SqlClient.Extensions.Azure.Test; + +// These tests were migrated from MDS ManualTests AADConnectionTest.cs. +public class AADConnectionTest +{ + [ConditionalFact( + typeof(Config), + nameof(Config.OnAdoPool), + nameof(Config.HasUserManagedIdentityClientId))] + public static void KustoDatabaseTest() + { + // This is a sample Kusto database that can be connected by any AD account. + using SqlConnection connection = new SqlConnection($"Data Source=help.kusto.windows.net; Authentication=Active Directory Default;Trust Server Certificate=True;User ID = {Config.UserManagedIdentityClientId};"); + connection.Open(); + Assert.True(connection.State == System.Data.ConnectionState.Open); + } + + [ConditionalFact( + typeof(Config), + nameof(Config.HasPasswordConnectionString))] + public static void AADPasswordWithWrongPassword() + { + string[] credKeys = { "Password", "PWD" }; + string connStr = RemoveKeysInConnStr(Config.PasswordConnectionString, credKeys) + "Password=TestPassword;"; + + Assert.Throws(() => ConnectAndDisconnect(connStr)); + + // We cannot verify error message with certainty as driver may cache token from other tests for current user + // and error message may change accordingly. + } + + [ConditionalFact( + typeof(Config), + nameof(Config.HasPasswordConnectionString))] + public static void TestADPasswordAuthentication() + { + // Connect to Azure DB with password and retrieve user name. + using (SqlConnection conn = new SqlConnection(Config.PasswordConnectionString)) + { + conn.Open(); + using (SqlCommand sqlCommand = new SqlCommand + ( + cmdText: $"SELECT SUSER_SNAME();", + connection: conn, + transaction: null + )) + { + string customerId = (string)sqlCommand.ExecuteScalar(); + string expected = RetrieveValueFromConnStr(Config.PasswordConnectionString, new string[] { "User ID", "UID" }); + Assert.Equal(expected, customerId); + } + } + } + + [ConditionalFact( + typeof(Config), + nameof(Config.HasPasswordConnectionString))] + public static void EmptyPasswordInConnStrAADPassword() + { + // connection fails with expected error message. + string[] pwdKey = { "Password", "PWD" }; + string connStr = RemoveKeysInConnStr(Config.PasswordConnectionString, pwdKey) + "Password=;"; + SqlException e = Assert.Throws(() => ConnectAndDisconnect(connStr)); + + string? user = FetchKeyInConnStr(Config.PasswordConnectionString, new string[] { "User Id", "UID" }); + string expectedMessage = string.Format("Failed to authenticate the user {0} in Active Directory (Authentication=ActiveDirectoryPassword).", user); + Assert.Contains(expectedMessage, e.Message); + } + + [ConditionalFact( + typeof(Config), + nameof(Config.OnWindows), + nameof(Config.HasPasswordConnectionString))] + public static void EmptyCredInConnStrAADPassword() + { + // connection fails with expected error message. + string[] removeKeys = { "User ID", "Password", "UID", "PWD" }; + string connStr = RemoveKeysInConnStr(Config.PasswordConnectionString, removeKeys) + "User ID=; Password=;"; + SqlException e = Assert.Throws(() => ConnectAndDisconnect(connStr)); + + string expectedMessage = "Failed to authenticate the user in Active Directory (Authentication=ActiveDirectoryPassword)."; + Assert.Contains(expectedMessage, e.Message); + } + + [ConditionalFact( + typeof(Config), + nameof(Config.OnUnix), + nameof(Config.HasPasswordConnectionString))] + public static void EmptyCredInConnStrAADPasswordAnyUnix() + { + // connection fails with expected error message. + string[] removeKeys = { "User ID", "Password", "UID", "PWD" }; + string connStr = RemoveKeysInConnStr(Config.PasswordConnectionString, removeKeys) + "User ID=; Password=;"; + SqlException e = Assert.Throws(() => ConnectAndDisconnect(connStr)); + + string expectedMessage = "MSAL cannot determine the username (UPN) of the currently logged in user.For Integrated Windows Authentication and Username/Password flows, please use .WithUsername() before calling ExecuteAsync()."; + Assert.Contains(expectedMessage, e.Message); + } + + [ConditionalFact( + typeof(Config), + nameof(Config.HasPasswordConnectionString))] + public static void AADPasswordWithInvalidUser() + { + // connection fails with expected error message. + string[] removeKeys = { "User ID", "UID" }; + string user = "testdotnet@domain.com"; + string connStr = RemoveKeysInConnStr(Config.PasswordConnectionString, removeKeys) + $"User ID={user}"; + SqlException e = Assert.Throws(() => ConnectAndDisconnect(connStr)); + + string expectedMessage = string.Format("Failed to authenticate the user {0} in Active Directory (Authentication=ActiveDirectoryPassword).", user); + Assert.Contains(expectedMessage, e.Message); + } + + [ConditionalFact( + typeof(Config), + nameof(Config.HasPasswordConnectionString))] + public static void NoCredentialsActiveDirectoryPassword() + { + // test Passes with correct connection string. + ConnectAndDisconnect(Config.PasswordConnectionString); + + // connection fails with expected error message. + string[] credKeys = { "User ID", "Password", "UID", "PWD" }; + string connStrWithNoCred = RemoveKeysInConnStr(Config.PasswordConnectionString, credKeys); + InvalidOperationException e = Assert.Throws(() => ConnectAndDisconnect(connStrWithNoCred)); + + string expectedMessage = "Either Credential or both 'User ID' and 'Password' (or 'UID' and 'PWD') connection string keywords must be specified, if 'Authentication=Active Directory Password'."; + Assert.Contains(expectedMessage, e.Message); + } + + [ConditionalFact( + typeof(Config), + nameof(Config.HasPasswordConnectionString), + nameof(Config.HasServicePrincipal))] + public static void NoCredentialsActiveDirectoryServicePrincipal() + { + // test Passes with correct connection string. + string[] removeKeys = { "Authentication", "User ID", "Password", "UID", "PWD" }; + string connStr = RemoveKeysInConnStr(Config.PasswordConnectionString, removeKeys) + + $"Authentication=Active Directory Service Principal; User ID={Config.ServicePrincipalId}; PWD={Config.ServicePrincipalSecret};"; + ConnectAndDisconnect(connStr); + + // connection fails with expected error message. + string[] credKeys = { "Authentication", "User ID", "Password", "UID", "PWD" }; + string connStrWithNoCred = RemoveKeysInConnStr(Config.PasswordConnectionString, credKeys) + + "Authentication=Active Directory Service Principal;"; + InvalidOperationException e = Assert.Throws(() => ConnectAndDisconnect(connStrWithNoCred)); + + string expectedMessage = "Either Credential or both 'User ID' and 'Password' (or 'UID' and 'PWD') connection string keywords must be specified, if 'Authentication=Active Directory Service Principal'."; + Assert.Contains(expectedMessage, e.Message); + } + + [ConditionalTheory( + typeof(Config), + nameof(Config.HasPasswordConnectionString), + nameof(Config.HasUserManagedIdentityClientId))] + [InlineData("2445343 2343253")] + [InlineData("2445343$#^@@%2343253")] + public static void ActiveDirectoryManagedIdentityWithInvalidUserIdMustFail(string userId) + { + // connection fails with expected error message. + string[] credKeys = { "Authentication", "User ID", "Password", "UID", "PWD" }; + string connStrWithNoCred = RemoveKeysInConnStr(Config.PasswordConnectionString, credKeys) + + $"Authentication=Active Directory Managed Identity; User Id={userId}"; + + SqlException e = Assert.Throws(() => ConnectAndDisconnect(connStrWithNoCred)); + + Regex expected = new( + @"(\[Managed Identity\]|ManagedIdentityCredential) Authentication unavailable", + RegexOptions.IgnoreCase); + + Assert.Matches(expected, e.GetBaseException().Message); + } + + [ConditionalFact( + typeof(Config), + nameof(Config.OnAdoPool), + nameof(Config.HasPasswordConnectionString), + nameof(Config.HasUserManagedIdentityClientId))] + public static void ActiveDirectoryDefaultMustPass() + { + string[] credKeys = { "Authentication", "User ID", "Password", "UID", "PWD" }; + string connStr = RemoveKeysInConnStr(Config.PasswordConnectionString, credKeys) + + $"Authentication=ActiveDirectoryDefault;User ID={Config.UserManagedIdentityClientId};"; + + // Connection should be established using Managed Identity by default. + ConnectAndDisconnect(connStr); + } + + // This test works on main in the existing jobs (like Win22_Sql22), but + // fails in the Azure project tests on a similar agent/image: + // + // Failed Microsoft.Data.SqlClient.Extensions.Azure.Test.AADConnectionTest.ADIntegratedUsingSSPI [59 ms] + // Error Message: + // Microsoft.Data.SqlClient.SqlException : Failed to authenticate the user NT Authority\Anonymous Logon in Active Directory (Authentication=ActiveDirectoryIntegrated). + // Error code 0xget_user_name_failed + // Failed to acquire access token for ActiveDirectoryIntegrated: Failed to get user name. + // + // ActiveIssue tests can be filtered out of test runs on the dotnet CLI + // using the filter "category != failing". + // + [ActiveIssue("https://sqlclientdrivers.visualstudio.com/ADO.Net/_workitems/edit/40107")] + [ConditionalFact( + typeof(Config), + nameof(Config.SupportsIntegratedSecurity), + nameof(Config.HasTcpConnectionString))] + public static void ADIntegratedUsingSSPI() + { + // test Passes with correct connection string. + string[] removeKeys = { "Authentication", "User ID", "Password", "UID", "PWD", "Trusted_Connection", "Integrated Security" }; + string connStr = RemoveKeysInConnStr(Config.TcpConnectionString, removeKeys) + + $"Authentication=Active Directory Integrated;"; + ConnectAndDisconnect(connStr); + } + + [ConditionalFact( + typeof(Config), + nameof(Config.SupportsManagedIdentity), + nameof(Config.SupportsSystemAssignedManagedIdentity), + nameof(Config.HasPasswordConnectionString))] + public static void SystemAssigned_ManagedIdentityTest() + { + string[] removeKeys = { "Authentication", "User ID", "Password", "UID", "PWD" }; + string connStr = RemoveKeysInConnStr(Config.PasswordConnectionString, removeKeys) + + $"Authentication=Active Directory Managed Identity;"; + ConnectAndDisconnect(connStr); + } + + [ConditionalFact( + typeof(Config), + nameof(Config.OnAdoPool), + nameof(Config.HasPasswordConnectionString), + nameof(Config.HasUserManagedIdentityClientId))] + public static void UserAssigned_ManagedIdentityTest() + { + string[] removeKeys = { "Authentication", "User ID", "Password", "UID", "PWD" }; + string connStr = RemoveKeysInConnStr(Config.PasswordConnectionString, removeKeys) + + $"Authentication=Active Directory Managed Identity; User Id={Config.UserManagedIdentityClientId};"; + ConnectAndDisconnect(connStr); + } + + [ConditionalFact( + typeof(Config), + nameof(Config.SupportsManagedIdentity), + nameof(Config.SupportsSystemAssignedManagedIdentity), + nameof(Config.HasTcpConnectionString), + nameof(Config.IsAzureSqlServer))] + public static void Azure_SystemManagedIdentityTest() + { + string[] removeKeys = { "Authentication", "User ID", "Password", "UID", "PWD", "Trusted_Connection", "Integrated Security" }; + string connectionString = RemoveKeysInConnStr(Config.TcpConnectionString, removeKeys) + + $"Authentication=Active Directory Managed Identity;"; + + using (SqlConnection conn = new SqlConnection(connectionString)) + { + conn.Open(); + + Assert.True(conn.State == System.Data.ConnectionState.Open); + } + } + + [ConditionalFact( + typeof(Config), + nameof(Config.OnAdoPool), + nameof(Config.SupportsManagedIdentity), + nameof(Config.HasTcpConnectionString), + nameof(Config.HasUserManagedIdentityClientId), + nameof(Config.IsAzureSqlServer))] + public static void Azure_UserManagedIdentityTest() + { + string[] removeKeys = { "Authentication", "User ID", "Password", "UID", "PWD", "Trusted_Connection", "Integrated Security" }; + string connectionString = RemoveKeysInConnStr(Config.TcpConnectionString, removeKeys) + + $"Authentication=Active Directory Managed Identity; User Id={Config.UserManagedIdentityClientId}"; + + using (SqlConnection conn = new SqlConnection(connectionString)) + { + conn.Open(); + + Assert.True(conn.State == System.Data.ConnectionState.Open); + } + } + + #region Helpers from AADConnectionTest.cs + + private static void ConnectAndDisconnect( + string connectionString, SqlCredential? credential = null) + { + using SqlConnection conn = new(connectionString); + + if (credential is not null) + { + conn.Credential = credential; + } + + conn.Open(); + + Assert.True(conn.State == System.Data.ConnectionState.Open); + } + + #endregion + + #region Helpers from ManualTests DataTestUtility.cs + + public static string RemoveKeysInConnStr(string connStr, string[] keysToRemove) + { + // tokenize connection string and remove input keys. + string res = ""; + if (connStr != null && keysToRemove != null) + { + string[] keys = connStr.Split(';'); + foreach (var key in keys) + { + if (!string.IsNullOrEmpty(key.Trim())) + { + bool removeKey = false; + foreach (var keyToRemove in keysToRemove) + { + if (key.Trim().ToLower().StartsWith(keyToRemove.Trim().ToLower(), StringComparison.Ordinal)) + { + removeKey = true; + break; + } + } + if (!removeKey) + { + res += key + ";"; + } + } + } + } + return res; + } + + public static string? FetchKeyInConnStr(string connStr, string[] keys) + { + // tokenize connection string and find matching key + if (connStr != null && keys != null) + { + string[] connProps = connStr.Split(';'); + foreach (string cp in connProps) + { + if (!string.IsNullOrEmpty(cp.Trim())) + { + foreach (var key in keys) + { + if (cp.Trim().ToLower().StartsWith(key.Trim().ToLower(), StringComparison.Ordinal)) + { + return cp.Substring(cp.IndexOf('=') + 1); + } + } + } + } + } + return null; + } + + public static string RetrieveValueFromConnStr(string connStr, string[] keywords) + { + // tokenize connection string and retrieve value for a specific key. + string res = ""; + if (connStr != null && keywords != null) + { + string[] keys = connStr.Split(';'); + foreach (var key in keys) + { + foreach (var keyword in keywords) + { + if (!string.IsNullOrEmpty(key.Trim())) + { + if (key.Trim().ToLower().StartsWith(keyword.Trim().ToLower(), StringComparison.Ordinal)) + { + res = key.Substring(key.IndexOf('=') + 1).Trim(); + break; + } + } + } + } + } + return res; + } + + #endregion +} + +#endif 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..c5fd72e34d --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/Azure.Test.csproj @@ -0,0 +1,62 @@ + + + + net462;net8.0;net9.0;net10.0 + enable + enable + false + true + Microsoft.Data.SqlClient.Extensions.Azure.Test + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + xunit.runner.json + + + + + + + + + + + + + + + + + + + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Azure/test/Config.cs b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/Config.cs new file mode 100644 index 0000000000..ad806e68e5 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/Config.cs @@ -0,0 +1,274 @@ +// 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.Runtime.InteropServices; +using System.Text.Json; +using Microsoft.Data.SqlClient.TestUtilities; + +namespace Microsoft.Data.SqlClient.Extensions.Azure.Test; + +/// +/// This class reads configuration information from environment variables and +/// the config.json file for use by our tests. +/// +/// Environment variables take precedence over config.json settings. Note that +/// variable names are case-sensitive on non-Windows platforms. +/// +/// The following variables are supported: +/// +/// ADO_POOL: +/// When defined, indicates that tests are running in an ADO-CI pool. +/// +/// SYSTEM_ACCESSTOKEN: +/// The Azure Pipelines $(System.AccessToken) to use for workload identity +/// federation. +/// +/// TEST_DEBUG_EMIT: +/// When defined, enables debug output of configuration values. +/// +/// TEST_MDS_CONFIG: +/// The path to the config file to use instead of the default. If not +/// supplied, the config file is assumed to be located next to the test +/// assembly and is named config.json. +/// +internal static class Config +{ + #region Config Properties + + internal static bool AdoPool { get; } = false; + internal static bool DebugEmit { get; } = false; + internal static bool IntegratedSecuritySupported { get; } = false; + internal static bool ManagedIdentitySupported { get; } = false; + internal static string PasswordConnectionString { get; } = string.Empty; + internal static string ServicePrincipalId { get; } = string.Empty; + internal static string ServicePrincipalSecret { get; } = string.Empty; + internal static string SystemAccessToken { get; } = string.Empty; + internal static bool SystemAssignedManagedIdentitySupported { get; } = false; + internal static string TcpConnectionString { get; } = string.Empty; + internal static string TenantId { get; } = string.Empty; + internal static bool UseManagedSniOnWindows { get; } = false; + internal static string UserManagedIdentityClientId { get; } = string.Empty; + internal static string WorkloadIdentityFederationServiceConnectionId { get; } = string.Empty; + + #endregion + + #region Conditional Fact/Theory Helpers + + internal static bool HasPasswordConnectionString() => !PasswordConnectionString.Empty(); + internal static bool HasServicePrincipal() => !ServicePrincipalId.Empty() && !ServicePrincipalSecret.Empty(); + internal static bool HasSystemAccessToken() => !SystemAccessToken.Empty(); + internal static bool HasTcpConnectionString() => !TcpConnectionString.Empty(); + internal static bool HasTenantId() => !TenantId.Empty(); + internal static bool HasUserManagedIdentityClientId() => !UserManagedIdentityClientId.Empty(); + internal static bool HasWorkloadIdentityFederationServiceConnectionId() => !WorkloadIdentityFederationServiceConnectionId.Empty(); + + // TODO(https://sqlclientdrivers.visualstudio.com/ADO.Net/_workitems/edit/39233): + // Uncomment this once the MDS Azure files have been removed. + // internal static bool IsAzureSqlServer() => + // Utils.IsAzureSqlServer(new SqlConnectionStringBuilder(TcpConnectionString).DataSource); + + internal static bool OnAdoPool() => AdoPool; + internal static bool OnLinux() => RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + internal static bool OnMacOS() => RuntimeInformation.IsOSPlatform(OSPlatform.OSX); + internal static bool OnWindows() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + internal static bool OnUnix() => OnLinux() || OnMacOS(); + + internal static bool SupportsIntegratedSecurity() => IntegratedSecuritySupported; + internal static bool SupportsManagedIdentity() => ManagedIdentitySupported; + internal static bool SupportsSystemAssignedManagedIdentity() => SystemAssignedManagedIdentitySupported; + + #endregion + + #region Static Construction + + /// + /// Static construction reads configuration settings from the config file + /// and environment variables. + /// + static Config() + { + // Read from the config.json file. If the TEST_MDS_CONFIG environment + // variable is set, use it. Otherwise, assume the config file is in the + // working directory and named config.json. + string configPath = GetEnvVar("TEST_MDS_CONFIG"); + if (configPath.Empty()) + { + configPath = "config.json"; + } + + try + { + using JsonDocument doc = + JsonDocument.Parse( + File.ReadAllText(configPath), + new JsonDocumentOptions + { + CommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true + }); + + JsonElement root = doc.RootElement; + // See the sample config file for information about these settings: + // + // src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/config.default.json + // + // The sample file is copied to the build output directory as + // config.json by the TestUtilities project file. + // + IntegratedSecuritySupported = GetBool(root, "SupportsIntegratedSecurity"); + ManagedIdentitySupported = GetBool(root, "ManagedIdentitySupported"); + PasswordConnectionString = GetString(root, "AADPasswordConnectionString"); + ServicePrincipalId = GetString(root, "AADServicePrincipalId"); + ServicePrincipalSecret = GetString(root, "AADServicePrincipalSecret"); + SystemAssignedManagedIdentitySupported = + GetBool(root, "SupportsSystemAssignedManagedIdentity"); + TcpConnectionString = GetString(root, "TCPConnectionString"); + TenantId = GetString(root, "AzureKeyVaultTenantId"); + UseManagedSniOnWindows = GetBool(root, "UseManagedSNIOnWindows"); + UserManagedIdentityClientId = GetString(root, "UserManagedIdentityClientId"); + WorkloadIdentityFederationServiceConnectionId = + GetString(root, "WorkloadIdentityFederationServiceConnectionId"); + } + catch (Exception ex) + { + Console.WriteLine( + $"Config: Failed to read config file={configPath}: {ex}"); + throw; + } + + // Apply environment variable overrides. + // + // Note that environment variables are case-sensitive on non-Windows + // platforms. + AdoPool = GetEnvFlag("ADO_POOL"); + DebugEmit = GetEnvFlag("TEST_DEBUG_EMIT"); + SystemAccessToken = GetEnvVar("SYSTEM_ACCESSTOKEN"); + + // Emit debug information if requested. + if (DebugEmit) + { + Console.WriteLine("Config:"); + Console.WriteLine( + $" AdoPool: {AdoPool}"); + Console.WriteLine( + $" DebugEmit: {DebugEmit}"); + Console.WriteLine( + $" IntegratedSecuritySupported: {IntegratedSecuritySupported}"); + Console.WriteLine( + $" ManagedIdentitySupported: {ManagedIdentitySupported}"); + Console.WriteLine( + $" PasswordConnectionString: {PasswordConnectionString}"); + Console.WriteLine( + $" ServicePrincipalId: {ServicePrincipalId}"); + Console.WriteLine( + $" ServicePrincipalSecret: {ServicePrincipalSecret.Length}"); + Console.WriteLine( + $" SystemAccessToken: {SystemAccessToken}"); + Console.WriteLine( + $" SystemAssignedManagedIdentitySupported: {SystemAssignedManagedIdentitySupported}"); + Console.WriteLine( + $" TcpConnectionString: {TcpConnectionString}"); + Console.WriteLine( + $" TenantId: {TenantId}"); + Console.WriteLine( + $" UseManagedSniOnWindows: {UseManagedSniOnWindows}"); + Console.WriteLine( + $" UserManagedIdentityClientId: {UserManagedIdentityClientId}"); + Console.WriteLine( + " WorkloadIdentityFederationServiceConnectionId: " + + WorkloadIdentityFederationServiceConnectionId); + } + + // Apply the SNI flag, if necessary. This must occur before any MDS + // APIs are used. + if (UseManagedSniOnWindows) + { + AppContext.SetSwitch( + "Switch.Microsoft.Data.SqlClient.UseManagedNetworkingOnWindows", + true); + } + } + + #endregion + + #region Private Methods + + /// + /// Get a string property from a JSON element. + /// + /// The JSON element to read from. + /// The name of the property to read. + /// The string value of the property, or an empty string if not found or invalid. + private static string GetString(JsonElement element, string name) + { + if (element.TryGetProperty(name, out var property)) + { + try + { + var value = property.GetString(); + if (value is not null) + { + return value; + } + } + catch (InvalidOperationException) + { + // Ignore invalid values. + } + } + + return string.Empty; + } + + /// + /// Get a boolean property from a JSON element. + /// + /// The JSON element to read from. + /// The name of the property to read. + /// The boolean value of the property, or false if not found or invalid. + private static bool GetBool(JsonElement element, string name) + { + if (element.TryGetProperty(name, out var property)) + { + try + { + return property.GetBoolean(); + } + catch (InvalidOperationException) + { + // Ignore invalid values. + } + } + + return false; + } + + /// + /// Get a boolean flag from an environment variable. The variable's value + /// is not examined; only its presence is checked. + /// + /// The name of the environment variable. + /// True if the environment variable is set; false otherwise. + private static bool GetEnvFlag(string name) + { + return Environment.GetEnvironmentVariable(name) is not null; + } + + /// + /// Get a string value from an environment variable. + /// + /// The name of the environment variable. + /// The value of the environment variable, or an empty string if not set. + private static string GetEnvVar(string name) + { + string? value = Environment.GetEnvironmentVariable(name); + if (string.IsNullOrEmpty(value)) + { + return string.Empty; + } + return value; + } + + #endregion +} diff --git a/src/Microsoft.Data.SqlClient.Extensions/Azure/test/DefaultAuthProviderTests.cs b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/DefaultAuthProviderTests.cs new file mode 100644 index 0000000000..1d107b2e0b --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/DefaultAuthProviderTests.cs @@ -0,0 +1,68 @@ +// 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. + +// TODO(https://sqlclientdrivers.visualstudio.com/ADO.Net/_workitems/edit/39233): +// Enable this file once the MDS Azure files have been removed. +#if false + +namespace Microsoft.Data.SqlClient.Extensions.Azure.Test; + +public class DefaultAuthProviderTests +{ + // Verify that our auth provider has been installed for all AAD/Entra + // authentication methods, and not for any other methods. + // + // Note that this isn't testing anything in the Azure package. It actually + // tests the static constructor of the SqlAuthenticationProviderManager + // class in the MDS package and the static GetProvider() and SetProvider() + // methods of the SqlAuthenticationProvider class in the Abstractions + // package. We're testing this here because this test project uses both of + // those packages, and this is a convenient place to put such a test. + [Fact] + public void AuthProviderInstalled() + { + // Iterate over all authentication methods rather than specifying them + // via Theory data so that we detect any new methods that don't meet + // our expectations. + foreach (var method in + #if NET + Enum.GetValues() + #else + Enum.GetValues(typeof(SqlAuthenticationMethod)).Cast() + #endif + ) + { + SqlAuthenticationProvider? provider = + SqlAuthenticationProvider.GetProvider(method); + + switch (method) + { + #pragma warning disable 0618 // Type or member is obsolete + case SqlAuthenticationMethod.ActiveDirectoryPassword: + #pragma warning restore 0618 // Type or member is obsolete + case SqlAuthenticationMethod.ActiveDirectoryIntegrated: + case SqlAuthenticationMethod.ActiveDirectoryInteractive: + case SqlAuthenticationMethod.ActiveDirectoryServicePrincipal: + case SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow: + case SqlAuthenticationMethod.ActiveDirectoryManagedIdentity: + case SqlAuthenticationMethod.ActiveDirectoryMSI: + case SqlAuthenticationMethod.ActiveDirectoryDefault: + case SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity: + Assert.NotNull(provider); + Assert.IsType(provider); + break; + + default: + // There is either no provider installed, or it is not ours. + if (provider is not null) + { + Assert.IsNotType(provider); + } + break; + } + } + } +} + +#endif diff --git a/src/Microsoft.Data.SqlClient.Extensions/Azure/test/StringExtensions.cs b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/StringExtensions.cs new file mode 100644 index 0000000000..def794cc21 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/StringExtensions.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +/// +/// Adds the missing Empty() method to string that doesn't waste time on null +/// checks like String.IsNullOrEmpty() does, and has a nice short name. +/// +internal static class StringExtensions +{ + internal static bool Empty(this string str) + { + return str.Length == 0; + } +} diff --git a/src/Microsoft.Data.SqlClient.Extensions/Azure/test/WorkloadIdentityFederationTests.cs b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/WorkloadIdentityFederationTests.cs new file mode 100644 index 0000000000..053e543ee1 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/WorkloadIdentityFederationTests.cs @@ -0,0 +1,65 @@ +// 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 Azure.Identity; + +namespace Microsoft.Data.SqlClient.Extensions.Azure.Test; + +// Verify that we're running in an environment that supports Azure Pipelines +// Workload Identity Federation authentication. +public class WorkloadIdentityFederationTests +{ + [ConditionalFact( + typeof(Config), + nameof(Config.HasSystemAccessToken), + nameof(Config.HasTenantId), + nameof(Config.HasUserManagedIdentityClientId), + nameof(Config.HasWorkloadIdentityFederationServiceConnectionId))] + public async void GetCredential() + { + AzurePipelinesCredential credential = new( + // The tenant ID of the managed identity associated to our workload + // identity federation service connection. See: + // + // https://ms.portal.azure.com/#@microsoft.onmicrosoft.com/resource/subscriptions/654fffd0-d02d-4894-b1b7-e2dfbc44a665/resourceGroups/aad-testlab-dl797892652000/providers/Microsoft.ManagedIdentity/userAssignedIdentities/dotnetMSI/properties + // + // Note that we need a service connection configured in each Azure DevOps project + // (Public and ADO.Net) that uses this tenant ID. + // + Config.TenantId, + + // The client ID of the managed identity associated to our workload + // identity federation service connection. See: + // + // https://ms.portal.azure.com/#@microsoft.onmicrosoft.com/resource/subscriptions/654fffd0-d02d-4894-b1b7-e2dfbc44a665/resourceGroups/aad-testlab-dl797892652000/providers/Microsoft.ManagedIdentity/userAssignedIdentities/dotnetMSI/overview + // + Config.UserManagedIdentityClientId, + + // The Azure Dev Ops service connection ID (resourceId found in the + // URL) of our workload identity federation setup. + // + // Note that we need a service connection configured in each Azure + // DevOps project (Public and ADO.Net). + // + // Public project: + // + // https://sqlclientdrivers.visualstudio.com/public/_settings/adminservices?resourceId=ec9623b2-829c-497f-ae1f-7461766f9a9c + // + // ADO.Net project: + // + // https://sqlclientdrivers.visualstudio.com/ADO.Net/_settings/adminservices?resourceId=c29947a8-df6a-4ceb-b2d4-1676c57c37b9 + // + Config.WorkloadIdentityFederationServiceConnectionId, + + // The system access token provided by Azure Pipelines. + Config.SystemAccessToken); + + // Acquire a token suitable for accessing Azure SQL databases. + var token = await credential.GetTokenAsync( + new(["https://database.windows.net/.default"]), + CancellationToken.None); + + Assert.NotEmpty(token.Token); + } +} diff --git a/src/Microsoft.Data.SqlClient.sln b/src/Microsoft.Data.SqlClient.sln index 526eb4c06b..1b91fbaf83 100644 --- a/src/Microsoft.Data.SqlClient.sln +++ b/src/Microsoft.Data.SqlClient.sln @@ -1,4 +1,5 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 + +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31912.275 MinimumVisualStudioVersion = 10.0.40219.1 @@ -320,6 +321,36 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Data.SqlClient.Sa EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Data.SqlClient.TestCommon", "Microsoft.Data.SqlClient\tests\Common\Microsoft.Data.SqlClient.TestCommon.csproj", "{3FF03FA9-E3C3-49E3-9DCB-C703A5B0278B}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Microsoft.Data.SqlClient.Extensions", "Microsoft.Data.SqlClient.Extensions", "{19F1F1E5-3013-7660-661A-2A15F7D606C1}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Abstractions", "Abstractions", "{556B486E-F9B0-7EA9-6A25-DA560C312761}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{210228A5-979A-DE06-EE1F-B35C65E1583C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Abstractions", "Microsoft.Data.SqlClient.Extensions\Abstractions\src\Abstractions.csproj", "{089582DC-FC8E-4DDE-99AC-E31BF95175B0}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{59667E4C-0BD2-9F48-FB50-9E55DD8B1011}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Abstractions.Test", "Microsoft.Data.SqlClient.Extensions\Abstractions\test\Abstractions.Test.csproj", "{21F71821-AC58-43A1-A0B1-A7DB5FA892D3}" +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", "{DCD79241-612B-4081-A8CC-BD7A4ABC1662}" +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", "{4B953573-C3CD-4845-896B-EA0A0B7A7B27}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Microsoft.Data.SqlClient", "Microsoft.Data.SqlClient", "{7289C27E-D7DF-2C71-84B4-151F3A162493}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{7E0602AC-7F0A-362A-D734-0FDDFCC600B5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{134A5E42-015B-3575-2B2B-722614F4C835}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{2B71F605-037E-5629-6E23-0FA3C297446D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -559,16 +590,16 @@ Global {A314812A-7820-4565-A2A8-ABBE391C11E4}.Release|x86.ActiveCfg = Release|Any CPU {A314812A-7820-4565-A2A8-ABBE391C11E4}.Release|x86.Build.0 = Release|Any CPU {9A8996A8-6484-4AA7-B50F-F861430EDE2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9A8996A8-6484-4AA7-B50F-F861430EDE2F}.Debug|x64.ActiveCfg = Debug|Any CPU - {9A8996A8-6484-4AA7-B50F-F861430EDE2F}.Debug|x86.ActiveCfg = Debug|Any CPU - {9A8996A8-6484-4AA7-B50F-F861430EDE2F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9A8996A8-6484-4AA7-B50F-F861430EDE2F}.Release|x64.ActiveCfg = Release|Any CPU - {9A8996A8-6484-4AA7-B50F-F861430EDE2F}.Release|x86.ActiveCfg = Release|Any CPU {9A8996A8-6484-4AA7-B50F-F861430EDE2F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9A8996A8-6484-4AA7-B50F-F861430EDE2F}.Debug|x64.ActiveCfg = Debug|Any CPU {9A8996A8-6484-4AA7-B50F-F861430EDE2F}.Debug|x64.Build.0 = Debug|Any CPU + {9A8996A8-6484-4AA7-B50F-F861430EDE2F}.Debug|x86.ActiveCfg = Debug|Any CPU {9A8996A8-6484-4AA7-B50F-F861430EDE2F}.Debug|x86.Build.0 = Debug|Any CPU + {9A8996A8-6484-4AA7-B50F-F861430EDE2F}.Release|Any CPU.ActiveCfg = Release|Any CPU {9A8996A8-6484-4AA7-B50F-F861430EDE2F}.Release|Any CPU.Build.0 = Release|Any CPU + {9A8996A8-6484-4AA7-B50F-F861430EDE2F}.Release|x64.ActiveCfg = Release|Any CPU {9A8996A8-6484-4AA7-B50F-F861430EDE2F}.Release|x64.Build.0 = Release|Any CPU + {9A8996A8-6484-4AA7-B50F-F861430EDE2F}.Release|x86.ActiveCfg = Release|Any CPU {9A8996A8-6484-4AA7-B50F-F861430EDE2F}.Release|x86.Build.0 = Release|Any CPU {4461063D-2F2B-274C-7E6F-F235119D258E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4461063D-2F2B-274C-7E6F-F235119D258E}.Debug|Any CPU.Build.0 = Debug|Any CPU @@ -672,6 +703,54 @@ Global {3FF03FA9-E3C3-49E3-9DCB-C703A5B0278B}.Release|x64.Build.0 = Release|x64 {3FF03FA9-E3C3-49E3-9DCB-C703A5B0278B}.Release|x86.ActiveCfg = Release|x86 {3FF03FA9-E3C3-49E3-9DCB-C703A5B0278B}.Release|x86.Build.0 = Release|x86 + {089582DC-FC8E-4DDE-99AC-E31BF95175B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {089582DC-FC8E-4DDE-99AC-E31BF95175B0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {089582DC-FC8E-4DDE-99AC-E31BF95175B0}.Debug|x64.ActiveCfg = Debug|Any CPU + {089582DC-FC8E-4DDE-99AC-E31BF95175B0}.Debug|x64.Build.0 = Debug|Any CPU + {089582DC-FC8E-4DDE-99AC-E31BF95175B0}.Debug|x86.ActiveCfg = Debug|Any CPU + {089582DC-FC8E-4DDE-99AC-E31BF95175B0}.Debug|x86.Build.0 = Debug|Any CPU + {089582DC-FC8E-4DDE-99AC-E31BF95175B0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {089582DC-FC8E-4DDE-99AC-E31BF95175B0}.Release|Any CPU.Build.0 = Release|Any CPU + {089582DC-FC8E-4DDE-99AC-E31BF95175B0}.Release|x64.ActiveCfg = Release|Any CPU + {089582DC-FC8E-4DDE-99AC-E31BF95175B0}.Release|x64.Build.0 = Release|Any CPU + {089582DC-FC8E-4DDE-99AC-E31BF95175B0}.Release|x86.ActiveCfg = Release|Any CPU + {089582DC-FC8E-4DDE-99AC-E31BF95175B0}.Release|x86.Build.0 = Release|Any CPU + {21F71821-AC58-43A1-A0B1-A7DB5FA892D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {21F71821-AC58-43A1-A0B1-A7DB5FA892D3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {21F71821-AC58-43A1-A0B1-A7DB5FA892D3}.Debug|x64.ActiveCfg = Debug|Any CPU + {21F71821-AC58-43A1-A0B1-A7DB5FA892D3}.Debug|x64.Build.0 = Debug|Any CPU + {21F71821-AC58-43A1-A0B1-A7DB5FA892D3}.Debug|x86.ActiveCfg = Debug|Any CPU + {21F71821-AC58-43A1-A0B1-A7DB5FA892D3}.Debug|x86.Build.0 = Debug|Any CPU + {21F71821-AC58-43A1-A0B1-A7DB5FA892D3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {21F71821-AC58-43A1-A0B1-A7DB5FA892D3}.Release|Any CPU.Build.0 = Release|Any CPU + {21F71821-AC58-43A1-A0B1-A7DB5FA892D3}.Release|x64.ActiveCfg = Release|Any CPU + {21F71821-AC58-43A1-A0B1-A7DB5FA892D3}.Release|x64.Build.0 = Release|Any CPU + {21F71821-AC58-43A1-A0B1-A7DB5FA892D3}.Release|x86.ActiveCfg = Release|Any CPU + {21F71821-AC58-43A1-A0B1-A7DB5FA892D3}.Release|x86.Build.0 = Release|Any CPU + {DCD79241-612B-4081-A8CC-BD7A4ABC1662}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DCD79241-612B-4081-A8CC-BD7A4ABC1662}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DCD79241-612B-4081-A8CC-BD7A4ABC1662}.Debug|x64.ActiveCfg = Debug|Any CPU + {DCD79241-612B-4081-A8CC-BD7A4ABC1662}.Debug|x64.Build.0 = Debug|Any CPU + {DCD79241-612B-4081-A8CC-BD7A4ABC1662}.Debug|x86.ActiveCfg = Debug|Any CPU + {DCD79241-612B-4081-A8CC-BD7A4ABC1662}.Debug|x86.Build.0 = Debug|Any CPU + {DCD79241-612B-4081-A8CC-BD7A4ABC1662}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DCD79241-612B-4081-A8CC-BD7A4ABC1662}.Release|Any CPU.Build.0 = Release|Any CPU + {DCD79241-612B-4081-A8CC-BD7A4ABC1662}.Release|x64.ActiveCfg = Release|Any CPU + {DCD79241-612B-4081-A8CC-BD7A4ABC1662}.Release|x64.Build.0 = Release|Any CPU + {DCD79241-612B-4081-A8CC-BD7A4ABC1662}.Release|x86.ActiveCfg = Release|Any CPU + {DCD79241-612B-4081-A8CC-BD7A4ABC1662}.Release|x86.Build.0 = Release|Any CPU + {4B953573-C3CD-4845-896B-EA0A0B7A7B27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4B953573-C3CD-4845-896B-EA0A0B7A7B27}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4B953573-C3CD-4845-896B-EA0A0B7A7B27}.Debug|x64.ActiveCfg = Debug|Any CPU + {4B953573-C3CD-4845-896B-EA0A0B7A7B27}.Debug|x64.Build.0 = Debug|Any CPU + {4B953573-C3CD-4845-896B-EA0A0B7A7B27}.Debug|x86.ActiveCfg = Debug|Any CPU + {4B953573-C3CD-4845-896B-EA0A0B7A7B27}.Debug|x86.Build.0 = Debug|Any CPU + {4B953573-C3CD-4845-896B-EA0A0B7A7B27}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4B953573-C3CD-4845-896B-EA0A0B7A7B27}.Release|Any CPU.Build.0 = Release|Any CPU + {4B953573-C3CD-4845-896B-EA0A0B7A7B27}.Release|x64.ActiveCfg = Release|Any CPU + {4B953573-C3CD-4845-896B-EA0A0B7A7B27}.Release|x64.Build.0 = Release|Any CPU + {4B953573-C3CD-4845-896B-EA0A0B7A7B27}.Release|x86.ActiveCfg = Release|Any CPU + {4B953573-C3CD-4845-896B-EA0A0B7A7B27}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -730,6 +809,19 @@ Global {FAA1E517-581A-D3DC-BAC9-FAD1D5A5142C} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {C09B9D2F-E463-BEBD-34E4-E8F2C201A277} = {ED952CF7-84DF-437A-B066-F516E9BE1C2C} {3FF03FA9-E3C3-49E3-9DCB-C703A5B0278B} = {0CC4817A-12F3-4357-912C-09315FAAD008} + {556B486E-F9B0-7EA9-6A25-DA560C312761} = {19F1F1E5-3013-7660-661A-2A15F7D606C1} + {210228A5-979A-DE06-EE1F-B35C65E1583C} = {556B486E-F9B0-7EA9-6A25-DA560C312761} + {089582DC-FC8E-4DDE-99AC-E31BF95175B0} = {210228A5-979A-DE06-EE1F-B35C65E1583C} + {59667E4C-0BD2-9F48-FB50-9E55DD8B1011} = {556B486E-F9B0-7EA9-6A25-DA560C312761} + {21F71821-AC58-43A1-A0B1-A7DB5FA892D3} = {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} + {DCD79241-612B-4081-A8CC-BD7A4ABC1662} = {0D2F834B-6D91-18D0-3F09-672D448751BD} + {5AF52CDD-DF78-3712-7516-5B49F94F9491} = {A20114E1-82D8-903A-C389-726EB4FD943F} + {4B953573-C3CD-4845-896B-EA0A0B7A7B27} = {5AF52CDD-DF78-3712-7516-5B49F94F9491} + {7E0602AC-7F0A-362A-D734-0FDDFCC600B5} = {7289C27E-D7DF-2C71-84B4-151F3A162493} + {134A5E42-015B-3575-2B2B-722614F4C835} = {7E0602AC-7F0A-362A-D734-0FDDFCC600B5} + {2B71F605-037E-5629-6E23-0FA3C297446D} = {7289C27E-D7DF-2C71-84B4-151F3A162493} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {01D48116-37A2-4D33-B9EC-94793C702431} 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..3cad874d58 100644 --- a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs +++ b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs @@ -5,7 +5,6 @@ // NOTE: The current Microsoft.VSDesigner editor attributes are implemented for System.Data.SqlClient, and are not publicly available. // New attributes that are designed to work with Microsoft.Data.SqlClient and are publicly documented should be included in future. -[assembly: System.CLSCompliant(true)] namespace Microsoft.Data { /// 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 a2751a6e57..871db24225 100644 --- a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.csproj @@ -23,7 +23,13 @@ $(SigningKeyPath) - + + + + <_Parameter1>true + + + 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 bbbf05277b..c5cc017919 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -31,6 +31,13 @@ $([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(TargetFramework)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)')) + + + + <_Parameter1>true + + + 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..1647dfe94c 100644 --- a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs +++ b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs @@ -5,7 +5,6 @@ // NOTE: The current Microsoft.VSDesigner editor attributes are implemented for System.Data.SqlClient, and are not publicly available. // New attributes that are designed to work with Microsoft.Data.SqlClient and are publicly documented should be included in future. -[assembly: System.CLSCompliant(true)] namespace Microsoft.Data { /// 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 2735cce404..557873ae77 100644 --- a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.csproj @@ -18,6 +18,13 @@ $(SigningKeyPath) + + + + <_Parameter1>true + + + 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 51e301a835..b8276bcb29 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -29,6 +29,13 @@ full + + + + <_Parameter1>true + + + <_Parameter1>UnitTests diff --git a/src/Microsoft.Data.SqlClient/netfx/tools/targets/GenerateAssemblyInfo.targets b/src/Microsoft.Data.SqlClient/netfx/tools/targets/GenerateAssemblyInfo.targets index 541cb245a6..9c43ce4989 100644 --- a/src/Microsoft.Data.SqlClient/netfx/tools/targets/GenerateAssemblyInfo.targets +++ b/src/Microsoft.Data.SqlClient/netfx/tools/targets/GenerateAssemblyInfo.targets @@ -13,7 +13,6 @@ - diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj index 2e38c79532..8505df0724 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj @@ -7,6 +7,13 @@ true + + + + <_Parameter1>true + + + $(SigningKeyPath) diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TDS.Servers.csproj b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TDS.Servers.csproj index c689554310..1a40bb21dd 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TDS.Servers.csproj +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TDS.Servers.csproj @@ -4,7 +4,6 @@ Microsoft.SqlServer.TDS.Servers {978063D3-FBB5-4E10-8C45-67C90BE1B931} netstandard2.0 - false false $(ObjFolder)$(Configuration).$(Platform)\$(AssemblyName) $(BinFolder)$(Configuration).$(Platform)\$(AssemblyName) @@ -38,4 +37,4 @@ TDS - \ No newline at end of file + diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/TDS.csproj b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/TDS.csproj index 58f7caef5f..eb83deb809 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/TDS.csproj +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/TDS.csproj @@ -4,7 +4,6 @@ Microsoft.SqlServer.TDS {8DC9D1A0-351B-47BC-A90F-B9DA542550E9} netstandard2.0 - false false $(ObjFolder)$(Configuration).$(Platform)\$(AssemblyName) $(BinFolder)$(Configuration).$(Platform)\$(AssemblyName) @@ -106,4 +105,4 @@ - \ No newline at end of file + diff --git a/tools/props/Versions.props b/tools/props/Versions.props index ee789db4c3..35678a59d9 100644 --- a/tools/props/Versions.props +++ b/tools/props/Versions.props @@ -17,6 +17,9 @@ $(SqlServerPackageVersion) + + + 7.0.0 @@ -54,6 +57,9 @@ $(MdsPackageVersion) + + + 7.0.0 diff --git a/tools/targets/GenerateThisAssemblyCs.targets b/tools/targets/GenerateThisAssemblyCs.targets index 6230af8ae7..5456d09056 100644 --- a/tools/targets/GenerateThisAssemblyCs.targets +++ b/tools/targets/GenerateThisAssemblyCs.targets @@ -1,46 +1,37 @@ - - - + + + + System + -[assembly: System.CLSCompliant(true)] -namespace System -{ -internal static class ThisAssembly +namespace $(ThisAssemblyNamespace) { -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 + } } - - - - - - + - - + + + + + From 066079201ae40eb061d6b2bf16c1be5e658cdb5d Mon Sep 17 00:00:00 2001 From: Paul Medynski <31868385+paulmedynski@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:25:43 -0400 Subject: [PATCH 2/6] Task 41736: Merge feat/azure-split to main: Step 2 - New files - Added ThisAssembly generation to Abstractions. - Fixed branch wildcards in PR pipeline and CodeQL workflow triggers. --- .github/workflows/codeql.yml | 4 ++-- .../sqlclient-pr-package-ref-pipeline.yml | 2 +- .../sqlclient-pr-project-ref-pipeline.yml | 2 +- .../Abstractions/src/Abstractions.csproj | 14 ++++++++++++++ .../Azure/src/Azure.csproj | 4 ---- 5 files changed, 18 insertions(+), 8 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 22f6a08a20..673cffc4da 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -13,9 +13,9 @@ name: "CodeQL Advanced" on: push: - branches: [ "main", "feat/*" ] + branches: [ "main", "feat/*", "dev/**/*" ] pull_request: - branches: [ "main", "feat/*" ] + branches: [ "main", "feat/*", "dev/**/*" ] schedule: - cron: '33 23 * * 6' diff --git a/eng/pipelines/sqlclient-pr-package-ref-pipeline.yml b/eng/pipelines/sqlclient-pr-package-ref-pipeline.yml index e58a1e1e85..3e9cf94d48 100644 --- a/eng/pipelines/sqlclient-pr-package-ref-pipeline.yml +++ b/eng/pipelines/sqlclient-pr-package-ref-pipeline.yml @@ -30,7 +30,7 @@ pr: branches: include: # GitHub repo branch targets that will trigger PR validation builds. - - dev/* + - dev/**/* - feat/* - main diff --git a/eng/pipelines/sqlclient-pr-project-ref-pipeline.yml b/eng/pipelines/sqlclient-pr-project-ref-pipeline.yml index 083a372867..f30e1e3605 100644 --- a/eng/pipelines/sqlclient-pr-project-ref-pipeline.yml +++ b/eng/pipelines/sqlclient-pr-project-ref-pipeline.yml @@ -30,7 +30,7 @@ pr: branches: include: # GitHub repo branch targets that will trigger PR validation builds. - - dev/* + - dev/**/* - feat/* - main diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Abstractions.csproj b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Abstractions.csproj index ddb74f9244..424d3e9e65 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Abstractions.csproj +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Abstractions.csproj @@ -83,4 +83,18 @@ + + + + $(AssemblyName) + + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Azure/src/Azure.csproj b/src/Microsoft.Data.SqlClient.Extensions/Azure/src/Azure.csproj index b6cb0e5f99..25816183e1 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Azure/src/Azure.csproj +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/src/Azure.csproj @@ -102,10 +102,6 @@ $(AssemblyName) - $(AbstractionsPackageVersion) + $(AzurePackageVersion) $(PackagesDir) true snupkg