diff --git a/.editorconfig b/.editorconfig index 9d8000b43f..7d425474e2 100644 --- a/.editorconfig +++ b/.editorconfig @@ -162,7 +162,7 @@ dotnet_diagnostic.xUnit1031.severity=none dotnet_diagnostic.xUnit1030.severity=none # Xml files -[*.{xml,csproj,stylecop,resx,ruleset,props,targets,config,nuspec}] +[*.{xml,slnx,proj,csproj,stylecop,resx,ruleset,props,targets,config,nuspec}] indent_size = 2 # Shell scripts diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 2197bda5f6..d96729772c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -13,9 +13,9 @@ name: "CodeQL Advanced" on: push: - branches: [ "main" ] + branches: [ "main", "feat/*" ] pull_request: - branches: [ "main" ] + branches: [ "main", "feat/*" ] schedule: - cron: '33 23 * * 6' @@ -84,9 +84,26 @@ jobs: - name: Run manual build steps if: matrix.build-mode == 'manual' shell: bash + # TODO(https://sqlclientdrivers.visualstudio.com/ADO.Net/_workitems/edit/38655): + # For some reason, the StressTests projects fail to restore in this + # environment, so we skip them by explicitly building all of the other + # projects instead of the main solution file. run: | - mkdir packages - dotnet build src/ + dotnet build src/Microsoft.SqlServer.Server + dotnet build src/Microsoft.Data.SqlClient.Extensions/Abstractions/src + dotnet build src/Microsoft.Data.SqlClient.Extensions/Abstractions/test + dotnet build src/Microsoft.Data.SqlClient/netcore/ref + dotnet build src/Microsoft.Data.SqlClient/netcore/src + dotnet build src/Microsoft.Data.SqlClient/netfx/ref + dotnet build src/Microsoft.Data.SqlClient/netfx/src + dotnet build src/Microsoft.Data.SqlClient/src + dotnet build src/Microsoft.Data.SqlClient.Extensions/Azure/src + dotnet build src/Microsoft.Data.SqlClient.Extensions/Azure/test + dotnet build src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider + dotnet build src/Microsoft.Data.SqlClient/tests/UnitTests + dotnet build src/Microsoft.Data.SqlClient/tests/FunctionalTests + dotnet build src/Microsoft.Data.SqlClient/tests/ManualTests + dotnet build src/Microsoft.Data.SqlClient/tests/PerformanceTests - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v4 diff --git a/BUILDGUIDE.md b/BUILDGUIDE.md index c168d4cd18..1372bf7a02 100644 --- a/BUILDGUIDE.md +++ b/BUILDGUIDE.md @@ -25,28 +25,33 @@ Once the environment is setup properly, execute the desired set of commands belo ### Targets +The following build targets are defined in `build.proj`: + |Target|Description| |-|-| |`BuildAllConfigurations`|Default target. Builds the .NET Framework and .NET drivers for all target frameworks and operating systems.| +|`BuildAbstractions`|Restore, build, and pack the Abstractions 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.| |`BuildTestsNetCore`|Builds tests for the .NET driver.| |`BuildTestsNetFx`|Builds tests for the .NET Framework driver.| -|`Clean`|Cleans generated files.| -|`Restore`|Restores Nuget packages.| +|`Clean`|Cleans all generated files.| +|`Restore`|Restores NuGet packages.| |`RunTests`|Runs the unit, functional, and manual tests for the .NET Framework and .NET drivers| |`RunUnitTests`|Runs just the unit tests for the .NET Framework and .NET drivers| |`RunFunctionalTests`|Runs just the functional tests for the .NET Framework and .NET drivers| |`RunManualTests`|Runs just the manual tests for the .NET Framework and .NET drivers| |`BuildAkv`|Builds the Azure Key Vault Provider package for all supported platforms.| - ### Parameters + +The following parameters may be defined as MSBuild properties to configure the +build: + |Name|Supported Values|Default|Description| |-|-|-|-| |`Configuration`|`Debug`, `Release`|`Debug`|Sets the release configuration.| -|`BuildNetFx`|`true`, `false`|`true` (Windows), `false` (other)|If false, skips building the .NET Framework driver on Windows.| |`OSGroup`|`Unix`, `Windows_NT`, `AnyOS`|typically defaults to the client system's OS, unless using `BuildAllConfigurations` or an `AnyOS` specific target|The operating system to target.| |`Platform`|`AnyCPU`, `x86`, `x64`, `ARM`, `ARM64`|`AnyCPU`|May only be set when using package reference type or running tests.| |`TestSet`|`1`, `2`, `3`, `AE`|all|Build or run a subset of the manual tests. Omit (default) to target all tests.| @@ -54,12 +59,11 @@ Once the environment is setup properly, execute the desired set of commands belo |`TF`|`net8.0`, `net462`, `net47`, `net471`, `net472`, `net48`, `net481`|`net8.0` in netcore, `net462` in netfx|Sets the target framework when building or running tests. Not applicable when building the drivers.| |`ResultsDirectory`|An absolute file path|./TestResults relative to current directory|Specifies where to write test results.| - ## Example Workflows using MSBuild (Recommended) + Using the default configuration and running all tests: ```bash -msbuild msbuild -t:BuildTestsNetFx -p:TF=net462 msbuild -t:BuildTestsNetCore msbuild -t:RunTests @@ -68,28 +72,25 @@ msbuild -t:RunTests Using the Release configuration: ```bash -msbuild -p:configuration=Release -msbuild -t:BuildTestsNetFx -p:TF=net462 -p:configuration=Release -msbuild -t:BuildTestsNetCore -p:configuration=Release -msbuild -t:RunTests -p:configuration=Release +msbuild -t:BuildTestsNetFx -p:TF=net462 -p:Configuration=Release +msbuild -t:BuildTestsNetCore -p:Configuration=Release +msbuild -t:RunTests -p:Configuration=Release ``` Running only the unit tests: ```bash -msbuild msbuild -t:BuildTestsNetFx -p:TF=net462 msbuild -t:BuildTestsNetCore msbuild -t:RunUnitTests ``` -Using a specific dotnet version/architecture: +Using a specific .NET runtime to run tests: ```bash -msbuild -p:configuration=Release -msbuild -t:BuildTestsNetFx -p:TF=net462 -p:configuration=Release -msbuild -t:BuildTestsNetCore -p:configuration=Release -msbuild -t:RunTests -p:configuration=Release -p:DotnetPath=C:\net8-win-x86\ +msbuild -t:BuildTestsNetFx -p:TF=net462 +msbuild -t:BuildTestsNetCore +msbuild -t:RunTests -p:DotnetPath=C:\net8-win-x86\ ``` ### Running Manual Tests @@ -128,15 +129,13 @@ Manual Tests require the below setup to run: |IsManagedInstance | (Optional) When set to `true` **TVP** related tests will use on non-Azure bs files to compare test results. this is needed when testing against Managed Instances or TVP Tests will fail on Test set 3. The default value is `false`. | |PowerShellPath | The full path to PowerShell.exe. This is not required if the path is present in the PATH environment variable. | `D:\\escaped\\absolute\\path\\to\\PowerShell.exe` | - ## Example workflows using the Dotnet SDK -#### Run Functional Tests +### Run Functional Tests - Windows (`netfx x86`): ```bash -msbuild dotnet test "src\Microsoft.Data.SqlClient\tests\FunctionalTests\Microsoft.Data.SqlClient.Tests.csproj" -p:Platform="x86" -p:Configuration="Release" -p:TestTargetOS="Windowsnetfx" --no-build -v n --filter "category!=nonnetfxtests&category!=failing&category!=nonwindowstests" ``` @@ -161,7 +160,8 @@ dotnet test "src\Microsoft.Data.SqlClient\tests\FunctionalTests\Microsoft.Data.S ```bash dotnet test "src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.Tests.csproj" -p:Platform="AnyCPU" -p:Configuration="Release" -p:TestTargetOS="Unixnetcoreapp" --no-build -v n --filter "category!=nonnetcoreapptests&category!=failing&category!=nonlinuxtests&category!=nonuaptests" ``` -#### Run Manual Tests + +### Run Manual Tests - Windows (`netfx x86`): @@ -201,37 +201,73 @@ dotnet test "src\Microsoft.Data.SqlClient\tests\ManualTests\Microsoft.Data.SqlCl ## Testing with Custom ReferenceType -Tests can be built and run with custom "Reference Type" property that enables different styles of testing: +The MDS driver consists of several components, each of which produces its own +NuGet package. During development, components reference each other via +`` properties by default. This means that building +and testing one component will implicitly build its project referenced +dependencies. + +Alternatively, the `ReferenceType` build property property may be specified with +a value of `Package`. This will change inter-component dependencies to use +`` dependencies, and require that dependent components be +built and packaged before building the depending component. In this scenario, +the root `NuGet.config` file must be updated to include the following entry +under the `` element: + +```xml + + + ... + + + +``` -- "Project" => Build and run tests with Microsoft.Data.SqlClient as Project Reference -- "Package" => Build and run tests with Microsoft.Data.SqlClient as Package Reference with configured "TestMicrosoftDataSqlClientVersion" in "Versions.props" file. +As a convenience, a `NuGet.config.local` file is supplied with the above +package source already present. You may simply copy it over `NuGet.config` +when using `Package` references. + +Then, you can specify `Package` references be used, for example: + +```bash +dotnet build -t:BuildAbstractions +dotnet build -t:BuildAzure -p:ReferenceType=Package +dotnet build -t:BuildAll -p:ReferenceType=Package +dotnet build -t:BuildAKVNetCore -p:ReferenceType=Package +dotnet build -t:GenerateMdsPackage +dotnet build -t:GenerateAkvPackage +dotnet build -t:BuildTestsNetCore -p:ReferenceType=Package +``` -> ************** IMPORTANT NOTE BEFORE PROCEEDING WITH "PACKAGE" REFERENCE TYPE *************** -> CREATE A NUGET PACKAGE WITH BELOW COMMAND AND ADD TO LOCAL FOLDER + UPDATE NUGET CONFIG FILE TO READ FROM THAT LOCATION -> -> ```bash -> msbuild -p:configuration=Release -> ``` +The above will build the Abstractions, Azure, MDS, and AKV components, place +their NuGet packages into the `packages/` directory, and then build the tests +using those packages. -A non-AnyCPU platform reference can only be used with package reference type. Otherwise, the specified platform will be replaced with AnyCPU in the build process. +A non-AnyCPU platform reference can only be used with package reference type. +Otherwise, the specified platform will be replaced with AnyCPU in the build +process. ### Building Tests with Reference Type -For .NET, all 4 reference types are supported: +For .NET: ```bash +# Project is the default reference type. The below commands are equivalent: +msbuild -t:BuildTestsNetCore msbuild -t:BuildTestsNetCore -p:ReferenceType=Project -# Default setting uses Project Reference. +# Package reference type: msbuild -t:BuildTestsNetCore -p:ReferenceType=Package ``` -For .NET Framework, below reference types are supported: +For .NET Framework: ```bash +# Project is the default reference type. The below commands are equivalent: +msbuild -t:BuildTestsNetFx -p:TF=net462 msbuild -t:BuildTestsNetFx -p:TF=net462 -p:ReferenceType=Project -# Default setting uses Project Reference. +# Package reference type: msbuild -t:BuildTestsNetFx -p:TF=net462 -p:ReferenceType=Package ``` @@ -250,26 +286,25 @@ Tests can be built and run with custom Target Frameworks. See the below examples ### Building Tests with custom target framework ```bash -msbuild -t:BuildTestsNetFx -p:TF=net462 # Build the tests for custom .NET Framework target +msbuild -t:BuildTestsNetFx -p:TF=net462 ``` ```bash -msbuild -t:BuildTestsNetCore -p:TF=net8.0 # Build the tests for custom .NET target +msbuild -t:BuildTestsNetCore -p:TF=net8.0 ``` ### Running Tests with custom target framework (traditional) ```bash +# Run tests with custom .NET Framework target dotnet test -p:TargetNetFxVersion=net462 ... -# Use above property to run Functional Tests with custom .NET Framework target +# Run tests with custom .NET target dotnet test -p:TargetNetCoreVersion=net8.0 ... -# Use above property to run Functional Tests with custom .NET target ``` - ## Using Managed SNI on Windows Managed SNI can be enabled on Windows by enabling the below AppContext switch: @@ -294,20 +329,6 @@ When connecting to a server, if a protocol lower than TLS 1.2 is negotiated, a s `Switch.Microsoft.Data.SqlClient.SuppressInsecureTLSWarning` -### Troubleshooting Docker issues - -There may be times where connection cannot be made to SQL Server, we found below ideas helpful: - -- Clear Docker images to create clean image from time-to-time, and clear docker cache if needed by running `docker system prune` in Command Prompt. - -- If you face `Microsoft.Data.SqlClient.SNI.dll not found` errors when debugging, try updating the below properties in the netcore\Microsoft.Data.SqlClient.csproj file and try again: - - ```xml - Unix - false - true - ``` - ## Collecting Code Coverage ### Using VSTest diff --git a/Directory.Packages.props b/Directory.Packages.props index c339554838..e86de72af2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,15 +3,36 @@ true true + + + + + + + + + + + - + + - + + @@ -20,7 +41,8 @@ - + + @@ -40,7 +62,8 @@ - + + @@ -54,14 +77,26 @@ --> + + + + + + + + + + + + - + @@ -73,11 +108,13 @@ + + @@ -89,12 +126,23 @@ - + + + - + + + + + + + + + + diff --git a/NuGet.config b/NuGet.config index 3233e60161..6c4f535818 100644 --- a/NuGet.config +++ b/NuGet.config @@ -1,7 +1,18 @@  + + - + + + diff --git a/NuGet.config.local b/NuGet.config.local new file mode 100644 index 0000000000..3e23ac372c --- /dev/null +++ b/NuGet.config.local @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + diff --git a/build.proj b/build.proj index d42e94b50c..df3b09b591 100644 --- a/build.proj +++ b/build.proj @@ -2,8 +2,9 @@ - - + + + @@ -27,10 +28,13 @@ $(TF) $(TF) true - Configuration=$(Configuration);AssemblyVersion=$(SqlServerAssemblyVersion);AssemblyFileVersion=$(SqlServerAssemblyFileVersion);Version=$(SqlServerPackageVersion); - Configuration=$(Configuration);AssemblyFileVersion=$(AssemblyFileVersion);TargetsWindows=$(TargetsWindows);TargetsUnix=$(TargetsUnix); - BuildProjectReferences=false;$(ProjectProperties);BuildForRelease=false;TargetNetCoreVersion=$(TargetNetCoreVersion);TargetNetFxVersion=$(TargetNetFxVersion) + + Configuration=$(Configuration);ReferenceType=$(ReferenceType) + $(CommonProperties);AssemblyVersion=$(SqlServerAssemblyVersion);AssemblyFileVersion=$(SqlServerAssemblyFileVersion);Version=$(SqlServerPackageVersion); + $(CommonProperties);AssemblyFileVersion=$(AssemblyFileVersion);TargetsWindows=$(TargetsWindows);TargetsUnix=$(TargetsUnix); + $(ProjectProperties);BuildForRelease=false;TargetNetCoreVersion=$(TargetNetCoreVersion);TargetNetFxVersion=$(TargetNetFxVersion) TestResults + + + + + + @@ -89,43 +98,169 @@ - - - + + + + + + $(CommonProperties) + + + + $(AbstractionsProperties);AbstractionsPackageVersion=$(AbstractionsPackageVersion) + + + + + $(AbstractionsProperties);AbstractionsAssemblyFileVersion=$(AbstractionsAssemblyFileVersion) + + + + + + + + + + + + + + + + + + + + + + $(CommonProperties) + + + + $(AzureProperties);AzurePackageVersion=$(AzurePackageVersion) + + + + + $(AzureProperties);AzureAssemblyFileVersion=$(AzureAssemblyFileVersion) + + + + + + + + + + + + + + + + + + + + - + - - + + - + - - + + - + - - - dotnet build -c Release -p:ReferenceType=$(ReferenceType) - - + + + + + + - + @@ -134,19 +269,23 @@ - + - + - + - + @@ -169,7 +308,7 @@ - + @@ -320,13 +459,12 @@ - + - - - - - + + + + @@ -362,7 +500,10 @@ - + @@ -372,7 +513,9 @@ - + @@ -382,7 +525,9 @@ - + diff --git a/eng/pipelines/akv-official-pipeline.yml b/eng/pipelines/akv-official-pipeline.yml index d7bc900bb8..f21ca656d3 100644 --- a/eng/pipelines/akv-official-pipeline.yml +++ b/eng/pipelines/akv-official-pipeline.yml @@ -49,7 +49,7 @@ resources: ref: 'refs/heads/main' extends: - template: 'v2/OneBranch.${{ parameters.oneBranchType }}.CrossPlat.yml@templates' + template: /v2/OneBranch.${{ parameters.oneBranchType }}.CrossPlat.yml@templates parameters: featureFlags: @@ -105,7 +105,8 @@ extends: roslyn: enabled: ${{ parameters.runSdlTasks }} break: true - # Requires RoslynAnalyzers task to be added after build task + # Requires RoslynAnalyzers task to be added somewhere in + # the build stage. publishLogs: enabled: ${{ parameters.runSdlTasks }} @@ -113,7 +114,7 @@ extends: sbom: enabled: ${{ parameters.runSdlTasks }} packageName: 'Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider' - packageVersion: ${{ variables.nugetPackageVersion }} + packageVersion: ${{ variables.akvPackageVersion }} tsa: # OneBranch publishes all sdl results to TSA. If TSA is disabled all SDL tools will @@ -127,11 +128,11 @@ extends: jobs: - template: /eng/pipelines/jobs/build-akv-official-job.yml@self parameters: + akvAssemblyFileVersion: '${{ variables.assemblyFileVersion }}' + akvPackageVersion: '${{ variables.akvPackageVersion }}' apiScanDllPath: '${{ variables.apiScanDllPath }}' apiScanPdbPath: '${{ variables.apiScanPdbPath }}' - assemblyFileVersion: '${{ variables.assemblyFileVersion }}' buildConfiguration: '${{ parameters.buildConfiguration }}' - nugetPackageVersion: '${{ variables.nugetPackageVersion }}' mdsPackageVersion: '${{ variables.mdsPackageVersion }}' publishSymbols: '${{ parameters.publishSymbols }}' signingAppRegistrationClientId: '$(SigningAppRegistrationClientId)' diff --git a/eng/pipelines/common/templates/jobs/build-signed-package-job.yml b/eng/pipelines/common/templates/jobs/build-signed-package-job.yml index a482761610..f11e8f0396 100644 --- a/eng/pipelines/common/templates/jobs/build-signed-package-job.yml +++ b/eng/pipelines/common/templates/jobs/build-signed-package-job.yml @@ -25,10 +25,12 @@ jobs: type: windows # read more about custom job pool types at https://aka.ms/obpipelines/yaml/jobs variables: - - template: ../../../libraries/variables.yml@self + - template: /eng/pipelines/libraries/variables.yml@self - ${{ if parameters.isPreview }}: - - name: NugetPackageVersion - value: $(PreviewNugetPackageVersion) + - name: abstractionsPackageVersion + value: $(abstractionsPackagePreviewVersion) + - name: mdsPackageVersion + value: $(previewMdsPackageVersion) steps: - script: SET @@ -37,37 +39,72 @@ jobs: - powershell: | Write-Host "##vso[task.setvariable variable=CDP_BUILD_TYPE_COPY;isOutput=true]$($env:CDP_BUILD_TYPE)" name: GetBuildType + + # Build our tooling, which is required by the analysis step below, but + # shouldn't be analyzed itself. + - task: MSBuild@1 + displayName: 'Build Tooling' + inputs: + solution: '**/build.proj' + configuration: $(Configuration) + msbuildArguments: -t:BuildTools + + # Perform analysis before building, since this step will clobber build output. + - template: /eng/pipelines/common/templates/steps/code-analyze-step.yml@self - - template: ../steps/build-all-configurations-signed-dlls-step.yml@self + # Update the root NuGet.config to use the packages/ directory as a source. + # When we build MDS in Package mode, it depends on the Abstractions package, + # which we will build and package into packages/. + - template: /eng/pipelines/common/templates/steps/update-nuget-config-local-feed-step.yml@self parameters: - buildConfiguration: Release + packagePath: $(REPOROOT)/packages - - template: ../steps/code-analyze-step.yml@self + # Build the Abstractions package. + - task: MSBuild@1 + displayName: Build Abstractions + inputs: + solution: '**/build.proj' + configuration: $(Configuration) + msbuildArguments: -t:BuildAbstractions + + # Build MDS in Package mode, producing signed DLLs. + - template: /eng/pipelines/common/templates/steps/build-all-configurations-signed-dlls-step.yml@self parameters: - analyzeType: all - - - template: ../steps/esrp-code-signing-step.yml@self + # These variables are sourced from common-variables.yml. + abstractionsAssemblyFileVersion: $(abstractionsAssemblyFileVersion) + abstractionsPackageVersion: $(abstractionsPackageVersion) + buildConfiguration: $(Configuration) + mdsAssemblyFileVersion: $(mdsAssemblyFileVersion) + mdsPackageVersion: $(mdsPackageVersion) + referenceType: Package + + - template: /eng/pipelines/common/templates/steps/esrp-code-signing-step.yml@self parameters: artifactType: dll - - template: ../steps/generate-nuget-package-step.yml@self + - template: /eng/pipelines/common/templates/steps/generate-nuget-package-step.yml@self parameters: buildConfiguration: Release - OutputDirectory: $(artifactDirectory) + displayName: 'Create MDS NuGet Package' installNuget: false + nuspecPath: $(nuspecPath) + outputDirectory: $(artifactDirectory) + packageVersion: $(mdsPackageVersion) + properties: 'AbstractionsPackageVersion=$(abstractionsPackageVersion)' + referenceType: Package - - template: ../steps/esrp-code-signing-step.yml@self + - template: /eng/pipelines/common/templates/steps/esrp-code-signing-step.yml@self parameters: artifactType: pkg - - template: ../steps/copy-dlls-for-test-step.yml@self + - template: /eng/pipelines/common/templates/steps/copy-dlls-for-test-step.yml@self parameters: buildConfiguration: Release product: MDS # Publish symbols to servers - - template: ../steps/publish-symbols-step.yml@self + - template: /eng/pipelines/common/templates/steps/publish-symbols-step.yml@self parameters: buildConfiguration: Release publishSymbols: ${{ parameters['PublishSymbols'] }} - symbolsArtifactName: mds_symbols_$(System.TeamProject)_$(Build.Repository.Name)_$(Build.SourceBranchName)_$(NuGetPackageVersion)_$(System.TimelineId) + symbolsArtifactName: mds_symbols_$(System.TeamProject)_$(Build.Repository.Name)_$(Build.SourceBranchName)_$(mdsPackageVersion)_$(System.TimelineId) diff --git a/eng/pipelines/common/templates/jobs/ci-build-nugets-job.yml b/eng/pipelines/common/templates/jobs/ci-build-nugets-job.yml index 7ed13a45a9..cd7673b4b5 100644 --- a/eng/pipelines/common/templates/jobs/ci-build-nugets-job.yml +++ b/eng/pipelines/common/templates/jobs/ci-build-nugets-job.yml @@ -4,6 +4,17 @@ # See the LICENSE file in the project root for more information. # ################################################################################# parameters: + + - name: 'debug' + type: boolean + default: false + + - name: referenceType + type: string + values: + - Package + - Project + - name: poolName type: string default: $(ci_var_defaultPoolName) @@ -12,9 +23,13 @@ parameters: type: string default: ADO-MMS22-SQL19 - - name: artifactName + - name: abstractionsArtifactName type: string - default: Artifacts + default: Abstractions.Artifact + + - name: mdsArtifactName + type: string + default: MDS.Artifact - name: platform type: string @@ -30,8 +45,15 @@ parameters: type: stepList default: [] + - name: abstractionsPackageVersion + type: string + + - name: mdsPackageVersion + type: string + jobs: -- job: build_nugets +- job: build_mds_akv_packages_job + displayName: Build MDS & AKV Packages pool: name: ${{parameters.poolName }} @@ -40,12 +62,28 @@ jobs: - msbuild variables: - - template: ../../../libraries/ci-build-variables.yml@self + - template: /eng/pipelines/libraries/ci-build-variables.yml@self steps: - ${{ if ne(parameters.prebuildSteps, '') }}: - ${{ parameters.prebuildSteps }} # extra steps to run before the build like downloading sni and the required configuration + # If we're testing in Package mode, download the Abstractions package artifacts into packages/, + # and then setup the top-level NuGet.config to look in packages/ for local + # NuGet dependencies. + - ${{ if eq(parameters.referenceType, 'Package') }}: + - task: DownloadPipelineArtifact@2 + displayName: Download Abstractions Package Artifact + inputs: + artifactName: ${{ parameters.abstractionsArtifactName }} + targetPath: $(packagePath) + # Note that packages/ will have been created by the above step, which is a + # pre-requisite for configuring NuGet. + - template: /eng/pipelines/common/templates/steps/ci-prebuild-step.yml@self + parameters: + debug: ${{ parameters.debug }} + referenceType: ${{ parameters.referenceType }} + # Install the .NET SDK. - template: /eng/pipelines/steps/install-dotnet.yml@self @@ -54,41 +92,56 @@ jobs: # compilation errors. We won't use the Release artifacts for anything else # though. - ${{ if eq(parameters.buildConfiguration, 'Debug') }}: - - template: ../steps/ci-project-build-step.yml@self + - template: /eng/pipelines/common/templates/steps/ci-project-build-step.yml@self parameters: platform: ${{ parameters.platform }} buildConfiguration: Release operatingSystem: Windows build: all - - template: ../steps/ci-project-build-step.yml@self + - template: /eng/pipelines/common/templates/steps/ci-project-build-step.yml@self parameters: platform: ${{ parameters.platform }} buildConfiguration: ${{ parameters.buildConfiguration }} + referenceType: ${{ parameters.referenceType }} operatingSystem: Windows - build: all + build: MDS + abstractionsPackageVersion: ${{parameters.abstractionsPackageVersion}} - - template: ../steps/generate-nuget-package-step.yml@self + - template: /eng/pipelines/common/templates/steps/generate-nuget-package-step.yml@self parameters: - NugetPackageVersion: $(NugetPackageVersion) buildConfiguration: ${{ parameters.buildConfiguration }} - nuspecPath: 'tools/specs/Microsoft.Data.SqlClient.nuspec' - OutputDirectory: $(packagePath) + displayName: 'Create MDS NuGet Package' generateSymbolsPackage: false - displayName: 'Generate NuGet package M.D.SqlClient' + packageVersion: ${{ parameters.mdsPackageVersion }} + nuspecPath: 'tools/specs/Microsoft.Data.SqlClient.nuspec' + outputDirectory: $(packagePath) + properties: 'AbstractionsPackageVersion=${{ parameters.abstractionsPackageVersion }}' + referenceType: ${{ parameters.referenceType }} - - template: ../steps/generate-nuget-package-step.yml@self + - template: /eng/pipelines/common/templates/steps/ci-project-build-step.yml@self parameters: - NugetPackageVersion: $(NugetPackageVersion) + platform: ${{ parameters.platform }} buildConfiguration: ${{ parameters.buildConfiguration }} - nuspecPath: 'tools/specs/add-ons/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.nuspec' - OutputDirectory: $(packagePath) + referenceType: ${{ parameters.referenceType }} + operatingSystem: Windows + build: AKV + mdsPackageVersion: ${{parameters.mdsPackageVersion}} + + - template: /eng/pipelines/common/templates/steps/generate-nuget-package-step.yml@self + parameters: + buildConfiguration: ${{ parameters.buildConfiguration }} + displayName: 'Create AKV NuGet Package' generateSymbolsPackage: false installNuget: false - displayName: 'Generate NuGet package AKV Provider' + nuspecPath: 'tools/specs/add-ons/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.nuspec' + outputDirectory: $(packagePath) + packageVersion: $(akvPackageVersion) + properties: 'MdsPackageVersion=${{ parameters.mdsPackageVersion }}' + referenceType: ${{ parameters.referenceType }} - - task: PublishBuildArtifacts@1 - displayName: 'Publish Artifact: Artifacts' + - task: PublishPipelineArtifact@1 + displayName: 'Publish Pipeline Artifact' inputs: - PathtoPublish: $(packagePath) - ArtifactName: ${{ parameters.artifactName }} + targetPath: $(packagePath) + artifactName: ${{ parameters.mdsArtifactName }} diff --git a/eng/pipelines/common/templates/jobs/ci-run-tests-job.yml b/eng/pipelines/common/templates/jobs/ci-run-tests-job.yml index 8ac7bcf087..d209c82a6a 100644 --- a/eng/pipelines/common/templates/jobs/ci-run-tests-job.yml +++ b/eng/pipelines/common/templates/jobs/ci-run-tests-job.yml @@ -4,12 +4,37 @@ # See the LICENSE file in the project root for more information. # ################################################################################# parameters: + - name: abstractionsArtifactName + type: string + + - name: abstractionsPackageVersion + type: string + + - name: azureArtifactName + type: string + + - name: azurePackageVersion + type: string + + - name: configProperties + type: object + default: {} # - key: 'value' + + - name: configSqlFor + type: string # local, azure, or enclave + default: local + - name: debug type: boolean default: false - - name: poolName - type: string + - name: enableX64Test + type: boolean + default: true + + - name: enableX86Test + type: boolean + default: false - name: hostedPool type: boolean @@ -21,46 +46,14 @@ parameters: - name: jobDisplayName type: string - - name: usemanagedSNI - type: boolean - default: false - - - name: configProperties - type: object - default: {} # - key: 'value' - - - name: prebuildSteps - type: stepList - default: [] - - - name: artifactName + - name: mdsArtifactName type: string - default: Artifacts - - name: targetFramework + - name: mdsPackageVersion type: string - name: netcoreVersionTestUtils type: string - - - name: enableX86Test - type: boolean - default: false - - - name: enableX64Test - type: boolean - default: true - - - name: testSet - type: string - - - name: publishTestResults - type: boolean - default: false - - - name: configSqlFor - type: string # local, azure, or enclave - default: local - name: operatingSystem type: string @@ -71,13 +64,30 @@ parameters: values: - Debug - Release + + - name: poolName + type: string + + - name: prebuildSteps + type: stepList + default: [] - - name: buildType - default: Project + - name: publishTestResults + type: boolean + default: false + + - name: referenceType + type: string values: - Project - Package + - name: targetFramework + type: string + + - name: testSet + type: string + # The timeout, in minutes, for this job. - name: timeout type: number @@ -87,6 +97,10 @@ parameters: type: boolean default: false + - name: usemanagedSNI + type: boolean + default: false + jobs: - job: ${{ format('{0}', coalesce(parameters.jobDisplayName, parameters.image, 'unknown_image')) }} @@ -108,6 +122,28 @@ jobs: steps: + # If we're testing in Package mode, download the Abstractions and MDS package + # artifacts and put them in the packages/ directory in the repo root. + - ${{ if eq(parameters.referenceType, 'Package') }}: + - task: DownloadPipelineArtifact@2 + displayName: Download Abstractions Package Artifact + inputs: + artifactName: ${{ parameters.abstractionsArtifactName }} + targetPath: $(Build.SourcesDirectory)/packages + + - task: DownloadPipelineArtifact@2 + displayName: Download MDS Package Artifact + inputs: + artifactName: ${{ parameters.mdsArtifactName }} + targetPath: $(Build.SourcesDirectory)/packages + + # TODO: Do we really need this to run the non-Azure tests? + - task: DownloadPipelineArtifact@2 + displayName: Download Azure Package Artifact + inputs: + artifactName: ${{ parameters.azureArtifactName }} + targetPath: $(Build.SourcesDirectory)/packages + # Install the .NET SDK and Runtimes. - template: /eng/pipelines/steps/install-dotnet.yml@self parameters: @@ -146,7 +182,7 @@ jobs: buildConfiguration: ${{ parameters.buildConfiguration }} - ${{ if ne(parameters.configProperties, '{}') }}: - - template: ../steps/update-config-file-step.yml@self # update config.json file + - template: /eng/pipelines/common/templates/steps/update-config-file-step.yml@self # update config.json file parameters: debug: ${{ parameters.debug }} UseManagedSNIOnWindows: ${{ parameters.usemanagedSNI }} @@ -221,7 +257,7 @@ jobs: displayName: 'Start Sql Browser' condition: eq(variables['Agent.OS'], 'Windows_NT') - ${{ elseif eq(parameters.configSqlFor, 'local') }}: - - template: ../steps/configure-sql-server-step.yml@self # configure SQL Server + - template: /eng/pipelines/common/templates/steps/configure-sql-server-step.yml@self # configure SQL Server parameters: operatingSystem: ${{ parameters.operatingSystem }} netcoreVersionTestUtils: ${{ parameters.netcoreVersionTestUtils }} @@ -252,22 +288,25 @@ jobs: ${{ if parameters.configProperties.FileStreamDirectory }}: fileStreamDirectory: ${{ parameters.configProperties.FileStreamDirectory }} - - template: ../steps/build-all-tests-step.yml@self # build tests + - template: /eng/pipelines/common/templates/steps/build-all-tests-step.yml@self # build tests parameters: targetFramework: ${{ parameters.targetFramework }} buildConfiguration: ${{ parameters.buildConfiguration }} - referenceType: ${{ parameters.buildType }} + referenceType: ${{ parameters.referenceType }} testSet: ${{ parameters.testSet }} + abstractionsPackageVersion: ${{ parameters.abstractionsPackageVersion }} + azurePackageVersion: ${{ parameters.azurePackageVersion }} + mdsPackageVersion: ${{ parameters.mdsPackageVersion }} ${{ if ne(parameters.operatingSystem, 'Windows') }}: OSGroup: Unix - ${{ if eq(parameters.enableX64Test, true) }}: # run native tests - - template: ../steps/run-all-tests-step.yml@self # run tests + - template: /eng/pipelines/common/templates/steps/run-all-tests-step.yml@self # run tests parameters: debug: ${{ parameters.debug }} targetFramework: ${{ parameters.targetFramework }} buildConfiguration: ${{ parameters.buildConfiguration }} - referenceType: ${{ parameters.buildType }} + referenceType: ${{ parameters.referenceType }} testSet: ${{ parameters.testSet }} operatingSystem: ${{ parameters.operatingSystem }} @@ -286,19 +325,19 @@ jobs: installDir: $(dotnetx86RootPath) runtimes: [8.x, 9.x] - - template: ../steps/run-all-tests-step.yml@self + - template: /eng/pipelines/common/templates/steps/run-all-tests-step.yml@self parameters: debug: ${{ parameters.debug }} targetFramework: ${{ parameters.targetFramework }} buildConfiguration: ${{ parameters.buildConfiguration }} - referenceType: ${{ parameters.buildType }} + referenceType: ${{ parameters.referenceType }} testSet: ${{ parameters.testSet }} msbuildArchitecture: x86 dotnetx86RootPath: $(dotnetx86RootPath) operatingSystem: ${{ parameters.operatingSystem }} - - ${{ if and(eq(parameters.publishTestResults, true), eq(parameters.buildType, 'Project')) }}: # publish test results if build type is project - - template: ../steps/publish-test-results-step.yml@self + - ${{ if and(eq(parameters.publishTestResults, true), eq(parameters.referenceType, 'Project')) }}: # publish test results if build type is project + - template: /eng/pipelines/common/templates/steps/publish-test-results-step.yml@self parameters: debug: ${{ parameters.debug }} targetFramework: ${{ parameters.targetFramework }} 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 59700587f7..3817fcb483 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 @@ -41,39 +41,37 @@ jobs: vmImage: 'ADO-MMS22-SQL19' variables: # More settings at https://aka.ms/obpipelines/yaml/jobs - - template: ../../../libraries/mds-validation-variables.yml@self + - template: /eng/pipelines/libraries/mds-validation-variables.yml@self steps: - - template: ../steps/pre-build-step.yml + - template: /eng/pipelines/common/templates/steps/pre-build-step.yml - ${{parameters.downloadPackageStep }} - - template: ../steps/update-nuget-config-local-feed-step.yml + - template: /eng/pipelines/common/templates/steps/update-nuget-config-local-feed-step.yml parameters: - downloadedNugetPath: $(Pipeline.Workspace)\${{parameters.packageFolderName }} - ${{ if parameters.isPreview }}: - nugetPackageVersion: $(PreviewNugetPackageVersion) + packagePath: $(Pipeline.Workspace)\${{parameters.packageFolderName }} - - template: ../steps/update-config-file-step.yml + - template: /eng/pipelines/common/templates/steps/update-config-file-step.yml parameters: TCPConnectionString: $(SQL_TCP_CONN_STRING) NPConnectionString: $(SQL_NP_CONN_STRING) SupportsIntegratedSecurity: false - - template: ../steps/prepare-test-db-step.yml + - template: /eng/pipelines/common/templates/steps/prepare-test-db-step.yml # build & test - - template: ../steps/build-and-run-tests-netfx-step.yml + - template: /eng/pipelines/common/templates/steps/build-and-run-tests-netfx-step.yml parameters: referenceType: Package buildConfiguration: Release ${{ if parameters.isPreview }}: - nugetPackageVersion: $(PreviewNugetPackageVersion) + mdsPackageVersion: $(previewMdsPackageVersion) - - template: ../steps/build-and-run-tests-netcore-step.yml + - template: /eng/pipelines/common/templates/steps/build-and-run-tests-netcore-step.yml parameters: referenceType: Package buildConfiguration: Release cleanFirst: true ${{ if parameters.isPreview }}: - nugetPackageVersion: $(PreviewNugetPackageVersion) + mdsPackageVersion: $(previewMdsPackageVersion) diff --git a/eng/pipelines/common/templates/jobs/validate-signed-package-job.yml b/eng/pipelines/common/templates/jobs/validate-signed-package-job.yml index 009e6f2647..b734f70de7 100644 --- a/eng/pipelines/common/templates/jobs/validate-signed-package-job.yml +++ b/eng/pipelines/common/templates/jobs/validate-signed-package-job.yml @@ -25,14 +25,6 @@ parameters: - pdb - both - - name: assembly_file_version_netfx - type: string - default: $(AssemblyFileVersion) - - - name: assembly_file_version_core - type: string - default: $(AssemblyFileVersion) - - name: isPreview type: boolean @@ -48,24 +40,19 @@ jobs: vmImage: 'ADO-MMS22-SQL19' variables: # More settings at https://aka.ms/obpipelines/yaml/jobs - - template: ../../../libraries/mds-validation-variables.yml@self + - template: /eng/pipelines/libraries/mds-validation-variables.yml@self - name: pathToDownloadedNuget # path to the downloaded nuget files value: $(Pipeline.Workspace)\${{parameters.packageFolderName }} - - name: ProductVersion #MDS product version (MDS validation) - value: $(NugetPackageVersion) - - name: BuildType value: $[ stageDependencies.buildMDS.build_signed_package.outputs['GetBuildType.CDP_BUILD_TYPE_COPY'] ] - ${{ if parameters.isPreview }}: - name: extractedNugetPath - value: $(extractedNugetRootPath).$(PreviewNugetPackageVersion) - - name: NugetPackageVersion - value: $(PreviewNugetPackageVersion) - - name: ProductVersion - value: $(PreviewNugetPackageVersion) + value: $(extractedNugetRootPath).$(previewMdsPackageVersion) + - name: mdsPackageVersion + value: $(previewMdsPackageVersion) steps: - script: SET @@ -75,7 +62,7 @@ jobs: displayName: 'Use NuGet' - powershell: | - #Sets Variables for AssemblyFileVersion, AssemblyVersion and NugetPackageVersion + # Sets the pipeline ASSEMBLY_VERSION variable. [Xml] $versionprops = Get-Content -Path ".\tools\props\Versions.props" Write-Host $versionprops.Project.PropertyGroup[0].AssemblyFileVersion @@ -283,71 +270,53 @@ jobs: displayName: 'Verify all dlls status are Valid' - powershell: | - # This will check for ProductVersion and FileVersion. - # - # For NetFx we have a different FileVersion, but product versions are all - # the same. Some may have extra numbering at the end. We only check for - # the first parts. + # This will check each DLL's ProductVersion and FileVersion against + # expected values. + $failed = 0 foreach ( $pVersion in Get-ChildItem *.dll -Path $(extractedNugetPath) -Recurse | ForEach-Object versioninfo ) { - if ($pVersion.ProductVersion -Like '$(ProductVersion)*') + if ($pVersion.ProductVersion -Like '$(mdsPackageVersion)*') { - Write-Host Valid Product Version:"$pVersion.ProductVersion" $pVersion.ProductVersion detected for $pVersion.FileName -ForegroundColor Green + Write-Host -ForegroundColor Green "Correct ProductVersion detected for $($pVersion.FileName): $($pVersion.ProductVersion)" } else { - Write-Host "Wrong ProductVersion detected. Expected: '$(ProductVersion)', but Detected: "$pVersion.ProductVersion"" - Exit -1 + Write-Host -ForegroundColor Red "Wrong ProductVersion detected for $($pVersion.FileName); expected: $(mdsPackageVersion); found: $($pVersion.ProductVersion)" + $failed = 1 } - if($pVersion.FileName -like '*lib\$(CurrentNetFxVersion)*'){ - - if($pVersion.FileVersion -eq '${{parameters.assembly_file_version_netfx }}') - { - Write-Host 'Correct File version Detected for net46,' $pVersion.FileVersion -ForegroundColor Green - } - else - { - Write-Host 'Wrong File version Detected for net46,' $pVersion.FileVersion -ForegroundColor Red - Exit -1 - } + if ($pVersion.FileVersion -eq '$(mdsAssemblyFileVersion)') + { + Write-Host -ForegroundColor Green "Correct FileVersion detected for $($pVersion.FileName): $($pVersion.FileVersion)" } else { - - if($pVersion.FileVersion -eq '${{parameters.assembly_file_version_core}}') - { - Write-Host 'Correct File version Detected for netcore,' $pVersion.FileVersion -ForegroundColor Green - } - else - { - Write-Host 'Wrong File version Detected for netcore and ref folder,' $pVersion.FileVersion -ForegroundColor Red - Exit -1 - } + Write-Host -ForegroundColor Red "Wrong FileVersion detected for $($pVersion.FileName); expected $(mdsAssemblyFileVersion); found: $($pVersion.FileVersion)" + $failed = 1 } } - Get-ChildItem *.dll -Path $(extractedNugetPath) -Recurse | ForEach-Object versioninfo - displayName: 'Verify "File Version" matches provided pipeline variable "ASSEMBLY_FILE_VERSION" for DLLs' - - - powershell: | - # Change TestMicrosoftDataSqlClientVersion - - [Xml] $versionprops = Get-Content -Path "tools/props/Versions.props" - $versionpropspath = "tools\props\Versions.props" - $versionprops.Project.PropertyGroup[$versionprops.Project.PropertyGroup.Count-1].TestMicrosoftDataSqlClientVersion ="$(NugetPackageVersion)" - Write-Host "Saving Test nuget version at $rootfolder\props ...." -ForegroundColor Green - $versionprops.Save($versionpropspath) + if ($failed -ne 0) + { + Exit -1 + } - displayName: 'Modify TestMicrosoftDataSqlClientVersion' + Get-ChildItem *.dll -Path $(extractedNugetPath) -Recurse | ForEach-Object VersionInfo | Format-List + displayName: 'Verify "File Version" matches expected values for DLLs' - powershell: | # Check assembly versions. + # + # GOTCHA: This expects the Versions.props file having XML elements in a + # certain order. If the order changes, this check will fail! + # + # TODO: This also isn't checking the versions of the actual assemblies in + # the package, so it isn't terribly useful. [Xml] $versionprops = Get-Content -Path "tools/props/Versions.props" - $AssemblyFileVersion = $versionprops.Project.PropertyGroup[0].AssemblyFileVersion - $AssemblyVersion = $versionprops.Project.PropertyGroup[0].AssemblyVersion + $AssemblyFileVersion = $versionprops.Project.PropertyGroup[2].AssemblyFileVersion + $AssemblyVersion = $versionprops.Project.PropertyGroup[2].AssemblyVersion if($AssemblyFileVersion -eq $AssemblyVersion) { diff --git a/eng/pipelines/common/templates/stages/ci-run-tests-stage.yml b/eng/pipelines/common/templates/stages/ci-run-tests-stage.yml index 7676574451..b01fb4d470 100644 --- a/eng/pipelines/common/templates/stages/ci-run-tests-stage.yml +++ b/eng/pipelines/common/templates/stages/ci-run-tests-stage.yml @@ -4,22 +4,17 @@ # See the LICENSE file in the project root for more information. # ################################################################################# parameters: - - name: debug - type: boolean - default: false + - name: abstractionsArtifactName + type: string - - name: testConfigurations - type: object + - name: abstractionsPackageVersion + type: string - - name: dependsOn + - name: azureArtifactName type: string - default: '' - - name: buildType - default: Project - values: - - Project - - Package + - name: azurePackageVersion + type: string - name: buildConfiguration type: string @@ -27,14 +22,38 @@ parameters: - Debug - Release - - name: prebuildSteps - type: stepList + - name: debug + type: boolean + default: false + + - name: dependsOn + type: object default: [] + + - name: mdsArtifactName + type: string + default: MDS.Artifact + + - name: mdsPackageVersion + type: string - name: postTestJobs type: jobList default: [] + - name: prebuildSteps + type: stepList + default: [] + + - name: referenceType + default: Project + values: + - Project + - Package + + - name: testConfigurations + type: object + # The timeout, in minutes, for each test job. - name: testJobTimeout type: number @@ -43,26 +62,29 @@ stages: - ${{ each config in parameters.testConfigurations }}: - ${{ each image in config.value.images }}: - stage: ${{ image.key }} - ${{ if ne(parameters.dependsOn, '') }}: - dependsOn: ${{ parameters.dependsOn }} - ${{ else }}: - dependsOn: [] + dependsOn: ${{ parameters.dependsOn }} jobs: - ${{ each targetFramework in config.value.TargetFrameworks }}: - ${{ each platform in config.value.buildPlatforms }}: - ${{ each testSet in config.value.TestSets }}: - ${{ if contains(targetFramework, 'net4') }}: # .NET Framework - - template: ../jobs/ci-run-tests-job.yml@self + - template: /eng/pipelines/common/templates/jobs/ci-run-tests-job.yml@self parameters: debug: ${{ parameters.debug }} buildConfiguration: ${{ parameters.buildConfiguration }} - buildType: ${{ parameters.buildType }} + referenceType: ${{ parameters.referenceType }} timeout: ${{ parameters.testJobTimeout }} poolName: ${{ config.value.pool }} hostedPool: ${{ eq(config.value.hostedPool, true) }} image: ${{ image.value }} jobDisplayName: ${{ format('{0}_{1}_{2}', replace(targetFramework, '.', '_'), platform, testSet) }} configProperties: ${{ config.value.configProperties }} + abstractionsArtifactName: ${{ parameters.abstractionsArtifactName }} + abstractionsPackageVersion: ${{ parameters.abstractionsPackageVersion }} + azureArtifactName: ${{ parameters.azureArtifactName }} + azurePackageVersion: ${{ parameters.azurePackageVersion }} + mdsArtifactName: ${{ parameters.mdsArtifactName }} + mdsPackageVersion: ${{ parameters.mdsPackageVersion }} prebuildSteps: ${{ parameters.prebuildSteps }} targetFramework: ${{ targetFramework }} netcoreVersionTestUtils: ${{config.value.netcoreVersionTestUtils }} @@ -80,11 +102,11 @@ stages: enableX64Test: false - ${{ else }}: # .NET - ${{ each useManagedSNI in config.value.useManagedSNI }}: - - template: ../jobs/ci-run-tests-job.yml@self + - template: /eng/pipelines/common/templates/jobs/ci-run-tests-job.yml@self parameters: debug: ${{ parameters.debug }} buildConfiguration: ${{ parameters.buildConfiguration }} - buildType: ${{ parameters.buildType }} + referenceType: ${{ parameters.referenceType }} timeout: ${{ parameters.testJobTimeout }} poolName: ${{ config.value.pool }} hostedPool: ${{ eq(config.value.hostedPool, true) }} @@ -95,6 +117,12 @@ stages: jobDisplayName: ${{ format('{0}_{1}_{2}_{3}', replace(targetFramework, '.', '_'), platform, 'NativeSNI', testSet) }} configProperties: ${{ config.value.configProperties }} useManagedSNI: ${{ useManagedSNI }} + abstractionsArtifactName: ${{ parameters.abstractionsArtifactName }} + abstractionsPackageVersion: ${{ parameters.abstractionsPackageVersion }} + azureArtifactName: ${{ parameters.azureArtifactName }} + azurePackageVersion: ${{ parameters.azurePackageVersion }} + mdsArtifactName: ${{ parameters.mdsArtifactName }} + mdsPackageVersion: ${{ parameters.mdsPackageVersion }} prebuildSteps: ${{ parameters.prebuildSteps }} targetFramework: ${{ targetFramework }} netcoreVersionTestUtils: ${{config.value.netcoreVersionTestUtils }} diff --git a/eng/pipelines/common/templates/steps/build-all-configurations-signed-dlls-step.yml b/eng/pipelines/common/templates/steps/build-all-configurations-signed-dlls-step.yml index 8981f2d392..e4bc9fcea8 100644 --- a/eng/pipelines/common/templates/steps/build-all-configurations-signed-dlls-step.yml +++ b/eng/pipelines/common/templates/steps/build-all-configurations-signed-dlls-step.yml @@ -4,9 +4,12 @@ # See the LICENSE file in the project root for more information. # ################################################################################# parameters: - - name: AssemblyFileVersion + + - name: abstractionsAssemblyFileVersion + type: string + + - name: abstractionsPackageVersion type: string - default: $(AssemblyFileVersion) - name: buildConfiguration type: string @@ -14,15 +17,17 @@ parameters: - Debug - Release - - name: packageRefMdsVersion + - name: mdsAssemblyFileVersion type: string - default: '' - - name: product - default: MDS + - name: mdsPackageVersion + type: string + + - name: referenceType + type: string values: - - MDS - - MSS + - Package + - Project steps: - task: DownloadSecureFile@1 @@ -34,10 +39,9 @@ steps: # Install the .NET SDK. - template: /eng/pipelines/steps/install-dotnet.yml@self - - ${{ if eq(parameters.product, 'MDS') }}: - - task: MSBuild@1 - displayName: 'BuildAllConfigurations using build.proj' - inputs: - solution: '**/build.proj' - configuration: '${{parameters.buildConfiguration }}' - msbuildArguments: '-p:AssemblyFileVersion=${{parameters.AssemblyFileVersion }} -t:BuildAllConfigurations -p:GenerateNuget=false -p:SigningKeyPath=$(Agent.TempDirectory)\netfxKeypair.snk' + - task: MSBuild@1 + displayName: 'BuildAllConfigurations using build.proj' + inputs: + solution: '**/build.proj' + configuration: '${{ parameters.buildConfiguration }}' + msbuildArguments: '-t:BuildAllConfigurations -p:ReferenceType=${{ parameters.referenceType }} -p:GenerateNuget=false -p:SigningKeyPath=$(Agent.TempDirectory)\netfxKeypair.snk -p:MdsPackageVersion=${{ parameters.mdsPackageVersion }} -p:AssemblyFileVersion=${{ parameters.mdsAssemblyFileVersion }} -p:AbstractionsPackageVersion=${{ parameters.abstractionsPackageVersion }} -p:AbstractionsAssemblyFileVersion=${{ parameters.abstractionsAssemblyFileVersion }}' diff --git a/eng/pipelines/common/templates/steps/build-all-tests-step.yml b/eng/pipelines/common/templates/steps/build-all-tests-step.yml index 1074c97632..7216451a3d 100644 --- a/eng/pipelines/common/templates/steps/build-all-tests-step.yml +++ b/eng/pipelines/common/templates/steps/build-all-tests-step.yml @@ -4,16 +4,11 @@ # See the LICENSE file in the project root for more information. # ################################################################################# parameters: - - name: targetFramework - type: string - - - name: nugetPackageVersion + - name: abstractionsPackageVersion type: string - default: $(NugetPackageVersion) - - - name: platform + + - name: azurePackageVersion type: string - default: $(Platform) - name: buildConfiguration type: string @@ -21,16 +16,26 @@ parameters: - Debug - Release + - name: mdsPackageVersion + type: string + + - name: osGroup + type: string + default: '' + + - name: platform + type: string + default: $(Platform) + - name: referenceType - default: Package + type: string values: - Project - Package - - - name: OSGroup - type: string - default: '' + - name: targetFramework + type: string + - name: testSet type: string @@ -42,26 +47,16 @@ steps: solution: build.proj platform: '${{parameters.platform }}' configuration: '${{parameters.buildConfiguration }}' - msbuildArguments: '-t:BuildTestsNetFx -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.nugetPackageVersion }}' - -# - ${{else if contains(parameters.targetFramework, 'netstandard')}}: # .NET Standard -# - task: MSBuild@1 -# displayName: 'Build Tests NetStandard' -# inputs: -# solution: build.proj -# platform: '${{parameters.platform }}' -# configuration: '${{parameters.buildConfiguration }}' -# msbuildArguments: '-t:BuildTestsNetCore -p:ReferenceType=NetStandard -p:TargetNetStandardVersion=${{parameters.targetNetStandardVersion }} -p:TF=${{parameters.targetFramework }} -p:ReferenceType=${{parameters.referenceType }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.nugetPackageVersion }}' -# condition: and(succeeded(), not(startsWith(variables['TF'], 'net4')), startsWith(variables['TargetNetStandardVersion'], 'netstandard')) + msbuildArguments: '-t:BuildTestsNetFx -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} -p:AbstractionsPackageVersion=${{ parameters.abstractionsPackageVersion }} -p:AzurePackageVersion=${{ parameters.azurePackageVersion }}' -- ${{elseif eq(parameters.OSGroup, '')}}: # .NET on Windows +- ${{elseif eq(parameters.osGroup, '')}}: # .NET on Windows - task: MSBuild@1 displayName: 'Build Tests NetCore [Win]' inputs: solution: build.proj platform: '${{parameters.platform }}' configuration: '${{parameters.buildConfiguration }}' - msbuildArguments: '-t:BuildTestsNetCore -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.nugetPackageVersion }}' + msbuildArguments: '-t:BuildTestsNetCore -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} -p:AbstractionsPackageVersion=${{ parameters.abstractionsPackageVersion }} -p:AzurePackageVersion=${{ parameters.azurePackageVersion }}' condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) - ${{ else }}: # .NET on Unix @@ -71,7 +66,7 @@ steps: command: custom projects: build.proj custom: msbuild - arguments: '-t:BuildTestsNetCore -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.nugetPackageVersion }} -p:OSGroup=${{parameters.OSGroup }} -p:platform=${{parameters.platform }} -p:Configuration=${{parameters.buildConfiguration }}' + arguments: '-t:BuildTestsNetCore -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} -p:OSGroup=${{parameters.osGroup }} -p:platform=${{parameters.platform }} -p:Configuration=${{parameters.buildConfiguration }} -p:AbstractionsPackageVersion=${{ parameters.abstractionsPackageVersion }} -p:AzurePackageVersion=${{ parameters.azurePackageVersion }}' verbosityRestore: Detailed verbosityPack: Detailed condition: and(succeeded(), ne(variables['Agent.OS'], 'Windows_NT')) diff --git a/eng/pipelines/common/templates/steps/build-and-run-tests-netcore-step.yml b/eng/pipelines/common/templates/steps/build-and-run-tests-netcore-step.yml index a15863fce0..05905d4884 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 @@ -20,9 +20,9 @@ parameters: - Project - Package - - name: NugetPackageVersion + - name: mdsPackageVersion type: string - default: $(NugetPackageVersion) + default: $(mdsPackageVersion) - name: platform type: string @@ -57,14 +57,14 @@ steps: inputs: solution: build.proj msbuildArchitecture: x64 - msbuildArguments: '-p:Configuration=${{parameters.buildConfiguration }} -t:BuildAKVNetCore -p:ReferenceType=${{parameters.referenceType }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.NugetPackageVersion }}' + msbuildArguments: '-p:Configuration=${{parameters.buildConfiguration }} -t:BuildAKVNetCore -p:ReferenceType=${{parameters.referenceType }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }}' - task: MSBuild@1 displayName: 'MSBuild Build Tests for ${{parameters.TargetNetCoreVersion }}' inputs: solution: build.proj msbuildArchitecture: x64 - msbuildArguments: '-t:BuildTestsNetCore -p:ReferenceType=${{parameters.referenceType }} -p:TargetNetCoreVersion=${{parameters.TargetNetCoreVersion }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.NugetPackageVersion }} -p:Configuration=${{parameters.buildConfiguration }}' + msbuildArguments: '-t:BuildTestsNetCore -p:ReferenceType=${{parameters.referenceType }} -p:TargetNetCoreVersion=${{parameters.TargetNetCoreVersion }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} -p:Configuration=${{parameters.buildConfiguration }}' # Don't run unit tests using package reference. Unit tests are only run using project reference. @@ -73,14 +73,14 @@ steps: inputs: command: test projects: 'src\Microsoft.Data.SqlClient\tests\FunctionalTests\Microsoft.Data.SqlClient.FunctionalTests.csproj' - arguments: '-p:Platform=${{parameters.platform }} -p:TestTargetOS="${{parameters.TestTargetOS }}" -p:TargetNetCoreVersion=${{parameters.TargetNetCoreVersion }} -p:ReferenceType=${{parameters.referenceType }} -p:Configuration=${{parameters.buildConfiguration }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.NugetPackageVersion }} --no-build -v n --filter "category!=failing&category!=flaky"' + arguments: '-p:Platform=${{parameters.platform }} -p:TestTargetOS="${{parameters.TestTargetOS }}" -p:TargetNetCoreVersion=${{parameters.TargetNetCoreVersion }} -p:ReferenceType=${{parameters.referenceType }} -p:Configuration=${{parameters.buildConfiguration }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} --no-build -v n --filter "category!=failing&category!=flaky"' - task: DotNetCoreCLI@2 displayName: 'Run Flaky Functional Tests for ${{parameters.TargetNetCoreVersion }}' inputs: command: test projects: 'src\Microsoft.Data.SqlClient\tests\FunctionalTests\Microsoft.Data.SqlClient.FunctionalTests.csproj' - arguments: '-p:Platform=${{parameters.platform }} -p:TestTargetOS="${{parameters.TestTargetOS }}" -p:TargetNetCoreVersion=${{parameters.TargetNetCoreVersion }} -p:ReferenceType=${{parameters.referenceType }} -p:Configuration=${{parameters.buildConfiguration }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.NugetPackageVersion }} --no-build -v n --filter "category=flaky"' + arguments: '-p:Platform=${{parameters.platform }} -p:TestTargetOS="${{parameters.TestTargetOS }}" -p:TargetNetCoreVersion=${{parameters.TargetNetCoreVersion }} -p:ReferenceType=${{parameters.referenceType }} -p:Configuration=${{parameters.buildConfiguration }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} --no-build -v n --filter "category=flaky"' continueOnError: true - task: DotNetCoreCLI@2 @@ -88,7 +88,7 @@ steps: inputs: command: test projects: 'src\Microsoft.Data.SqlClient\tests\ManualTests\Microsoft.Data.SqlClient.ManualTesting.Tests.csproj' - arguments: '-p:Platform=${{parameters.platform }} -p:TestTargetOS="${{parameters.TestTargetOS }}" -p:TargetNetCoreVersion=${{parameters.TargetNetCoreVersion }} -p:ReferenceType=${{parameters.referenceType }} -p:Configuration=${{parameters.buildConfiguration }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.NugetPackageVersion }} --no-build -v n --filter "category!=failing&category!=flaky" --collect "Code Coverage"' + arguments: '-p:Platform=${{parameters.platform }} -p:TestTargetOS="${{parameters.TestTargetOS }}" -p:TargetNetCoreVersion=${{parameters.TargetNetCoreVersion }} -p:ReferenceType=${{parameters.referenceType }} -p:Configuration=${{parameters.buildConfiguration }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} --no-build -v n --filter "category!=failing&category!=flaky" --collect "Code Coverage"' retryCountOnTaskFailure: ${{parameters.retryCountOnManualTests }} - task: DotNetCoreCLI@2 @@ -96,6 +96,6 @@ steps: inputs: command: test projects: 'src\Microsoft.Data.SqlClient\tests\ManualTests\Microsoft.Data.SqlClient.ManualTesting.Tests.csproj' - arguments: '-p:Platform=${{parameters.platform }} -p:TestTargetOS="${{parameters.TestTargetOS }}" -p:TargetNetCoreVersion=${{parameters.TargetNetCoreVersion }} -p:ReferenceType=${{parameters.referenceType }} -p:Configuration=${{parameters.buildConfiguration }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.NugetPackageVersion }} --no-build -v n --filter "category=flaky" --collect "Code Coverage"' + arguments: '-p:Platform=${{parameters.platform }} -p:TestTargetOS="${{parameters.TestTargetOS }}" -p:TargetNetCoreVersion=${{parameters.TargetNetCoreVersion }} -p:ReferenceType=${{parameters.referenceType }} -p:Configuration=${{parameters.buildConfiguration }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} --no-build -v n --filter "category=flaky" --collect "Code Coverage"' retryCountOnTaskFailure: ${{parameters.retryCountOnManualTests }} - continueOnError: true \ No newline at end of file + continueOnError: true 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 b52bbf5686..078804e885 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 @@ -20,9 +20,9 @@ parameters: - Project - Package - - name: NugetPackageVersion + - name: mdsPackageVersion type: string - default: $(NugetPackageVersion) + default: $(mdsPackageVersion) - name: platform type: string @@ -57,13 +57,13 @@ steps: inputs: solution: build.proj msbuildArchitecture: x64 - msbuildArguments: '-p:Configuration=${{parameters.buildConfiguration }} -t:BuildAKVNetFx -p:ReferenceType=${{parameters.referenceType }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.NugetPackageVersion }}' + msbuildArguments: '-p:Configuration=${{parameters.buildConfiguration }} -t:BuildAKVNetFx -p:ReferenceType=${{parameters.referenceType }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }}' - task: MSBuild@1 displayName: 'MSBuild Build Tests for ${{parameters.TargetNetFxVersion }}' inputs: solution: build.proj - msbuildArguments: ' -t:BuildTestsNetFx -p:ReferenceType=${{parameters.referenceType }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.NugetPackageVersion }} -p:TargetNetFxVersion=${{parameters.TargetNetFxVersion }} -p:Configuration=${{parameters.buildConfiguration }} -p:Platform=${{parameters.platform }}' + msbuildArguments: ' -t:BuildTestsNetFx -p:ReferenceType=${{parameters.referenceType }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} -p:TargetNetFxVersion=${{parameters.TargetNetFxVersion }} -p:Configuration=${{parameters.buildConfiguration }} -p:Platform=${{parameters.platform }}' # Don't run unit tests using package reference. Unit tests are only run using project reference. @@ -72,14 +72,14 @@ steps: inputs: command: test projects: 'src\Microsoft.Data.SqlClient\tests\FunctionalTests\Microsoft.Data.SqlClient.FunctionalTests.csproj' - arguments: '-p:Platform=${{parameters.platform }} -p:TestTargetOS="${{parameters.TestTargetOS }}" -p:TargetNetFxVersion=${{parameters.TargetNetFxVersion }} -p:ReferenceType=${{parameters.referenceType }} -p:Configuration=${{parameters.buildConfiguration }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.NugetPackageVersion }} --no-build -v n --filter "category!=failing&category!=flaky" --collect "Code Coverage"' + arguments: '-p:Platform=${{parameters.platform }} -p:TestTargetOS="${{parameters.TestTargetOS }}" -p:TargetNetFxVersion=${{parameters.TargetNetFxVersion }} -p:ReferenceType=${{parameters.referenceType }} -p:Configuration=${{parameters.buildConfiguration }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} --no-build -v n --filter "category!=failing&category!=flaky" --collect "Code Coverage"' - task: DotNetCoreCLI@2 displayName: 'Run Flaky Functional Tests for ${{parameters.TargetNetFxVersion }}' inputs: command: test projects: 'src\Microsoft.Data.SqlClient\tests\FunctionalTests\Microsoft.Data.SqlClient.FunctionalTests.csproj' - arguments: '-p:Platform=${{parameters.platform }} -p:TestTargetOS="${{parameters.TestTargetOS }}" -p:TargetNetFxVersion=${{parameters.TargetNetFxVersion }} -p:ReferenceType=${{parameters.referenceType }} -p:Configuration=${{parameters.buildConfiguration }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.NugetPackageVersion }} --no-build -v n --filter "category=flaky" --collect "Code Coverage"' + arguments: '-p:Platform=${{parameters.platform }} -p:TestTargetOS="${{parameters.TestTargetOS }}" -p:TargetNetFxVersion=${{parameters.TargetNetFxVersion }} -p:ReferenceType=${{parameters.referenceType }} -p:Configuration=${{parameters.buildConfiguration }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} --no-build -v n --filter "category=flaky" --collect "Code Coverage"' continueOnError: true - task: DotNetCoreCLI@2 @@ -87,7 +87,7 @@ steps: inputs: command: test projects: 'src\Microsoft.Data.SqlClient\tests\ManualTests\Microsoft.Data.SqlClient.ManualTesting.Tests.csproj' - arguments: '-p:Platform=${{parameters.platform }} -p:TestTargetOS="${{parameters.TestTargetOS }}" -p:TargetNetFxVersion=${{parameters.TargetNetFxVersion }} -p:ReferenceType=${{parameters.referenceType }} -p:Configuration=${{parameters.buildConfiguration }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.NugetPackageVersion }} --no-build -v n --filter "category!=failing&category!=flaky" --collect "Code Coverage"' + arguments: '-p:Platform=${{parameters.platform }} -p:TestTargetOS="${{parameters.TestTargetOS }}" -p:TargetNetFxVersion=${{parameters.TargetNetFxVersion }} -p:ReferenceType=${{parameters.referenceType }} -p:Configuration=${{parameters.buildConfiguration }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} --no-build -v n --filter "category!=failing&category!=flaky" --collect "Code Coverage"' retryCountOnTaskFailure: ${{parameters.retryCountOnManualTests }} - task: DotNetCoreCLI@2 @@ -95,6 +95,6 @@ steps: inputs: command: test projects: 'src\Microsoft.Data.SqlClient\tests\ManualTests\Microsoft.Data.SqlClient.ManualTesting.Tests.csproj' - arguments: '-p:Platform=${{parameters.platform }} -p:TestTargetOS="${{parameters.TestTargetOS }}" -p:TargetNetFxVersion=${{parameters.TargetNetFxVersion }} -p:ReferenceType=${{parameters.referenceType }} -p:Configuration=${{parameters.buildConfiguration }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.NugetPackageVersion }} --no-build -v n --filter "category=flaky" --collect "Code Coverage"' + arguments: '-p:Platform=${{parameters.platform }} -p:TestTargetOS="${{parameters.TestTargetOS }}" -p:TargetNetFxVersion=${{parameters.TargetNetFxVersion }} -p:ReferenceType=${{parameters.referenceType }} -p:Configuration=${{parameters.buildConfiguration }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} --no-build -v n --filter "category=flaky" --collect "Code Coverage"' retryCountOnTaskFailure: ${{parameters.retryCountOnManualTests }} continueOnError: true diff --git a/eng/pipelines/common/templates/steps/ci-prebuild-step.yml b/eng/pipelines/common/templates/steps/ci-prebuild-step.yml index 3774363ec3..162479997c 100644 --- a/eng/pipelines/common/templates/steps/ci-prebuild-step.yml +++ b/eng/pipelines/common/templates/steps/ci-prebuild-step.yml @@ -8,38 +8,19 @@ parameters: type: boolean default: false - - name: artifactName - type: string - default: Artifacts - - - name: buildConfiguration + - name: referenceType type: string - values: - - Debug - - Release - - - name: buildType - default: Project values: - Project - Package steps: -- ${{if eq(parameters.debug, true)}}: - - powershell: | - Get-ChildItem env: | Sort-Object Name - displayName: 'List Environment Variables [debug]' - -- ${{if eq(parameters.buildType, 'Package')}}: - - task: DownloadPipelineArtifact@2 - displayName: 'Download NuGet Package' - inputs: - buildType: current - artifact: ${{parameters.artifactName }} - patterns: '**/*.nupkg' - targetPath: $(Pipeline.Workspace)/${{parameters.artifactName }} + - ${{if eq(parameters.debug, true)}}: + - powershell: | + Get-ChildItem env: | Sort-Object Name + displayName: 'List Environment Variables [debug]' - - template: update-nuget-config-local-feed-step.yml@self - parameters: - downloadedNugetPath: $(Pipeline.Workspace)\${{parameters.artifactName }} - debug: ${{ parameters.debug }} + - ${{if eq(parameters.referenceType, 'Package')}}: + - template: /eng/pipelines/common/templates/steps/update-nuget-config-local-feed-step.yml@self + parameters: + debug: ${{ parameters.debug }} 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 919958e8a2..8c91dac669 100644 --- a/eng/pipelines/common/templates/steps/ci-project-build-step.yml +++ b/eng/pipelines/common/templates/steps/ci-project-build-step.yml @@ -13,6 +13,12 @@ parameters: values: - Debug - Release + + - name: referenceType + type: string + values: + - Package + - Project - name: buildNumber type: string @@ -36,28 +42,38 @@ parameters: - all - allNoDocs + # Used when MDS is built with ReferenceType = Package. + - name: abstractionsPackageVersion + type: string + default: '' + + # Used when AKV is built with ReferenceType = Package. + - name: mdsPackageVersion + type: string + default: '' + steps: - ${{ if or(eq(parameters.operatingSystem, 'Windows'), eq(parameters.operatingSystem, 'deferedToRuntime')) }}: - ${{ if or(eq(parameters.build, 'MDS'), eq(parameters.build, 'all'), eq(parameters.build, 'allNoDocs')) }}: - task: MSBuild@1 - displayName: 'Restore nugets [Win]' + displayName: 'Restore [Win]' condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) inputs: solution: build.proj msbuildArchitecture: x64 - msbuildArguments: '-t:restore' + msbuildArguments: '-t:restore -p:ReferenceType=${{ parameters.ReferenceType }} -p:AbstractionsPackageVersion=${{parameters.abstractionsPackageVersion}}' retryCountOnTaskFailure: 1 - ${{ if eq(parameters.build, 'allNoDocs') }}: - task: MSBuild@1 - displayName: 'Build Driver [Win]' + displayName: 'Build Driver (no docs) [Win]' condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) inputs: solution: build.proj msbuildArchitecture: x64 platform: '${{ parameters.platform }}' configuration: '${{ parameters.buildConfiguration }}' - msbuildArguments: '-t:BuildAllConfigurations -p:GenerateDocumentationFile=false -p:GenerateNuGet=false -p:BuildNumber=${{ parameters.buildNumber }}' + msbuildArguments: '-t:BuildAllConfigurations -p:ReferenceType=${{ parameters.ReferenceType }} -p:GenerateDocumentationFile=false -p:GenerateNuGet=false -p:BuildNumber=${{ parameters.buildNumber }} -p:AbstractionsPackageVersion=${{parameters.abstractionsPackageVersion}}' clean: true - ${{ if or(eq(parameters.build, 'MDS'), eq(parameters.build, 'all')) }}: @@ -69,7 +85,7 @@ steps: msbuildArchitecture: x64 platform: '${{ parameters.platform }}' configuration: '${{ parameters.buildConfiguration }}' - msbuildArguments: '-t:BuildAllConfigurations -p:GenerateNuGet=false -p:BuildNumber=${{ parameters.buildNumber }}' + msbuildArguments: '-t:BuildAllConfigurations -p:ReferenceType=${{ parameters.ReferenceType }} -p:GenerateNuGet=false -p:BuildNumber=${{ parameters.buildNumber }} -p:AbstractionsPackageVersion=${{parameters.abstractionsPackageVersion}}' clean: true - ${{ if or(eq(parameters.build, 'AKV'), eq(parameters.build, 'all'), eq(parameters.build, 'allNoDocs')) }}: @@ -81,7 +97,7 @@ steps: msbuildArchitecture: x64 platform: '${{ parameters.platform }}' configuration: '${{ parameters.buildConfiguration }}' - msbuildArguments: '-t:BuildAKVNetFx -p:BuildNumber=${{ parameters.buildNumber }}' + msbuildArguments: '-t:BuildAKVNetFx -p:ReferenceType=${{ parameters.ReferenceType }} -p:BuildNumber=${{ parameters.buildNumber }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion}}' - task: MSBuild@1 displayName: 'Build AKV Provider NetCore All OS [Win]' @@ -91,17 +107,17 @@ steps: msbuildArchitecture: x64 platform: '${{ parameters.platform }}' configuration: '${{ parameters.buildConfiguration }}' - msbuildArguments: '-t:BuildAKVNetCoreAllOS -p:BuildNumber=${{ parameters.buildNumber }}' + msbuildArguments: '-t:BuildAKVNetCoreAllOS -p:ReferenceType=${{ parameters.ReferenceType }} -p:BuildNumber=${{ parameters.buildNumber }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion}}' - ${{ if or(eq(parameters.operatingSystem, 'Linux'), eq(parameters.operatingSystem, 'MacOS'), eq(parameters.operatingSystem, 'deferedToRuntime')) }}: - task: DotNetCoreCLI@2 - displayName: 'Build Driver [non-Win]' + displayName: 'Build Driver [${{ parameters.operatingSystem }}]' condition: and(succeeded(), ne(variables['Agent.OS'], 'Windows_NT')) inputs: command: custom projects: build.proj custom: msbuild - arguments: '-t:BuildAll -p:TestEnabled=true -p:GenerateDocumentationFile=false -p:configuration=${{ parameters.buildConfiguration }}' + arguments: '-t:BuildAll -p:ReferenceType=${{ parameters.ReferenceType }} -p:TestEnabled=true -p:GenerateDocumentationFile=false -p:configuration=${{ parameters.configuration }} -p:AbstractionsPackageVersion=${{parameters.abstractionsPackageVersion}}' verbosityRestore: Detailed verbosityPack: Detailed retryCountOnTaskFailure: 1 diff --git a/eng/pipelines/common/templates/steps/code-analyze-step.yml b/eng/pipelines/common/templates/steps/code-analyze-step.yml index 9807541a70..bad64f55d3 100644 --- a/eng/pipelines/common/templates/steps/code-analyze-step.yml +++ b/eng/pipelines/common/templates/steps/code-analyze-step.yml @@ -3,40 +3,30 @@ # The .NET Foundation licenses this file to you under the MIT license. # # See the LICENSE file in the project root for more information. # ################################################################################# -parameters: - - name: analyzeType - values: - - roslyn - - inspect - - all +# This template defines a step to run Roslyn Analyzers on the MDS project build. +# It uses the RoslynAnalyzers@3 task from the Secure Development Team's SDL +# extension: +# +# https://eng.ms/docs/cloud-ai-platform/devdiv/one-engineering-system-1es/1es-mohanb/security-integration/guardian-wiki/sdl-azdo-extension/roslyn-analyzers-build-task +# +# GOTCHA: This step will clobber any existing build output. It should be run +# _before_ any build steps that perform versioning or signing. + +# @TODO: This can probably be made generic and pass in the command lines for msbuild +# BUT, they should be kept separate by now as we rebuild build.proj in parallel, we won't +# affect >1 project at a time. + +parameters: - name: sourceRoot type: string default: $(REPOROOT) - - name: packageRefMdsVersion - type: string - default: '' - - - name: product - default: MDS - values: - - MDS - - MSS - steps: -- ${{ if or(eq(parameters.analyzeType, 'roslyn'), eq(parameters.analyzeType, 'all')) }}: - - ${{ if eq(parameters.product, 'MDS') }}: - - task: securedevelopmentteam.vss-secure-development-tools.build-task-roslynanalyzers.RoslynAnalyzers@3 - displayName: 'Guardian Dotnet Analyzers ' - inputs: - msBuildVersion: 17.0 - msBuildArchitecture: x64 - setupCommandlinePicker: vs2022 - msBuildCommandline: 'msbuild ${{parameters.sourceRoot}}\build.proj -p:configuration=Release -p:GenerateNuget=false -p:BuildTools=false -p:SigningKeyPath=$(Agent.TempDirectory)\netfxKeypair.snk' - -- ${{ if or(eq(parameters.analyzeType, 'inspect'), eq(parameters.analyzeType, 'all')) }}: - - task: securedevelopmentteam.vss-secure-development-tools.build-task-codeinspector.CodeInspector@2 - displayName: 'Run Code Inspector' + - task: securedevelopmentteam.vss-secure-development-tools.build-task-roslynanalyzers.RoslynAnalyzers@3 + displayName: Roslyn Analyzers inputs: - LogLevel: Error + msBuildVersion: 17.0 + msBuildArchitecture: x64 + setupCommandlinePicker: vs2022 + msBuildCommandline: 'msbuild ${{parameters.sourceRoot}}\build.proj -p:configuration=Release -p:GenerateNuget=false -p:BuildTools=false -p:SigningKeyPath=$(Agent.TempDirectory)\netfxKeypair.snk' diff --git a/eng/pipelines/common/templates/steps/configure-sql-server-step.yml b/eng/pipelines/common/templates/steps/configure-sql-server-step.yml index bfb89e34b9..a11dd38409 100644 --- a/eng/pipelines/common/templates/steps/configure-sql-server-step.yml +++ b/eng/pipelines/common/templates/steps/configure-sql-server-step.yml @@ -71,7 +71,7 @@ parameters: steps: - ${{ if eq(parameters.operatingSystem, 'Windows') }}: # windows only steps - - template: configure-sql-server-win-step.yml@self + - template: /eng/pipelines/common/templates/steps/configure-sql-server-win-step.yml@self parameters: instanceName: ${{parameters.instanceName}} user: ${{parameters.user}} @@ -89,13 +89,13 @@ steps: - ${{ elseif eq(parameters.operatingSystem, 'Linux') }}: # Linux only steps - - template: configure-sql-server-linux-step.yml@self + - template: /eng/pipelines/common/templates/steps/configure-sql-server-linux-step.yml@self parameters: password: ${{parameters.password}} - ${{ elseif eq(parameters.operatingSystem, 'Mac') }}: # macOS only steps - - template: configure-sql-server-macos-step.yml@self + - template: /eng/pipelines/common/templates/steps/configure-sql-server-macos-step.yml@self parameters: password: ${{parameters.password}} diff --git a/eng/pipelines/common/templates/steps/copy-dlls-for-test-step.yml b/eng/pipelines/common/templates/steps/copy-dlls-for-test-step.yml index c33bd36645..711c3a9b8f 100644 --- a/eng/pipelines/common/templates/steps/copy-dlls-for-test-step.yml +++ b/eng/pipelines/common/templates/steps/copy-dlls-for-test-step.yml @@ -85,6 +85,6 @@ steps: $software = '${{parameters.softwareFolder}}' $symbols = '${{parameters.symbolsFolder}}' - Get-ChildItem -recurse "$software\*.dll" - Get-ChildItem -recurse "$symbols\*.pdb" + Get-ChildItem -recurse "$software\*.dll" | ForEach-Object VersionInfo | Format-List + Get-ChildItem -recurse "$symbols\*.pdb" | ForEach-Object VersionInfo | Format-List displayName: 'List the prepared files' diff --git a/eng/pipelines/common/templates/steps/generate-nuget-package-step.yml b/eng/pipelines/common/templates/steps/generate-nuget-package-step.yml index 0754735e28..7ed33cbdc2 100644 --- a/eng/pipelines/common/templates/steps/generate-nuget-package-step.yml +++ b/eng/pipelines/common/templates/steps/generate-nuget-package-step.yml @@ -6,13 +6,11 @@ parameters: - name: nuspecPath type: string - default: '$(nuspecPath)' - - name: NugetPackageVersion + - name: packageVersion type: string - default: '$(NugetPackageVersion)' - - name: OutputDirectory + - name: outputDirectory type: string default: '$(Build.SourcesDirectory)/packages' @@ -29,17 +27,22 @@ parameters: - name: displayName type: string - default: 'NuGet pack with snupkg' - name: installNuget type: boolean default: true - name: referenceType - default: project + type: string values: - - project - - package + - Package + - Project + + # Semi-colon separated properties to pass to nuget via the -properties + # argument. + - name: properties + type: string + default: '' steps: - ${{ if parameters.installNuget }}: @@ -58,6 +61,6 @@ steps: inputs: command: custom ${{ if parameters.generateSymbolsPackage }}: - arguments: 'pack -Symbols -SymbolPackageFormat snupkg ${{parameters.nuspecPath}} -Version ${{parameters.NugetPackageVersion}} -OutputDirectory ${{parameters.OutputDirectory}} -properties "COMMITID=$(CommitHead);Configuration=${{parameters.buildConfiguration}};ReferenceType=${{parameters.referenceType}}"' + arguments: 'pack -Symbols -SymbolPackageFormat snupkg ${{parameters.nuspecPath}} -Version ${{parameters.packageVersion}} -OutputDirectory ${{parameters.outputDirectory}} -properties "COMMITID=$(CommitHead);Configuration=${{parameters.buildConfiguration}};ReferenceType=${{parameters.referenceType}};${{parameters.properties}}"' ${{else }}: - arguments: 'pack ${{parameters.nuspecPath}} -Version ${{parameters.NugetPackageVersion}} -OutputDirectory ${{parameters.OutputDirectory}} -properties "COMMITID=$(CommitHead);Configuration=${{parameters.buildConfiguration}};ReferenceType=${{parameters.referenceType}}"' + arguments: 'pack ${{parameters.nuspecPath}} -Version ${{parameters.packageVersion}} -OutputDirectory ${{parameters.outputDirectory}} -properties "COMMITID=$(CommitHead);Configuration=${{parameters.buildConfiguration}};ReferenceType=${{parameters.referenceType}};${{parameters.properties}}"' diff --git a/eng/pipelines/common/templates/steps/publish-symbols-step.yml b/eng/pipelines/common/templates/steps/publish-symbols-step.yml index a3cd272958..3f80ae8d2d 100644 --- a/eng/pipelines/common/templates/steps/publish-symbols-step.yml +++ b/eng/pipelines/common/templates/steps/publish-symbols-step.yml @@ -15,7 +15,7 @@ parameters: - name: symbolsVersion type: string - default: '$(NuGetPackageVersion)' + default: '$(mdsPackageVersion)' - name: symbolServer type: string diff --git a/eng/pipelines/common/templates/steps/run-all-tests-step.yml b/eng/pipelines/common/templates/steps/run-all-tests-step.yml index c4042dd8b5..bdbaa91e15 100644 --- a/eng/pipelines/common/templates/steps/run-all-tests-step.yml +++ b/eng/pipelines/common/templates/steps/run-all-tests-step.yml @@ -11,9 +11,9 @@ parameters: - name: targetFramework type: string - - name: nugetPackageVersion + - name: mdsPackageVersion type: string - default: $(NugetPackageVersion) + default: $(mdsPackageVersion) - name: platform type: string @@ -68,9 +68,9 @@ steps: platform: '${{parameters.platform }}' configuration: '${{parameters.buildConfiguration }}' ${{ if eq(parameters.msbuildArchitecture, 'x64') }}: - msbuildArguments: '-t:RunUnitTests -p:TF=${{parameters.targetFramework }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.nugetPackageVersion }}' + msbuildArguments: '-t:RunUnitTests -p:TF=${{parameters.targetFramework }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }}' ${{ else }}: # x86 - msbuildArguments: '-t:RunUnitTests -p:TF=${{parameters.targetFramework }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.nugetPackageVersion }} -p:DotnetPath=${{parameters.dotnetx86RootPath }}' + msbuildArguments: '-t:RunUnitTests -p:TF=${{parameters.targetFramework }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} -p:DotnetPath=${{parameters.dotnetx86RootPath }}' - task: MSBuild@1 displayName: 'Run Flaky Unit Tests ${{parameters.msbuildArchitecture }}' @@ -80,9 +80,9 @@ steps: platform: '${{parameters.platform }}' configuration: '${{parameters.buildConfiguration }}' ${{ if eq(parameters.msbuildArchitecture, 'x64') }}: - msbuildArguments: '-t:RunUnitTests -p:TF=${{parameters.targetFramework }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.nugetPackageVersion }} -p:Filter="category=flaky" -p:CollectCodeCoverage=false' + msbuildArguments: '-t:RunUnitTests -p:TF=${{parameters.targetFramework }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} -p:Filter="category=flaky" -p:CollectCodeCoverage=false' ${{ else }}: # x86 - msbuildArguments: '-t:RunUnitTests -p:TF=${{parameters.targetFramework }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.nugetPackageVersion }} -p:DotnetPath=${{parameters.dotnetx86RootPath }} -p:Filter="category=flaky" -p:CollectCodeCoverage=false' + msbuildArguments: '-t:RunUnitTests -p:TF=${{parameters.targetFramework }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} -p:DotnetPath=${{parameters.dotnetx86RootPath }} -p:Filter="category=flaky" -p:CollectCodeCoverage=false' continueOnError: true - task: MSBuild@1 @@ -93,9 +93,9 @@ steps: platform: '${{parameters.platform }}' configuration: '${{parameters.buildConfiguration }}' ${{ if eq(parameters.msbuildArchitecture, 'x64') }}: - msbuildArguments: '-t:RunFunctionalTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.nugetPackageVersion }}' + msbuildArguments: '-t:RunFunctionalTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }}' ${{ else }}: # x86 - msbuildArguments: '-t:RunFunctionalTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.nugetPackageVersion }} -p:DotnetPath=${{parameters.dotnetx86RootPath }}' + msbuildArguments: '-t:RunFunctionalTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} -p:DotnetPath=${{parameters.dotnetx86RootPath }}' - task: MSBuild@1 displayName: 'Run Flaky Functional Tests ${{parameters.msbuildArchitecture }}' @@ -105,9 +105,9 @@ steps: platform: '${{parameters.platform }}' configuration: '${{parameters.buildConfiguration }}' ${{ if eq(parameters.msbuildArchitecture, 'x64') }}: - msbuildArguments: '-t:RunFunctionalTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.nugetPackageVersion }} -p:Filter="category=flaky" -p:CollectCodeCoverage=false' + msbuildArguments: '-t:RunFunctionalTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} -p:Filter="category=flaky" -p:CollectCodeCoverage=false' ${{ else }}: # x86 - msbuildArguments: '-t:RunFunctionalTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.nugetPackageVersion }} -p:DotnetPath=${{parameters.dotnetx86RootPath }} -p:Filter="category=flaky" -p:CollectCodeCoverage=false' + msbuildArguments: '-t:RunFunctionalTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} -p:DotnetPath=${{parameters.dotnetx86RootPath }} -p:Filter="category=flaky" -p:CollectCodeCoverage=false' continueOnError: true - task: MSBuild@1 @@ -118,9 +118,9 @@ steps: platform: '${{parameters.platform }}' configuration: '${{parameters.buildConfiguration }}' ${{ if eq(parameters.msbuildArchitecture, 'x64') }}: - msbuildArguments: '-t:RunManualTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.nugetPackageVersion }}' + msbuildArguments: '-t:RunManualTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }}' ${{ else }}: # x86 - msbuildArguments: '-t:RunManualTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.nugetPackageVersion }} -p:DotnetPath=${{parameters.dotnetx86RootPath }}' + msbuildArguments: '-t:RunManualTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} -p:DotnetPath=${{parameters.dotnetx86RootPath }}' retryCountOnTaskFailure: ${{parameters.retryCountOnManualTests }} - task: MSBuild@1 @@ -131,9 +131,9 @@ steps: platform: '${{parameters.platform }}' configuration: '${{parameters.buildConfiguration }}' ${{ if eq(parameters.msbuildArchitecture, 'x64') }}: - msbuildArguments: '-t:RunManualTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.nugetPackageVersion }} -p:Filter="category=flaky" -p:CollectCodeCoverage=false' + msbuildArguments: '-t:RunManualTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} -p:Filter="category=flaky" -p:CollectCodeCoverage=false' ${{ else }}: # x86 - msbuildArguments: '-t:RunManualTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.nugetPackageVersion }} -p:DotnetPath=${{parameters.dotnetx86RootPath }} -p:Filter="category=flaky" -p:CollectCodeCoverage=false' + msbuildArguments: '-t:RunManualTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} -p:DotnetPath=${{parameters.dotnetx86RootPath }} -p:Filter="category=flaky" -p:CollectCodeCoverage=false' continueOnError: true retryCountOnTaskFailure: ${{parameters.retryCountOnManualTests }} @@ -145,7 +145,7 @@ steps: command: custom projects: build.proj custom: msbuild - arguments: '-t:RunUnitTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.nugetPackageVersion }} -p:platform=${{parameters.platform }} -p:Configuration=${{parameters.buildConfiguration }}' + arguments: '-t:RunUnitTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} -p:platform=${{parameters.platform }} -p:Configuration=${{parameters.buildConfiguration }}' verbosityRestore: Detailed verbosityPack: Detailed @@ -155,7 +155,7 @@ steps: command: custom projects: build.proj custom: msbuild - arguments: '-t:RunUnitTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.nugetPackageVersion }} -p:platform=${{parameters.platform }} -p:Configuration=${{parameters.buildConfiguration }} -p:Filter="category=flaky"' + arguments: '-t:RunUnitTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} -p:platform=${{parameters.platform }} -p:Configuration=${{parameters.buildConfiguration }} -p:Filter="category=flaky"' verbosityRestore: Detailed verbosityPack: Detailed continueOnError: true @@ -166,7 +166,7 @@ steps: command: custom projects: build.proj custom: msbuild - arguments: '-t:RunFunctionalTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.nugetPackageVersion }} -p:platform=${{parameters.platform }} -p:Configuration=${{parameters.buildConfiguration }}' + arguments: '-t:RunFunctionalTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} -p:platform=${{parameters.platform }} -p:Configuration=${{parameters.buildConfiguration }}' verbosityRestore: Detailed verbosityPack: Detailed @@ -176,7 +176,7 @@ steps: command: custom projects: build.proj custom: msbuild - arguments: '-t:RunFunctionalTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.nugetPackageVersion }} -p:platform=${{parameters.platform }} -p:Configuration=${{parameters.buildConfiguration }} -p:Filter="category=flaky"' + arguments: '-t:RunFunctionalTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} -p:platform=${{parameters.platform }} -p:Configuration=${{parameters.buildConfiguration }} -p:Filter="category=flaky"' verbosityRestore: Detailed verbosityPack: Detailed continueOnError: true @@ -187,7 +187,7 @@ steps: command: custom projects: build.proj custom: msbuild - arguments: '-t:RunManualTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.nugetPackageVersion }} -p:platform=${{parameters.platform }} -p:Configuration=${{parameters.buildConfiguration }}' + arguments: '-t:RunManualTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} -p:platform=${{parameters.platform }} -p:Configuration=${{parameters.buildConfiguration }}' verbosityRestore: Detailed verbosityPack: Detailed retryCountOnTaskFailure: ${{parameters.retryCountOnManualTests }} @@ -198,7 +198,7 @@ steps: command: custom projects: build.proj custom: msbuild - arguments: '-t:RunManualTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:TestMicrosoftDataSqlClientVersion=${{parameters.nugetPackageVersion }} -p:platform=${{parameters.platform }} -p:Configuration=${{parameters.buildConfiguration }} -p:Filter="category=flaky"' + arguments: '-t:RunManualTests -p:TF=${{parameters.targetFramework }} -p:TestSet=${{parameters.testSet }} -p:ReferenceType=${{parameters.referenceType }} -p:MdsPackageVersion=${{parameters.mdsPackageVersion }} -p:platform=${{parameters.platform }} -p:Configuration=${{parameters.buildConfiguration }} -p:Filter="category=flaky"' verbosityRestore: Detailed verbosityPack: Detailed continueOnError: true diff --git a/eng/pipelines/common/templates/steps/update-config-file-step.yml b/eng/pipelines/common/templates/steps/update-config-file-step.yml index f49e552323..2bb7426be2 100644 --- a/eng/pipelines/common/templates/steps/update-config-file-step.yml +++ b/eng/pipelines/common/templates/steps/update-config-file-step.yml @@ -124,6 +124,10 @@ parameters: type: boolean default: true + - name: WorkloadIdentityFederationServiceConnectionId + type: string + default: '' + steps: # All properties should be added here, and this template should be used for any manipulation of the config.json file. - pwsh: | @@ -180,6 +184,7 @@ steps: $p.IsDNSCachingSupportedCR=[System.Convert]::ToBoolean("${{parameters.IsDNSCachingSupportedCR }}") $p.TracingEnabled=[System.Convert]::ToBoolean("${{parameters.TracingEnabled }}") $p.EnclaveEnabled=[System.Convert]::ToBoolean("${{parameters.EnclaveEnabled }}") + $p.WorkloadIdentityFederationServiceConnectionId="${{parameters.WorkloadIdentityFederationServiceConnectionId }}" } $jdata | ConvertTo-Json | Set-Content "config.json" workingDirectory: src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities @@ -196,4 +201,4 @@ steps: } } workingDirectory: src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities - displayName: 'Read config.json [debug]' + displayName: '[Debug] Read config.json' diff --git a/eng/pipelines/common/templates/steps/update-nuget-config-local-feed-step.yml b/eng/pipelines/common/templates/steps/update-nuget-config-local-feed-step.yml index 4eac341108..ac2c517aee 100644 --- a/eng/pipelines/common/templates/steps/update-nuget-config-local-feed-step.yml +++ b/eng/pipelines/common/templates/steps/update-nuget-config-local-feed-step.yml @@ -8,76 +8,56 @@ parameters: type: boolean default: false - - name: downloadedNugetPath # path to the downloaded nuget files + # The path to add as a NuGet source. + - name: packagePath type: string - - name: nugetPackageVersion - type: string - default: $(NugetPackageVersion) - steps: -- powershell: | +- pwsh: | + # Resolve the path to an absolute path. + # + # Resolve-Path throws an error if the path doesn't exist yet, which is an + # expected scenario here, so we must dance around it. + $PackagePath = Resolve-Path "${{ parameters.packagePath }}" -ErrorAction SilentlyContinue -ErrorVariable resolveError + if (-Not ($PackagePath)) + { + $PackagePath = $resolveError[0].TargetObject + } + + # Ensure the package path exists, for example if we are going to generate + # packages in a later step. + if (-Not (Test-Path -Path "$PackagePath")) + { + New-Item -ItemType Directory -Path "$PackagePath" -Force + Write-Host "Created package path: $PackagePath" + } + + Write-Host "Adding package path: $PackagePath" + # Get a list of package sources available Get-PackageSource - #Current location + # Current location Get-Location # Register the local nuget folder to be used by nuget.config - Register-PackageSource -Name "Package Source" -Location ${{parameters.downloadedNugetPath }} -Force -ProviderName NuGet -Trusted + Register-PackageSource -Name "Pipeline Source" -Location "$PackagePath" -Force -ProviderName NuGet -Trusted # Get a list of package sources available after the change Get-PackageSource - #Set the NuGet.config file in the project to use extracted package + # Set the NuGet.config file in the project to use extracted package $rootFolder = Get-location [Xml] $nugetConfig = Get-Content -Path "NuGet.config" - $Value = Resolve-Path ${{parameters.downloadedNugetPath }} $newAdd = $nugetConfig.CreateElement("add") - $newAdd.SetAttribute("key","Package source") - $newAdd.SetAttribute("value", "$Value/" ) + $newAdd.SetAttribute("key","pipeline_source") + $newAdd.SetAttribute("value", "$PackagePath" ) $nugetConfig.configuration.packageSources.AppendChild($newAdd) $nugetConfig.Save("$rootFolder/NuGet.config") - displayName: 'Update NuGet config file to read from Nuget folder' + displayName: 'Add source to NuGet.config' - ${{ if parameters.debug }}: - - powershell: | + - pwsh: | # Display the content of the NuGet.config file Get-Content -Path "NuGet.config" displayName: 'Read NuGet.config [debug]' - -- task: DotNetCoreCLI@2 - displayName: 'Restore NuGets' - inputs: - command: 'custom' - custom: 'msbuild' - arguments: 'build.proj -t:restore' - feedsToUse: 'select' - -- powershell: | - $Doc = [xml](Get-Content "./Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.csproj") - $parent_xpath = '/Project/ItemGroup/ProjectReference' - $node = $Doc.SelectSingleNode($parent_xpath) - $parentNode = $node.ParentNode - while($node -ne $null) { - $node.ParentNode.RemoveChild($node) - $node = $Doc.SelectSingleNode($parent_xpath) - } - - $parent_xpath = '/Project/ItemGroup/PackageReference[@Include="Microsoft.Data.SqlClient"]' - $node = $Doc.SelectSingleNode($parent_xpath) - - if($node -eq $null){ - $packagerefnode = $doc.createelement("packagereference") - $value = $doc.selectsinglenode('/project/itemgroup/projectreference') - $attrinclude = $doc.createattribute("include") - $attrinclude.value = "microsoft.data.sqlclient" - $packagerefnode.attributes.append($attrinclude) - $parentNode.AppendChild($packageRefNode) - } - - $currentFolder = Get-Location - $filePath = Join-Path $currentFolder "Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.csproj" - $Doc.Save($filePath) - workingDirectory: 'src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider' - displayName: 'Update AKV Project Ref to Package Ref (.NET Framework/Core)' diff --git a/eng/pipelines/dotnet-sqlclient-ci-core.yml b/eng/pipelines/dotnet-sqlclient-ci-core.yml index f1aaef6ec6..ca81d700e7 100644 --- a/eng/pipelines/dotnet-sqlclient-ci-core.yml +++ b/eng/pipelines/dotnet-sqlclient-ci-core.yml @@ -68,9 +68,12 @@ parameters: type: object default: [net462, net8.0, net9.0, net10.0] -- name: buildType - displayName: 'Build Type' - default: Project +# The way we will reference sibling projects in the .csproj files: +# Project - use references. +# Package - use references to NuGet packages in the +# packages/ directory. +- name: referenceType + displayName: 'Reference Type' values: - Project - Package @@ -100,72 +103,143 @@ parameters: type: boolean default: true +- name: dotnetVerbosity + displayName: dotnet CLI Verbosity + type: string + default: normal + values: + - quiet + - minimal + - normal + - detailed + - diagnostic + variables: - - template: libraries/ci-build-variables.yml@self + - template: /eng/pipelines/libraries/ci-build-variables.yml@self - - name: artifactName - value: Artifacts + - name: abstractionsArtifactName + value: Abstractions.Artifact + + - name: azureArtifactName + value: Azure.Artifact + + - name: mdsArtifactName + value: MDS.Artifact stages: - - stage: build_nugets - displayName: 'Build NuGet Packages' + + # 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: + abstractionsArtifactName: $(abstractionsArtifactName) + 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: common/templates/jobs/ci-build-nugets-job.yml@self + - template: /eng/pipelines/common/templates/jobs/ci-build-nugets-job.yml@self parameters: buildConfiguration: ${{ parameters.buildConfiguration }} - artifactName: $(artifactName) + referenceType: ${{ parameters.referenceType }} + abstractionsPackageVersion: $(abstractionsPackageVersion) + abstractionsArtifactName: $(abstractionsArtifactName) + mdsPackageVersion: $(mdsPackageVersion) + mdsArtifactName: $(mdsArtifactName) ${{if ne(parameters.SNIVersion, '')}}: prebuildSteps: - - template: common/templates/steps/override-sni-version.yml@self + - template: /eng/pipelines/common/templates/steps/override-sni-version.yml@self parameters: 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: + abstractionsArtifactName: $(abstractionsArtifactName) + abstractionsPackageVersion: $(abstractionsPackageVersion) + azureArtifactName: $(azureArtifactName) + azurePackageVersion: $(azurePackageVersion) + buildConfiguration: ${{ parameters.buildConfiguration }} + debug: ${{ parameters.debug }} + mdsArtifactName: $(mdsArtifactName) + mdsPackageVersion: $(mdsPackageVersion) + # When building via package references, we must depend on the Abstractions + # and MDS packages + ${{ if eq(parameters.referenceType, 'Package') }}: + dependsOn: + - build_abstractions_package_stage + - build_mds_akv_packages_stage + referenceType: ${{ parameters.referenceType }} + ${{if eq(parameters.debug, 'true')}}: + dotnetVerbosity: diagnostic + + # Run the stress tests, if desired. - ${{ if eq(parameters.enableStressTests, true) }}: - - template: stages/stress-tests-ci-stage.yml@self + - template: /eng/pipelines/stages/stress-tests-ci-stage.yml@self parameters: buildConfiguration: ${{ parameters.buildConfiguration }} - dependsOn: [build_nugets] + dependsOn: + - build_mds_akv_packages_stage + - build_azure_package_stage pipelineArtifactName: $(artifactName) - mdsPackageVersion: $(NugetPackageVersion) - ${{ if eq(parameters.debug, 'true') }}: - verbosity: 'detailed' - - - template: common/templates/stages/ci-run-tests-stage.yml@self + mdsPackageVersion: $(mdsPackageVersion) + azurePackageVersion: $(azurePackageVersion) + dotnetVerbosity: ${{ parameters.dotnetVerbosity }} + + # Run the MDS and AKV tests. + - template: /eng/pipelines/common/templates/stages/ci-run-tests-stage.yml@self parameters: debug: ${{ parameters.debug }} buildConfiguration: ${{ parameters.buildConfiguration }} - buildType: ${{ parameters.buildType }} + referenceType: ${{ parameters.referenceType }} + abstractionsArtifactName: $(abstractionsArtifactName) + abstractionsPackageVersion: $(abstractionsPackageVersion) + azureArtifactName: $(azureArtifactName) + azurePackageVersion: $(azurePackageVersion) + mdsArtifactName: $(mdsArtifactName) + mdsPackageVersion: $(mdsPackageVersion) testJobTimeout: ${{ parameters.testJobTimeout }} - ${{ if eq(parameters.buildType, 'Package') }}: - dependsOn: build_nugets - ${{if ne(parameters.SNIVersion, '')}}: - prebuildSteps: # steps to run prior to building and running tests on each job - - template: common/templates/steps/override-sni-version.yml@self + # When testing MDS via packages, we must depend on the Abstractions, + # Azure, and MDS packages. + ${{ if eq(parameters.referenceType, 'Package') }}: + dependsOn: + - build_abstractions_package_stage + - build_azure_package_stage + - build_mds_akv_packages_stage + + prebuildSteps: # steps to run prior to building and running tests on each job + - ${{if ne(parameters.SNIVersion, '')}}: + - template: /eng/pipelines/common/templates/steps/override-sni-version.yml@self parameters: SNIVersion: ${{parameters.SNIVersion}} SNIValidationFeed: ${{parameters.SNIValidationFeed}} - - template: common/templates/steps/ci-prebuild-step.yml@self - parameters: - debug: ${{ parameters.debug }} - artifactName: $(artifactName) - buildType: ${{ parameters.buildType }} - buildConfiguration: ${{ parameters.buildConfiguration }} - ${{else}}: - prebuildSteps: # steps to run prior to building and running tests on each job - - template: common/templates/steps/ci-prebuild-step.yml@self - parameters: - debug: ${{ parameters.debug }} - artifactName: $(artifactName) - buildType: ${{ parameters.buildType }} - buildConfiguration: ${{ parameters.buildConfiguration }} - + + - template: /eng/pipelines/common/templates/steps/ci-prebuild-step.yml@self + parameters: + debug: ${{ parameters.debug }} + referenceType: ${{ parameters.referenceType }} + # Include the code coverage job if the build type is Project. - ${{ if eq(parameters.buildType, 'Project') }}: + ${{ if eq(parameters.referenceType, 'Project') }}: # Jobs to run as part of the tests stage, after the tests are done. postTestJobs: - - template: common/templates/jobs/ci-code-coverage-job.yml@self + - template: /eng/pipelines/common/templates/jobs/ci-code-coverage-job.yml@self parameters: debug: ${{ parameters.debug }} image: ADO-MMS22-CodeCov @@ -382,9 +456,8 @@ stages: # ways to detect if they were present) won't run consistently across forks and non-forks. ${{ if eq(variables['system.pullRequest.isFork'], 'False') }}: AADPasswordConnectionString: $(AAD_PASSWORD_CONN_STR) - AADServicePrincipalId: $(AADServicePrincipalId) - ${{ if eq(variables['system.pullRequest.isFork'], 'False') }}: AADServicePrincipalSecret: $(AADServicePrincipalSecret) + AADServicePrincipalId: $(AADServicePrincipalId) AzureKeyVaultUrl: $(AzureKeyVaultUrl) AzureKeyVaultTenantId: $(AzureKeyVaultTenantId) SupportsIntegratedSecurity: false @@ -412,9 +485,8 @@ stages: AADAuthorityURL: $(AADAuthorityURL) ${{ if eq(variables['system.pullRequest.isFork'], 'False') }}: AADPasswordConnectionString: $(AAD_PASSWORD_CONN_STR_eastus) - AADServicePrincipalId: $(AADServicePrincipalId) - ${{ if eq(variables['system.pullRequest.isFork'], 'False') }}: AADServicePrincipalSecret: $(AADServicePrincipalSecret) + AADServicePrincipalId: $(AADServicePrincipalId) AzureKeyVaultUrl: $(AzureKeyVaultUrl) AzureKeyVaultTenantId: $(AzureKeyVaultTenantId) SupportsIntegratedSecurity: false @@ -422,39 +494,6 @@ stages: LocalDbAppName: $(LocalDbAppName) LocalDbSharedInstanceName: $(LocalDbSharedInstanceName) - # Only run the AE tests if enable and if we have access to the necessary - # secrets. - ${{ if and(eq(parameters.runAlwaysEncryptedTests, true), eq(variables['system.pullRequest.isFork'], 'False')) }}: - windows_enclave_sql: - pool: ADO-CI-AE-1ES-Pool - images: - Win22_Enclave_Sql19: ADO-MMS22-SQL19 - TargetFrameworks: ${{parameters.targetFrameworks }} - netcoreVersionTestUtils: ${{parameters.netcoreVersionTestUtils }} - buildPlatforms: ${{parameters.buildPlatforms }} - testSets: [AE] - useManagedSNI: ${{parameters.useManagedSNI }} - codeCovTargetFrameworks: ${{parameters.codeCovTargetFrameworks }} - configSqlFor: enclave - operatingSystem: Windows - configProperties: - # config.json properties - TCPConnectionStringHGSVBS: $(SQL_TCP_CONN_STRING_HGSVBS) - TCPConnectionStringNoneVBS: $(SQL_TCP_CONN_STRING_NoneVBS) - TCPConnectionStringAASSGX: $(SQL_TCP_CONN_STRING_AASSGX) - EnclaveEnabled: true - AADAuthorityURL: $(AADAuthorityURL) - AADServicePrincipalId: $(AADServicePrincipalId) - ${{ if eq(variables['system.pullRequest.isFork'], 'False') }}: - AADServicePrincipalSecret: $(AADServicePrincipalSecret) - AzureKeyVaultUrl: $(AzureKeyVaultUrl) - AzureKeyVaultTenantId: $(AzureKeyVaultTenantId) - SupportsIntegratedSecurity: $(SupportsIntegratedSecurity) - UserManagedIdentityClientId: $(UserManagedIdentityClientId) - AliasName: $(SQLAliasName) - LocalDbAppName: $(LocalDbAppName) - LocalDbSharedInstanceName: $(LocalDbSharedInstanceName) - # self hosted SQL Server on Linux linux_ub20_22_sql_22: pool: ${{parameters.defaultPoolName }} @@ -501,9 +540,8 @@ stages: AADAuthorityURL: $(AADAuthorityURL) ${{ if eq(variables['system.pullRequest.isFork'], 'False') }}: AADPasswordConnectionString: $(AAD_PASSWORD_CONN_STR) - AADServicePrincipalId: $(AADServicePrincipalId) - ${{ if eq(variables['system.pullRequest.isFork'], 'False') }}: AADServicePrincipalSecret: $(AADServicePrincipalSecret) + AADServicePrincipalId: $(AADServicePrincipalId) AzureKeyVaultUrl: $(AzureKeyVaultUrl) AzureKeyVaultTenantId: $(AzureKeyVaultTenantId) SupportsIntegratedSecurity: false @@ -511,9 +549,64 @@ stages: LocalDbAppName: $(LocalDbAppName) LocalDbSharedInstanceName: $(LocalDbSharedInstanceName) + # Self hosted SQL Server on Mac + mac_sql_22: + pool: Azure Pipelines + hostedPool: true + images: + MacOSLatest_Sql22: macos-latest + TargetFrameworks: ${{parameters.targetFrameworksUnix }} + netcoreVersionTestUtils: ${{parameters.netcoreVersionTestUtils }} + buildPlatforms: [AnyCPU] + testSets: ${{parameters.testSets }} + useManagedSNI: [true] + codeCovTargetFrameworks: ${{parameters.codeCovTargetFrameworks }} + configSqlFor: local + operatingSystem: Mac + configProperties: + # config.json properties + TCPConnectionString: $(SQL_TCP_CONN_STRING) + NPConnectionString: $(SQL_NP_CONN_STRING) + SupportsIntegratedSecurity: false + ManagedIdentitySupported: false + LocalDbAppName: $(LocalDbAppName) + LocalDbSharedInstanceName: $(LocalDbSharedInstanceName) + + # Enclave tests + # # Only run the AE tests if enable and if we have access to the necessary # secrets. ${{ if and(eq(parameters.runAlwaysEncryptedTests, true), eq(variables['system.pullRequest.isFork'], 'False')) }}: + windows_enclave_sql: + pool: ADO-CI-AE-1ES-Pool + images: + Win22_Enclave_Sql19: ADO-MMS22-SQL19 + TargetFrameworks: ${{parameters.targetFrameworks }} + netcoreVersionTestUtils: ${{parameters.netcoreVersionTestUtils }} + buildPlatforms: ${{parameters.buildPlatforms }} + testSets: [AE] + useManagedSNI: ${{parameters.useManagedSNI }} + codeCovTargetFrameworks: ${{parameters.codeCovTargetFrameworks }} + configSqlFor: enclave + operatingSystem: Windows + configProperties: + # config.json properties + TCPConnectionStringHGSVBS: $(SQL_TCP_CONN_STRING_HGSVBS) + TCPConnectionStringNoneVBS: $(SQL_TCP_CONN_STRING_NoneVBS) + TCPConnectionStringAASSGX: $(SQL_TCP_CONN_STRING_AASSGX) + EnclaveEnabled: true + AADAuthorityURL: $(AADAuthorityURL) + AADServicePrincipalId: $(AADServicePrincipalId) + ${{ if eq(variables['system.pullRequest.isFork'], 'False') }}: + AADServicePrincipalSecret: $(AADServicePrincipalSecret) + AzureKeyVaultUrl: $(AzureKeyVaultUrl) + AzureKeyVaultTenantId: $(AzureKeyVaultTenantId) + SupportsIntegratedSecurity: $(SupportsIntegratedSecurity) + UserManagedIdentityClientId: $(UserManagedIdentityClientId) + AliasName: $(SQLAliasName) + LocalDbAppName: $(LocalDbAppName) + LocalDbSharedInstanceName: $(LocalDbSharedInstanceName) + linux_enclave_sql: pool: ADO-CI-AE-1ES-Pool images: @@ -541,26 +634,3 @@ stages: UserManagedIdentityClientId: $(UserManagedIdentityClientId) LocalDbAppName: $(LocalDbAppName) LocalDbSharedInstanceName: $(LocalDbSharedInstanceName) - - # Self hosted SQL Server on Mac - mac_sql_22: - pool: Azure Pipelines - hostedPool: true - images: - MacOSLatest_Sql22: macos-latest - TargetFrameworks: ${{parameters.targetFrameworksUnix }} - netcoreVersionTestUtils: ${{parameters.netcoreVersionTestUtils }} - buildPlatforms: [AnyCPU] - testSets: ${{parameters.testSets }} - useManagedSNI: [true] - codeCovTargetFrameworks: ${{parameters.codeCovTargetFrameworks }} - configSqlFor: local - operatingSystem: Mac - configProperties: - # config.json properties - TCPConnectionString: $(SQL_TCP_CONN_STRING) - NPConnectionString: $(SQL_NP_CONN_STRING) - SupportsIntegratedSecurity: false - ManagedIdentitySupported: false - LocalDbAppName: $(LocalDbAppName) - LocalDbSharedInstanceName: $(LocalDbSharedInstanceName) diff --git a/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml b/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml index 81183d5ad2..3cf72c4bd5 100644 --- a/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml +++ b/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml @@ -168,14 +168,27 @@ parameters: type: object default: [false, true] + # Dotnet CLI verbosity level. + - name: dotnetVerbosity + displayName: dotnet CLI Verbosity + type: string + default: normal + values: + - quiet + - minimal + - normal + - detailed + - diagnostic + extends: - template: dotnet-sqlclient-ci-core.yml@self + template: /eng/pipelines/dotnet-sqlclient-ci-core.yml@self parameters: buildConfiguration: ${{ parameters.buildConfiguration }} buildPlatforms: ${{ parameters.buildPlatforms }} - buildType: Package + referenceType: Package codeCovTargetFrameworks: ${{ parameters.codeCovTargetFrameworks }} debug: ${{ parameters.debug }} + dotnetVerbosity: ${{ parameters.dotnetVerbosity }} enableStressTests: ${{ parameters.enableStressTests }} targetFrameworks: ${{ parameters.targetFrameworks }} targetFrameworksUnix: ${{ parameters.targetFrameworksUnix }} diff --git a/eng/pipelines/dotnet-sqlclient-ci-project-reference-pipeline.yml b/eng/pipelines/dotnet-sqlclient-ci-project-reference-pipeline.yml index 02ce475e9c..3a2adc3a49 100644 --- a/eng/pipelines/dotnet-sqlclient-ci-project-reference-pipeline.yml +++ b/eng/pipelines/dotnet-sqlclient-ci-project-reference-pipeline.yml @@ -168,14 +168,27 @@ parameters: type: object default: [false, true] + # Dotnet CLI verbosity level. + - name: dotnetVerbosity + displayName: dotnet CLI Verbosity + type: string + default: normal + values: + - quiet + - minimal + - normal + - detailed + - diagnostic + extends: - template: dotnet-sqlclient-ci-core.yml@self + template: /eng/pipelines/dotnet-sqlclient-ci-core.yml@self parameters: buildConfiguration: ${{ parameters.buildConfiguration }} buildPlatforms: ${{ parameters.buildPlatforms }} - buildType: Project + referenceType: Project codeCovTargetFrameworks: ${{ parameters.codeCovTargetFrameworks }} debug: ${{ parameters.debug }} + dotnetVerbosity: ${{ parameters.dotnetVerbosity }} enableStressTests: ${{ parameters.enableStressTests }} targetFrameworks: ${{ parameters.targetFrameworks }} targetFrameworksUnix: ${{ parameters.targetFrameworksUnix }} diff --git a/eng/pipelines/dotnet-sqlclient-signing-pipeline.yml b/eng/pipelines/dotnet-sqlclient-signing-pipeline.yml index 111412a0dd..66b46aadd2 100644 --- a/eng/pipelines/dotnet-sqlclient-signing-pipeline.yml +++ b/eng/pipelines/dotnet-sqlclient-signing-pipeline.yml @@ -8,6 +8,9 @@ name: $(Year:YY)$(DayOfYear)$(Rev:.r) trigger: branches: include: + + # This pipeline is intended to only run against the ADO dotnet-sqlclient + # repo. - internal/main paths: include: @@ -86,7 +89,7 @@ resources: ref: refs/heads/main extends: - template: v2/OneBranch.${{parameters.oneBranchType }}.CrossPlat.yml@templates # https://aka.ms/obpipelines/templates + template: /v2/OneBranch.${{parameters.oneBranchType }}.CrossPlat.yml@templates # https://aka.ms/obpipelines/templates parameters: featureFlags: # Suggested by MerlinBot (https://sqlclientdrivers.visualstudio.com/ADO.Net/_git/dotnet-sqlclient/pullrequest/4882) @@ -108,14 +111,14 @@ extends: softwareFolder: $(softwareFolder) symbolsFolder: $(symbolsFolder) softwarename: Microsoft.Data.SqlClient - versionNumber: $(AssemblyFileVersion) + versionNumber: $(mdsAssemblyFileVersion) codeql: compiled: enabled: ${{ not(parameters['isPreview']) }} sbom: enabled: ${{ not(parameters['isPreview']) }} packageName: Microsoft.Data.SqlClient - packageVersion: $(NugetPackageVersion) + packageVersion: $(mdsPackageVersion) policheck: enabled: ${{ not(parameters['isPreview']) }} break: true # always break the build on policheck issues. You can disable it by setting to 'false' @@ -143,7 +146,7 @@ extends: - stage: buildMDS displayName: 'Build MDS' jobs: - - template: eng/pipelines/common/templates/jobs/build-signed-package-job.yml@self + - template: /eng/pipelines/common/templates/jobs/build-signed-package-job.yml@self parameters: symbolsFolder: $(symbolsFolder) softwareFolder: $(softwareFolder) @@ -154,7 +157,7 @@ extends: displayName: 'MDS Package Validation' dependsOn: buildMDS jobs: - - template: eng/pipelines/common/templates/jobs/validate-signed-package-job.yml@self + - template: /eng/pipelines/common/templates/jobs/validate-signed-package-job.yml@self parameters: packageFolderName: $(packageFolderName) isPreview: ${{ parameters['isPreview'] }} @@ -165,7 +168,7 @@ extends: displayName: 'Download NuGet Package' # Disabling as of 10/15/2025 due to OneBranch apparently disallowing MSBuild tasks in validation stages. -# - template: eng/pipelines/common/templates/jobs/run-tests-package-reference-job.yml@self +# - template: /eng/pipelines/common/templates/jobs/run-tests-package-reference-job.yml@self # parameters: # packageFolderName: $(packageFolderName) # isPreview: ${{ parameters['isPreview'] }} diff --git a/eng/pipelines/jobs/build-akv-official-job.yml b/eng/pipelines/jobs/build-akv-official-job.yml index 5ce8376845..5b2f15d3f4 100644 --- a/eng/pipelines/jobs/build-akv-official-job.yml +++ b/eng/pipelines/jobs/build-akv-official-job.yml @@ -5,14 +5,17 @@ ################################################################################# parameters: + - name: akvAssemblyFileVersion + type: string + + - name: akvPackageVersion + type: string + - name: apiScanDllPath type: string - name: apiScanPdbPath type: string - - - name: assemblyFileVersion - type: string - name: buildConfiguration type: string @@ -20,9 +23,6 @@ parameters: - Debug - Release - - name: nugetPackageVersion - type: string - - name: mdsPackageVersion type: string @@ -82,21 +82,30 @@ jobs: ob_outputDirectory: '$(ARTIFACT_PATH)' steps: - - template: ../steps/script-output-environment-variables-step.yml@self + - template: /eng/pipelines/steps/script-output-environment-variables-step.yml@self - powershell: | $jsonParams = '${{ convertToJson(parameters) }}' -replace '\\', '\\' $jsonParams | ConvertFrom-Json | Format-List displayName: 'Output Job Parameters' - - template: ../steps/compound-build-akv-step.yml@self + # Perform analysis before building, since this step will clobber build + # output. + - template: /eng/pipelines/steps/roslyn-analyzers-akv-step.yml@self parameters: - assemblyFileVersion: '${{ parameters.assemblyFileVersion }}' + akvPackageVersion: '${{ parameters.akvPackageVersion }}' + buildConfiguration: '${{ parameters.buildConfiguration }}' + mdsPackageVersion: '${{ parameters.mdsPackageVersion }}' + + - template: /eng/pipelines/steps/compound-build-akv-step.yml@self + parameters: + akvAssemblyFileVersion: '${{ parameters.akvAssemblyFileVersion }}' + akvPackageVersion: '${{ parameters.akvPackageVersion }}' buildConfiguration: '${{ parameters.buildConfiguration }}' mdsPackageVersion: '${{ parameters.mdsPackageVersion }}' - ${{ each targetFramework in parameters.targetFrameworks }}: - - template: ../steps/compound-extract-akv-apiscan-files-step.yml + - template: /eng/pipelines/steps/compound-extract-akv-apiscan-files-step.yml@self parameters: buildConfiguration: '${{ parameters.buildConfiguration }}' dllPath: '${{ parameters.apiScanDllPath }}' @@ -104,12 +113,7 @@ jobs: referenceType: Package targetFramework: '${{ targetFramework }}' - - template: ../steps/roslyn-analyzers-akv-step.yml@self - parameters: - buildConfiguration: '${{ parameters.buildConfiguration }}' - mdsPackageVersion: '${{ parameters.mdsPackageVersion }}' - - - template: ../steps/compound-esrp-code-signing-step.yml@self + - template: /eng/pipelines/steps/compound-esrp-code-signing-step.yml@self parameters: appRegistrationClientId: '${{ parameters.signingAppRegistrationClientId }}' appRegistrationTenantId: '${{ parameters.signingAppRegistrationTenantId }}' @@ -119,16 +123,17 @@ jobs: esrpClientId: '${{ parameters.signingEsrpClientId }}' esrpConnectedServiceName: '${{ parameters.signingEsrpConnectedServiceName }}' - - template: ../steps/compound-nuget-pack-step.yml@self + - template: /eng/pipelines/steps/compound-nuget-pack-step.yml@self parameters: buildConfiguration: '${{ parameters.buildConfiguration }}' generateSymbolsPackage: true # Always generate symbols, even if they are not published - packageVersion: '${{ parameters.nugetPackageVersion }}' + packageVersion: '${{ parameters.akvPackageVersion }}' nuspecPath: '$(REPO_ROOT)/tools/specs/add-ons/$(PACKAGE_NAME).nuspec' outputDirectory: '$(ARTIFACT_PATH)' - referenceType: 'Package' + referenceType: Package + properties: MdsPackageVersion=${{ parameters.mdsPackageVersion }} - - template: ../steps/compound-esrp-code-signing-step.yml@self + - template: /eng/pipelines/steps/compound-esrp-code-signing-step.yml@self parameters: appRegistrationClientId: '${{ parameters.signingAppRegistrationClientId }}' appRegistrationTenantId: '${{ parameters.signingAppRegistrationTenantId }}' @@ -139,9 +144,9 @@ jobs: esrpConnectedServiceName: '${{ parameters.signingEsrpConnectedServiceName }}' - ${{ if parameters.publishSymbols }}: - - template: ../steps/compound-publish-symbols-step.yml@self + - template: /eng/pipelines/steps/compound-publish-symbols-step.yml@self parameters: - artifactName: 'akv_symbols_$(System.TeamProject)_$(Build.Repository.Name)_$(Build.SourceBranchName)_${{ parameters.nugetPackageVersion }}_$(System.TimelineId)' + artifactName: 'akv_symbols_$(System.TeamProject)_$(Build.Repository.Name)_$(Build.SourceBranchName)_${{ parameters.akvPackageVersion }}_$(System.TimelineId)' azureSubscription: '${{ parameters.symbolsAzureSubscription }}' publishProjectName: '${{ parameters.symbolsPublishProjectName }}' packageName: '$(PACKAGE_NAME)' @@ -154,4 +159,4 @@ jobs: Windows_NT/${{ parameters.buildConfiguration }}.AnyCPU/AzureKeyVaultProvider/**/$(PACKAGE_NAME).pdb AnyOS/${{ parameters.buildConfiguration }}.AnyCPU/AzureKeyVaultProvider/**/$(PACKAGE_NAME).pdb uploadAccount: '${{ parameters.symbolsUploadAccount }}' - version: '${{ parameters.nugetPackageVersion }}' + version: '${{ parameters.akvPackageVersion }}' 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..4fd0d8d73d --- /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 artifact. + - name: abstractionsArtifactName + 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.abstractionsArtifactName }} + 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..837fcc5409 --- /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 artifact to download. + # + # This is used when the referenceType is 'Package'. + - name: abstractionsArtifactName + type: string + default: Abstractions.Artifact + + # The Abstractions package verion to depend on. + # + # This is used when the referenceType is 'Package'. + - name: abstractionsPackageVersion + type: string + + # The name of the pipeline artifact to publish. + - name: azureArtifactName + type: string + default: Azure.Artifact + + # The version to apply to the NuGet package and DLLs. + - name: azurePackageVersion + type: string + + # The type of build to test (Release or Debug) + - name: buildConfiguration + type: string + values: + - Release + - Debug + + # 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 Artifact + inputs: + artifactName: ${{ parameters.abstractionsArtifactName }} + targetPath: $(Build.SourcesDirectory)/packages + + # Use the local NuGet.config that references the packages/ directory. + - pwsh: cp $(Build.SourcesDirectory)/NuGet.config.local $(Build.SourcesDirectory)/NuGet.config + displayName: Use local NuGet.config + + # Install the .NET 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/stress-tests-ci-job.yml b/eng/pipelines/jobs/stress-tests-ci-job.yml index 5683382597..ccdf3378c4 100644 --- a/eng/pipelines/jobs/stress-tests-ci-job.yml +++ b/eng/pipelines/jobs/stress-tests-ci-job.yml @@ -4,7 +4,7 @@ # file in the project root for more information. ################################################################################ -# This stage builds and runs stress tests against an MDS NuGet package available +# This job builds and runs stress tests against an MDS NuGet package available # as a pipeline artifact. # # The stress tests are located here: 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..288030f3b8 --- /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 artifact to download. + # + # This is used when the referenceType is 'Package'. + - name: abstractionsArtifactName + type: string + default: Abstractions.Artifact + + # The Abstractions package verion to depend on. + # + # This is used when the referenceType is 'Package'. + - name: abstractionsPackageVersion + type: string + + # The type of build to test (Release or Debug) + - name: buildConfiguration + type: string + values: + - Release + - Debug + + # 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 artifact to download. + # + # This is used when the referenceType is 'Package'. + - name: mdsArtifactName + type: string + default: MDS.Artifact + + # 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 Artifact + inputs: + artifactName: ${{ parameters.abstractionsArtifactName }} + 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 Artifact + inputs: + artifactName: ${{ parameters.mdsArtifactName }} + 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 3dd87fe7d7..a5b9fc99c4 100644 --- a/eng/pipelines/libraries/build-variables.yml +++ b/eng/pipelines/libraries/build-variables.yml @@ -5,5 +5,5 @@ ################################################################################# variables: - - template: common-variables.yml@self - - template: mds-variables.yml@self + - 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 0d9154da16..f561c922b9 100644 --- a/eng/pipelines/libraries/ci-build-variables.yml +++ b/eng/pipelines/libraries/ci-build-variables.yml @@ -6,15 +6,20 @@ variables: - group: 'ADO Build properties' - - group: 'ADO CI Packaging' - group: 'ADO Test Configuration Properties' - name: buildNumber value: '$(Build.BuildNumber)' - name: SQLTarget value: 'localhost' - - name: NugetPackageVersion - value: $(Major).$(Minor)$(Patch)-pull.1$(buildnumber) + - 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 value: true - name: runCodesignValidationInjection diff --git a/eng/pipelines/libraries/common-variables.yml b/eng/pipelines/libraries/common-variables.yml index 9dd1726c44..7ced0c28d2 100644 --- a/eng/pipelines/libraries/common-variables.yml +++ b/eng/pipelines/libraries/common-variables.yml @@ -24,6 +24,39 @@ variables: - name: artifactDirectory value: '$(REPOROOT)/packages' + # C# assembly versions must be in the format: Major.Minor.Build.Revision, but + # $(Build.BuildNumber) has the format XXX.YY. Additionally, each version part + # must be a positive 16-bit integer less than 65535. Simply concatenating + # both parts of $(Build.BuildNumber) could produce values larger than 65534, + # so we must omit the second part entirely. Unfortunately, this may result + # in multiple subsequent pipline builds using the same C# assembly versions. + # The package versions will not be affected and will show the complete + # $(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 + # Update this after every release. This is used to generate the MDS NuGet package version. - name: Major value: '7' @@ -32,17 +65,17 @@ variables: - name: Patch value: '0' - # Update this for preview releases. + # Update this for preview releases. - name: Preview value: '-preview' - name: Revision value: '3' - - name: NugetPackageVersion + - name: mdsPackageVersion value: $(Major).$(Minor).$(Patch) - - name: PreviewNugetPackageVersion + - name: previewMdsPackageVersion value: $(Major).$(Minor).$(Patch)$(Preview)$(Revision).$(Build.BuildNumber) - - name: AssemblyFileVersion - value: '$(Major).$(Minor)$(Patch).$(Build.BuildNumber)' + - name: mdsAssemblyFileVersion + value: $(Major).$(Minor).$(Patch).$(assemblyBuildNumber) - name: nuspecPath value: '$(REPOROOT)/tools/specs/Microsoft.Data.SqlClient.nuspec' diff --git a/eng/pipelines/libraries/mds-validation-variables.yml b/eng/pipelines/libraries/mds-validation-variables.yml index d7723a059f..f465c9c390 100644 --- a/eng/pipelines/libraries/mds-validation-variables.yml +++ b/eng/pipelines/libraries/mds-validation-variables.yml @@ -5,15 +5,15 @@ ################################################################################# variables: - - template: common-variables.yml@self - - template: mds-variables.yml@self + - template: /eng/pipelines/libraries/common-variables.yml@self + - template: /eng/pipelines/libraries/mds-variables.yml@self - name: TempFolderName # extract the nuget package here value: temp - name: extractedNugetRootPath value: $(Build.SourcesDirectory)\$(TempFolderName)\Microsoft.Data.SqlClient - name: extractedNugetPath - value: $(extractedNugetRootPath).$(NugetPackageVersion) + value: $(extractedNugetRootPath).$(mdsPackageVersion) - name: expectedFolderNames value: lib,ref,runtimes - name: expectedDotnetVersions diff --git a/eng/pipelines/libraries/variables.yml b/eng/pipelines/libraries/variables.yml index 57894459d3..03cf141318 100644 --- a/eng/pipelines/libraries/variables.yml +++ b/eng/pipelines/libraries/variables.yml @@ -5,7 +5,7 @@ ################################################################################# variables: - - template: build-variables.yml@self + - template: /eng/pipelines/libraries/build-variables.yml@self # onebranch template variables - name: ob_outputDirectory value: '$(artifactDirectory)' # this directory is uploaded to pipeline artifacts, reddog and cloudvault. More info at https://aka.ms/obpipelines/artifacts diff --git a/eng/pipelines/sqlclient-pr-package-ref-pipeline.yml b/eng/pipelines/sqlclient-pr-package-ref-pipeline.yml index 1d45844a26..e58a1e1e85 100644 --- a/eng/pipelines/sqlclient-pr-package-ref-pipeline.yml +++ b/eng/pipelines/sqlclient-pr-package-ref-pipeline.yml @@ -124,11 +124,11 @@ parameters: default: [false, true] extends: - template: dotnet-sqlclient-ci-core.yml@self + template: /eng/pipelines/dotnet-sqlclient-ci-core.yml@self parameters: buildConfiguration: ${{ parameters.buildConfiguration }} buildPlatforms: ${{ parameters.buildPlatforms }} - buildType: Package + referenceType: Package codeCovTargetFrameworks: ${{ parameters.codeCovTargetFrameworks }} debug: ${{ parameters.debug }} enableStressTests: ${{ parameters.enableStressTests }} diff --git a/eng/pipelines/sqlclient-pr-project-ref-pipeline.yml b/eng/pipelines/sqlclient-pr-project-ref-pipeline.yml index 96321781b8..083a372867 100644 --- a/eng/pipelines/sqlclient-pr-project-ref-pipeline.yml +++ b/eng/pipelines/sqlclient-pr-project-ref-pipeline.yml @@ -124,11 +124,11 @@ parameters: default: [false, true] extends: - template: dotnet-sqlclient-ci-core.yml@self + template: /eng/pipelines/dotnet-sqlclient-ci-core.yml@self parameters: buildConfiguration: ${{ parameters.buildConfiguration }} buildPlatforms: ${{ parameters.buildPlatforms }} - buildType: Project + referenceType: Project codeCovTargetFrameworks: ${{ parameters.codeCovTargetFrameworks }} debug: ${{ parameters.debug }} enableStressTests: ${{ parameters.enableStressTests }} 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..1f28d01a8e --- /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 artifact to publish. + - name: abstractionsArtifactName + type: string + default: Abstractions.Artifact + + # 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: + abstractionsArtifactName: ${{ parameters.abstractionsArtifactName }} + 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..35504b2c85 --- /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 artifact to download. + # + # This is used when the referenceType is 'Package'. + - name: abstractionsArtifactName + type: string + default: Abstractions.Artifact + + # The Abstractions package verion to depend on. + # + # This is used when the referenceType is 'Package'. + - name: abstractionsPackageVersion + type: string + + # The name of the 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 artifact to publish. + - name: azureArtifactName + type: string + default: Azure.Artifact + + # The version to apply to the NuGet package and DLLs. + - name: azurePackageVersion + type: string + + # The 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 artifact to download. + # + # This is used when the referenceType is 'Package'. + - name: mdsArtifactName + type: string + default: MDS.Artifact + + # 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: + abstractionsArtifactName: ${{ parameters.abstractionsArtifactName }} + abstractionsPackageVersion: ${{ parameters.abstractionsPackageVersion }} + buildConfiguration: ${{ parameters.buildConfiguration }} + debug: ${{ parameters.debug }} + displayNamePrefix: Linux + dotnetVerbosity: ${{ parameters.dotnetVerbosity }} + jobNameSuffix: linux + mdsArtifactName: MDS.Artifact + 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: + abstractionsArtifactName: ${{ parameters.abstractionsArtifactName }} + abstractionsPackageVersion: ${{ parameters.abstractionsPackageVersion }} + buildConfiguration: ${{ parameters.buildConfiguration }} + debug: ${{ parameters.debug }} + displayNamePrefix: Linux Integration + dotnetVerbosity: ${{ parameters.dotnetVerbosity }} + jobNameSuffix: linux_integration + mdsArtifactName: MDS.Artifact + 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: + abstractionsArtifactName: ${{ parameters.abstractionsArtifactName }} + abstractionsPackageVersion: ${{ parameters.abstractionsPackageVersion }} + buildConfiguration: ${{ parameters.buildConfiguration }} + debug: ${{ parameters.debug }} + displayNamePrefix: Win + dotnetVerbosity: ${{ parameters.dotnetVerbosity }} + jobNameSuffix: windows + mdsArtifactName: MDS.Artifact + 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: + abstractionsArtifactName: ${{ parameters.abstractionsArtifactName }} + abstractionsPackageVersion: ${{ parameters.abstractionsPackageVersion }} + buildConfiguration: ${{ parameters.buildConfiguration }} + debug: ${{ parameters.debug }} + displayNamePrefix: Win Integration + dotnetVerbosity: ${{ parameters.dotnetVerbosity }} + jobNameSuffix: windows_integration + mdsArtifactName: MDS.Artifact + 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: + abstractionsArtifactName: ${{ parameters.abstractionsArtifactName }} + abstractionsPackageVersion: ${{ parameters.abstractionsPackageVersion }} + buildConfiguration: ${{ parameters.buildConfiguration }} + debug: ${{ parameters.debug }} + displayNamePrefix: macOS + dotnetVerbosity: ${{ parameters.dotnetVerbosity }} + jobNameSuffix: macos + mdsArtifactName: MDS.Artifact + 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: + abstractionsArtifactName: ${{ parameters.abstractionsArtifactName }} + abstractionsPackageVersion: ${{ parameters.abstractionsPackageVersion }} + azureArtifactName: ${{ parameters.azureArtifactName }} + 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/stages/stress-tests-ci-stage.yml b/eng/pipelines/stages/stress-tests-ci-stage.yml index 1a2ace53c0..e60f5a7a38 100644 --- a/eng/pipelines/stages/stress-tests-ci-stage.yml +++ b/eng/pipelines/stages/stress-tests-ci-stage.yml @@ -20,6 +20,14 @@ # depended on by downstream stages. parameters: + + # The Azure package version to stress test. This version must be available in + # one of the configured NuGet sources. + - name: azurePackageVersion + displayName: Azure Package Version + type: string + default: '' + # The type of build to produce (Debug or Release) - name: buildConfiguration displayName: Build Configuration @@ -36,12 +44,17 @@ parameters: type: object default: [] - # The name of the pipeline artifact to download that contains the MDS package - # to stress test. - - name: pipelineArtifactName - displayName: Pipeline Artifact Name + # The verbosity level for the dotnet CLI commands. + - name: dotnetVerbosity + displayName: dotnet CLI verbosity type: string - default: Artifacts + default: normal + values: + - quiet + - minimal + - normal + - detailed + - diagnostic # The MDS package version to stress test. This version must be available in # one of the configured NuGet sources. @@ -50,29 +63,24 @@ parameters: type: string default: '' + # The list of .NET Framework runtimes to test against. + - name: netFrameworkTestRuntimes + displayName: .NET Framework Test Runtimes + type: object + default: [net462, net47, net471, net472, net48, net481] + # The list of .NET runtimes to test against. - name: netTestRuntimes displayName: .NET Test Runtimes type: object default: [net8.0, net9.0] - # The list of .NET Framework runtimes to test against. - - name: netFrameworkTestRuntimes - displayName: .NET Framework Test Runtimes - type: object - default: [net462] - - # The verbosity level for the dotnet CLI commands. - - name: verbosity - displayName: Dotnet CLI verbosity + # The name of the pipeline artifact to download that contains the MDS package + # to stress test. + - name: pipelineArtifactName + displayName: Pipeline Artifact Name type: string - default: normal - values: - - quiet - - minimal - - normal - - detailed - - diagnostic + default: Artifacts stages: - stage: run_stress_tests_stage @@ -96,9 +104,10 @@ stages: # dotnet CLI arguments common to all commands. - name: commonArguments value: >- - --verbosity ${{parameters.verbosity}} + --verbosity ${{parameters.dotnetVerbosity}} --artifacts-path $(dotnetArtifactsDir) -p:MdsPackageVersion=${{parameters.mdsPackageVersion}} + -p:AzurePackageVersion=${{parameters.azurePackageVersion}} # dotnet CLI arguments for build/run commands. - name: buildArguments @@ -132,7 +141,7 @@ stages: # -------------------------------------------------------------------------- # Build and test on Linux. - - template: ../jobs/stress-tests-ci-job.yml@self + - template: /eng/pipelines/jobs/stress-tests-ci-job.yml@self parameters: jobNameSuffix: linux displayNamePrefix: Linux @@ -150,7 +159,7 @@ stages: # -------------------------------------------------------------------------- # Build and test on Windows - - template: ../jobs/stress-tests-ci-job.yml + - template: /eng/pipelines/jobs/stress-tests-ci-job.yml@self parameters: jobNameSuffix: windows displayNamePrefix: Win @@ -173,7 +182,7 @@ stages: # -------------------------------------------------------------------------- # Build and test on macOS. - - template: ../jobs/stress-tests-ci-job.yml + - template: /eng/pipelines/jobs/stress-tests-ci-job.yml@self parameters: jobNameSuffix: macos displayNamePrefix: macOS diff --git a/eng/pipelines/steps/compound-build-akv-step.yml b/eng/pipelines/steps/compound-build-akv-step.yml index ce8da41d52..7a09eb3f2c 100644 --- a/eng/pipelines/steps/compound-build-akv-step.yml +++ b/eng/pipelines/steps/compound-build-akv-step.yml @@ -7,10 +7,13 @@ # @TODO: This can probably be made generic and pass in the command lines for msbuild # BUT, they should be kept separate by now as we rebuild build.proj in parallel, we won't # affect >1 project at a time. -# @TODO: NugetPackageVersion should not be used for MDS package version +# @TODO: mdsPackageVersion should not be used for MDS package version parameters: - - name: assemblyFileVersion + - name: akvAssemblyFileVersion + type: string + + - name: akvPackageVersion type: string - name: buildConfiguration @@ -39,8 +42,9 @@ steps: configuration: '${{ parameters.buildConfiguration }}' msbuildArguments: >- -t:BuildAkv - -p:AssemblyFileVersion=${{ parameters.assemblyFileVersion }} - -p:NugetPackageVersion=${{ parameters.mdsPackageVersion }} + -p:AssemblyFileVersion=${{ parameters.akvAssemblyFileVersion }} + -p:AkvPackageVersion=${{ parameters.akvPackageVersion }} + -p:MdsPackageVersion=${{ parameters.mdsPackageVersion }} -p:ReferenceType=Package -p:SigningKeyPath=$(Agent.TempDirectory)/netfxKeypair.snk diff --git a/eng/pipelines/steps/compound-nuget-pack-step.yml b/eng/pipelines/steps/compound-nuget-pack-step.yml index b07252e29d..41905c8483 100644 --- a/eng/pipelines/steps/compound-nuget-pack-step.yml +++ b/eng/pipelines/steps/compound-nuget-pack-step.yml @@ -26,8 +26,14 @@ parameters: - name: referenceType type: string values: - - Package - - Project + - Package + - Project + + # Semi-colon separated properties to pass to nuget via the -properties + # argument. + - name: properties + type: string + default: '' steps: # This tool is failing on OneBranch pipelines, possibly due to new @@ -63,7 +69,7 @@ steps: -SymbolPackageFormat snupkg -Version ${{ parameters.packageVersion }} -OutputDirectory ${{ parameters.outputDirectory }} - -Properties "COMMITID=$(Build.SourceVersion);Configuration=${{ parameters.buildConfiguration }};ReferenceType=${{ parameters.referenceType }}" + -Properties "COMMITID=$(Build.SourceVersion);Configuration=${{ parameters.buildConfiguration }};ReferenceType=${{ parameters.referenceType }};${{ parameters.properties }}" - ${{ else }}: - task: NuGetCommand@2 displayName: 'Generate NuGet Package' @@ -74,4 +80,4 @@ steps: ${{ parameters.nuspecPath }} -Version ${{ parameters.packageVersion }} -OutputDirectory ${{ parameters.outputDirectory }} - -Properties "COMMITID=$(Build.SourceVersion);Configuration=${{ parameters.buildConfiguration }};ReferenceType=${{ parameters.referenceType }}" + -Properties "COMMITID=$(Build.SourceVersion);Configuration=${{ parameters.buildConfiguration }};ReferenceType=${{ parameters.referenceType }};${{ parameters.properties }}" diff --git a/eng/pipelines/steps/roslyn-analyzers-akv-step.yml b/eng/pipelines/steps/roslyn-analyzers-akv-step.yml index ebb6ff0801..8b2b4bd331 100644 --- a/eng/pipelines/steps/roslyn-analyzers-akv-step.yml +++ b/eng/pipelines/steps/roslyn-analyzers-akv-step.yml @@ -4,11 +4,23 @@ # See the LICENSE file in the project root for more information. # ################################################################################# +# This template defines a step to run Roslyn Analyzers on the AKV project build. +# It uses the RoslynAnalyzers@3 task from the Secure Development Team's SDL +# extension: +# +# https://eng.ms/docs/cloud-ai-platform/devdiv/one-engineering-system-1es/1es-mohanb/security-integration/guardian-wiki/sdl-azdo-extension/roslyn-analyzers-build-task +# +# GOTCHA: This step will clobber any existing build output. It should be run +# _before_ any build steps that perform versioning or signing. + # @TODO: This can probably be made generic and pass in the command lines for msbuild # BUT, they should be kept separate by now as we rebuild build.proj in parallel, we won't # affect >1 project at a time. parameters: + - name: akvPackageVersion + type: string + - name: buildConfiguration type: string values: @@ -28,7 +40,8 @@ steps: $(REPO_ROOT)/build.proj -t:BuildAkv -p:Configuration=${{ parameters.buildConfiguration }} - -p:NugetPackageVersion=${{ parameters.mdsPackageVersion }} + -p:AkvPackageVersion=${{ parameters.akvPackageVersion }} + -p:MdsPackageVersion=${{ parameters.mdsPackageVersion }} -p:ReferenceType=Package msBuildVersion: 17.0 setupCommandLinePicker: vs2022 diff --git a/eng/pipelines/variables/akv-official-variables.yml b/eng/pipelines/variables/akv-official-variables.yml index aaf8de7c5e..591a562686 100644 --- a/eng/pipelines/variables/akv-official-variables.yml +++ b/eng/pipelines/variables/akv-official-variables.yml @@ -35,9 +35,7 @@ variables: value: '-preview1' # Compound Variables --------------------------------------------------- - - name: assemblyFileVersion + - name: akvAssemblyFileVersion value: '${{ variables.versionMajor }}.${{ variables.versionMinor }}${{ variables.versionPatch }}.$(Build.BuildNumber)' - - name: nugetPackageVersion + - name: akvPackageVersion value: '${{ variables.versionMajor }}.${{ variables.versionMinor }}.${{ variables.versionPatch }}${{ variables.versionPreview }}' - - diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 16535cf9a7..7a3e277e2a 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -12,15 +12,12 @@ - Project - $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + Project $([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)')) 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/doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationMethod.xml b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationMethod.xml similarity index 95% rename from doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationMethod.xml rename to src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationMethod.xml index cd15a65ec2..c804a8fb3a 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationMethod.xml +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationMethod.xml @@ -1,3 +1,8 @@ + diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationParameters.xml b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationParameters.xml similarity index 62% rename from doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationParameters.xml rename to src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationParameters.xml index 7c409cdc5c..087045b2c9 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationParameters.xml +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationParameters.xml @@ -1,4 +1,9 @@ - + + @@ -6,23 +11,34 @@ - One of the enumeration values that specifies the authentication method. + + Construct with values for all properties. + + The authentication method. The server name. The database name. The resource URI. The authority URI. - The user login name/ID. - The user password. + The user login name/ID, or null if not applicable. + The user password, or null if not applicable. The connection ID. - The connection timeout value in seconds. - - Initializes a new instance of the class using the specified authentication method, server name, database name, resource URI, authority URI, user login name/ID, user password, connection ID and connection timeout value. - + + The authentication timeout, in seconds. The overall connection timeout + is managed by the driver; this timeout only applies to authentication. + Gets the authentication method. The authentication method. + + Gets the server name. + The server name. + + + Gets the database name. + The database name. + The resource URIs. The resource URI. @@ -33,27 +49,18 @@ Gets the user login name/ID. - The user login name/ID. + The user login name/ID, or null if not applicable. Gets the user password. - The user password. + The user password, or null if not applicable. Gets the connection ID. The connection ID. - - Gets the server name. - The server name. - - - Gets the database name. - The database name. - - Gets the connection timeout value. - The connection timeout value to be passed to Cancellation Token Source. + Gets the authentication timeout value, in seconds. diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationProvider.xml b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationProvider.xml similarity index 92% rename from doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationProvider.xml rename to src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationProvider.xml index 75cc752d76..7848aaec1a 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationProvider.xml +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationProvider.xml @@ -1,4 +1,9 @@ - + + Defines the core behavior of authentication providers and provides a base class for derived classes. @@ -89,49 +94,50 @@ - - - Called from constructors in derived classes to initialize the class. - - - - The authentication method. - Gets an authentication provider by method. - The authentication provider or if not found. - - - The authentication method. - The authentication provider. - Sets an authentication provider by method. - - if the operation succeeded; otherwise, (for example, the existing provider disallows overriding). - - - The authentication method. This method is called immediately before the provider is added to the SQL authentication provider registry. Avoid performing long-waiting tasks in this method, since it can block other threads from accessing the provider registry. + + This method must not throw. + The authentication method. - The authentication method. This method is called immediately before the provider is removed from the SQL authentication provider registry. For example, this method is called when a different provider with the same authentication method overrides this provider in the SQL authentication provider registry. Avoid performing long-waiting task in this method, since it can block other threads from accessing the provider registry. + + This method must not throw. + The authentication method. - The authentication method. Indicates whether the specified authentication method is supported. + This method must not throw. if the specified authentication method is supported; otherwise, . + The authentication method. - The Active Directory authentication parameters passed by the driver to authentication providers. - Acquires a security token from the authority. + Acquires an access token from the authority. + The parameters passed to the provider by the driver. + If any errors occur. Represents an asynchronous operation that returns the AD authentication token. + + Gets an authentication provider by method. + The authentication method. + The authentication provider or if not found. + + + Sets an authentication provider by method. + The authentication method. + The authentication provider. + + if the operation succeeded; otherwise, (for example, the existing provider disallows overriding). + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationProviderException.xml b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationProviderException.xml new file mode 100644 index 0000000000..648e4a7e2f --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationProviderException.xml @@ -0,0 +1,82 @@ + + + + + + This exception is thrown for any errors that occur during the + authentication process. + + + + + Protected construction for derived classes to supply a minimal set of + values. + + Method will be NotSpecified. + FailureCode will be "Unknown". + ShouldRetry will be false. + RetryPeriod will be 0. + + The error message. + The exception that caused this exception, if any. + + + + Protected construction for derived classes to supply values for all + public properties. + + + The authentication method that failed, or NotSpecified if not known. + + + The failure code, or "Unknown" if not known. + + + True if the action should be retried, false otherwise. + + + The period of time, in milliseconds, to wait before retrying the action. + Specify 0 if no retry period is suggested. Ignored if negative. Not + used when ShouldRetry is false, in which cases 0 is assumed. + + + The error message. + + + The exception that caused this exception, if any. + + + + + The authentication method that failed, or NotSpecified if not known. + + + + + The failure code, or "Unknown" if not known. + + + + + True if the action should be retried, false otherwise. + + + + + The period of time, in milliseconds, to wait before retrying the action. + 0 if no retry period is suggested. Never negative. Always 0 when + ShouldRetry is false. + + + + + A string that includes the base exception information along with all + property values. + + + + diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationToken.xml b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationToken.xml similarity index 55% rename from doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationToken.xml rename to src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationToken.xml index 52f79dd535..4eb9a78f52 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationToken.xml +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/doc/SqlAuthenticationToken.xml @@ -1,25 +1,30 @@ - + + Represents an authentication token. - The access token. - The token expiration time. - Initializes a new instance of the class. + Construct with values for all properties. - - The parameter is or empty. - + The token string. + The token expiration time. + + is null or empty. + - - Gets the token expiration time. - The token expiration time. - Gets the token string. The token string. + + Gets the token expiration time. + The token expiration time. + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Abstractions.csproj b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Abstractions.csproj 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/doc/snippets/Microsoft.Data.SqlClient/ActiveDirectoryAuthenticationProvider.xml b/src/Microsoft.Data.SqlClient.Extensions/Azure/doc/ActiveDirectoryAuthenticationProvider.xml similarity index 97% rename from doc/snippets/Microsoft.Data.SqlClient/ActiveDirectoryAuthenticationProvider.xml rename to src/Microsoft.Data.SqlClient.Extensions/Azure/doc/ActiveDirectoryAuthenticationProvider.xml index a940011eb3..c680d0dd8b 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/ActiveDirectoryAuthenticationProvider.xml +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/doc/ActiveDirectoryAuthenticationProvider.xml @@ -1,4 +1,9 @@ - + + 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..9ebb0806ac --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/src/Azure.csproj @@ -0,0 +1,126 @@ + + + + + + + + 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 + + + + + + + + + + + + + + + + + + + + + + false + + + $(AssemblyName) + + + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Azure/src/AzureVersions.props b/src/Microsoft.Data.SqlClient.Extensions/Azure/src/AzureVersions.props new file mode 100644 index 0000000000..8c9d16969e --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/src/AzureVersions.props @@ -0,0 +1,69 @@ + + + + + + + + + + + + + 1 + + + <_OurPackageVersion Condition="'$(AzurePackageVersion)' != ''">$(AzurePackageVersion) + <_OurPackageVersion Condition="'$(AzurePackageVersion)' == ''">$(AzureDefaultMajorVersion).0.0.$(BuildNumber)-dev + + + + <_OurAssemblyFileVersion Condition="'$(AzureAssemblyFileVersion)' != ''">$(AzureAssemblyFileVersion) + + <_OurAssemblyFileVersion Condition="'$(AzureAssemblyFileVersion)' == '' and '$(AzurePackageVersion)' != ''">$(AzurePackageVersion.Split('-')[0]) + + <_OurAssemblyFileVersion Condition="'$(AzureAssemblyFileVersion)' == '' and '$(AzurePackageVersion)' == ''">$(AzureDefaultMajorVersion).0.0.$(BuildNumber) + + + $(_OurPackageVersion) + $(_OurAssemblyFileVersion) + + + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Azure/test/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..dd6f3bcdf0 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/AADConnectionTest.cs @@ -0,0 +1,390 @@ +// 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.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 +} 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..bc4e5b14b1 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/Azure.Test.csproj @@ -0,0 +1,57 @@ + + + + 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..2a01a12384 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/Config.cs @@ -0,0 +1,272 @@ +// 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(); + + 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..50ae1c7c6e --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/DefaultAuthProviderTests.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.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; + } + } + } +} 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..2f37588c1c 100644 --- a/src/Microsoft.Data.SqlClient.sln +++ b/src/Microsoft.Data.SqlClient.sln @@ -1,6 +1,7 @@ -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 +VisualStudioVersion = 17.14.36203.30 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Data.SqlClient", "Microsoft.Data.SqlClient\netfx\src\Microsoft.Data.SqlClient.csproj", "{407890AC-9876-4FEF-A6F1-F36A876BAADE}" EndProject @@ -315,8 +316,28 @@ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SqlClient.Stress.Runner", "Microsoft.Data.SqlClient\tests\StressTests\SqlClient.Stress.Runner\SqlClient.Stress.Runner.csproj", "{4A9C11F4-9577-ABEC-C070-83A194746D9B}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SqlClient.Stress.Tests", "Microsoft.Data.SqlClient\tests\StressTests\SqlClient.Stress.Tests\SqlClient.Stress.Tests.csproj", "{FAA1E517-581A-D3DC-BAC9-FAD1D5A5142C}" +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", "{B21E7C94-D805-427E-928A-8DE8EA2F08CC}" +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", "{04ACBF75-CFF2-41AB-B652-776BC0533490}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Data.SqlClient.Samples", "..\doc\Samples\Microsoft.Data.SqlClient.Samples.csproj", "{C09B9D2F-E463-BEBD-34E4-E8F2C201A277}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Azure", "Azure", "{A20114E1-82D8-903A-C389-726EB4FD943F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0D2F834B-6D91-18D0-3F09-672D448751BD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure", "Microsoft.Data.SqlClient.Extensions\Azure\src\Azure.csproj", "{20C16035-7293-45AC-8217-9B86A389E571}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{5AF52CDD-DF78-3712-7516-5B49F94F9491}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure.Test", "Microsoft.Data.SqlClient.Extensions\Azure\test\Azure.Test.csproj", "{A7C0B6C7-A4B2-43CA-921B-D4FAEE86ACBC}" EndProject 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 @@ -660,6 +681,54 @@ Global {C09B9D2F-E463-BEBD-34E4-E8F2C201A277}.Release|Any CPU.ActiveCfg = Release|Any CPU {C09B9D2F-E463-BEBD-34E4-E8F2C201A277}.Release|x64.ActiveCfg = Release|x64 {C09B9D2F-E463-BEBD-34E4-E8F2C201A277}.Release|x86.ActiveCfg = Release|x86 + {B21E7C94-D805-427E-928A-8DE8EA2F08CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B21E7C94-D805-427E-928A-8DE8EA2F08CC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B21E7C94-D805-427E-928A-8DE8EA2F08CC}.Debug|x64.ActiveCfg = Debug|Any CPU + {B21E7C94-D805-427E-928A-8DE8EA2F08CC}.Debug|x64.Build.0 = Debug|Any CPU + {B21E7C94-D805-427E-928A-8DE8EA2F08CC}.Debug|x86.ActiveCfg = Debug|Any CPU + {B21E7C94-D805-427E-928A-8DE8EA2F08CC}.Debug|x86.Build.0 = Debug|Any CPU + {B21E7C94-D805-427E-928A-8DE8EA2F08CC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B21E7C94-D805-427E-928A-8DE8EA2F08CC}.Release|Any CPU.Build.0 = Release|Any CPU + {B21E7C94-D805-427E-928A-8DE8EA2F08CC}.Release|x64.ActiveCfg = Release|Any CPU + {B21E7C94-D805-427E-928A-8DE8EA2F08CC}.Release|x64.Build.0 = Release|Any CPU + {B21E7C94-D805-427E-928A-8DE8EA2F08CC}.Release|x86.ActiveCfg = Release|Any CPU + {B21E7C94-D805-427E-928A-8DE8EA2F08CC}.Release|x86.Build.0 = Release|Any CPU + {04ACBF75-CFF2-41AB-B652-776BC0533490}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {04ACBF75-CFF2-41AB-B652-776BC0533490}.Debug|Any CPU.Build.0 = Debug|Any CPU + {04ACBF75-CFF2-41AB-B652-776BC0533490}.Debug|x64.ActiveCfg = Debug|Any CPU + {04ACBF75-CFF2-41AB-B652-776BC0533490}.Debug|x64.Build.0 = Debug|Any CPU + {04ACBF75-CFF2-41AB-B652-776BC0533490}.Debug|x86.ActiveCfg = Debug|Any CPU + {04ACBF75-CFF2-41AB-B652-776BC0533490}.Debug|x86.Build.0 = Debug|Any CPU + {04ACBF75-CFF2-41AB-B652-776BC0533490}.Release|Any CPU.ActiveCfg = Release|Any CPU + {04ACBF75-CFF2-41AB-B652-776BC0533490}.Release|Any CPU.Build.0 = Release|Any CPU + {04ACBF75-CFF2-41AB-B652-776BC0533490}.Release|x64.ActiveCfg = Release|Any CPU + {04ACBF75-CFF2-41AB-B652-776BC0533490}.Release|x64.Build.0 = Release|Any CPU + {04ACBF75-CFF2-41AB-B652-776BC0533490}.Release|x86.ActiveCfg = Release|Any CPU + {04ACBF75-CFF2-41AB-B652-776BC0533490}.Release|x86.Build.0 = Release|Any CPU + {20C16035-7293-45AC-8217-9B86A389E571}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {20C16035-7293-45AC-8217-9B86A389E571}.Debug|Any CPU.Build.0 = Debug|Any CPU + {20C16035-7293-45AC-8217-9B86A389E571}.Debug|x64.ActiveCfg = Debug|Any CPU + {20C16035-7293-45AC-8217-9B86A389E571}.Debug|x64.Build.0 = Debug|Any CPU + {20C16035-7293-45AC-8217-9B86A389E571}.Debug|x86.ActiveCfg = Debug|Any CPU + {20C16035-7293-45AC-8217-9B86A389E571}.Debug|x86.Build.0 = Debug|Any CPU + {20C16035-7293-45AC-8217-9B86A389E571}.Release|Any CPU.ActiveCfg = Release|Any CPU + {20C16035-7293-45AC-8217-9B86A389E571}.Release|Any CPU.Build.0 = Release|Any CPU + {20C16035-7293-45AC-8217-9B86A389E571}.Release|x64.ActiveCfg = Release|Any CPU + {20C16035-7293-45AC-8217-9B86A389E571}.Release|x64.Build.0 = Release|Any CPU + {20C16035-7293-45AC-8217-9B86A389E571}.Release|x86.ActiveCfg = Release|Any CPU + {20C16035-7293-45AC-8217-9B86A389E571}.Release|x86.Build.0 = Release|Any CPU + {A7C0B6C7-A4B2-43CA-921B-D4FAEE86ACBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A7C0B6C7-A4B2-43CA-921B-D4FAEE86ACBC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A7C0B6C7-A4B2-43CA-921B-D4FAEE86ACBC}.Debug|x64.ActiveCfg = Debug|Any CPU + {A7C0B6C7-A4B2-43CA-921B-D4FAEE86ACBC}.Debug|x64.Build.0 = Debug|Any CPU + {A7C0B6C7-A4B2-43CA-921B-D4FAEE86ACBC}.Debug|x86.ActiveCfg = Debug|Any CPU + {A7C0B6C7-A4B2-43CA-921B-D4FAEE86ACBC}.Debug|x86.Build.0 = Debug|Any CPU + {A7C0B6C7-A4B2-43CA-921B-D4FAEE86ACBC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A7C0B6C7-A4B2-43CA-921B-D4FAEE86ACBC}.Release|Any CPU.Build.0 = Release|Any CPU + {A7C0B6C7-A4B2-43CA-921B-D4FAEE86ACBC}.Release|x64.ActiveCfg = Release|Any CPU + {A7C0B6C7-A4B2-43CA-921B-D4FAEE86ACBC}.Release|x64.Build.0 = Release|Any CPU + {A7C0B6C7-A4B2-43CA-921B-D4FAEE86ACBC}.Release|x86.ActiveCfg = Release|Any CPU + {A7C0B6C7-A4B2-43CA-921B-D4FAEE86ACBC}.Release|x86.Build.0 = Release|Any CPU {3FF03FA9-E3C3-49E3-9DCB-C703A5B0278B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3FF03FA9-E3C3-49E3-9DCB-C703A5B0278B}.Debug|Any CPU.Build.0 = Debug|Any CPU {3FF03FA9-E3C3-49E3-9DCB-C703A5B0278B}.Debug|x64.ActiveCfg = Debug|x64 @@ -729,6 +798,16 @@ Global {4A9C11F4-9577-ABEC-C070-83A194746D9B} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {FAA1E517-581A-D3DC-BAC9-FAD1D5A5142C} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {C09B9D2F-E463-BEBD-34E4-E8F2C201A277} = {ED952CF7-84DF-437A-B066-F516E9BE1C2C} + {556B486E-F9B0-7EA9-6A25-DA560C312761} = {19F1F1E5-3013-7660-661A-2A15F7D606C1} + {210228A5-979A-DE06-EE1F-B35C65E1583C} = {556B486E-F9B0-7EA9-6A25-DA560C312761} + {B21E7C94-D805-427E-928A-8DE8EA2F08CC} = {210228A5-979A-DE06-EE1F-B35C65E1583C} + {59667E4C-0BD2-9F48-FB50-9E55DD8B1011} = {556B486E-F9B0-7EA9-6A25-DA560C312761} + {04ACBF75-CFF2-41AB-B652-776BC0533490} = {59667E4C-0BD2-9F48-FB50-9E55DD8B1011} + {A20114E1-82D8-903A-C389-726EB4FD943F} = {19F1F1E5-3013-7660-661A-2A15F7D606C1} + {0D2F834B-6D91-18D0-3F09-672D448751BD} = {A20114E1-82D8-903A-C389-726EB4FD943F} + {20C16035-7293-45AC-8217-9B86A389E571} = {0D2F834B-6D91-18D0-3F09-672D448751BD} + {5AF52CDD-DF78-3712-7516-5B49F94F9491} = {A20114E1-82D8-903A-C389-726EB4FD943F} + {A7C0B6C7-A4B2-43CA-921B-D4FAEE86ACBC} = {5AF52CDD-DF78-3712-7516-5B49F94F9491} {3FF03FA9-E3C3-49E3-9DCB-C703A5B0278B} = {0CC4817A-12F3-4357-912C-09315FAAD008} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.csproj b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.csproj index 8bdb0e520a..967196ab83 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.csproj +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.csproj @@ -29,11 +29,18 @@ - - - - + + + + + + + + + + + diff --git a/src/Microsoft.Data.SqlClient/add-ons/Directory.Build.props b/src/Microsoft.Data.SqlClient/add-ons/Directory.Build.props index 7776439adc..d5bd53f012 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/Directory.Build.props +++ b/src/Microsoft.Data.SqlClient/add-ons/Directory.Build.props @@ -8,8 +8,6 @@ $(OS) true true - Project - $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb true $([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(TargetFramework)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)')) diff --git a/src/Microsoft.Data.SqlClient/add-ons/Directory.Packages.props b/src/Microsoft.Data.SqlClient/add-ons/Directory.Packages.props deleted file mode 100644 index 197a396a0c..0000000000 --- a/src/Microsoft.Data.SqlClient/add-ons/Directory.Packages.props +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs index 0a04d047f8..6c74d2e85f 100644 --- a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs +++ b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs @@ -142,30 +142,6 @@ public SqlVector(System.ReadOnlyMemory memory) { } } namespace Microsoft.Data.SqlClient { - /// - public sealed partial class ActiveDirectoryAuthenticationProvider : SqlAuthenticationProvider - { - /// - public ActiveDirectoryAuthenticationProvider() { } - /// - public ActiveDirectoryAuthenticationProvider(string applicationClientId) { } - /// - public static void ClearUserTokenCache() { } - /// - public ActiveDirectoryAuthenticationProvider(System.Func deviceCodeFlowCallbackMethod, string applicationClientId = null) { } - /// - public override System.Threading.Tasks.Task AcquireTokenAsync(SqlAuthenticationParameters parameters) { throw null; } - /// - public void SetDeviceCodeFlowCallback(System.Func deviceCodeFlowCallbackMethod) { } - /// - public void SetAcquireAuthorizationCodeAsyncCallback(System.Func> acquireAuthorizationCodeAsyncCallback) { } - /// - public override bool IsSupported(SqlAuthenticationMethod authentication) { throw null; } - /// - public override void BeforeLoad(SqlAuthenticationMethod authentication) { } - /// - public override void BeforeUnload(SqlAuthenticationMethod authentication) { } - } /// public enum ApplicationIntent { @@ -204,85 +180,6 @@ protected SqlAuthenticationInitializer() { } /// public abstract void Initialize(); } - /// - public enum SqlAuthenticationMethod - { - /// - NotSpecified = 0, - /// - SqlPassword = 1, - /// - [System.Obsolete("ActiveDirectoryPassword is deprecated, use a more secure authentication method. See https://aka.ms/SqlClientEntraIDAuthentication for more details.")] - ActiveDirectoryPassword = 2, - /// - ActiveDirectoryIntegrated = 3, - /// - ActiveDirectoryInteractive = 4, - /// - ActiveDirectoryServicePrincipal = 5, - /// - ActiveDirectoryDeviceCodeFlow = 6, - /// - ActiveDirectoryManagedIdentity = 7, - /// - ActiveDirectoryMSI = 8, - /// - ActiveDirectoryDefault = 9, - /// - ActiveDirectoryWorkloadIdentity = 10 - } - /// - public class SqlAuthenticationParameters - { - /// - protected SqlAuthenticationParameters(Microsoft.Data.SqlClient.SqlAuthenticationMethod authenticationMethod, string serverName, string databaseName, string resource, string authority, string userId, string password, System.Guid connectionId, int connectionTimeout) { } - /// - public Microsoft.Data.SqlClient.SqlAuthenticationMethod AuthenticationMethod { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } - /// - public string Authority { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } - /// - public System.Guid ConnectionId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } - /// - public string DatabaseName { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } - /// - public string Password { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } - /// - public string Resource { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } - /// - public string ServerName { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } - /// - public string UserId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } - /// - public int ConnectionTimeout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } - } - /// - public abstract partial class SqlAuthenticationProvider - { - /// - protected SqlAuthenticationProvider() { } - /// - public abstract System.Threading.Tasks.Task AcquireTokenAsync(Microsoft.Data.SqlClient.SqlAuthenticationParameters parameters); - /// - public virtual void BeforeLoad(Microsoft.Data.SqlClient.SqlAuthenticationMethod authenticationMethod) { } - /// - public virtual void BeforeUnload(Microsoft.Data.SqlClient.SqlAuthenticationMethod authenticationMethod) { } - /// - public static Microsoft.Data.SqlClient.SqlAuthenticationProvider GetProvider(Microsoft.Data.SqlClient.SqlAuthenticationMethod authenticationMethod) { throw null; } - /// - public abstract bool IsSupported(Microsoft.Data.SqlClient.SqlAuthenticationMethod authenticationMethod); - /// - public static bool SetProvider(Microsoft.Data.SqlClient.SqlAuthenticationMethod authenticationMethod, Microsoft.Data.SqlClient.SqlAuthenticationProvider provider) { throw null; } - } - /// - public partial class SqlAuthenticationToken - { - /// - public SqlAuthenticationToken(string accessToken, System.DateTimeOffset expiresOn) { } - /// - public string AccessToken { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } - /// - public System.DateTimeOffset ExpiresOn { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } - } /// public sealed partial class SqlBulkCopy : System.IDisposable { diff --git a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.csproj index e6ffc2526b..8618e36fd7 100644 --- a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.csproj @@ -1,14 +1,17 @@  - false + Microsoft.Data.SqlClient + net8.0;net9.0;netstandard2.0 + + false $(ObjFolder)$(Configuration)\$(AssemblyName)\ref\ netcoreapp $(BinFolder)$(Configuration)\$(AssemblyName)\ref\ $(BinFolder)$(Configuration).$(Platform)\$(AssemblyName)\netstandard\ - $(OutputPath)\$(TargetFramework)\Microsoft.Data.SqlClient.xml + $(OutputPath)\$(TargetFramework)\$(AssemblyName).xml Core $(BaseProduct) Debug;Release; AnyCPU;x64;x86 @@ -32,8 +35,6 @@ - - @@ -50,6 +51,19 @@ + + + + + + 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 9263cb1063..9ef3c1998b 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -1,9 +1,11 @@  Microsoft.Data.SqlClient + net8.0;net9.0 - Microsoft.Data.SqlClient is not supported on this platform. + $(OS) + Microsoft.Data.SqlClient is not supported on this platform. false netcoreapp @@ -11,7 +13,7 @@ AnyCPU;x64;x86 $(ObjFolder)$(Configuration).$(Platform)\$(AssemblyName)\netcore\ $(BinFolder)$(Configuration).$(Platform)\$(AssemblyName)\netcore\ - $(OutputPath)\$(TargetFramework)\Microsoft.Data.SqlClient.xml + $(OutputPath)\$(TargetFramework)\$(AssemblyName).xml true Core $(BaseProduct) true @@ -242,9 +244,6 @@ Microsoft\Data\SqlClient\AAsyncCallContext.cs - - Microsoft\Data\SqlClient\ActiveDirectoryAuthenticationProvider.cs - Microsoft\Data\SqlClient\ActiveDirectoryAuthenticationTimeoutRetryHelper.cs @@ -647,18 +646,12 @@ Microsoft\Data\SqlClient\SqlAppContextSwitchManager.netcore.cs - - Microsoft\Data\SqlClient\SqlAuthenticationParameters.cs - - - Microsoft\Data\SqlClient\SqlAuthenticationProvider.cs + + Microsoft\Data\SqlClient\SqlAuthenticationParametersBuilder.cs Microsoft\Data\SqlClient\SqlAuthenticationProviderManager.cs - - Microsoft\Data\SqlClient\SqlAuthenticationToken.cs - Microsoft\Data\SqlClient\SqlBatch.cs @@ -1073,8 +1066,6 @@ - - @@ -1085,6 +1076,19 @@ + + + + + + diff --git a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs index e662fc0560..30816757eb 100644 --- a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs +++ b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs @@ -57,32 +57,6 @@ public sealed class SqlDataSourceEnumerator : System.Data.Common.DbDataSourceEnu namespace Microsoft.Data.SqlClient { - /// - public sealed class ActiveDirectoryAuthenticationProvider : SqlAuthenticationProvider - { - /// - public ActiveDirectoryAuthenticationProvider() { } - /// - public ActiveDirectoryAuthenticationProvider(string applicationClientId) { } - /// - public static void ClearUserTokenCache() { } - /// - public ActiveDirectoryAuthenticationProvider(System.Func deviceCodeFlowCallbackMethod, string applicationClientId = null) { } - /// - public override System.Threading.Tasks.Task AcquireTokenAsync(SqlAuthenticationParameters parameters) { throw null; } - /// - public void SetDeviceCodeFlowCallback(System.Func deviceCodeFlowCallbackMethod) { } - /// - public void SetAcquireAuthorizationCodeAsyncCallback(System.Func> acquireAuthorizationCodeAsyncCallback) { } - /// - public void SetIWin32WindowFunc(System.Func iWin32WindowFunc) { } - /// - public override bool IsSupported(SqlAuthenticationMethod authentication) { throw null; } - /// - public override void BeforeLoad(SqlAuthenticationMethod authentication) { } - /// - public override void BeforeUnload(SqlAuthenticationMethod authentication) { } - } /// public enum ApplicationIntent { @@ -122,85 +96,6 @@ protected SqlAuthenticationInitializer() { } /// public abstract void Initialize(); } - /// - public enum SqlAuthenticationMethod - { - /// - NotSpecified = 0, - /// - SqlPassword = 1, - /// - [System.ObsoleteAttribute("ActiveDirectoryPassword is deprecated, use a more secure authentication method. See https://aka.ms/SqlClientEntraIDAuthentication for more details.")] - ActiveDirectoryPassword = 2, - /// - ActiveDirectoryIntegrated = 3, - /// - ActiveDirectoryInteractive = 4, - /// - ActiveDirectoryServicePrincipal = 5, - /// - ActiveDirectoryDeviceCodeFlow = 6, - /// - ActiveDirectoryManagedIdentity = 7, - /// - ActiveDirectoryMSI = 8, - /// - ActiveDirectoryDefault = 9, - /// - ActiveDirectoryWorkloadIdentity = 10 - } - /// - public class SqlAuthenticationParameters - { - /// - protected SqlAuthenticationParameters(Microsoft.Data.SqlClient.SqlAuthenticationMethod authenticationMethod, string serverName, string databaseName, string resource, string authority, string userId, string password, System.Guid connectionId, int connectionTimeout) { } - /// - public Microsoft.Data.SqlClient.SqlAuthenticationMethod AuthenticationMethod { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - /// - public string Authority { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - /// - public System.Guid ConnectionId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - /// - public string DatabaseName { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - /// - public string Password { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - /// - public string Resource { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - /// - public string ServerName { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - /// - public string UserId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - /// - public int ConnectionTimeout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - } - /// - public abstract partial class SqlAuthenticationProvider - { - /// - protected SqlAuthenticationProvider() { } - /// - public abstract System.Threading.Tasks.Task AcquireTokenAsync(Microsoft.Data.SqlClient.SqlAuthenticationParameters parameters); - /// - public virtual void BeforeLoad(Microsoft.Data.SqlClient.SqlAuthenticationMethod authenticationMethod) { } - /// - public virtual void BeforeUnload(Microsoft.Data.SqlClient.SqlAuthenticationMethod authenticationMethod) { } - /// - public static Microsoft.Data.SqlClient.SqlAuthenticationProvider GetProvider(Microsoft.Data.SqlClient.SqlAuthenticationMethod authenticationMethod) { throw null; } - /// - public abstract bool IsSupported(Microsoft.Data.SqlClient.SqlAuthenticationMethod authenticationMethod); - /// - public static bool SetProvider(Microsoft.Data.SqlClient.SqlAuthenticationMethod authenticationMethod, Microsoft.Data.SqlClient.SqlAuthenticationProvider provider) { throw null; } - } - /// - public partial class SqlAuthenticationToken - { - /// - public SqlAuthenticationToken(string accessToken, System.DateTimeOffset expiresOn) { } - /// - public string AccessToken { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - /// - public System.DateTimeOffset ExpiresOn { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - } /// public sealed partial class SqlBulkCopy : System.IDisposable { diff --git a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.csproj index 1d9c1985bd..e3e46f2349 100644 --- a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.csproj @@ -1,10 +1,13 @@  - false + Microsoft.Data.SqlClient + net462 + + false $(ObjFolder)$(Configuration)\$(AssemblyName)\ref\ $(BinFolder)$(Configuration)\$(AssemblyName)\ref\ - $(OutputPath)\Microsoft.Data.SqlClient.xml + $(OutputPath)\$(AssemblyName).xml Framework $(BaseProduct) Debug;Release @@ -32,8 +35,6 @@ - - All @@ -49,5 +50,19 @@ + + + + + + + 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 5d64d4f0ed..649a88523a 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -1,16 +1,18 @@  Microsoft.Data.SqlClient + net462 - $(OS) + {407890AC-9876-4FEF-A6F1-F36A876BAADE} + $(OS) true AnyCPU $(BinFolder)$(Configuration).$(OutputPlatform)\ $(ObjFolder)$(Configuration).$(OutputPlatform)\ $(BinPath)$(AssemblyName)\netfx\ - $(OutputPath)\Microsoft.Data.SqlClient.xml + $(OutputPath)\$(AssemblyName).xml $(ObjPath)$(AssemblyName)\netfx\ Framework $(BaseProduct) false @@ -306,9 +308,6 @@ Microsoft\Data\SqlClient\AAsyncCallContext.cs - - Microsoft\Data\SqlClient\ActiveDirectoryAuthenticationProvider.cs - Microsoft\Data\SqlClient\ActiveDirectoryAuthenticationTimeoutRetryHelper.cs @@ -645,18 +644,12 @@ Microsoft\Data\SqlClient\SqlAeadAes256CbcHmac256Factory.cs - - Microsoft\Data\SqlClient\SqlAuthenticationParameters.cs - - - Microsoft\Data\SqlClient\SqlAuthenticationProvider.cs + + Microsoft\Data\SqlClient\SqlAuthenticationParametersBuilder.cs Microsoft\Data\SqlClient\SqlAuthenticationProviderManager.cs - - Microsoft\Data\SqlClient\SqlAuthenticationToken.cs - Microsoft\Data\SqlClient\SqlBatchCommand.cs @@ -907,7 +900,7 @@ Microsoft\Data\SqlClient\SqlUtil.cs - Microsoft\Data\SqlClient\SSPI\NativeSspiContextProvider.windows..cs + Microsoft\Data\SqlClient\SSPI\NativeSspiContextProvider.windows.cs Microsoft\Data\SqlClient\SSPI\NegotiateSspiContextProvider.cs @@ -1056,8 +1049,6 @@ - - All @@ -1076,6 +1067,19 @@ + + + + + + diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj index 4adf180433..4551d8dde1 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj @@ -1,6 +1,7 @@  + Microsoft.Data.SqlClient Debug;Release; true @@ -15,6 +16,11 @@ $(SigningKeyPath) + + + net462;net8.0;net9.0 + + @@ -25,7 +31,18 @@ - + + Windows_NT $(OS) @@ -40,13 +57,6 @@ $(DefineConstants);_WINDOWS - - - - net8.0;net9.0 - $(TargetFrameworks);net462 - - @@ -95,8 +105,6 @@ - - All @@ -117,8 +125,6 @@ - - @@ -129,6 +135,19 @@ + + + + + + diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs index abbeeda0eb..16a482d4de 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs @@ -21,7 +21,6 @@ using Microsoft.Data.Common.ConnectionString; using Microsoft.Data.SqlClient; using Microsoft.Data.SqlClient.Connection; -using Microsoft.Identity.Client; using Microsoft.SqlServer.Server; using IsolationLevel = System.Data.IsolationLevel; @@ -502,7 +501,7 @@ internal static ArgumentException InvalidArgumentLength(string argumentName, int internal static ArgumentException MustBeReadOnly(string argumentName) => Argument(StringsHelper.GetString(Strings.ADP_MustBeReadOnly, argumentName)); internal static Exception CreateSqlException( - MsalException msalException, + SqlAuthenticationProviderException authException, SqlConnectionString connectionOptions, SqlConnectionInternal sender, string username) @@ -513,20 +512,20 @@ internal static Exception CreateSqlException( sqlErs.Add(new SqlError(0, (byte)0x00, (byte)TdsEnums.MIN_ERROR_CLASS, connectionOptions.DataSource, StringsHelper.GetString(Strings.SQL_MSALFailure, username, connectionOptions.Authentication.ToString("G")), - ActiveDirectoryAuthentication.MSALGetAccessTokenFunctionName, 0)); + authException.Method.ToString(), 0)); // Error[1] - string errorMessage1 = StringsHelper.GetString(Strings.SQL_MSALInnerException, msalException.ErrorCode); + string errorMessage1 = StringsHelper.GetString(Strings.SQL_MSALInnerException, authException.FailureCode); sqlErs.Add(new SqlError(0, (byte)0x00, (byte)TdsEnums.MIN_ERROR_CLASS, - connectionOptions.DataSource, errorMessage1, - ActiveDirectoryAuthentication.MSALGetAccessTokenFunctionName, 0)); + connectionOptions.DataSource, errorMessage1, + authException.Method.ToString(), 0)); // Error[2] - if (!string.IsNullOrEmpty(msalException.Message)) + if (!string.IsNullOrEmpty(authException.Message)) { sqlErs.Add(new SqlError(0, (byte)0x00, (byte)TdsEnums.MIN_ERROR_CLASS, - connectionOptions.DataSource, msalException.Message, - ActiveDirectoryAuthentication.MSALGetAccessTokenFunctionName, 0)); + connectionOptions.DataSource, authException.Message, + authException.Method.ToString(), 0)); } return SqlException.CreateException(sqlErs, "", sender, innerException: null, batchCommand: null); } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.netfx.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.netfx.cs index c7427a6a8d..8b8d8b3c5b 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.netfx.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.netfx.cs @@ -13,7 +13,10 @@ using System.Security.Permissions; using System.Text; using System.Threading.Tasks; + +#if _WINDOWS using Interop.Windows.Kernel32; +#endif namespace Microsoft.Data.Common { diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ActiveDirectoryAuthenticationProvider.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ActiveDirectoryAuthenticationProvider.cs deleted file mode 100644 index 0393de9beb..0000000000 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ActiveDirectoryAuthenticationProvider.cs +++ /dev/null @@ -1,743 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Concurrent; -using System.Security.Cryptography; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Azure.Core; -using Azure.Identity; -using Microsoft.Data.Common.ConnectionString; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Identity.Client; -using Microsoft.Identity.Client.Extensibility; - -namespace Microsoft.Data.SqlClient -{ - /// - public sealed class ActiveDirectoryAuthenticationProvider : SqlAuthenticationProvider - { - /// - /// This is a static cache instance meant to hold instances of "PublicClientApplication" mapping to information available in PublicClientAppKey. - /// The purpose of this cache is to allow re-use of Access Tokens fetched for a user interactively or with any other mode - /// to avoid interactive authentication request every-time, within application scope making use of MSAL's userTokenCache. - /// - private static readonly ConcurrentDictionary s_pcaMap = new(); - private static readonly ConcurrentDictionary s_tokenCredentialMap = new(); - private static SemaphoreSlim s_pcaMapModifierSemaphore = new(1, 1); - private static SemaphoreSlim s_tokenCredentialMapModifierSemaphore = new(1, 1); - private static readonly MemoryCache s_accountPwCache = new MemoryCache(new MemoryCacheOptions()); - private static readonly int s_accountPwCacheTtlInHours = 2; - private static readonly string s_nativeClientRedirectUri = "https://login.microsoftonline.com/common/oauth2/nativeclient"; - private static readonly string s_defaultScopeSuffix = "/.default"; - private readonly string _type = typeof(ActiveDirectoryAuthenticationProvider).Name; - private readonly SqlClientLogger _logger = new(); - private Func _deviceCodeFlowCallback; - private ICustomWebUi _customWebUI = null; - private readonly string _applicationClientId = ActiveDirectoryAuthentication.AdoClientId; - - /// - public ActiveDirectoryAuthenticationProvider() - : this(DefaultDeviceFlowCallback) - { - } - - /// - public ActiveDirectoryAuthenticationProvider(string applicationClientId) - : this(DefaultDeviceFlowCallback, applicationClientId) - { - } - - /// - public ActiveDirectoryAuthenticationProvider(Func deviceCodeFlowCallbackMethod, string applicationClientId = null) - { - if (applicationClientId != null) - { - _applicationClientId = applicationClientId; - } - SetDeviceCodeFlowCallback(deviceCodeFlowCallbackMethod); - } - - /// - public static void ClearUserTokenCache() - { - if (!s_pcaMap.IsEmpty) - { - s_pcaMap.Clear(); - } - - if (!s_tokenCredentialMap.IsEmpty) - { - s_tokenCredentialMap.Clear(); - } - } - - /// - public void SetDeviceCodeFlowCallback(Func deviceCodeFlowCallbackMethod) => _deviceCodeFlowCallback = deviceCodeFlowCallbackMethod; - - /// - public void SetAcquireAuthorizationCodeAsyncCallback(Func> acquireAuthorizationCodeAsyncCallback) => _customWebUI = new CustomWebUi(acquireAuthorizationCodeAsyncCallback); - - /// - public override bool IsSupported(SqlAuthenticationMethod authentication) - { - return authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated - #pragma warning disable 0618 // Type or member is obsolete - || authentication == SqlAuthenticationMethod.ActiveDirectoryPassword - #pragma warning restore 0618 // Type or member is obsolete - || authentication == SqlAuthenticationMethod.ActiveDirectoryInteractive - || authentication == SqlAuthenticationMethod.ActiveDirectoryServicePrincipal - || authentication == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow - || authentication == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity - || authentication == SqlAuthenticationMethod.ActiveDirectoryMSI - || authentication == SqlAuthenticationMethod.ActiveDirectoryDefault - || authentication == SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity; - } - - /// - public override void BeforeLoad(SqlAuthenticationMethod authentication) - { - _logger.LogInfo(_type, "BeforeLoad", $"being loaded into SqlAuthProviders for {authentication}."); - } - - /// - public override void BeforeUnload(SqlAuthenticationMethod authentication) - { - _logger.LogInfo(_type, "BeforeUnload", $"being unloaded from SqlAuthProviders for {authentication}."); - } - -#if NETFRAMEWORK - private Func _iWin32WindowFunc = null; - - /// - public void SetIWin32WindowFunc(Func iWin32WindowFunc) => this._iWin32WindowFunc = iWin32WindowFunc; -#endif - - /// - - public override async Task AcquireTokenAsync(SqlAuthenticationParameters parameters) - { - using CancellationTokenSource cts = new(); - - // Use Connection timeout value to cancel token acquire request after certain period of time. - int timeout = parameters.ConnectionTimeout * 1000; // Convert to milliseconds - if (timeout > 0) // if ConnectionTimeout is 0 or the millis overflows an int, no need to set CancelAfter - { - cts.CancelAfter(timeout); - } - - string scope = parameters.Resource.EndsWith(s_defaultScopeSuffix, StringComparison.Ordinal) ? parameters.Resource : parameters.Resource + s_defaultScopeSuffix; - string[] scopes = new string[] { scope }; - TokenRequestContext tokenRequestContext = new(scopes); - - /* We split audience from Authority URL here. Audience can be one of the following: - * The Azure AD authority audience enumeration - * The tenant ID, which can be: - * - A GUID (the ID of your Azure AD instance), for single-tenant applications - * - A domain name associated with your Azure AD instance (also for single-tenant applications) - * One of these placeholders as a tenant ID in place of the Azure AD authority audience enumeration: - * - `organizations` for a multitenant application - * - `consumers` to sign in users only with their personal accounts - * - `common` to sign in users with their work and school accounts or their personal Microsoft accounts - * - * MSAL will throw a meaningful exception if you specify both the Azure AD authority audience and the tenant ID. - * If you don't specify an audience, your app will target Azure AD and personal Microsoft accounts as an audience. (That is, it will behave as though `common` were specified.) - * More information: https://docs.microsoft.com/azure/active-directory/develop/msal-client-application-configuration - **/ - - int separatorIndex = parameters.Authority.LastIndexOf('/'); - string authority = parameters.Authority.Remove(separatorIndex + 1); - string audience = parameters.Authority.Substring(separatorIndex + 1); - string clientId = string.IsNullOrWhiteSpace(parameters.UserId) ? null : parameters.UserId; - - if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryDefault) - { - // Cache DefaultAzureCredenial based on scope, authority, audience, and clientId - TokenCredentialKey tokenCredentialKey = new(typeof(DefaultAzureCredential), authority, scope, audience, clientId); - AccessToken accessToken = await GetTokenAsync(tokenCredentialKey, string.Empty, tokenRequestContext, cts.Token).ConfigureAwait(false); - SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token for Default auth mode. Expiry Time: {0}", accessToken.ExpiresOn); - return new SqlAuthenticationToken(accessToken.Token, accessToken.ExpiresOn); - } - - TokenCredentialOptions tokenCredentialOptions = new() { AuthorityHost = new Uri(authority) }; - - if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity || parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryMSI) - { - // Cache ManagedIdentityCredential based on scope, authority, and clientId - TokenCredentialKey tokenCredentialKey = new(typeof(ManagedIdentityCredential), authority, scope, string.Empty, clientId); - AccessToken accessToken = await GetTokenAsync(tokenCredentialKey, string.Empty, tokenRequestContext, cts.Token).ConfigureAwait(false); - SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token for Managed Identity auth mode. Expiry Time: {0}", accessToken.ExpiresOn); - return new SqlAuthenticationToken(accessToken.Token, accessToken.ExpiresOn); - } - - if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryServicePrincipal) - { - // Cache ClientSecretCredential based on scope, authority, audience, and clientId - TokenCredentialKey tokenCredentialKey = new(typeof(ClientSecretCredential), authority, scope, audience, clientId); - AccessToken accessToken = await GetTokenAsync(tokenCredentialKey, parameters.Password, tokenRequestContext, cts.Token).ConfigureAwait(false); - SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token for Active Directory Service Principal auth mode. Expiry Time: {0}", accessToken.ExpiresOn); - return new SqlAuthenticationToken(accessToken.Token, accessToken.ExpiresOn); - } - - if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity) - { - // Cache WorkloadIdentityCredential based on authority and clientId - TokenCredentialKey tokenCredentialKey = new(typeof(WorkloadIdentityCredential), authority, string.Empty, string.Empty, clientId); - // If either tenant id, client id, or the token file path are not specified when fetching the token, - // a CredentialUnavailableException will be thrown instead - AccessToken accessToken = await GetTokenAsync(tokenCredentialKey, string.Empty, tokenRequestContext, cts.Token).ConfigureAwait(false); - SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token for Workload Identity auth mode. Expiry Time: {0}", accessToken.ExpiresOn); - return new SqlAuthenticationToken(accessToken.Token, accessToken.ExpiresOn); - } - - /* - * Today, MSAL.NET uses another redirect URI by default in desktop applications that run on Windows - * (urn:ietf:wg:oauth:2.0:oob). In the future, we'll want to change this default, so we recommend - * that you use https://login.microsoftonline.com/common/oauth2/nativeclient. - * - * https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-desktop-app-registration#redirect-uris - */ - string redirectUri = s_nativeClientRedirectUri; - -#if NET - if (parameters.AuthenticationMethod != SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow) - { - redirectUri = "http://localhost"; - } -#endif - PublicClientAppKey pcaKey = new(parameters.Authority, redirectUri, _applicationClientId -#if NETFRAMEWORK - , _iWin32WindowFunc -#endif - ); - - AuthenticationResult result = null; - IPublicClientApplication app = await GetPublicClientAppInstanceAsync(pcaKey, cts.Token).ConfigureAwait(false); - - if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryIntegrated) - { - result = await TryAcquireTokenSilent(app, parameters, scopes, cts).ConfigureAwait(false); - - if (result == null) - { - if (!string.IsNullOrEmpty(parameters.UserId)) - { - // The AcquireTokenByIntegratedWindowsAuth method is marked as obsolete in MSAL.NET - // but it is still a supported way to acquire tokens for Active Directory Integrated authentication. -#pragma warning disable CS0618 // Type or member is obsolete - result = await app.AcquireTokenByIntegratedWindowsAuth(scopes) -#pragma warning restore CS0618 // Type or member is obsolete - .WithCorrelationId(parameters.ConnectionId) - .WithUsername(parameters.UserId) - .ExecuteAsync(cancellationToken: cts.Token) - .ConfigureAwait(false); - } - else - { -#pragma warning disable CS0618 // Type or member is obsolete - result = await app.AcquireTokenByIntegratedWindowsAuth(scopes) -#pragma warning restore CS0618 // Type or member is obsolete - .WithCorrelationId(parameters.ConnectionId) - .ExecuteAsync(cancellationToken: cts.Token) - .ConfigureAwait(false); - } - SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token for Active Directory Integrated auth mode. Expiry Time: {0}", result?.ExpiresOn); - } - } - #pragma warning disable 0618 // Type or member is obsolete - else if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryPassword) - #pragma warning restore 0618 // Type or member is obsolete - { - string pwCacheKey = GetAccountPwCacheKey(parameters); - object previousPw = s_accountPwCache.Get(pwCacheKey); - byte[] currPwHash = GetHash(parameters.Password); - - if (previousPw != null && - previousPw is byte[] previousPwBytes && - // Only get the cached token if the current password hash matches the previously used password hash - AreEqual(currPwHash, previousPwBytes)) - { - result = await TryAcquireTokenSilent(app, parameters, scopes, cts).ConfigureAwait(false); - } - - if (result == null) - { -#pragma warning disable CS0618 // Type or member is obsolete - result = await app.AcquireTokenByUsernamePassword(scopes, parameters.UserId, parameters.Password) -#pragma warning restore CS0618 // Type or member is obsolete - .WithCorrelationId(parameters.ConnectionId) - .ExecuteAsync(cancellationToken: cts.Token) - .ConfigureAwait(false); - - // We cache the password hash to ensure future connection requests include a validated password - // when we check for a cached MSAL account. Otherwise, a connection request with the same username - // against the same tenant could succeed with an invalid password when we re-use the cached token. - using (ICacheEntry entry = s_accountPwCache.CreateEntry(pwCacheKey)) - { - entry.Value = GetHash(parameters.Password); - entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(s_accountPwCacheTtlInHours); - } - SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token for Active Directory Password auth mode. Expiry Time: {0}", result?.ExpiresOn); - } - } - else if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryInteractive || - parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow) - { - try - { - result = await TryAcquireTokenSilent(app, parameters, scopes, cts).ConfigureAwait(false); - SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token (silent) for {0} auth mode. Expiry Time: {1}", parameters.AuthenticationMethod, result?.ExpiresOn); - } - catch (MsalUiRequiredException) - { - // An 'MsalUiRequiredException' is thrown in the case where an interaction is required with the end user of the application, - // for instance, if no refresh token was in the cache, or the user needs to consent, or re-sign-in (for instance if the password expired), - // or the user needs to perform two factor authentication. - result = await AcquireTokenInteractiveDeviceFlowAsync(app, scopes, parameters.ConnectionId, parameters.UserId, parameters.AuthenticationMethod, cts, _customWebUI, _deviceCodeFlowCallback).ConfigureAwait(false); - SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token (interactive) for {0} auth mode. Expiry Time: {1}", parameters.AuthenticationMethod, result?.ExpiresOn); - } - - if (result == null) - { - // If no existing 'account' is found, we request user to sign in interactively. - result = await AcquireTokenInteractiveDeviceFlowAsync(app, scopes, parameters.ConnectionId, parameters.UserId, parameters.AuthenticationMethod, cts, _customWebUI, _deviceCodeFlowCallback).ConfigureAwait(false); - SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token (interactive) for {0} auth mode. Expiry Time: {1}", parameters.AuthenticationMethod, result?.ExpiresOn); - } - } - else - { - SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | {0} authentication mode not supported by ActiveDirectoryAuthenticationProvider class.", parameters.AuthenticationMethod); - throw SQL.UnsupportedAuthenticationSpecified(parameters.AuthenticationMethod); - } - - return new SqlAuthenticationToken(result.AccessToken, result.ExpiresOn); - } - - private static async Task TryAcquireTokenSilent(IPublicClientApplication app, SqlAuthenticationParameters parameters, - string[] scopes, CancellationTokenSource cts) - { - AuthenticationResult result = null; - - // Fetch available accounts from 'app' instance - System.Collections.Generic.IEnumerator accounts = (await app.GetAccountsAsync().ConfigureAwait(false)).GetEnumerator(); - - IAccount account = default; - if (accounts.MoveNext()) - { - if (!string.IsNullOrEmpty(parameters.UserId)) - { - do - { - IAccount currentVal = accounts.Current; - if (string.Compare(parameters.UserId, currentVal.Username, StringComparison.InvariantCultureIgnoreCase) == 0) - { - account = currentVal; - break; - } - } - while (accounts.MoveNext()); - } - else - { - account = accounts.Current; - } - } - - if (account != null) - { - // If 'account' is available in 'app', we use the same to acquire token silently. - // Read More on API docs: https://docs.microsoft.com/dotnet/api/microsoft.identity.client.clientapplicationbase.acquiretokensilent - result = await app.AcquireTokenSilent(scopes, account).ExecuteAsync(cancellationToken: cts.Token).ConfigureAwait(false); - SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token (silent) for {0} auth mode. Expiry Time: {1}", parameters.AuthenticationMethod, result?.ExpiresOn); - } - - return result; - } - - private static async Task AcquireTokenInteractiveDeviceFlowAsync(IPublicClientApplication app, string[] scopes, Guid connectionId, string userId, - SqlAuthenticationMethod authenticationMethod, CancellationTokenSource cts, ICustomWebUi customWebUI, Func deviceCodeFlowCallback) - { - try - { - if (authenticationMethod == SqlAuthenticationMethod.ActiveDirectoryInteractive) - { - CancellationTokenSource ctsInteractive = new(); -#if NET - /* - * On .NET Core, MSAL will start the system browser as a separate process. MSAL does not have control over this browser, - * but once the user finishes authentication, the web page is redirected in such a way that MSAL can intercept the Uri. - * MSAL cannot detect if the user navigates away or simply closes the browser. Apps using this technique are encouraged - * to define a timeout (via CancellationToken). We recommend a timeout of at least a few minutes, to take into account - * cases where the user is prompted to change password or perform 2FA. - * - * https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/System-Browser-on-.Net-Core#system-browser-experience - */ - ctsInteractive.CancelAfter(180000); -#endif - if (customWebUI != null) - { - return await app.AcquireTokenInteractive(scopes) - .WithCorrelationId(connectionId) - .WithCustomWebUi(customWebUI) - .WithLoginHint(userId) - .ExecuteAsync(ctsInteractive.Token) - .ConfigureAwait(false); - } - else - { - /* - * We will use the MSAL Embedded or System web browser which changes by Default in MSAL according to this table: - * - * Framework Embedded System Default - * ------------------------------------------- - * .NET Classic Yes Yes^ Embedded - * .NET Core No Yes^ System - * .NET Standard No No NONE - * UWP Yes No Embedded - * Xamarin.Android Yes Yes System - * Xamarin.iOS Yes Yes System - * Xamarin.Mac Yes No Embedded - * - * ^ Requires "http://localhost" redirect URI - * - * https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/MSAL.NET-uses-web-browser#at-a-glance - */ - return await app.AcquireTokenInteractive(scopes) - .WithCorrelationId(connectionId) - .WithLoginHint(userId) - .ExecuteAsync(ctsInteractive.Token) - .ConfigureAwait(false); - } - } - else - { - AuthenticationResult result = await app.AcquireTokenWithDeviceCode(scopes, - deviceCodeResult => deviceCodeFlowCallback(deviceCodeResult)) - .WithCorrelationId(connectionId) - .ExecuteAsync(cancellationToken: cts.Token) - .ConfigureAwait(false); - return result; - } - } - catch (OperationCanceledException) - { - SqlClientEventSource.Log.TryTraceEvent("AcquireTokenInteractiveDeviceFlowAsync | Operation timed out while acquiring access token."); - throw (authenticationMethod == SqlAuthenticationMethod.ActiveDirectoryInteractive) ? - SQL.ActiveDirectoryInteractiveTimeout() : - SQL.ActiveDirectoryDeviceFlowTimeout(); - } - } - - private static Task DefaultDeviceFlowCallback(DeviceCodeResult result) - { - // This will print the message on the console which tells the user where to go sign-in using - // a separate browser and the code to enter once they sign in. - // The AcquireTokenWithDeviceCode() method will poll the server after firing this - // device code callback to look for the successful login of the user via that browser. - // This background polling (whose interval and timeout data is also provided as fields in the - // deviceCodeCallback class) will occur until: - // * The user has successfully logged in via browser and entered the proper code - // * The timeout specified by the server for the lifetime of this code (typically ~15 minutes) has been reached - // * The developing application calls the Cancel() method on a CancellationToken sent into the method. - // If this occurs, an OperationCanceledException will be thrown (see catch below for more details). - SqlClientEventSource.Log.TryTraceEvent("AcquireTokenInteractiveDeviceFlowAsync | Callback triggered with Device Code Result: {0}", result.Message); - Console.WriteLine(result.Message); - return Task.FromResult(0); - } - - private class CustomWebUi : ICustomWebUi - { - private readonly Func> _acquireAuthorizationCodeAsyncCallback; - - internal CustomWebUi(Func> acquireAuthorizationCodeAsyncCallback) => _acquireAuthorizationCodeAsyncCallback = acquireAuthorizationCodeAsyncCallback; - - public Task AcquireAuthorizationCodeAsync(Uri authorizationUri, Uri redirectUri, CancellationToken cancellationToken) - => _acquireAuthorizationCodeAsyncCallback.Invoke(authorizationUri, redirectUri, cancellationToken); - } - - private async Task GetPublicClientAppInstanceAsync(PublicClientAppKey publicClientAppKey, CancellationToken cancellationToken) - { - if (!s_pcaMap.TryGetValue(publicClientAppKey, out IPublicClientApplication clientApplicationInstance)) - { - await s_pcaMapModifierSemaphore.WaitAsync(cancellationToken); - try - { - // Double-check in case another thread added it while we waited for the semaphore - if (!s_pcaMap.TryGetValue(publicClientAppKey, out clientApplicationInstance)) - { - clientApplicationInstance = CreateClientAppInstance(publicClientAppKey); - s_pcaMap.TryAdd(publicClientAppKey, clientApplicationInstance); - } - } - finally - { - s_pcaMapModifierSemaphore.Release(); - } - } - - return clientApplicationInstance; - } - - private static async Task GetTokenAsync(TokenCredentialKey tokenCredentialKey, string secret, - TokenRequestContext tokenRequestContext, CancellationToken cancellationToken) - { - if (!s_tokenCredentialMap.TryGetValue(tokenCredentialKey, out TokenCredentialData tokenCredentialInstance)) - { - await s_tokenCredentialMapModifierSemaphore.WaitAsync(cancellationToken); - try - { - // Double-check in case another thread added it while we waited for the semaphore - if (!s_tokenCredentialMap.TryGetValue(tokenCredentialKey, out tokenCredentialInstance)) - { - tokenCredentialInstance = CreateTokenCredentialInstance(tokenCredentialKey, secret); - s_tokenCredentialMap.TryAdd(tokenCredentialKey, tokenCredentialInstance); - } - } - finally - { - s_tokenCredentialMapModifierSemaphore.Release(); - } - } - - if (!AreEqual(tokenCredentialInstance._secretHash, GetHash(secret))) - { - // If the secret hash has changed, we need to remove the old token credential instance and create a new one. - await s_tokenCredentialMapModifierSemaphore.WaitAsync(cancellationToken); - try - { - s_tokenCredentialMap.TryRemove(tokenCredentialKey, out _); - tokenCredentialInstance = CreateTokenCredentialInstance(tokenCredentialKey, secret); - s_tokenCredentialMap.TryAdd(tokenCredentialKey, tokenCredentialInstance); - } - finally - { - s_tokenCredentialMapModifierSemaphore.Release(); - } - } - - return await tokenCredentialInstance._tokenCredential.GetTokenAsync(tokenRequestContext, cancellationToken); - } - - private static string GetAccountPwCacheKey(SqlAuthenticationParameters parameters) - { - return parameters.Authority + "+" + parameters.UserId; - } - - private static byte[] GetHash(string input) - { - byte[] unhashedBytes = Encoding.Unicode.GetBytes(input); - SHA256 sha256 = SHA256.Create(); - byte[] hashedBytes = sha256.ComputeHash(unhashedBytes); - return hashedBytes; - } - - private static bool AreEqual(byte[] a1, byte[] a2) - { - if (ReferenceEquals(a1, a2)) - { - return true; - } - else if (a1 is null || a2 is null) - { - return false; - } - else if (a1.Length != a2.Length) - { - return false; - } - - return a1.AsSpan().SequenceEqual(a2.AsSpan()); - } - - private IPublicClientApplication CreateClientAppInstance(PublicClientAppKey publicClientAppKey) - { - PublicClientApplicationBuilder builder = PublicClientApplicationBuilder - .CreateWithApplicationOptions(new PublicClientApplicationOptions - { - ClientId = publicClientAppKey._applicationClientId, - ClientName = DbConnectionStringDefaults.ApplicationName, - ClientVersion = Common.ADP.GetAssemblyVersion().ToString(), - RedirectUri = publicClientAppKey._redirectUri, - }) - .WithAuthority(publicClientAppKey._authority); - - #if NETFRAMEWORK - if (_iWin32WindowFunc is not null) - { - builder.WithParentActivityOrWindow(_iWin32WindowFunc); - } - #endif - - return builder.Build(); - } - - private static TokenCredentialData CreateTokenCredentialInstance(TokenCredentialKey tokenCredentialKey, string secret) - { - if (tokenCredentialKey._tokenCredentialType == typeof(DefaultAzureCredential)) - { - DefaultAzureCredentialOptions defaultAzureCredentialOptions = new() - { - AuthorityHost = new Uri(tokenCredentialKey._authority), - TenantId = tokenCredentialKey._audience, - ExcludeInteractiveBrowserCredential = true // Force disabled, even though it's disabled by default to respect driver specifications. - }; - - // Optionally set clientId when available - if (tokenCredentialKey._clientId is not null) - { - defaultAzureCredentialOptions.ManagedIdentityClientId = tokenCredentialKey._clientId; -#pragma warning disable CS0618 // Type or member is obsolete - defaultAzureCredentialOptions.SharedTokenCacheUsername = tokenCredentialKey._clientId; -#pragma warning restore CS0618 // Type or member is obsolete - defaultAzureCredentialOptions.WorkloadIdentityClientId = tokenCredentialKey._clientId; - } - - // SqlClient is a library and provides support to acquire access - // token using 'DefaultAzureCredential' on user demand when they - // specify 'Authentication = Active Directory Default' in - // connection string. - // - // Default Azure Credential is instantiated by the calling - // application when using "Active Directory Default" - // authentication code to connect to Azure SQL instance. - // SqlClient is a library, doesn't instantiate the credential - // without running application instructions. - // - // Note that CodeQL suppression support can only detect - // suppression comments that appear immediately above the - // flagged statement, or appended to the end of the statement. - // Multi-line justifications are not supported. - // - // https://eng.ms/docs/cloud-ai-platform/devdiv/one-engineering-system-1es/1es-docs/codeql/codeql-semmle#guidance-on-suppressions - // - // CodeQL [SM05137] See above for justification. - DefaultAzureCredential cred = new(defaultAzureCredentialOptions); - - return new TokenCredentialData(cred, GetHash(secret)); - } - - TokenCredentialOptions tokenCredentialOptions = new() { AuthorityHost = new Uri(tokenCredentialKey._authority) }; - - if (tokenCredentialKey._tokenCredentialType == typeof(ManagedIdentityCredential)) - { - return new TokenCredentialData(new ManagedIdentityCredential(tokenCredentialKey._clientId, tokenCredentialOptions), GetHash(secret)); - } - else if (tokenCredentialKey._tokenCredentialType == typeof(ClientSecretCredential)) - { - return new TokenCredentialData(new ClientSecretCredential(tokenCredentialKey._audience, tokenCredentialKey._clientId, secret, tokenCredentialOptions), GetHash(secret)); - } - else if (tokenCredentialKey._tokenCredentialType == typeof(WorkloadIdentityCredential)) - { - // The WorkloadIdentityCredentialOptions object initialization populates its instance members - // from the environment variables AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_FEDERATED_TOKEN_FILE, - // and AZURE_ADDITIONALLY_ALLOWED_TENANTS. AZURE_CLIENT_ID may be overridden by the User Id. - WorkloadIdentityCredentialOptions options = new() { AuthorityHost = new Uri(tokenCredentialKey._authority) }; - - if (tokenCredentialKey._clientId is not null) - { - options.ClientId = tokenCredentialKey._clientId; - } - - return new TokenCredentialData(new WorkloadIdentityCredential(options), GetHash(secret)); - } - - // This should never be reached, but if it is, throw an exception that will be noticed during development - throw new ArgumentException(nameof(ActiveDirectoryAuthenticationProvider)); - } - - internal class PublicClientAppKey - { - public readonly string _authority; - public readonly string _redirectUri; - public readonly string _applicationClientId; -#if NETFRAMEWORK - public readonly Func _iWin32WindowFunc; -#endif - - public PublicClientAppKey(string authority, string redirectUri, string applicationClientId -#if NETFRAMEWORK - , Func iWin32WindowFunc -#endif - ) - { - _authority = authority; - _redirectUri = redirectUri; - _applicationClientId = applicationClientId; -#if NETFRAMEWORK - _iWin32WindowFunc = iWin32WindowFunc; -#endif - } - - public override bool Equals(object obj) - { - if (obj != null && obj is PublicClientAppKey pcaKey) - { - return (string.CompareOrdinal(_authority, pcaKey._authority) == 0 - && string.CompareOrdinal(_redirectUri, pcaKey._redirectUri) == 0 - && string.CompareOrdinal(_applicationClientId, pcaKey._applicationClientId) == 0 -#if NETFRAMEWORK - && pcaKey._iWin32WindowFunc == _iWin32WindowFunc -#endif - ); - } - return false; - } - - public override int GetHashCode() => Tuple.Create(_authority, _redirectUri, _applicationClientId -#if NETFRAMEWORK - , _iWin32WindowFunc -#endif - ).GetHashCode(); - } - - internal class TokenCredentialData - { - public TokenCredential _tokenCredential; - public byte[] _secretHash; - - public TokenCredentialData(TokenCredential tokenCredential, byte[] secretHash) - { - _tokenCredential = tokenCredential; - _secretHash = secretHash; - } - } - - internal class TokenCredentialKey - { - public readonly Type _tokenCredentialType; - public readonly string _authority; - public readonly string _scope; - public readonly string _audience; - public readonly string _clientId; - - public TokenCredentialKey(Type tokenCredentialType, string authority, string scope, string audience, string clientId) - { - _tokenCredentialType = tokenCredentialType; - _authority = authority; - _scope = scope; - _audience = audience; - _clientId = clientId; - } - - public override bool Equals(object obj) - { - if (obj != null && obj is TokenCredentialKey tcKey) - { - return string.CompareOrdinal(nameof(_tokenCredentialType), nameof(tcKey._tokenCredentialType)) == 0 - && string.CompareOrdinal(_authority, tcKey._authority) == 0 - && string.CompareOrdinal(_scope, tcKey._scope) == 0 - && string.CompareOrdinal(_audience, tcKey._audience) == 0 - && string.CompareOrdinal(_clientId, tcKey._clientId) == 0 - ; - } - return false; - } - - public override int GetHashCode() => Tuple.Create(_tokenCredentialType, _authority, _scope, _audience, _clientId).GetHashCode(); - } - - } -} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ActiveDirectoryAuthenticationTimeoutRetryHelper.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ActiveDirectoryAuthenticationTimeoutRetryHelper.cs index e972102260..721e31ca6f 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ActiveDirectoryAuthenticationTimeoutRetryHelper.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ActiveDirectoryAuthenticationTimeoutRetryHelper.cs @@ -132,7 +132,8 @@ private static string GetTokenHash(SqlFedAuthToken token) } // Here we mimic how ADAL calculates hash for token. They use UTF8 instead of Unicode. - var originalTokenString = SqlAuthenticationToken.AccessTokenStringFromBytes(token.accessToken); + var originalTokenString = Encoding.Unicode.GetString(token.AccessToken); + var bytesInUtf8 = Encoding.UTF8.GetBytes(originalTokenString); using (var sha256 = SHA256.Create()) { diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs index 5567dac6d5..efd54ae383 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs @@ -8,7 +8,6 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Net.Http.Headers; using System.Security; using System.Text; using System.Threading; @@ -19,7 +18,6 @@ using Microsoft.Data.ProviderBase; using Microsoft.Data.SqlClient.Connection; using Microsoft.Data.SqlClient.ConnectionPool; -using Microsoft.Identity.Client; using IsolationLevel = System.Data.IsolationLevel; namespace Microsoft.Data.SqlClient.Connection @@ -34,13 +32,6 @@ internal class SqlConnectionInternal : DbConnectionInternal, IDisposable // @TODO: Can be private? internal const int MaxNumberOfRedirectRoute = 10; - /// - /// Status code that indicates MSAL request 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 - /// - // @TODO: Can be private? - internal const int MsalHttpRetryStatusCode = 429; - /// /// The timespan defining the amount of time the authentication context needs to be valid /// for at-least, to re-use the cached context, without making an attempt to refresh it. IF @@ -132,20 +123,6 @@ internal class SqlConnectionInternal : DbConnectionInternal, IDisposable #region Debug/Test Behavior Overrides #if DEBUG - /// - /// This is a test hook to enable testing of the retry paths for MSAL get access token. - /// - /// - /// Type type = typeof(SqlConnection).Assembly.GetType("Microsoft.Data.SqlClient.SQLInternalConnectionTds"); - /// FieldInfo field = type.GetField("_forceMsalRetry", BindingFlags.NonPublic | BindingFlags.Static); - /// if (field != null) - /// { - /// field.SetValue(null, true); - /// } - /// - /// @TODO: For unit tests, it should not be necessary to do this via reflection. - internal static bool _forceMsalRetry = false; - /// /// This is a test hook to simulate a token expiring within the next 45 minutes. /// @@ -619,7 +596,7 @@ internal bool Is2008OrNewer internal override bool IsAccessTokenExpired { get => _federatedAuthenticationInfoRequested && - DateTime.FromFileTimeUtc(_fedAuthToken.expirationFileTime) < DateTime.UtcNow.AddSeconds(accessTokenExpirationBufferTime); + DateTime.FromFileTimeUtc(_fedAuthToken.ExpirationFileTime) < DateTime.UtcNow.AddSeconds(accessTokenExpirationBufferTime); } /// @@ -1806,8 +1783,8 @@ internal void OnFedAuthInfo(SqlFedAuthInfo fedAuthInfo) // Construct the dbAuthenticationContextKey with information from FedAuthInfo and // store for later use, when inserting in to the token cache. _dbConnectionPoolAuthenticationContextKey = new DbConnectionPoolAuthenticationContextKey( - fedAuthInfo.stsurl, - fedAuthInfo.spn); + fedAuthInfo.StsUrl, + fedAuthInfo.Spn); // Try to retrieve the authentication context from the pool, if one does exist for // this key. @@ -1928,14 +1905,12 @@ internal void OnFedAuthInfo(SqlFedAuthInfo fedAuthInfo) // If the code flow is here, then we are re-using the context from the cache for // this connection attempt and not generating a new access token on this thread. - _fedAuthToken = new SqlFedAuthToken - { - accessToken = dbConnectionPoolAuthenticationContext.AccessToken, - expirationFileTime = dbConnectionPoolAuthenticationContext.ExpirationTime.ToFileTime() - }; + _fedAuthToken = new( + dbConnectionPoolAuthenticationContext.AccessToken, + dbConnectionPoolAuthenticationContext.ExpirationTime.ToFileTime()); } - Debug.Assert(_fedAuthToken?.accessToken != null, + Debug.Assert(_fedAuthToken?.AccessToken != null, "_fedAuthToken and _fedAuthToken.accessToken cannot be null."); _parser.SendFedAuthToken(_fedAuthToken); @@ -2703,61 +2678,47 @@ private void FailoverPermissionDemand() => PoolGroupProviderInfo?.FailoverPermissionDemand(); #endif + #nullable enable + /// /// Get the Federated Authentication Token. /// /// Information obtained from server as Federated Authentication Info. private SqlFedAuthToken GetFedAuthToken(SqlFedAuthInfo fedAuthInfo) { - Debug.Assert(fedAuthInfo != null, "fedAuthInfo should not be null."); - - // Number of milliseconds to sleep for the initial back off. - int sleepInterval = 100; - - // Number of attempts, for tracing purposes, if we underwent retries. - int numberOfAttempts = 0; + // Number of milliseconds to sleep for the initial back off, if a + // retry period is not specified by the provider. + const int defaultRetryPeriod = 100; - // Object that will be returned to the caller, containing all required data about the token. - _fedAuthToken = new SqlFedAuthToken(); + // Number of attempts we are willing to perform. + const int maxAttempts = 1; // Username to use in error messages. - string username = null; - - SqlAuthenticationProvider authProvider = - SqlAuthenticationProvider.GetProvider(ConnectionOptions.Authentication); + string? username = null; + SqlAuthenticationProvider? authProvider = SqlAuthenticationProviderManager.GetProvider(ConnectionOptions.Authentication); if (authProvider == null && _accessTokenCallback == null) { throw SQL.CannotFindAuthProvider(ConnectionOptions.Authentication.ToString()); } - // Retry getting access token once if MsalException.error_code is unknown_error. - // extra logic to deal with HTTP 429 (Retry after). - // @TODO: Can we pick one or the other? - // @TODO: Wait ... are we counting up but only looping while the number of attempts is <=1 ? Huh? - // @TODO: Can we consider using a for loop here since there's a fixed number of times to loop - - #if NET - while (numberOfAttempts <= 1) - #else - while (numberOfAttempts <= 1 && sleepInterval <= _timeout.MillisecondsRemaining) - #endif + // We will perform retries if the provider indicates an error that + // is retryable. + for (int attempt = 0; attempt <= maxAttempts; ++attempt) { - numberOfAttempts++; try { - var authParamsBuilder = new SqlAuthenticationParameters.Builder( - authenticationMethod: ConnectionOptions.Authentication, - resource: fedAuthInfo.spn, - authority: fedAuthInfo.stsurl, - serverName: ConnectionOptions.DataSource, - databaseName: ConnectionOptions.InitialCatalog) + var authParamsBuilder = new SqlAuthenticationParametersBuilder( + authenticationMethod: ConnectionOptions.Authentication, + resource: fedAuthInfo.Spn, + authority: fedAuthInfo.StsUrl, + serverName: ConnectionOptions.DataSource, + databaseName: ConnectionOptions.InitialCatalog) .WithConnectionId(_clientConnectionId) - .WithConnectionTimeout(ConnectionOptions.ConnectTimeout); + .WithAuthenticationTimeout(ConnectionOptions.ConnectTimeout); switch (ConnectionOptions.Authentication) { case SqlAuthenticationMethod.ActiveDirectoryIntegrated: - #if NET // In some scenarios for .NET Core, MSAL cannot detect the current user and needs it passed in // for Integrated auth. Allow the user/application to pass it in to work around those scenarios. if (!string.IsNullOrEmpty(ConnectionOptions.UserID)) @@ -2769,9 +2730,6 @@ private SqlFedAuthToken GetFedAuthToken(SqlFedAuthInfo fedAuthInfo) { username = TdsEnums.NTAUTHORITYANONYMOUSLOGON; } - #else - username = TdsEnums.NTAUTHORITYANONYMOUSLOGON; - #endif if (_activeDirectoryAuthTimeoutRetryHelper.State == ActiveDirectoryAuthenticationTimeoutRetryState.Retrying) { @@ -2779,20 +2737,13 @@ private SqlFedAuthToken GetFedAuthToken(SqlFedAuthInfo fedAuthInfo) } else { - // We use Task.Run here in all places to execute task synchronously - // in the same context. Fixes block-over-async deadlock possibilities - // https://github.com/dotnet/SqlClient/issues/1209 - // @TODO: Verify that the wrapping/unwrapping is necessary. - _fedAuthToken = Task.Run(async () => - await authProvider.AcquireTokenAsync(authParamsBuilder)) - .GetAwaiter() - .GetResult() - .ToSqlFedAuthToken(); + // We use Task.Run here in all places to execute task synchronously in the same context. + // Fixes block-over-async deadlock possibilities https://github.com/dotnet/SqlClient/issues/1209 + _fedAuthToken = new(Task.Run(async () => await authProvider!.AcquireTokenAsync(authParamsBuilder)).GetAwaiter().GetResult()); + _activeDirectoryAuthTimeoutRetryHelper.CachedToken = _fedAuthToken; } - break; - case SqlAuthenticationMethod.ActiveDirectoryInteractive: case SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow: case SqlAuthenticationMethod.ActiveDirectoryManagedIdentity: @@ -2806,16 +2757,13 @@ await authProvider.AcquireTokenAsync(authParamsBuilder)) else { authParamsBuilder.WithUserId(ConnectionOptions.UserID); - _fedAuthToken = Task.Run(async () => await authProvider.AcquireTokenAsync(authParamsBuilder)).GetAwaiter().GetResult().ToSqlFedAuthToken(); + _fedAuthToken = new(Task.Run(async () => await authProvider!.AcquireTokenAsync(authParamsBuilder)).GetAwaiter().GetResult()); _activeDirectoryAuthTimeoutRetryHelper.CachedToken = _fedAuthToken; } - break; - #pragma warning disable 0618 // Type or member is obsolete case SqlAuthenticationMethod.ActiveDirectoryPassword: #pragma warning restore 0618 // Type or member is obsolete - case SqlAuthenticationMethod.ActiveDirectoryServicePrincipal: if (_activeDirectoryAuthTimeoutRetryHelper.State == ActiveDirectoryAuthenticationTimeoutRetryState.Retrying) { @@ -2823,32 +2771,21 @@ await authProvider.AcquireTokenAsync(authParamsBuilder)) } else { - // @TODO: _fedAuthToken assignment is identical in both cases - move outside if (_credential != null) { username = _credential.UserId; authParamsBuilder.WithUserId(username).WithPassword(_credential.Password); - _fedAuthToken = Task.Run(async () => - await authProvider.AcquireTokenAsync(authParamsBuilder)) - .GetAwaiter() - .GetResult() - .ToSqlFedAuthToken(); + _fedAuthToken = new(Task.Run(async () => await authProvider!.AcquireTokenAsync(authParamsBuilder)).GetAwaiter().GetResult()); } else { username = ConnectionOptions.UserID; authParamsBuilder.WithUserId(username).WithPassword(ConnectionOptions.Password); - _fedAuthToken = Task.Run(async () => - await authProvider.AcquireTokenAsync(authParamsBuilder)) - .GetAwaiter() - .GetResult() - .ToSqlFedAuthToken(); + _fedAuthToken = new(Task.Run(async () => await authProvider!.AcquireTokenAsync(authParamsBuilder)).GetAwaiter().GetResult()); } _activeDirectoryAuthTimeoutRetryHelper.CachedToken = _fedAuthToken; } - break; - default: if (_accessTokenCallback == null) { @@ -2871,150 +2808,87 @@ await authProvider.AcquireTokenAsync(authParamsBuilder)) authParamsBuilder.WithUserId(ConnectionOptions.UserID); authParamsBuilder.WithPassword(ConnectionOptions.Password); } - SqlAuthenticationParameters parameters = authParamsBuilder; - using CancellationTokenSource cts = new(); - - // Use Connection timeout value to cancel token acquire request - // after certain period of time.(int) - if (_timeout.MillisecondsRemaining < int.MaxValue) + CancellationTokenSource cts = new(); + // Use Connection timeout value to cancel token acquire request after certain period of time.(int) + if (_timeout.MillisecondsRemaining < Int32.MaxValue) { cts.CancelAfter((int)_timeout.MillisecondsRemaining); } - - _fedAuthToken = Task.Run(async () => - await _accessTokenCallback(parameters, cts.Token)) - .GetAwaiter() - .GetResult() - .ToSqlFedAuthToken(); + _fedAuthToken = new(Task.Run(async () => await _accessTokenCallback(parameters, cts.Token)).GetAwaiter().GetResult()); _activeDirectoryAuthTimeoutRetryHelper.CachedToken = _fedAuthToken; } break; } - Debug.Assert(_fedAuthToken.accessToken != null, "AccessToken should not be null."); - - #if DEBUG - if (_forceMsalRetry) - { - // 3399614468 is 0xCAA20004L just for testing. - throw new MsalServiceException(MsalError.UnknownError, "Force retry in GetFedAuthToken"); - } - #endif + Debug.Assert(_fedAuthToken.AccessToken != null, "AccessToken should not be null."); // Break out of the retry loop in successful case. break; } - catch (MsalServiceException serviceException) + catch (SqlAuthenticationProviderException ex) { - // Deal with Msal service exceptions first, retry if 429 received. - if (serviceException.StatusCode == MsalHttpRetryStatusCode) + // Is the error fatal or retryable? + if (!ex.ShouldRetry) { - RetryConditionHeaderValue retryAfter = serviceException.Headers.RetryAfter; - if (retryAfter.Delta.HasValue) - { - sleepInterval = retryAfter.Delta.Value.Milliseconds; - } - else if (retryAfter.Date.HasValue) - { - sleepInterval = Convert.ToInt32(retryAfter.Date.Value.Offset.TotalMilliseconds); - } + // It's fatal, so translate into a SqlException. + throw ADP.CreateSqlException(ex, ConnectionOptions, this, username); + } - // if there's enough time to retry before timeout, then retry, otherwise - // break out the retry loop. - if (sleepInterval < _timeout.MillisecondsRemaining) - { - Thread.Sleep(sleepInterval); - } - else - { - SqlClientEventSource.Log.TryTraceEvent( - $"SqlInternalConnectionTds.GetFedAuthToken.MsalServiceException | ERR | " + - $"Timeout: {serviceException.ErrorCode}"); + // We should retry. - throw SQL.ActiveDirectoryTokenRetrievingTimeout( - Enum.GetName(typeof(SqlAuthenticationMethod), ConnectionOptions.Authentication), - serviceException.ErrorCode, - serviceException); - } - } - else + // Could we retry if we wanted to? + if (_timeout.IsExpired || _timeout.MillisecondsRemaining <= 0) { - SqlClientEventSource.Log.TryTraceEvent( - $"SqlInternalConnectionTds.GetFedAuthToken.MsalServiceException | ERR | " + - $"{serviceException.ErrorCode}"); - - throw ADP.CreateSqlException(serviceException, ConnectionOptions, this, username); + // No, so we throw. + SqlClientEventSource.Log.TryTraceEvent(" Attempt: {0}, Timeout: {1}", attempt, ex.FailureCode); + throw SQL.ActiveDirectoryTokenRetrievingTimeout(Enum.GetName(typeof(SqlAuthenticationMethod), ConnectionOptions.Authentication), ex.FailureCode, ex); } - } - catch (MsalException msalException) - { - // Deal with normal MsalExceptions. - if (MsalError.UnknownError != msalException.ErrorCode || - _timeout.IsExpired || - _timeout.MillisecondsRemaining <= sleepInterval) - { - SqlClientEventSource.Log.TryTraceEvent( - $"SqlInternalConnectionTds.GetFedAuthToken.MSALException | ERR | " + - $"{msalException.ErrorCode}"); - throw ADP.CreateSqlException(msalException, ConnectionOptions, this, username); - } + // We use a doubling backoff if the provider didn't provide + // a retry period. + int retryPeriod = + ex.RetryPeriod > 0 + ? ex.RetryPeriod + : defaultRetryPeriod * (2 ^ attempt); - SqlClientEventSource.Log.TryAdvancedTraceEvent( - $"SqlInternalConnectionTds.GetFedAuthToken | ADV | " + - $"Object ID: {ObjectID}, " + - $"sleeping {sleepInterval}ms"); - SqlClientEventSource.Log.TryAdvancedTraceEvent( - $"SqlInternalConnectionTds.GetFedAuthToken | ADV | " + - $"Object ID: {ObjectID}, " + - $"remaining {_timeout.MillisecondsRemaining}ms"); + SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Attempt: {1}, sleeping {2}[Milliseconds]", ObjectID, attempt, retryPeriod); + SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Attempt: {1}, remaining {2}[Milliseconds]", ObjectID, attempt, _timeout.MillisecondsRemaining); - Thread.Sleep(sleepInterval); + // Sleep for the desired period. + Thread.Sleep(retryPeriod); - sleepInterval *= 2; - } - // All other exceptions from MSAL/Azure Identity APIs - catch (Exception e) - { - SqlError error = new( - infoNumber: 0, - errorState: 0x00, - errorClass: TdsEnums.FATAL_ERROR_CLASS, - server: ConnectionOptions.DataSource, - errorMessage: e.Message, - procedure: ActiveDirectoryAuthentication.MSALGetAccessTokenFunctionName, - lineNumber: 0); - - throw SqlException.CreateException( - error, - serverVersion: string.Empty, - internalConnection: this, - innerException: e); + // Fall through to retry... } } - Debug.Assert(_fedAuthToken != null, "fedAuthToken should not be null."); - Debug.Assert(_fedAuthToken.accessToken?.Length > 0, - "fedAuthToken.accessToken should not be null or empty."); + // Nullable context has exposed that _fedAuthToken may be null here, + // which the existing code didn't handle. + if (_fedAuthToken is null) + { + // We failed to acquire a token, so use a default one. + // + // TODO: The old code actually allowed the AccessToken byte + // array to be null, and then had Debug.Assert()s to verify it + // wasn't null. We never test in debug, so those were never + // firing, and we were happily using a _fedAuthToken with a null + // AccessToken. Now that SqlFedAuthToken doesn't allow a null + // AccessToken, we just create an empty one instead. + _fedAuthToken = new SqlFedAuthToken([], 0); + } // Store the newly generated token in _newDbConnectionPoolAuthenticationContext, only if using pooling. if (_dbConnectionPool != null) { - DateTime expirationTime = DateTime.FromFileTimeUtc(_fedAuthToken.expirationFileTime); - _newDbConnectionPoolAuthenticationContext = new DbConnectionPoolAuthenticationContext( - _fedAuthToken.accessToken, - expirationTime); + DateTime expirationTime = DateTime.FromFileTimeUtc(_fedAuthToken.ExpirationFileTime); + _newDbConnectionPoolAuthenticationContext = new DbConnectionPoolAuthenticationContext(_fedAuthToken.AccessToken, expirationTime); } - - SqlClientEventSource.Log.TryTraceEvent( - $"SqlInternalConnectionTds.GetFedAuthToken | " + - $"Object ID {ObjectID}, " + - $"Finished generating federated authentication token."); - + SqlClientEventSource.Log.TryTraceEvent(" {0}, Finished generating federated authentication token.", ObjectID); return _fedAuthToken; } + #nullable disable + private void Login( ServerInfo server, TimeoutTimer timeout, diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/DbConnectionPoolIdentity.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/DbConnectionPoolIdentity.cs index 7eca7bd0f6..057433cf90 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/DbConnectionPoolIdentity.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/DbConnectionPoolIdentity.cs @@ -80,7 +80,7 @@ internal static DbConnectionPoolIdentity GetCurrent() #endif } - #if NETFRAMEWORK + #if NETFRAMEWORK && _WINDOWS internal static WindowsIdentity GetCurrentWindowsIdentity() => WindowsIdentity.GetCurrent(); #endif diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Diagnostics/SqlClientMetrics.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Diagnostics/SqlClientMetrics.cs index f6eb1c84c1..a813a03003 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Diagnostics/SqlClientMetrics.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Diagnostics/SqlClientMetrics.cs @@ -8,7 +8,9 @@ #if NET using System.Threading; #else +#if _WINDOWS using Interop.Windows.Kernel32; +#endif using Microsoft.Data.Common; using System.Diagnostics; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationParameters.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationParameters.cs deleted file mode 100644 index 9c74b937b8..0000000000 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationParameters.cs +++ /dev/null @@ -1,162 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Runtime.InteropServices; -using System.Security; -using Microsoft.Data.Common; - -namespace Microsoft.Data.SqlClient -{ - - /// - public class SqlAuthenticationParameters - { - /// - public SqlAuthenticationMethod AuthenticationMethod { get; } - - /// - public string Resource { get; } - - /// - public string Authority { get; } - - /// - public string UserId { get; } - - /// - public string Password { get; } - - /// - public Guid ConnectionId { get; } - - /// - public string ServerName { get; } - - /// - public string DatabaseName { get; } - - /// - public int ConnectionTimeout { get; } = ADP.DefaultConnectionTimeout; - - /// - protected SqlAuthenticationParameters( - SqlAuthenticationMethod authenticationMethod, - string serverName, - string databaseName, - string resource, - string authority, - string userId, - string password, - Guid connectionId, - int connectionTimeout) - { - AuthenticationMethod = authenticationMethod; - ServerName = serverName; - DatabaseName = databaseName; - Resource = resource; - Authority = authority; - UserId = userId; - Password = password; - ConnectionId = connectionId; - ConnectionTimeout = connectionTimeout; - } - - /// - /// AD authentication parameter builder. - /// - internal class Builder - { - private readonly SqlAuthenticationMethod _authenticationMethod; - private readonly string _serverName; - private readonly string _databaseName; - private readonly string _resource; - private readonly string _authority; - private string _userId; - private string _password; - private Guid _connectionId = Guid.NewGuid(); - private int _connectionTimeout = ADP.DefaultConnectionTimeout; - - /// - /// Implicitly converts to . - /// - public static implicit operator SqlAuthenticationParameters(Builder builder) - { - return new SqlAuthenticationParameters( - authenticationMethod: builder._authenticationMethod, - serverName: builder._serverName, - databaseName: builder._databaseName, - resource: builder._resource, - authority: builder._authority, - userId: builder._userId, - password: builder._password, - connectionId: builder._connectionId, - connectionTimeout: builder._connectionTimeout); - } - - /// - /// Set user id. - /// - public Builder WithUserId(string userId) - { - _userId = userId; - return this; - } - - /// - /// Set password. - /// - public Builder WithPassword(string password) - { - _password = password; - return this; - } - - /// - /// Set password. - /// - public Builder WithPassword(SecureString password) - { - IntPtr valuePtr = IntPtr.Zero; - try - { - valuePtr = Marshal.SecureStringToGlobalAllocUnicode(password); - _password = Marshal.PtrToStringUni(valuePtr); - } - finally - { - Marshal.ZeroFreeGlobalAllocUnicode(valuePtr); - } - return this; - } - - /// - /// Set a specific connection id instead of using a random one. - /// - public Builder WithConnectionId(Guid connectionId) - { - _connectionId = connectionId; - return this; - } - - /// - /// Set connection timeout. - /// - public Builder WithConnectionTimeout(int timeout) - { - _connectionTimeout = timeout; - return this; - } - - internal Builder(SqlAuthenticationMethod authenticationMethod, string resource, string authority, string serverName, string databaseName) - { - _authenticationMethod = authenticationMethod; - _serverName = serverName; - _databaseName = databaseName; - _resource = resource; - _authority = authority; - } - } - } -} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationParametersBuilder.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationParametersBuilder.cs new file mode 100644 index 0000000000..a9863ee2ac --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationParametersBuilder.cs @@ -0,0 +1,104 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; +using System.Security; +using Microsoft.Data.Common; + +namespace Microsoft.Data.SqlClient +{ + internal sealed class SqlAuthenticationParametersBuilder + { + private readonly SqlAuthenticationMethod _authenticationMethod; + private readonly string _serverName; + private readonly string _databaseName; + private readonly string _resource; + private readonly string _authority; + private string _userId; + private string _password; + private Guid _connectionId = Guid.NewGuid(); + private int _authenticationTimeout = ADP.DefaultConnectionTimeout; + + /// + /// Implicitly converts to . + /// + public static implicit operator SqlAuthenticationParameters(SqlAuthenticationParametersBuilder builder) + { + return new SqlAuthenticationParameters( + authenticationMethod: builder._authenticationMethod, + serverName: builder._serverName, + databaseName: builder._databaseName, + resource: builder._resource, + authority: builder._authority, + userId: builder._userId, + password: builder._password, + connectionId: builder._connectionId, + connectionTimeout: builder._authenticationTimeout); + } + + /// + /// Set user id. + /// + public SqlAuthenticationParametersBuilder WithUserId(string userId) + { + _userId = userId; + return this; + } + + /// + /// Set password. + /// + public SqlAuthenticationParametersBuilder WithPassword(string password) + { + _password = password; + return this; + } + + /// + /// Set password. + /// + public SqlAuthenticationParametersBuilder WithPassword(SecureString password) + { + IntPtr valuePtr = IntPtr.Zero; + try + { + valuePtr = Marshal.SecureStringToGlobalAllocUnicode(password); + _password = Marshal.PtrToStringUni(valuePtr); + } + finally + { + Marshal.ZeroFreeGlobalAllocUnicode(valuePtr); + } + return this; + } + + /// + /// Set a specific connection id instead of using a random one. + /// + public SqlAuthenticationParametersBuilder WithConnectionId(Guid connectionId) + { + _connectionId = connectionId; + return this; + } + + /// + /// Set authentication timeout. + /// + public SqlAuthenticationParametersBuilder WithAuthenticationTimeout(int timeout) + { + _authenticationTimeout = timeout; + return this; + } + + internal SqlAuthenticationParametersBuilder(SqlAuthenticationMethod authenticationMethod, string resource, string authority, string serverName, string databaseName) + { + _authenticationMethod = authenticationMethod; + _serverName = serverName; + _databaseName = databaseName; + _resource = resource; + _authority = authority; + } + } +} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProvider.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProvider.cs deleted file mode 100644 index 25e1cf006e..0000000000 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProvider.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Threading.Tasks; - -namespace Microsoft.Data.SqlClient -{ - - /// - public abstract class SqlAuthenticationProvider - { - /// - public static SqlAuthenticationProvider GetProvider(SqlAuthenticationMethod authenticationMethod) - { - return SqlAuthenticationProviderManager.Instance.GetProvider(authenticationMethod); - } - - /// - public static bool SetProvider(SqlAuthenticationMethod authenticationMethod, SqlAuthenticationProvider provider) - { - return SqlAuthenticationProviderManager.Instance.SetProvider(authenticationMethod, provider); - } - - /// - public virtual void BeforeLoad(SqlAuthenticationMethod authenticationMethod) { } - - /// - public virtual void BeforeUnload(SqlAuthenticationMethod authenticationMethod) { } - - /// - public abstract bool IsSupported(SqlAuthenticationMethod authenticationMethod); - - /// - public abstract Task AcquireTokenAsync(SqlAuthenticationParameters parameters); - } -} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs index 447ea0e9c5..06d7423028 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs @@ -6,12 +6,13 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Configuration; +using System.IO; +using System.Reflection; + +#nullable enable namespace Microsoft.Data.SqlClient { - /// - /// Authentication provider manager. - /// internal sealed class SqlAuthenticationProviderManager { [Obsolete("ActiveDirectoryPassword is deprecated, use a more secure authentication method. See https://aka.ms/SqlClientEntraIDAuthentication for more details.")] @@ -27,7 +28,7 @@ internal sealed class SqlAuthenticationProviderManager static SqlAuthenticationProviderManager() { - SqlAuthenticationProviderConfigurationSection configurationSection = null; + SqlAuthenticationProviderConfigurationSection? configurationSection = null; try { @@ -46,49 +47,125 @@ static SqlAuthenticationProviderManager() } Instance = new SqlAuthenticationProviderManager(configurationSection); - SetDefaultAuthProviders(Instance); - } - /// - /// Sets default supported Active Directory Authentication providers by the driver - /// on the SqlAuthenticationProviderManager instance. - /// - private static void SetDefaultAuthProviders(SqlAuthenticationProviderManager instance) - { - if (instance != null) + // If our Azure extensions package is present, use its + // authentication provider as our default. + const string assemblyName = "Microsoft.Data.SqlClient.Extensions.Azure"; + + try { - var activeDirectoryAuthProvider = new ActiveDirectoryAuthenticationProvider(instance._applicationClientId); - instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryIntegrated, activeDirectoryAuthProvider); + // Try to load our Azure extension. + var assembly = Assembly.Load(assemblyName); + + if (assembly is null) + { + SqlClientEventSource.Log.TryTraceEvent( + nameof(SqlAuthenticationProviderManager) + + $": Azure extension assembly={assemblyName} not found; " + + "no default Active Directory provider installed"); + return; + } + + // TODO(ADO-39845): Verify the assembly is signed by us? + + SqlClientEventSource.Log.TryTraceEvent( + nameof(SqlAuthenticationProviderManager) + + $": Azure extension assembly={assemblyName} found; " + + "attempting to set as default provider for all Active " + + "Directory authentication methods"); + + // Look for the authentication provider class. + const string className = "Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProvider"; + var type = assembly.GetType(className); + + if (type is null) + { + SqlClientEventSource.Log.TryTraceEvent( + nameof(SqlAuthenticationProviderManager) + + $": Azure extension does not contain class={className}; " + + "no default Active Directory provider installed"); + + return; + } + + // Try to instantiate it. + var instance = Activator.CreateInstance( + type, + [Instance._applicationClientId]) + as SqlAuthenticationProvider; + + if (instance is null) + { + SqlClientEventSource.Log.TryTraceEvent( + nameof(SqlAuthenticationProviderManager) + + $": Failed to instantiate Azure extension class={className}; " + + "no default Active Directory provider installed"); + + return; + } + + // We successfully instantiated the provider, so set it as the + // default for all Active Directory authentication methods. + // + // Note that SetProvider() will refuse to clobber an application + // specified provider, so these defaults will only be applied + // for methods that do not already have a provider. + SetProvider(SqlAuthenticationMethod.ActiveDirectoryIntegrated, instance); #pragma warning disable 0618 // Type or member is obsolete - instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryPassword, activeDirectoryAuthProvider); + SetProvider(SqlAuthenticationMethod.ActiveDirectoryPassword, instance); #pragma warning restore 0618 // Type or member is obsolete - instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryInteractive, activeDirectoryAuthProvider); - instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryServicePrincipal, activeDirectoryAuthProvider); - instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, activeDirectoryAuthProvider); - instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryManagedIdentity, activeDirectoryAuthProvider); - instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryMSI, activeDirectoryAuthProvider); - instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryDefault, activeDirectoryAuthProvider); - instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity, activeDirectoryAuthProvider); + SetProvider(SqlAuthenticationMethod.ActiveDirectoryInteractive, instance); + SetProvider(SqlAuthenticationMethod.ActiveDirectoryServicePrincipal, instance); + SetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, instance); + SetProvider(SqlAuthenticationMethod.ActiveDirectoryManagedIdentity, instance); + SetProvider(SqlAuthenticationMethod.ActiveDirectoryMSI, instance); + SetProvider(SqlAuthenticationMethod.ActiveDirectoryDefault, instance); + SetProvider(SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity, instance); + + SqlClientEventSource.Log.TryTraceEvent( + nameof(SqlAuthenticationProviderManager) + + $": Azure extension class={className} installed as " + + "provider for all Active Directory authentication methods"); } + // All of these exceptions mean we couldn't find or instantiate the + // Azure extension's authentication provider, in which case we + // simply have no default and the app must provide one if they + // attempt to use Active Directory authentication. + catch (Exception ex) + when (ex is ArgumentNullException || + ex is ArgumentException || + ex is BadImageFormatException || + ex is FileLoadException || + ex is FileNotFoundException || + ex is MemberAccessException || + ex is MethodAccessException || + ex is MissingMethodException || + ex is NotSupportedException || + ex is TargetInvocationException || + ex is TypeLoadException) + { + SqlClientEventSource.Log.TryTraceEvent( + nameof(SqlAuthenticationProviderManager) + + $": Azure extension assembly={assemblyName} not found or " + + "not usable; no default provider installed; " + + $"{ex.GetType().Name}: {ex.Message}"); + } + // Any other exceptions are fatal. } - public static readonly SqlAuthenticationProviderManager Instance; + private static readonly SqlAuthenticationProviderManager Instance; - private readonly SqlAuthenticationInitializer _initializer; - private readonly IReadOnlyCollection _authenticationsWithAppSpecifiedProvider; - private readonly ConcurrentDictionary _providers; + private readonly HashSet _authenticationsWithAppSpecifiedProvider = new(); + private readonly ConcurrentDictionary _providers = new(); private readonly SqlClientLogger _sqlAuthLogger = new SqlClientLogger(); - private readonly string _applicationClientId = ActiveDirectoryAuthentication.AdoClientId; + private readonly string? _applicationClientId = null; /// /// Constructor. /// - public SqlAuthenticationProviderManager(SqlAuthenticationProviderConfigurationSection configSection = null) + private SqlAuthenticationProviderManager(SqlAuthenticationProviderConfigurationSection? configSection) { var methodName = "Ctor"; - _providers = new ConcurrentDictionary(); - var authenticationsWithAppSpecifiedProvider = new HashSet(); - _authenticationsWithAppSpecifiedProvider = authenticationsWithAppSpecifiedProvider; if (configSection == null) { @@ -112,8 +189,14 @@ public SqlAuthenticationProviderManager(SqlAuthenticationProviderConfigurationSe try { var initializerType = Type.GetType(configSection.InitializerType, true); - _initializer = (SqlAuthenticationInitializer)Activator.CreateInstance(initializerType); - _initializer.Initialize(); + if (initializerType is not null) + { + var initializer = (SqlAuthenticationInitializer?)Activator.CreateInstance(initializerType); + if (initializer is not null) + { + initializer.Initialize(); + } + } } catch (Exception e) { @@ -132,23 +215,31 @@ public SqlAuthenticationProviderManager(SqlAuthenticationProviderConfigurationSe foreach (ProviderSettings providerSettings in configSection.Providers) { SqlAuthenticationMethod authentication = AuthenticationEnumFromString(providerSettings.Name); - SqlAuthenticationProvider provider; + SqlAuthenticationProvider? provider; try { var providerType = Type.GetType(providerSettings.Type, true); - provider = (SqlAuthenticationProvider)Activator.CreateInstance(providerType); + if (providerType is null) + { + continue; + } + provider = (SqlAuthenticationProvider?)Activator.CreateInstance(providerType); } catch (Exception e) { throw SQL.CannotCreateAuthProvider(authentication.ToString(), providerSettings.Type, e); } + if (provider is null) + { + continue; + } if (!provider.IsSupported(authentication)) { throw SQL.UnsupportedAuthenticationByProvider(authentication.ToString(), providerSettings.Type); } _providers[authentication] = provider; - authenticationsWithAppSpecifiedProvider.Add(authentication); + _authenticationsWithAppSpecifiedProvider.Add(authentication); _sqlAuthLogger.LogInfo(nameof(SqlAuthenticationProviderManager), methodName, string.Format("Added user-defined auth provider: {0} for authentication {1}.", providerSettings?.Type, authentication)); } } @@ -158,54 +249,48 @@ public SqlAuthenticationProviderManager(SqlAuthenticationProviderConfigurationSe } } - /// - /// Get an authentication provider by method. - /// - /// Authentication method. - /// Authentication provider or null if not found. - public SqlAuthenticationProvider GetProvider(SqlAuthenticationMethod authenticationMethod) + internal static SqlAuthenticationProvider? GetProvider(SqlAuthenticationMethod authenticationMethod) { - SqlAuthenticationProvider value; - return _providers.TryGetValue(authenticationMethod, out value) ? value : null; + SqlAuthenticationProvider? value; + return Instance._providers.TryGetValue(authenticationMethod, out value) ? value : null; } - /// - /// Set an authentication provider by method. - /// - /// Authentication method. - /// Authentication provider. - /// True if succeeded, false otherwise, e.g., the existing provider disallows overriding. - public bool SetProvider(SqlAuthenticationMethod authenticationMethod, SqlAuthenticationProvider provider) + internal static bool SetProvider(SqlAuthenticationMethod authenticationMethod, SqlAuthenticationProvider provider) { if (!provider.IsSupported(authenticationMethod)) { throw SQL.UnsupportedAuthenticationByProvider(authenticationMethod.ToString(), provider.GetType().Name); } var methodName = "SetProvider"; - if (_authenticationsWithAppSpecifiedProvider.Count > 0) + if (Instance._authenticationsWithAppSpecifiedProvider.Count > 0) { - foreach (SqlAuthenticationMethod candidateMethod in _authenticationsWithAppSpecifiedProvider) + foreach (SqlAuthenticationMethod candidateMethod in Instance._authenticationsWithAppSpecifiedProvider) { if (candidateMethod == authenticationMethod) { - _sqlAuthLogger.LogError(nameof(SqlAuthenticationProviderManager), methodName, $"Failed to add provider {GetProviderType(provider)} because a user-defined provider with type {GetProviderType(_providers[authenticationMethod])} already existed for authentication {authenticationMethod}."); - return false; // return here to avoid replacing user-defined provider + Instance._sqlAuthLogger.LogError(nameof(SqlAuthenticationProviderManager), methodName, $"Failed to add provider {GetProviderType(provider)} because a user-defined provider with type {GetProviderType(Instance._providers[authenticationMethod])} already existed for authentication {authenticationMethod}."); + + // The app has already specified a Provider for this + // authentication method, so we won't override it. + return false; } } } - _providers.AddOrUpdate(authenticationMethod, provider, (key, oldProvider) => - { - if (oldProvider != null) - { - oldProvider.BeforeUnload(authenticationMethod); - } - if (provider != null) + Instance._providers.AddOrUpdate( + authenticationMethod, + provider, + (SqlAuthenticationMethod key, SqlAuthenticationProvider oldProvider) => { + if (oldProvider != null) + { + oldProvider.BeforeUnload(authenticationMethod); + } + provider.BeforeLoad(authenticationMethod); - } - _sqlAuthLogger.LogInfo(nameof(SqlAuthenticationProviderManager), methodName, $"Added auth provider {GetProviderType(provider)}, overriding existed provider {GetProviderType(oldProvider)} for authentication {authenticationMethod}."); - return provider; - }); + + Instance._sqlAuthLogger.LogInfo(nameof(SqlAuthenticationProviderManager), methodName, $"Added auth provider {GetProviderType(provider)}, overriding existed provider {GetProviderType(oldProvider)} for authentication {authenticationMethod}."); + return provider; + }); return true; } @@ -216,7 +301,7 @@ public bool SetProvider(SqlAuthenticationMethod authenticationMethod, SqlAuthent /// /// /// - private static T FetchConfigurationSection(string name) + private static T? FetchConfigurationSection(string name) where T : class { Type t = typeof(T); @@ -265,14 +350,13 @@ private static SqlAuthenticationMethod AuthenticationEnumFromString(string authe } } - private static string GetProviderType(SqlAuthenticationProvider provider) + private static string GetProviderType(SqlAuthenticationProvider? provider) { - if (provider == null) + if (provider is null) { return "null"; } - - return provider.GetType().FullName; + return provider.GetType().FullName ?? "unknown"; } } @@ -293,13 +377,13 @@ internal class SqlAuthenticationProviderConfigurationSection : ConfigurationSect /// User-defined initializer. /// [ConfigurationProperty("initializerType")] - public string InitializerType => this["initializerType"] as string; + public string InitializerType => this["initializerType"] as string ?? string.Empty; /// /// Application Client Id /// [ConfigurationProperty("applicationClientId", IsRequired = false)] - public string ApplicationClientId => this["applicationClientId"] as string; + public string ApplicationClientId => this["applicationClientId"] as string ?? string.Empty; } /// diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationToken.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationToken.cs deleted file mode 100644 index c405cac2cb..0000000000 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationToken.cs +++ /dev/null @@ -1,67 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Text; - -namespace Microsoft.Data.SqlClient -{ - /// - public class SqlAuthenticationToken - { - /// - public DateTimeOffset ExpiresOn { get; } - - /// - public string AccessToken { get; } - - /// - public SqlAuthenticationToken(string accessToken, DateTimeOffset expiresOn) - { - if (string.IsNullOrEmpty(accessToken)) - { - throw SQL.ParameterCannotBeEmpty("AccessToken"); - } - - AccessToken = accessToken; - ExpiresOn = expiresOn; - } - - /// - /// Constructor. - /// - internal SqlAuthenticationToken(byte[] accessToken, DateTimeOffset expiresOn) - : this(AccessTokenStringFromBytes(accessToken), expiresOn) { } - - /// - /// Convert to driver's internal token class. - /// - internal SqlFedAuthToken ToSqlFedAuthToken() - { - var tokenBytes = AccessTokenBytesFromString(AccessToken); - return new SqlFedAuthToken - { - accessToken = tokenBytes, - dataLen = (uint)tokenBytes.Length, - expirationFileTime = ExpiresOn.ToFileTime() - }; - } - - /// - /// Convert token bytes to string. - /// - internal static string AccessTokenStringFromBytes(byte[] bytes) - { - return Encoding.Unicode.GetString(bytes); - } - - /// - /// Convert token string to bytes. - /// - internal static byte[] AccessTokenBytesFromString(string token) - { - return Encoding.Unicode.GetBytes(token); - } - } -} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDependency.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDependency.cs index e237e0e3e7..9ce5c20342 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDependency.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDependency.cs @@ -20,7 +20,9 @@ using System.Runtime.Serialization; using System.Runtime.Versioning; using System.Security.Permissions; +#if _WINDOWS using Interop.Windows.Sni; +#endif using Microsoft.Data.SqlClient.LocalDb; #endif diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlErrorCollection.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlErrorCollection.cs index 4684747627..fe20d0d2e5 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlErrorCollection.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlErrorCollection.cs @@ -45,6 +45,11 @@ internal SqlErrorCollection() { } /// public IEnumerator GetEnumerator() => _errors.GetEnumerator(); - internal void Add(SqlError error) => _errors.Add(error); + // Append the error to our list, and return ourselves for chaining. + internal SqlErrorCollection Add(SqlError error) + { + _errors.Add(error); + return this; + } } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlException.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlException.cs index 27d29d45e1..1a606242c2 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlException.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlException.cs @@ -255,8 +255,6 @@ internal static SqlException CreateException( Exception innerException = null, SqlBatchCommand batchCommand = null) { - Debug.Assert(errorCollection != null && errorCollection.Count > 0, "no errorCollection?"); - StringBuilder message = new(); for (int i = 0; i < errorCollection.Count; i++) { @@ -267,7 +265,11 @@ internal static SqlException CreateException( message.Append(errorCollection[i].Message); } - if (innerException == null && errorCollection[0].Win32ErrorCode != 0 && errorCollection[0].Win32ErrorCode != -1) + if (innerException is null && + errorCollection is not null && + errorCollection.Count > 0 && + errorCollection[0].Win32ErrorCode != 0 && + errorCollection[0].Win32ErrorCode != -1) { innerException = new Win32Exception(errorCollection[0].Win32ErrorCode); } @@ -280,7 +282,10 @@ internal static SqlException CreateException( exception.Data.Add("HelpLink.ProdVer", serverVersion); } exception.Data.Add("HelpLink.EvtSrc", "MSSQLServer"); - exception.Data.Add("HelpLink.EvtID", errorCollection[0].Number.ToString(CultureInfo.InvariantCulture)); + if (errorCollection is not null && errorCollection.Count > 0) + { + exception.Data.Add("HelpLink.EvtID", errorCollection[0].Number.ToString(CultureInfo.InvariantCulture)); + } exception.Data.Add("HelpLink.BaseHelpUrl", "https://go.microsoft.com/fwlink"); exception.Data.Add("HelpLink.LinkId", "20476"); diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs index 7c7d3e8c87..9054f88ab7 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs @@ -21,7 +21,9 @@ #if NETFRAMEWORK using System.Runtime.InteropServices; using System.Runtime.Versioning; +#if _WINDOWS using Interop.Windows.Kernel32; +#endif #else using System.Net.Sockets; #endif diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs index 8931f445be..4183515fe7 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs @@ -1125,44 +1125,6 @@ public enum SqlCommandColumnEncryptionSetting /// Disabled, } - - /// - public enum SqlAuthenticationMethod - { - /// - NotSpecified = 0, - - /// - SqlPassword, - - /// - [Obsolete("ActiveDirectoryPassword is deprecated, use a more secure authentication method. See https://aka.ms/SqlClientEntraIDAuthentication for more details.")] - ActiveDirectoryPassword, - - /// - ActiveDirectoryIntegrated, - - /// - ActiveDirectoryInteractive, - - /// - ActiveDirectoryServicePrincipal, - - /// - ActiveDirectoryDeviceCodeFlow, - - /// - ActiveDirectoryManagedIdentity, - - /// - ActiveDirectoryMSI, - - /// - ActiveDirectoryDefault, - - /// - ActiveDirectoryWorkloadIdentity - } // This enum indicates the state of TransparentNetworkIPResolution // The first attempt when TNIR is on should be sequential. If the first attempt fails next attempts should be parallel. internal enum TransparentNetworkResolutionState @@ -1172,12 +1134,6 @@ internal enum TransparentNetworkResolutionState ParallelMode }; - internal class ActiveDirectoryAuthentication - { - internal const string AdoClientId = "2fd908ad-0664-4344-b9be-cd3e8b574c38"; - internal const string MSALGetAccessTokenFunctionName = "AcquireToken"; - } - // Fields in the first resultset of "sp_describe_parameter_encryption". // We expect the server to return the fields in the resultset in the same order as mentioned below. // If the server changes the below order, then transparent parameter encryption will break. diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs index dfdf69022f..d99b40cdea 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -30,7 +30,9 @@ #if NETFRAMEWORK using System.Runtime.CompilerServices; +#if _WINDOWS using Interop.Windows.Sni; +#endif using Microsoft.Data.SqlTypes; #endif @@ -4317,7 +4319,8 @@ private TdsOperationStatus TryProcessLoginAck(TdsParserStateObject stateObj, out private TdsOperationStatus TryProcessFedAuthInfo(TdsParserStateObject stateObj, int tokenLen, out SqlFedAuthInfo sqlFedAuthInfo) { sqlFedAuthInfo = null; - SqlFedAuthInfo tempFedAuthInfo = new SqlFedAuthInfo(); + string spn = null; + string stsUrl = null; // Skip reading token length, since it has already been read in caller SqlClientEventSource.Log.TryAdvancedTraceEvent(" FEDAUTHINFO token stream length = {0}", tokenLen); @@ -4410,15 +4413,15 @@ private TdsOperationStatus TryProcessFedAuthInfo(TdsParserStateObject stateObj, } SqlClientEventSource.Log.TryAdvancedTraceEvent(" FedAuthInfoData: {0}", data); - // store data in tempFedAuthInfo + // Store data in temporaries. switch ((TdsEnums.FedAuthInfoId)id) { case TdsEnums.FedAuthInfoId.Spn: - tempFedAuthInfo.spn = data; + spn = data; break; case TdsEnums.FedAuthInfoId.Stsurl: - tempFedAuthInfo.stsurl = data; + stsUrl = data; break; default: @@ -4433,15 +4436,16 @@ private TdsOperationStatus TryProcessFedAuthInfo(TdsParserStateObject stateObj, throw SQL.ParsingErrorLength(ParsingErrorState.FedAuthInfoLengthTooShortForData, tokenLen); } - SqlClientEventSource.Log.TryTraceEvent(" Processed FEDAUTHINFO token stream: {0}", tempFedAuthInfo); - if (string.IsNullOrWhiteSpace(tempFedAuthInfo.stsurl) || string.IsNullOrWhiteSpace(tempFedAuthInfo.spn)) + if (string.IsNullOrWhiteSpace(spn) || string.IsNullOrWhiteSpace(stsUrl)) { // We should be receiving both stsurl and spn SqlClientEventSource.Log.TryTraceEvent(" FEDAUTHINFO token stream does not contain both STSURL and SPN."); throw SQL.ParsingError(ParsingErrorState.FedAuthInfoDoesNotContainStsurlAndSpn); } - sqlFedAuthInfo = tempFedAuthInfo; + sqlFedAuthInfo = new(spn, stsUrl); + SqlClientEventSource.Log.TryTraceEvent(" Processed FEDAUTHINFO token stream: {0}", sqlFedAuthInfo); + return TdsOperationStatus.Done; } @@ -9601,11 +9605,11 @@ private int ApplyFeatureExData(TdsEnums.FeatureExtension requestedFeatures, internal void SendFedAuthToken(SqlFedAuthToken fedAuthToken) { Debug.Assert(fedAuthToken != null, "fedAuthToken cannot be null"); - Debug.Assert(fedAuthToken.accessToken != null, "fedAuthToken.accessToken cannot be null"); + Debug.Assert(fedAuthToken.AccessToken != null, "fedAuthToken.AccessToken cannot be null"); SqlClientEventSource.Log.TryTraceEvent(" Sending federated authentication token"); _physicalStateObj._outputMessageType = TdsEnums.MT_FEDAUTH; - byte[] accessToken = fedAuthToken.accessToken; + byte[] accessToken = fedAuthToken.AccessToken; // Send total length (length of token plus 4 bytes for the token length field) // If we were sending a nonce, this would include that length as well diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserHelperClasses.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserHelperClasses.cs index 8f0bb915c0..16386e1d9d 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserHelperClasses.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserHelperClasses.cs @@ -13,6 +13,7 @@ using System.Security; using System.Security.Authentication; using System.Text; +using System.Text.Encodings; using Microsoft.Data.Common; using Microsoft.Data.Common.ConnectionString; @@ -128,23 +129,53 @@ internal sealed class SqlLoginAck internal uint tdsVersion; } + #nullable enable + internal sealed class SqlFedAuthInfo { - internal string spn; - internal string stsurl; + internal string Spn { get; } + internal string StsUrl { get; } + + internal SqlFedAuthInfo(string spn, string stsurl) + { + Spn = spn; + StsUrl = stsurl; + } + public override string ToString() { - return $"STSURL: {stsurl}, SPN: {spn}"; + return $"SPN: {Spn}, STSURL: {StsUrl}"; } } internal sealed class SqlFedAuthToken { - internal uint dataLen; - internal byte[] accessToken; - internal long expirationFileTime; + internal byte[] AccessToken { get; } + internal uint DataLen { get; } + internal long ExpirationFileTime { get; } + + internal SqlFedAuthToken( + byte[] accessToken, + long expirationFileTime) + { + AccessToken = accessToken; + DataLen = (uint)AccessToken.Length; + ExpirationFileTime = expirationFileTime; + } + + /// + /// Convert from a SqlAuthenticationToken. + /// + internal SqlFedAuthToken(SqlAuthenticationToken token) + { + AccessToken = Encoding.Unicode.GetBytes(token.AccessToken); + DataLen = (uint)AccessToken.Length; + ExpirationFileTime = token.ExpiresOn.ToFileTime(); + } } + #nullable disable + internal sealed class _SqlMetaData : SqlMetaDataPriv { [Flags] diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Common.csproj b/src/Microsoft.Data.SqlClient/tests/Common/Common.csproj index ec728d6a98..1e827794ed 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/Common.csproj +++ b/src/Microsoft.Data.SqlClient/tests/Common/Common.csproj @@ -40,10 +40,10 @@ - + PreserveNewest xunit.runner.json - + @@ -51,12 +51,11 @@ - - PreserveNewest - %(Filename)%(Extension) - + + + diff --git a/src/Microsoft.Data.SqlClient/tests/CustomConfigurableRetryLogic/CustomRetryLogicProvider.csproj b/src/Microsoft.Data.SqlClient/tests/CustomConfigurableRetryLogic/CustomRetryLogicProvider.csproj index 1b35e8f80d..f420579f9e 100644 --- a/src/Microsoft.Data.SqlClient/tests/CustomConfigurableRetryLogic/CustomRetryLogicProvider.csproj +++ b/src/Microsoft.Data.SqlClient/tests/CustomConfigurableRetryLogic/CustomRetryLogicProvider.csproj @@ -10,9 +10,15 @@ - - - - + + + + + + + + + + diff --git a/src/Microsoft.Data.SqlClient/tests/Directory.Build.props b/src/Microsoft.Data.SqlClient/tests/Directory.Build.props index 71335f36ba..d790614874 100644 --- a/src/Microsoft.Data.SqlClient/tests/Directory.Build.props +++ b/src/Microsoft.Data.SqlClient/tests/Directory.Build.props @@ -11,7 +11,6 @@ true Debug;Release; AnyCPU;x86;x64 - Project diff --git a/src/Microsoft.Data.SqlClient/tests/Directory.Packages.props b/src/Microsoft.Data.SqlClient/tests/Directory.Packages.props index 4a031a49a6..e8d684f944 100644 --- a/src/Microsoft.Data.SqlClient/tests/Directory.Packages.props +++ b/src/Microsoft.Data.SqlClient/tests/Directory.Packages.props @@ -14,9 +14,4 @@ - - - - - diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AADAuthenticationTests.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AADAuthenticationTests.cs index f354e6f806..d86b6684d1 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AADAuthenticationTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AADAuthenticationTests.cs @@ -49,50 +49,29 @@ private void InvalidCombinationCheck(SqlCredential credential) Assert.Throws(() => connection.AccessToken = "SampleAccessToken"); } } - - #if NETFRAMEWORK - // This test is only valid for .NET Framework /// - /// Tests whether SQL Auth provider is overridden using app.config file. - /// This use case is only supported for .NET Framework applications, as driver doesn't support reading configuration from appsettings.json file. - /// In future if need be, appsettings.json support can be added. + /// Tests whether a dummy SQL Auth provider is registered due to + /// configuration in an app.config file. Only .NET Framework reads + /// from the app.config file, so this test is only valid for that + /// runtime. + /// + /// See the app.config file in the same directory as this file. + /// + /// .NET (Core) reads similar configuration from appsettings.json, but + /// our SqlAuthenticationProviderManager does not currently support + /// that configuration source. /// - [Fact] + [ConditionalFact(typeof(TestUtility), nameof(TestUtility.IsNetFramework))] public async Task IsDummySqlAuthenticationProviderSetByDefault() { var provider = SqlAuthenticationProvider.GetProvider(SqlAuthenticationMethod.ActiveDirectoryInteractive); Assert.NotNull(provider); - Assert.Equal(typeof(DummySqlAuthenticationProvider), provider.GetType()); + Assert.IsType(provider); var token = await provider.AcquireTokenAsync(null); Assert.Equal(token.AccessToken, DummySqlAuthenticationProvider.DUMMY_TOKEN_STR); } - #endif - - [Fact] - public void CustomActiveDirectoryProviderTest() - { - SqlAuthenticationProvider authProvider = new ActiveDirectoryAuthenticationProvider(static (result) => Task.CompletedTask); - SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, authProvider); - Assert.Equal(authProvider, SqlAuthenticationProvider.GetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow)); - } - - [Fact] - public void CustomActiveDirectoryProviderTest_AppClientId() - { - SqlAuthenticationProvider authProvider = new ActiveDirectoryAuthenticationProvider(Guid.NewGuid().ToString()); - SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, authProvider); - Assert.Equal(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.Equal(authProvider, SqlAuthenticationProvider.GetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow)); - } } } diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AssertExtensions.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AssertExtensions.cs index 1a37bc6f12..7a2acc1160 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AssertExtensions.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AssertExtensions.cs @@ -13,7 +13,7 @@ namespace System { public static class AssertExtensions { - private static bool IsFullFramework => TestUtility.IsFullFramework; + private static bool IsFullFramework => TestUtility.IsNetFramework; public static void Throws(Action action, string message) where T : Exception @@ -36,7 +36,7 @@ public static void Throws(string netCoreParamName, string netFxParamName, Act IsFullFramework ? netFxParamName : netCoreParamName; - if (!TestUtility.NetNative) + if (!TestUtility.IsNetNative) { Assert.Equal(expectedParamName, exception.ParamName); } @@ -57,7 +57,7 @@ public static void Throws(string netCoreParamName, string netFxParamName, Fun IsFullFramework ? netFxParamName : netCoreParamName; - if (!TestUtility.NetNative) + if (!TestUtility.IsNetNative) { Assert.Equal(expectedParamName, exception.ParamName); } @@ -68,7 +68,7 @@ public static T Throws(string paramName, Action action) { T exception = Assert.Throws(action); - if (!TestUtility.NetNative) + if (!TestUtility.IsNetNative) { Assert.Equal(paramName, exception.ParamName); } @@ -89,7 +89,7 @@ public static T Throws(string paramName, Func testCode) { T exception = Assert.Throws(testCode); - if (!TestUtility.NetNative) + if (!TestUtility.IsNetNative) { Assert.Equal(paramName, exception.ParamName); } @@ -102,7 +102,7 @@ public static async Task ThrowsAsync(string paramName, Func testCode { T exception = await Assert.ThrowsAsync(testCode); - if (!TestUtility.NetNative) + if (!TestUtility.IsNetNative) { Assert.Equal(paramName, exception.ParamName); } diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/DataCommon/DummySqlAuthenticationProvider.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/DataCommon/DummySqlAuthenticationProvider.cs index bb5e2e2e52..c68baf63eb 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/DataCommon/DummySqlAuthenticationProvider.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/DataCommon/DummySqlAuthenticationProvider.cs @@ -20,8 +20,9 @@ public class DummySqlAuthenticationProvider : SqlAuthenticationProvider public override Task AcquireTokenAsync(SqlAuthenticationParameters parameters) => Task.FromResult(new SqlAuthenticationToken(DUMMY_TOKEN_STR, new DateTimeOffset(DateTime.Now.AddHours(2)))); - // Supported authentication modes don't matter for dummy test, but added to demonstrate config file usage. public override bool IsSupported(SqlAuthenticationMethod authenticationMethod) - => authenticationMethod == SqlAuthenticationMethod.ActiveDirectoryInteractive; + { + return authenticationMethod == SqlAuthenticationMethod.ActiveDirectoryInteractive; + } } } diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/DataCommon/TestUtility.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/DataCommon/TestUtility.cs index c4c2ac4d0f..bcbc68a502 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/DataCommon/TestUtility.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/DataCommon/TestUtility.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; using System.Runtime.InteropServices; namespace Microsoft.Data.SqlClient.Tests @@ -10,7 +9,18 @@ namespace Microsoft.Data.SqlClient.Tests public static class TestUtility { public static readonly bool IsNotArmProcess = RuntimeInformation.ProcessArchitecture != Architecture.Arm; - public static bool IsFullFramework => RuntimeInformation.FrameworkDescription.StartsWith(".NET Framework"); - public static bool NetNative => RuntimeInformation.FrameworkDescription.StartsWith(".NET Native"); + public static bool IsNet + { + get + { + return + !IsNetCore && !IsNetFramework && !IsNetNative && + RuntimeInformation.FrameworkDescription.StartsWith(".NET"); + } + } + + public static bool IsNetCore => RuntimeInformation.FrameworkDescription.StartsWith(".NET Core"); + public static bool IsNetFramework => RuntimeInformation.FrameworkDescription.StartsWith(".NET Framework"); + public static bool IsNetNative => RuntimeInformation.FrameworkDescription.StartsWith(".NET Native"); } } diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlAuthenticationProviderManagerTests.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlAuthenticationProviderManagerTests.cs new file mode 100644 index 0000000000..ecc9d8fa0f --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlAuthenticationProviderManagerTests.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Data.SqlClient.FunctionalTests.DataCommon; +using Xunit; + +namespace Microsoft.Data.SqlClient.Tests +{ + public class SqlAuthenticationProviderManagerTests + { + // The FunctionalTests project employs a .NET Framework app.config file + // that configures a dummy authentication provider for + // ActiveDirectoryInteractive authentication. Verify that this is + // respected. + [ConditionalFact(typeof(TestUtility), nameof(TestUtility.IsNetFramework))] + public void DefaultAuthenticationProviders_AppConfig() + { + // The provider for ActiveDirectoryInteractive should be our dummy + // provider. + Assert.IsType( + SqlAuthenticationProvider.GetProvider( + SqlAuthenticationMethod.ActiveDirectoryInteractive)); + + // There should be no provider for other methods. Spot-check a few. + Assert.Null(SqlAuthenticationProvider.GetProvider( + #pragma warning disable CS0618 // Type or member is obsolete + SqlAuthenticationMethod.ActiveDirectoryPassword)); + #pragma warning restore CS0618 // Type or member is obsolete + + Assert.Null(SqlAuthenticationProvider.GetProvider( + SqlAuthenticationMethod.ActiveDirectoryManagedIdentity)); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlAuthenticationProviderTest.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlAuthenticationProviderTest.cs deleted file mode 100644 index 8168c26f8e..0000000000 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlAuthenticationProviderTest.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using Microsoft.Data.SqlClient.FunctionalTests.DataCommon; -using Xunit; - -namespace Microsoft.Data.SqlClient.Tests -{ - public class SqlAuthenticationProviderTest - { - [Theory] - [InlineData(SqlAuthenticationMethod.ActiveDirectoryIntegrated)] - #pragma warning disable 0618 // Type or member is obsolete - [InlineData(SqlAuthenticationMethod.ActiveDirectoryPassword)] - #pragma warning restore 0618 // Type or member is obsolete - [InlineData(SqlAuthenticationMethod.ActiveDirectoryServicePrincipal)] - [InlineData(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow)] - [InlineData(SqlAuthenticationMethod.ActiveDirectoryManagedIdentity)] - [InlineData(SqlAuthenticationMethod.ActiveDirectoryMSI)] - [InlineData(SqlAuthenticationMethod.ActiveDirectoryDefault)] - [InlineData(SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity)] - public void DefaultAuthenticationProviders(SqlAuthenticationMethod method) - { - Assert.IsType(SqlAuthenticationProvider.GetProvider(method)); - } - - #if NETFRAMEWORK - // This test is only valid for .NET Framework - - // Overridden by app.config in this project - [Theory] - [InlineData(SqlAuthenticationMethod.ActiveDirectoryInteractive)] - public void DefaultAuthenticationProviders_Interactive(SqlAuthenticationMethod method) - { - Assert.IsType(SqlAuthenticationProvider.GetProvider(method)); - } - - #endif - } -} diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/app.config b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/app.config index fb7f63f65f..9fc08c65a7 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/app.config +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/app.config @@ -7,6 +7,7 @@ + diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs index 0bb9654efc..a62bb9787b 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs @@ -239,6 +239,15 @@ static DataTestUtility() AEConnStringsSetup.Add(TCPConnectionString); } } + + // Many of our tests require a Managed Identity provider to be + // registered. + // + // TODO: Figure out which ones and install on-demand rather than + // globally. + SqlAuthenticationProvider.SetProvider( + SqlAuthenticationMethod.ActiveDirectoryManagedIdentity, + new ManagedIdentityProvider()); } public static IEnumerable ConnectionStrings => GetConnectionStrings(withEnclave: true); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/ManagedIdentityProvider.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/ManagedIdentityProvider.cs new file mode 100644 index 0000000000..0e590be0f9 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/ManagedIdentityProvider.cs @@ -0,0 +1,97 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; + +using Azure.Core; +using Azure.Identity; + +#nullable enable + +namespace Microsoft.Data.SqlClient.ManualTesting.Tests; + +internal class ManagedIdentityProvider : SqlAuthenticationProvider +{ + // Our cache of managed identity user Ids to credential instances. + private readonly ConcurrentDictionary + _credentialCache = new(); + + // The default suffix to apply to resource scopes. + private const string s_defaultScopeSuffix = "/.default"; + + // Acquire a token using Managed Identity. The UserId in the parameters is + // used as the managed identity client ID. Tokens are cached per UserId. + // + // GOTCHA: This assumes that the Resource and Authority in the parameters + // never change for a given UserId, which is probably a safe assumption for + // tests. + // + public override async Task AcquireTokenAsync( + SqlAuthenticationParameters parameters) + { + if (parameters.UserId is null) + { + throw new TokenException( + "Refusing to acquire token for ManagedIdentity with null UserId"); + } + + try + { + // Build an appropriate scope. + string scope = parameters.Resource.EndsWith( + s_defaultScopeSuffix, StringComparison.Ordinal) + ? parameters.Resource + : parameters.Resource + s_defaultScopeSuffix; + + TokenRequestContext context = new([scope]); + + TokenCredentialOptions options = new() + { + AuthorityHost = new Uri(parameters.Authority) + }; + + // Create or re-use the ManagedIdentityCredential for this UserId. + ManagedIdentityCredential credential = + _credentialCache.GetOrAdd( + parameters.UserId, + (_) => new(parameters.UserId, options)); + + // Set up a cancellation token based on the authentication timeout, + // ignoring overflow since this is just test code. + using CancellationTokenSource cancellor = new(); + cancellor.CancelAfter(parameters.ConnectionTimeout * 1000); + + // Acquire the token, which may be cached by the credential. + AccessToken token = + await credential.GetTokenAsync(context, cancellor.Token) + .ConfigureAwait(false); + + return new(token.Token, token.ExpiresOn); + } + catch (Exception ex) + { + throw new TokenException( + $"Failed to acquire token for ManagedIdentity " + + $"userId ={parameters.UserId} error={ex.Message}", ex); + } + } + + /// We support only the Managed Identity authentication method. + public override bool IsSupported(SqlAuthenticationMethod authenticationMethod) + { + return authenticationMethod == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity; + } + + // The exception we throw on any errors acquiring tokens. + private sealed class TokenException : SqlAuthenticationProviderException + { + internal TokenException(string message, Exception? causedBy = null) + : base(message, causedBy) + { + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/UsernamePasswordProvider.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/UsernamePasswordProvider.cs new file mode 100644 index 0000000000..5f9ca1eff0 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/UsernamePasswordProvider.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Security; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Identity.Client; + +#nullable enable + +namespace Microsoft.Data.SqlClient.ManualTesting.Tests; + +internal class UsernamePasswordProvider : SqlAuthenticationProvider +{ + readonly string _appClientId; + const string s_defaultScopeSuffix = "/.default"; + + internal UsernamePasswordProvider(string appClientId) + { + _appClientId = appClientId; + } + + public override async Task AcquireTokenAsync(SqlAuthenticationParameters parameters) + { + try + { + string scope = + parameters.Resource.EndsWith(s_defaultScopeSuffix, StringComparison.Ordinal) + ? parameters.Resource + : parameters.Resource + s_defaultScopeSuffix; + + using CancellationTokenSource cts = new(); + cts.CancelAfter(parameters.ConnectionTimeout * 1000); + + AuthenticationResult result = + #pragma warning disable CS0618 // Type or member is obsolete + await PublicClientApplicationBuilder.Create(_appClientId) + .WithAuthority(parameters.Authority) + .Build() + .AcquireTokenByUsernamePassword([scope], parameters.UserId, parameters.Password) + #pragma warning restore CS0618 // Type or member is obsolete + .WithCorrelationId(parameters.ConnectionId) + .ExecuteAsync(cancellationToken: cts.Token); + + return new SqlAuthenticationToken(result.AccessToken, result.ExpiresOn); + } + catch (Exception ex) + { + throw new TokenException( + $"Failed to acquire token for username/password " + + $"userId ={parameters.UserId} error={ex.Message}", ex); + } + } + + public override bool IsSupported(SqlAuthenticationMethod authenticationMethod) + { + #pragma warning disable 0618 // Type or member is obsolete + return authenticationMethod.Equals(SqlAuthenticationMethod.ActiveDirectoryPassword); + #pragma warning restore 0618 // Type or member is obsolete + } + + // The exception we throw on any errors acquiring tokens. + private sealed class TokenException : SqlAuthenticationProviderException + { + internal TokenException(string message, Exception? causedBy = null) + : base(message, causedBy) + { + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj index 3155b87f81..945270d65a 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj @@ -272,9 +272,11 @@ + + @@ -293,6 +295,8 @@ + + Common @@ -313,11 +317,30 @@ - - - + + + + + + + + + + + + + + + @@ -333,7 +356,6 @@ - @@ -348,11 +370,11 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - - PreserveNewest - %(Filename)%(Extension) - + + + + PreserveNewest @@ -366,9 +388,9 @@ Always - + PreserveNewest xunit.runner.json - + diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AADFedAuthTokenRefreshTest/AADFedAuthTokenRefreshTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AADFedAuthTokenRefreshTest/AADFedAuthTokenRefreshTest.cs index 54900a4dd0..e99192732d 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AADFedAuthTokenRefreshTest/AADFedAuthTokenRefreshTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AADFedAuthTokenRefreshTest/AADFedAuthTokenRefreshTest.cs @@ -22,10 +22,19 @@ public AADFedAuthTokenRefreshTest(ITestOutputHelper testOutputHelper) [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAADPasswordConnStrSetup))] public void FedAuthTokenRefreshTest() { - string connectionString = DataTestUtility.AADPasswordConnectionString; + #pragma warning disable 0618 // Type or member is obsolete + SqlAuthenticationProvider original = SqlAuthenticationProvider.GetProvider(SqlAuthenticationMethod.ActiveDirectoryPassword); + #pragma warning restore 0618 // Type or member is obsolete - using (SqlConnection connection = new SqlConnection(connectionString)) + try { + #pragma warning disable 0618 // Type or member is obsolete + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryPassword, new UsernamePasswordProvider(DataTestUtility.ApplicationClientId)); + #pragma warning restore 0618 // Type or member is obsolete + + string connectionString = DataTestUtility.AADPasswordConnectionString; + + using SqlConnection connection = new SqlConnection(connectionString); connection.Open(); string oldTokenHash = ""; @@ -65,6 +74,16 @@ public void FedAuthTokenRefreshTest() Assert.True(newLocalExpiryTime > oldLocalExpiryTime, "The refreshed token must have a new or later expiry time."); } } + finally + { + if (original is not null) + { + // Reset to driver internal provider. + #pragma warning disable 0618 // Type or member is obsolete + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryPassword, original); + #pragma warning restore 0618 // Type or member is obsolete + } + } } [Conditional("DEBUG")] diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/AADConnectionTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/AADConnectionTest.cs index 608c34c977..f2c0128c46 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/AADConnectionTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/AADConnectionTest.cs @@ -5,60 +5,14 @@ using System; using System.Diagnostics; using System.Security; -using System.Threading; using System.Threading.Tasks; using Azure.Core; -using Azure.Identity; -using Microsoft.Identity.Client; using Xunit; namespace Microsoft.Data.SqlClient.ManualTesting.Tests { - public class AADConnectionsTest + public class AADConnectionTest { - class CustomSqlAuthenticationProvider : SqlAuthenticationProvider - { - string _appClientId; - - internal CustomSqlAuthenticationProvider(string appClientId) - { - _appClientId = appClientId; - } - - public override async Task AcquireTokenAsync(SqlAuthenticationParameters parameters) - { - string s_defaultScopeSuffix = "/.default"; - string scope = parameters.Resource.EndsWith(s_defaultScopeSuffix, StringComparison.Ordinal) ? parameters.Resource : parameters.Resource + s_defaultScopeSuffix; - - _ = parameters.ServerName; - _ = parameters.DatabaseName; - _ = parameters.ConnectionId; - - var cts = new CancellationTokenSource(); - cts.CancelAfter(parameters.ConnectionTimeout * 1000); - - string[] scopes = new string[] { scope }; - SecureString password = new SecureString(); - -#pragma warning disable CS0618 // Type or member is obsolete - AuthenticationResult result = await PublicClientApplicationBuilder.Create(_appClientId) - .WithAuthority(parameters.Authority) - .Build().AcquireTokenByUsernamePassword(scopes, parameters.UserId, parameters.Password) - .WithCorrelationId(parameters.ConnectionId) - .ExecuteAsync(cancellationToken: cts.Token); -#pragma warning restore CS0618 // Type or member is obsolete - - return new SqlAuthenticationToken(result.AccessToken, result.ExpiresOn); - } - - public override bool IsSupported(SqlAuthenticationMethod authenticationMethod) - { - #pragma warning disable 0618 // Type or member is obsolete - return authenticationMethod.Equals(SqlAuthenticationMethod.ActiveDirectoryPassword); - #pragma warning restore 0618 // Type or member is obsolete - } - } - private static void ConnectAndDisconnect(string connectionString, SqlCredential credential = null) { using (SqlConnection conn = new SqlConnection(connectionString)) @@ -80,15 +34,6 @@ private static void ConnectAndDisconnect(string connectionString, SqlCredential private static bool IsManagedIdentitySetup() => DataTestUtility.ManagedIdentitySupported; private static bool SupportsSystemAssignedManagedIdentity() => DataTestUtility.SupportsSystemAssignedManagedIdentity; - [PlatformSpecific(TestPlatforms.Windows)] - [ConditionalFact(nameof(IsAccessTokenSetup), nameof(IsAADConnStringsSetup), nameof(IsManagedIdentitySetup))] - 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 = {DataTestUtility.UserManagedIdentityClientId};"); - connection.Open(); - Assert.True(connection.State == System.Data.ConnectionState.Open); - } [ConditionalFact(nameof(IsAccessTokenSetup), nameof(IsAADConnStringsSetup))] public static void AccessTokenTest() @@ -214,47 +159,33 @@ public static void AADPasswordWithIntegratedSecurityTrue() Assert.Contains(expectedMessage, e.Message); } - [ConditionalFact(nameof(IsAccessTokenSetup), nameof(IsAADConnStringsSetup))] - public static void AADPasswordWithWrongPassword() - { - string[] credKeys = { "Password", "PWD" }; - string connStr = DataTestUtility.RemoveKeysInConnStr(DataTestUtility.AADPasswordConnectionString, credKeys) + "Password=TestPassword;"; - - Assert.Throws(() => ConnectAndDisconnect(connStr)); - - // We cannot verify error message with certainity as driver may cache token from other tests for current user - // and error message may change accordingly. - } - [ConditionalFact(nameof(IsAADConnStringsSetup))] public static void GetAccessTokenByPasswordTest() { - // Clear token cache for code coverage. - ActiveDirectoryAuthenticationProvider.ClearUserTokenCache(); - using (SqlConnection connection = new SqlConnection(DataTestUtility.AADPasswordConnectionString)) + #pragma warning disable 0618 // Type or member is obsolete + SqlAuthenticationProvider original = SqlAuthenticationProvider.GetProvider(SqlAuthenticationMethod.ActiveDirectoryPassword); + #pragma warning restore 0618 // Type or member is obsolete + + try { - connection.Open(); - Assert.True(connection.State == System.Data.ConnectionState.Open); - } - } + #pragma warning disable 0618 // Type or member is obsolete + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryPassword, new UsernamePasswordProvider(DataTestUtility.ApplicationClientId)); + #pragma warning restore 0618 // Type or member is obsolete - [ConditionalFact(nameof(IsAADConnStringsSetup))] - public static void TestADPasswordAuthentication() - { - // Connect to Azure DB with password and retrieve user name. - using (SqlConnection conn = new SqlConnection(DataTestUtility.AADPasswordConnectionString)) + using (SqlConnection connection = new SqlConnection(DataTestUtility.AADPasswordConnectionString)) + { + connection.Open(); + Assert.True(connection.State == System.Data.ConnectionState.Open); + } + } + finally { - conn.Open(); - using (SqlCommand sqlCommand = new SqlCommand - ( - cmdText: $"SELECT SUSER_SNAME();", - connection: conn, - transaction: null - )) + if (original is not null) { - string customerId = (string)sqlCommand.ExecuteScalar(); - string expected = DataTestUtility.RetrieveValueFromConnStr(DataTestUtility.AADPasswordConnectionString, new string[] { "User ID", "UID" }); - Assert.Equal(expected, customerId); + // Reset to driver internal provider. + #pragma warning disable 0618 // Type or member is obsolete + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryPassword, original); + #pragma warning restore 0618 // Type or member is obsolete } } } @@ -263,28 +194,41 @@ public static void TestADPasswordAuthentication() public static void TestCustomProviderAuthentication() { #pragma warning disable 0618 // Type or member is obsolete - SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryPassword, new CustomSqlAuthenticationProvider(DataTestUtility.ApplicationClientId)); + SqlAuthenticationProvider original = SqlAuthenticationProvider.GetProvider(SqlAuthenticationMethod.ActiveDirectoryPassword); #pragma warning restore 0618 // Type or member is obsolete - // Connect to Azure DB with password and retrieve user name using custom authentication provider - using (SqlConnection conn = new SqlConnection(DataTestUtility.AADPasswordConnectionString)) + + try { - conn.Open(); - using (SqlCommand sqlCommand = new SqlCommand - ( - cmdText: $"SELECT SUSER_SNAME();", - connection: conn, - transaction: null - )) + #pragma warning disable 0618 // Type or member is obsolete + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryPassword, new UsernamePasswordProvider(DataTestUtility.ApplicationClientId)); + #pragma warning restore 0618 // Type or member is obsolete + // Connect to Azure DB with password and retrieve user name using custom authentication provider + using (SqlConnection conn = new SqlConnection(DataTestUtility.AADPasswordConnectionString)) { - string customerId = (string)sqlCommand.ExecuteScalar(); - string expected = DataTestUtility.RetrieveValueFromConnStr(DataTestUtility.AADPasswordConnectionString, new string[] { "User ID", "UID" }); - Assert.Equal(expected, customerId); + conn.Open(); + using (SqlCommand sqlCommand = new SqlCommand + ( + cmdText: $"SELECT SUSER_SNAME();", + connection: conn, + transaction: null + )) + { + string customerId = (string)sqlCommand.ExecuteScalar(); + string expected = DataTestUtility.RetrieveValueFromConnStr(DataTestUtility.AADPasswordConnectionString, new string[] { "User ID", "UID" }); + Assert.Equal(expected, customerId); + } + } + } + finally + { + if (original is not null) + { + // Reset to driver internal provider. + #pragma warning disable 0618 // Type or member is obsolete + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryPassword, original); + #pragma warning restore 0618 // Type or member is obsolete } } - // Reset to driver internal provider. - #pragma warning disable 0618 // Type or member is obsolete - SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryPassword, new ActiveDirectoryAuthenticationProvider(DataTestUtility.ApplicationClientId)); - #pragma warning restore 0618 // Type or member is obsolete } [ConditionalFact(nameof(IsAADConnStringsSetup))] @@ -321,92 +265,6 @@ public static void MFAAuthWithPassword() Assert.Contains(expectedMessage, e.Message); } - [ConditionalFact(nameof(IsAADConnStringsSetup))] - public static void EmptyPasswordInConnStrAADPassword() - { - // connection fails with expected error message. - string[] pwdKey = { "Password", "PWD" }; - string connStr = DataTestUtility.RemoveKeysInConnStr(DataTestUtility.AADPasswordConnectionString, pwdKey) + "Password=;"; - SqlException e = Assert.Throws(() => ConnectAndDisconnect(connStr)); - - string user = DataTestUtility.FetchKeyInConnStr(DataTestUtility.AADPasswordConnectionString, 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); - } - - [PlatformSpecific(TestPlatforms.Windows)] - [ConditionalFact(nameof(IsAADConnStringsSetup))] - public static void EmptyCredInConnStrAADPassword() - { - // connection fails with expected error message. - string[] removeKeys = { "User ID", "Password", "UID", "PWD" }; - string connStr = DataTestUtility.RemoveKeysInConnStr(DataTestUtility.AADPasswordConnectionString, 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); - } - - [PlatformSpecific(TestPlatforms.AnyUnix)] - [ConditionalFact(nameof(IsAADConnStringsSetup))] - public static void EmptyCredInConnStrAADPasswordAnyUnix() - { - // connection fails with expected error message. - string[] removeKeys = { "User ID", "Password", "UID", "PWD" }; - string connStr = DataTestUtility.RemoveKeysInConnStr(DataTestUtility.AADPasswordConnectionString, 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(nameof(IsAADConnStringsSetup))] - public static void AADPasswordWithInvalidUser() - { - // connection fails with expected error message. - string[] removeKeys = { "User ID", "UID" }; - string user = "testdotnet@domain.com"; - string connStr = DataTestUtility.RemoveKeysInConnStr(DataTestUtility.AADPasswordConnectionString, 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(nameof(IsAADConnStringsSetup))] - public static void NoCredentialsActiveDirectoryPassword() - { - // test Passes with correct connection string. - ConnectAndDisconnect(DataTestUtility.AADPasswordConnectionString); - - // connection fails with expected error message. - string[] credKeys = { "User ID", "Password", "UID", "PWD" }; - string connStrWithNoCred = DataTestUtility.RemoveKeysInConnStr(DataTestUtility.AADPasswordConnectionString, 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(DataTestUtility), nameof(DataTestUtility.IsAADServicePrincipalSetup))] - public static void NoCredentialsActiveDirectoryServicePrincipal() - { - // test Passes with correct connection string. - string[] removeKeys = { "Authentication", "User ID", "Password", "UID", "PWD" }; - string connStr = DataTestUtility.RemoveKeysInConnStr(DataTestUtility.AADPasswordConnectionString, removeKeys) + - $"Authentication=Active Directory Service Principal; User ID={DataTestUtility.AADServicePrincipalId}; PWD={DataTestUtility.AADServicePrincipalSecret};"; - ConnectAndDisconnect(connStr); - - // connection fails with expected error message. - string[] credKeys = { "Authentication", "User ID", "Password", "UID", "PWD" }; - string connStrWithNoCred = DataTestUtility.RemoveKeysInConnStr(DataTestUtility.AADPasswordConnectionString, 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); - } - [ConditionalFact(nameof(IsAADConnStringsSetup))] public static void ActiveDirectoryDeviceCodeFlowWithUserIdMustFail() { @@ -497,22 +355,6 @@ public static void ActiveDirectoryManagedIdentityWithPasswordMustFail() Assert.Contains(expectedMessage, e.Message); } - [InlineData("2445343 2343253")] - [InlineData("2445343$#^@@%2343253")] - [ConditionalTheory(nameof(IsAADConnStringsSetup), nameof(IsManagedIdentitySetup))] - public static void ActiveDirectoryManagedIdentityWithInvalidUserIdMustFail(string userId) - { - // connection fails with expected error message. - string[] credKeys = { "Authentication", "User ID", "Password", "UID", "PWD" }; - string connStrWithNoCred = DataTestUtility.RemoveKeysInConnStr(DataTestUtility.AADPasswordConnectionString, credKeys) + - $"Authentication=Active Directory Managed Identity; User Id={userId}"; - - SqlException e = Assert.Throws(() => ConnectAndDisconnect(connStrWithNoCred)); - - string expectedMessage = "[Managed Identity] Authentication unavailable"; - Assert.Contains(expectedMessage, e.GetBaseException().Message, StringComparison.OrdinalIgnoreCase); - } - [ConditionalFact(nameof(IsAADConnStringsSetup))] public static void ActiveDirectoryMSIWithCredentialsMustFail() { @@ -654,85 +496,66 @@ public static void AccessTokenCallbackReceivesUsernameAndPassword() } } - [ConditionalFact(nameof(IsAADConnStringsSetup), nameof(IsManagedIdentitySetup))] - public static void ActiveDirectoryDefaultMustPass() - { - string[] credKeys = { "Authentication", "User ID", "Password", "UID", "PWD" }; - string connStr = DataTestUtility.RemoveKeysInConnStr(DataTestUtility.AADPasswordConnectionString, credKeys) + - $"Authentication=ActiveDirectoryDefault;User ID={DataTestUtility.UserManagedIdentityClientId};"; - - // Connection should be established using Managed Identity by default. - ConnectAndDisconnect(connStr); - } - - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsIntegratedSecuritySetup), nameof(DataTestUtility.AreConnStringsSetup))] - public static void ADIntegratedUsingSSPI() - { - // test Passes with correct connection string. - string[] removeKeys = { "Authentication", "User ID", "Password", "UID", "PWD", "Trusted_Connection", "Integrated Security" }; - string connStr = DataTestUtility.RemoveKeysInConnStr(DataTestUtility.TCPConnectionString, removeKeys) + - $"Authentication=Active Directory Integrated;"; - ConnectAndDisconnect(connStr); - } - // Test passes locally everytime, but in pieplines fails randomly with uncertainity. // e.g. Second AAD connection too slow (802ms)! (More than 30% of the first (576ms).) [ActiveIssue("16058")] [ConditionalFact(nameof(IsAADConnStringsSetup))] public static void ConnectionSpeed() { - var connString = DataTestUtility.AADPasswordConnectionString; + #pragma warning disable 0618 // Type or member is obsolete + SqlAuthenticationProvider original = SqlAuthenticationProvider.GetProvider(SqlAuthenticationMethod.ActiveDirectoryPassword); + #pragma warning restore 0618 // Type or member is obsolete - //Ensure server endpoints are warm - using (var connectionDrill = new SqlConnection(connString)) + try { - connectionDrill.Open(); - } + #pragma warning disable 0618 // Type or member is obsolete + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryPassword, new UsernamePasswordProvider(DataTestUtility.ApplicationClientId)); + #pragma warning restore 0618 // Type or member is obsolete - SqlConnection.ClearAllPools(); - ActiveDirectoryAuthenticationProvider.ClearUserTokenCache(); + var connString = DataTestUtility.AADPasswordConnectionString; - Stopwatch firstConnectionTime = new Stopwatch(); - Stopwatch secondConnectionTime = new Stopwatch(); + //Ensure server endpoints are warm + using (var connectionDrill = new SqlConnection(connString)) + { + connectionDrill.Open(); + } + + SqlConnection.ClearAllPools(); - using (var connectionDrill = new SqlConnection(connString)) + Stopwatch firstConnectionTime = new Stopwatch(); + Stopwatch secondConnectionTime = new Stopwatch(); + + using (var connectionDrill = new SqlConnection(connString)) + { + firstConnectionTime.Start(); + connectionDrill.Open(); + firstConnectionTime.Stop(); + using (var connectionDrill2 = new SqlConnection(connString)) + { + secondConnectionTime.Start(); + connectionDrill2.Open(); + secondConnectionTime.Stop(); + } + } + + // Subsequent AAD connections within a short timeframe should use an auth token cached from the connection pool + // Second connection speed in tests was typically 10-15% of the first connection time. Using 30% since speeds may vary. + Assert.True(((double)secondConnectionTime.ElapsedMilliseconds / firstConnectionTime.ElapsedMilliseconds) < 0.30, $"Second AAD connection too slow ({secondConnectionTime.ElapsedMilliseconds}ms)! (More than 30% of the first ({firstConnectionTime.ElapsedMilliseconds}ms).)"); + } + finally { - firstConnectionTime.Start(); - connectionDrill.Open(); - firstConnectionTime.Stop(); - using (var connectionDrill2 = new SqlConnection(connString)) + if (original is not null) { - secondConnectionTime.Start(); - connectionDrill2.Open(); - secondConnectionTime.Stop(); + // Reset to driver internal provider. + #pragma warning disable 0618 // Type or member is obsolete + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryPassword, original); + #pragma warning restore 0618 // Type or member is obsolete } } - - // Subsequent AAD connections within a short timeframe should use an auth token cached from the connection pool - // Second connection speed in tests was typically 10-15% of the first connection time. Using 30% since speeds may vary. - Assert.True(((double)secondConnectionTime.ElapsedMilliseconds / firstConnectionTime.ElapsedMilliseconds) < 0.30, $"Second AAD connection too slow ({secondConnectionTime.ElapsedMilliseconds}ms)! (More than 30% of the first ({firstConnectionTime.ElapsedMilliseconds}ms).)"); } #region Managed Identity Authentication tests - [ConditionalFact(nameof(IsAADConnStringsSetup), nameof(IsManagedIdentitySetup), nameof(SupportsSystemAssignedManagedIdentity))] - public static void SystemAssigned_ManagedIdentityTest() - { - string[] removeKeys = { "Authentication", "User ID", "Password", "UID", "PWD" }; - string connStr = DataTestUtility.RemoveKeysInConnStr(DataTestUtility.AADPasswordConnectionString, removeKeys) + - $"Authentication=Active Directory Managed Identity;"; - ConnectAndDisconnect(connStr); - } - - [ConditionalFact(nameof(IsAADConnStringsSetup), nameof(IsManagedIdentitySetup))] - public static void UserAssigned_ManagedIdentityTest() - { - string[] removeKeys = { "Authentication", "User ID", "Password", "UID", "PWD" }; - string connStr = DataTestUtility.RemoveKeysInConnStr(DataTestUtility.AADPasswordConnectionString, removeKeys) + - $"Authentication=Active Directory Managed Identity; User Id={DataTestUtility.UserManagedIdentityClientId};"; - ConnectAndDisconnect(connStr); - } - [ConditionalFact(nameof(IsAADConnStringsSetup), nameof(IsManagedIdentitySetup), nameof(SupportsSystemAssignedManagedIdentity))] public static void AccessToken_SystemManagedIdentityTest() { @@ -761,36 +584,6 @@ public static void AccessToken_UserManagedIdentityTest() } } - [ConditionalFact(nameof(AreConnStringsSetup), nameof(IsAzure), nameof(IsManagedIdentitySetup), nameof(SupportsSystemAssignedManagedIdentity))] - public static void Azure_SystemManagedIdentityTest() - { - string[] removeKeys = { "Authentication", "User ID", "Password", "UID", "PWD", "Trusted_Connection", "Integrated Security" }; - string connectionString = DataTestUtility.RemoveKeysInConnStr(DataTestUtility.TCPConnectionString, removeKeys) - + $"Authentication=Active Directory Managed Identity;"; - - using (SqlConnection conn = new SqlConnection(connectionString)) - { - conn.Open(); - - Assert.True(conn.State == System.Data.ConnectionState.Open); - } - } - - [ConditionalFact(nameof(AreConnStringsSetup), nameof(IsAzure), nameof(IsManagedIdentitySetup))] - public static void Azure_UserManagedIdentityTest() - { - string[] removeKeys = { "Authentication", "User ID", "Password", "UID", "PWD", "Trusted_Connection", "Integrated Security" }; - string connectionString = DataTestUtility.RemoveKeysInConnStr(DataTestUtility.TCPConnectionString, removeKeys) - + $"Authentication=Active Directory Managed Identity; User Id={DataTestUtility.UserManagedIdentityClientId}"; - - using (SqlConnection conn = new SqlConnection(connectionString)) - { - conn.Open(); - - Assert.True(conn.State == System.Data.ConnectionState.Open); - } - } - [ConditionalFact(nameof(AreConnStringsSetup), nameof(IsAzure), nameof(IsAccessTokenSetup), nameof(IsManagedIdentitySetup), nameof(SupportsSystemAssignedManagedIdentity))] public static void Azure_AccessToken_SystemManagedIdentityTest() { diff --git a/src/Microsoft.Data.SqlClient/tests/PerformanceTests/Microsoft.Data.SqlClient.PerformanceTests.csproj b/src/Microsoft.Data.SqlClient/tests/PerformanceTests/Microsoft.Data.SqlClient.PerformanceTests.csproj index 380e3c8a8e..239be4d3a2 100644 --- a/src/Microsoft.Data.SqlClient/tests/PerformanceTests/Microsoft.Data.SqlClient.PerformanceTests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/PerformanceTests/Microsoft.Data.SqlClient.PerformanceTests.csproj @@ -15,12 +15,22 @@ - + + + + + + + + + + + diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/Directory.Build.props b/src/Microsoft.Data.SqlClient/tests/StressTests/Directory.Build.props index 6ff30d72c8..fb26623a2e 100644 --- a/src/Microsoft.Data.SqlClient/tests/StressTests/Directory.Build.props +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/Directory.Build.props @@ -6,7 +6,7 @@ - net462;net47;net471;net472;net48;net481;net8.0;net9.0;net10.0 + net462;net8.0;net9.0;net10.0 + + + true true @@ -21,18 +27,30 @@ - - - - - - 6.1.0-preview2.25178.5 + + + 6.1.3 + + + + + + + + + $(AzurePackageVersion) + + + + + 1.0.0 + diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/SqlClient.Stress.Framework.csproj b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/SqlClient.Stress.Framework.csproj index 91fe0f81ee..ba086b58a8 100644 --- a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/SqlClient.Stress.Framework.csproj +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/SqlClient.Stress.Framework.csproj @@ -6,6 +6,7 @@ + diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlAuthenticationProviderManagerTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlAuthenticationProviderManagerTests.cs new file mode 100644 index 0000000000..5409b0f7e4 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlAuthenticationProviderManagerTests.cs @@ -0,0 +1,76 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Data.SqlClient.Test.UnitTests; + +public class SqlAuthenticationProviderManagerTests +{ + private class Provider : SqlAuthenticationProvider + { + public override Task AcquireTokenAsync( + SqlAuthenticationParameters parameters) + { + return Task.FromResult( + new SqlAuthenticationToken( + "SampleAccessToken", DateTimeOffset.UtcNow.AddMinutes(5))); + } + + public override bool IsSupported(SqlAuthenticationMethod authenticationMethod) + { + return authenticationMethod == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow; + } + } + + // Verify that we can get and set providers via both the Abstractions + // package and Manager class interchangeably. + // + // This tests the dynamic assembly loading code in the Abstractions + // package. + [Fact] + public void Abstractions_And_Manager_GetSetProvider_Equivalent() + { + // Set via Manager, get via both. + Provider provider1 = new(); + + Assert.True( + SqlAuthenticationProviderManager.SetProvider( + // GOTCHA: On .NET Framework, the dummy provider is already + // registered as the default provider for Interactive, so we + // use DeviceCodeFlow instead. + SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, + provider1)); + + Assert.Same( + provider1, + SqlAuthenticationProviderManager.GetProvider( + SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow)); + + Assert.Same( + provider1, + SqlAuthenticationProvider.GetProvider( + SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow)); + + // Set via Abstractions, get via both. + Provider provider2 = new(); + + Assert.True( + SqlAuthenticationProvider.SetProvider( + SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, + provider2)); + + Assert.Same( + provider2, + SqlAuthenticationProviderManager.GetProvider( + SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow)); + + Assert.Same( + provider2, + SqlAuthenticationProvider.GetProvider( + SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow)); + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/Config.cs b/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/Config.cs index 257903ce41..72ed2e63a0 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/Config.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/Config.cs @@ -45,6 +45,7 @@ public class Config public static Config Load(string configPath = @"config.json") { + // Allow an override of the config path via an environment variable. configPath = Environment.GetEnvironmentVariable("MDS_TEST_CONFIG") ?? configPath; try diff --git a/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/Microsoft.Data.SqlClient.TestUtilities.csproj b/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/Microsoft.Data.SqlClient.TestUtilities.csproj index 45ba5c2cf9..2f802adc75 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/Microsoft.Data.SqlClient.TestUtilities.csproj +++ b/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/Microsoft.Data.SqlClient.TestUtilities.csproj @@ -1,9 +1,6 @@  - netfx - netcoreapp - win - win-$(Platform) + netstandard2.0 $(ObjFolder)$(Configuration).$(Platform)\$(AssemblyName) $(BinFolder)$(Configuration).$(Platform)\$(AssemblyName) @@ -14,10 +11,6 @@ PreserveNewest - - - - diff --git a/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/Utils.cs b/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/Utils.cs index 5d708e757d..bb1e2220a8 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/Utils.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/Utils.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Generic; namespace Microsoft.Data.SqlClient.TestUtilities { diff --git a/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/config.default.json b/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/config.default.json index 5ef2e9c99e..4362d07c75 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/config.default.json +++ b/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/config.default.json @@ -35,5 +35,6 @@ "ManagedIdentitySupported": true, "UserManagedIdentityClientId": "", "PowerShellPath": "", - "AliasName": "" + "AliasName": "", + "WorkloadIdentityFederationServiceConnectionId": "" } diff --git a/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/xunit.runner.json b/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/xunit.runner.json index 42755c93ec..38ccf678dd 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/xunit.runner.json +++ b/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/xunit.runner.json @@ -1,9 +1,18 @@ { - "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", - "diagnosticMessages": true, - "parallelizeAssembly": true, - "shadowCopy": false, - "printMaxEnumerableLength": 0, - "printMaxStringLength": 0, - "showLiveOutput": false + "_comment1": "xUnit v2 doesn't allow proper JSON comments, so we put our comments in keys that are ignored.", + "_comment2": "Options for v3+ are prefixed with '_v3_'", + + "$schema": "https://xunit.net/schema/v2.8/xunit.runner.schema.json", + "_v3_$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", + + "diagnosticMessages": true, + "parallelizeAssembly": true, + "shadowCopy": false, + + "_v3_culture": "invariant", + "_v3_printMaxEnumerableLength": 0, + "_v3_printMaxObjectDepth": 0, + "_v3_printMaxObjectMemberCount": 0, + "_v3_printMaxStringLength": 0, + "_v3_showLiveOutput": true } diff --git a/src/Microsoft.SqlServer.Server/Microsoft.SqlServer.Server.csproj b/src/Microsoft.SqlServer.Server/Microsoft.SqlServer.Server.csproj index 48d163cab6..89d6d91b37 100644 --- a/src/Microsoft.SqlServer.Server/Microsoft.SqlServer.Server.csproj +++ b/src/Microsoft.SqlServer.Server/Microsoft.SqlServer.Server.csproj @@ -6,7 +6,7 @@ net46;netstandard2.0 $(ObjFolder)$(Configuration).$(Platform)\$(AssemblyName)\ $(BinFolder)$(Configuration).$(Platform)\$(AssemblyName)\ - $(OutputPath)$(TargetFramework)\Microsoft.SqlServer.Server.xml + $(OutputPath)$(TargetFramework)\$(AssemblyName).xml portable false true diff --git a/src/Microsoft.SqlServer.Server/StringsHelper.cs b/src/Microsoft.SqlServer.Server/StringsHelper.cs index 610839adbf..8f3915e18c 100644 --- a/src/Microsoft.SqlServer.Server/StringsHelper.cs +++ b/src/Microsoft.SqlServer.Server/StringsHelper.cs @@ -48,13 +48,17 @@ public static string GetResourceString(string res) { StringsHelper sys = GetLoader(); if (sys == null) + { return null; + } // If "res" is a resource id, temp will not be null, "res" will contain the retrieved resource string. // If "res" is not a resource id, temp will be null. string temp = sys._resources.GetString(res, Culture); if (temp != null) + { res = temp; + } return res; } diff --git a/tools/props/Versions.props b/tools/props/Versions.props index ad42c4964a..51e16268a7 100644 --- a/tools/props/Versions.props +++ b/tools/props/Versions.props @@ -1,29 +1,12 @@ - + + + - 7.0.0 0 - - - $(MdsVersionDefault).$(BuildNumber) - - 7.0.0.0 - - $(AssemblyFileVersion) - $(MdsVersionDefault)-dev - $(NugetPackageVersion) + + @@ -33,17 +16,53 @@ 1.0.0-dev $(SqlServerPackageVersion) + + + + + - $(NugetPackageVersion) + 7.0.0 + + $(MdsVersionDefault).$(BuildNumber)-dev + + + $(MdsVersionDefault).$(BuildNumber) + + + 7.0.0.0 - See the 'Modify TestMicrosoftDataSqlClientVersion' step. + $(AssemblyFileVersion) + + - $(NugetPackageVersion) + $(MdsPackageVersion) + + + + + + + + 7.0.0 + $(AkvVersionDefault).$(BuildNumber)-dev diff --git a/tools/specs/Microsoft.Data.SqlClient.nuspec b/tools/specs/Microsoft.Data.SqlClient.nuspec index 9cb42a9ed6..4792149ed2 100644 --- a/tools/specs/Microsoft.Data.SqlClient.nuspec +++ b/tools/specs/Microsoft.Data.SqlClient.nuspec @@ -29,9 +29,8 @@ sqlclient microsoft.data.sqlclient - - + @@ -46,9 +45,8 @@ - - + @@ -58,9 +56,8 @@ - - + @@ -70,9 +67,8 @@ - - + @@ -111,103 +107,108 @@ + + - - + + - - + + - - + + - - + + - - - - - - - - - - - - - + + + + + + + + + + + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + - - + + - - - - + + + + - - - - + + + + diff --git a/tools/specs/Microsoft.SqlServer.Server.nuspec b/tools/specs/Microsoft.SqlServer.Server.nuspec index eec275b292..df75137b92 100644 --- a/tools/specs/Microsoft.SqlServer.Server.nuspec +++ b/tools/specs/Microsoft.SqlServer.Server.nuspec @@ -36,15 +36,20 @@ Microsoft.SqlServer.Server.Format + + - - - + + + - - - + + + diff --git a/tools/specs/add-ons/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.nuspec b/tools/specs/add-ons/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.nuspec index 0816fd3086..e55fb4e7cb 100644 --- a/tools/specs/add-ons/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.nuspec +++ b/tools/specs/add-ons/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.nuspec @@ -25,13 +25,13 @@ Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyStoreProvider.SqlColumnEncrypti sqlclient microsoft.data.sqlclient azurekeyvaultprovider akvprovider alwaysencrypted - + - + @@ -45,6 +45,11 @@ Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyStoreProvider.SqlColumnEncrypti + + diff --git a/tools/targets/CopySniDllsForNetFxProjectReferenceBuilds.targets b/tools/targets/CopySniDllsForNetFxProjectReferenceBuilds.targets new file mode 100644 index 0000000000..47c45c41cd --- /dev/null +++ b/tools/targets/CopySniDllsForNetFxProjectReferenceBuilds.targets @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + diff --git a/tools/targets/GenerateMdsPackage.targets b/tools/targets/GenerateMdsPackage.targets new file mode 100644 index 0000000000..50f9b8fddd --- /dev/null +++ b/tools/targets/GenerateMdsPackage.targets @@ -0,0 +1,14 @@ + + + + + + + + + + + + + diff --git a/tools/targets/GenerateNugetPackage.targets b/tools/targets/GenerateNugetPackage.targets deleted file mode 100644 index 4c8cea4159..0000000000 --- a/tools/targets/GenerateNugetPackage.targets +++ /dev/null @@ -1,31 +0,0 @@ - - - - - $(NugetPackageVersion)-debug - - - - - - - - - - - - - $(SqlServerPackageVersion)-debug - - - - - - - - - - - diff --git a/tools/targets/GenerateSqlServerPackage.targets b/tools/targets/GenerateSqlServerPackage.targets new file mode 100644 index 0000000000..909c0da6e0 --- /dev/null +++ b/tools/targets/GenerateSqlServerPackage.targets @@ -0,0 +1,13 @@ + + + + + + + + + + + + diff --git a/tools/targets/GenerateThisAssemblyCs.targets b/tools/targets/GenerateThisAssemblyCs.targets index 6230af8ae7..86e896013c 100644 --- a/tools/targets/GenerateThisAssemblyCs.targets +++ b/tools/targets/GenerateThisAssemblyCs.targets @@ -4,18 +4,31 @@ + + + +[assembly: System.CLSCompliant(true)] + + + + System + + -[assembly: System.CLSCompliant(true)] -namespace System +$(ThisAssemblyClsCompliantContent) +namespace $(ThisAssemblyNamespace) { -internal static class ThisAssembly -{ -internal const string InformationalVersion = "$(AssemblyFileVersion)"%3B -internal const string NuGetPackageVersion = "$(Version)"%3B -} + internal static class ThisAssembly + { + internal const string InformationalVersion = "$(AssemblyFileVersion)"%3B + internal const string NuGetPackageVersion = "$(Version)"%3B + } } diff --git a/tools/targets/add-ons/GenerateAKVProviderNugetPackage.targets b/tools/targets/add-ons/GenerateAKVProviderNugetPackage.targets deleted file mode 100644 index 78da74bf32..0000000000 --- a/tools/targets/add-ons/GenerateAKVProviderNugetPackage.targets +++ /dev/null @@ -1,12 +0,0 @@ - - - - - $(NugetPackageVersion)-debug - - - - - - diff --git a/tools/targets/add-ons/GenerateAkvPackage.targets b/tools/targets/add-ons/GenerateAkvPackage.targets new file mode 100644 index 0000000000..2aefc55ca4 --- /dev/null +++ b/tools/targets/add-ons/GenerateAkvPackage.targets @@ -0,0 +1,13 @@ + + + + + + + + + + + +