diff --git a/build.proj b/build.proj index d5cdcf73ef..8d21a86c63 100644 --- a/build.proj +++ b/build.proj @@ -515,7 +515,7 @@ - + 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 939ad9ab0d..8a90a42953 100644 --- a/eng/pipelines/common/templates/jobs/build-signed-package-job.yml +++ b/eng/pipelines/common/templates/jobs/build-signed-package-job.yml @@ -73,7 +73,7 @@ jobs: # These variables are sourced from common-variables.yml. abstractionsAssemblyFileVersion: $(abstractionsAssemblyFileVersion) abstractionsPackageVersion: $(abstractionsPackageVersion) - configuration: $(Configuration) + buildConfiguration: $(Configuration) mdsAssemblyFileVersion: $(mdsAssemblyFileVersion) mdsPackageVersion: $(mdsPackageVersion) referenceType: Package diff --git a/eng/pipelines/common/templates/steps/ensure-dotnet-version.yml b/eng/pipelines/common/templates/steps/ensure-dotnet-version.yml index fd6ba9674b..91724bf566 100644 --- a/eng/pipelines/common/templates/steps/ensure-dotnet-version.yml +++ b/eng/pipelines/common/templates/steps/ensure-dotnet-version.yml @@ -10,6 +10,13 @@ # Reason for not using UseDotNet task: # [BUG]: UseDotNet task installs x86 build on Windows arm64 # https://github.com/microsoft/azure-pipelines-tasks/issues/20300 +# +# A possible workaround is discussed here: +# +# https://github.com/microsoft/azure-pipelines-tasks/issues/16501 +# +# TODO: See if we can eliminate this template and just use the above workaround +# for the Windows x86 builds. parameters: - # Directory where dotnet binaries should be installed. If not specified, defaults to the 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 3a57aa93a8..7ed33cbdc2 100644 --- a/eng/pipelines/common/templates/steps/generate-nuget-package-step.yml +++ b/eng/pipelines/common/templates/steps/generate-nuget-package-step.yml @@ -61,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}};${{parameters.properties}}"' + 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}};${{parameters.properties}}"' + 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/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/dotnet-sqlclient-ci-core.yml b/eng/pipelines/dotnet-sqlclient-ci-core.yml index 9885684a12..9bf4fe85f2 100644 --- a/eng/pipelines/dotnet-sqlclient-ci-core.yml +++ b/eng/pipelines/dotnet-sqlclient-ci-core.yml @@ -93,6 +93,17 @@ 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 @@ -114,11 +125,11 @@ stages: # under the given artifact name. - template: stages/build-abstractions-package-ci-stage.yml@self parameters: - buildConfiguration: ${{ parameters.buildConfiguration }} + abstractionsArtifactName: $(abstractionsArtifactName) abstractionsPackageVersion: $(abstractionsPackageVersion) - artifactName: $(abstractionsArtifactName) - ${{if eq(parameters.debug, 'true')}}: - verbosity: diagnostic + buildConfiguration: ${{ parameters.buildConfiguration }} + debug: ${{ parameters.debug }} + dotnetVerbosity: ${{ parameters.dotnetVerbosity }} # Build MDS and its NuGet packages. - stage: build_mds_akv_packages_stage @@ -157,25 +168,29 @@ stages: 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 - # package. + # 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')}}: - verbosity: diagnostic + dotnetVerbosity: ${{ parameters.dotnetVerbosity }} # Run the stress tests, if desired. - ${{ if eq(parameters.enableStressTests, true) }}: - template: stages/stress-tests-ci-stage.yml@self parameters: buildConfiguration: ${{ parameters.buildConfiguration }} - dependsOn: [build_mds_akv_packages_stage] + dependsOn: + - build_mds_akv_packages_stage + - build_azure_package_stage pipelineArtifactName: $(artifactName) mdsPackageVersion: $(mdsPackageVersion) - ${{ if eq(parameters.debug, 'true') }}: - verbosity: 'detailed' + dotnetVerbosity: ${{ parameters.dotnetVerbosity }} # Run the MDS and AKV tests. - template: common/templates/stages/ci-run-tests-stage.yml@self @@ -190,9 +205,6 @@ stages: mdsArtifactName: $(mdsArtifactName) mdsPackageVersion: $(mdsPackageVersion) testJobTimeout: ${{ parameters.testJobTimeout }} - ${{ if eq(parameters.buildType, 'Package') }}: - dependsOn: build_nugets - # When testing MDS via packages, we must depend on the Abstractions, # Azure, and MDS packages. ${{ if eq(parameters.referenceType, 'Package') }}: diff --git a/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml b/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml index f9fc8ff8f8..4ce3004d5d 100644 --- a/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml +++ b/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml @@ -149,6 +149,18 @@ 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 parameters: @@ -157,6 +169,7 @@ extends: referenceType: Package codeCovTargetFrameworks: ${{ parameters.codeCovTargetFrameworks }} debug: ${{ parameters.debug }} + dotnetVerbosity: ${{ parameters.dotnetVerbosity }} enableStressTests: ${{ parameters.enableStressTests }} targetFrameworks: ${{ parameters.targetFrameworks }} targetFrameworksLinux: ${{ parameters.targetFrameworksLinux }} diff --git a/eng/pipelines/dotnet-sqlclient-ci-project-reference-pipeline.yml b/eng/pipelines/dotnet-sqlclient-ci-project-reference-pipeline.yml index 71f0bbf718..d35cca867c 100644 --- a/eng/pipelines/dotnet-sqlclient-ci-project-reference-pipeline.yml +++ b/eng/pipelines/dotnet-sqlclient-ci-project-reference-pipeline.yml @@ -149,6 +149,18 @@ 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 parameters: @@ -157,6 +169,7 @@ extends: referenceType: Project codeCovTargetFrameworks: ${{ parameters.codeCovTargetFrameworks }} debug: ${{ parameters.debug }} + dotnetVerbosity: ${{ parameters.dotnetVerbosity }} enableStressTests: ${{ parameters.enableStressTests }} targetFrameworks: ${{ parameters.targetFrameworks }} targetFrameworksLinux: ${{ parameters.targetFrameworksLinux }} diff --git a/eng/pipelines/jobs/pack-abstractions-package-ci-job.yml b/eng/pipelines/jobs/pack-abstractions-package-ci-job.yml index 13bf3618fb..d08cc1f7a6 100644 --- a/eng/pipelines/jobs/pack-abstractions-package-ci-job.yml +++ b/eng/pipelines/jobs/pack-abstractions-package-ci-job.yml @@ -12,21 +12,26 @@ parameters: - # The version to apply to the Abstractions NuGet package and its assemblies. - - name: abstractionsPackageVersion - type: string - # The name to apply to the published pipeline artifact. - - name: artifactName + - 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 @@ -34,7 +39,7 @@ parameters: default: [] # The verbosity level for the dotnet CLI commands. - - name: verbosity + - name: dotnetVerbosity type: string default: normal values: @@ -69,13 +74,14 @@ jobs: # dotnet CLI arguments common to all commands. - name: commonArguments value: >- - --verbosity ${{ parameters.verbosity }} + --verbosity ${{ parameters.dotnetVerbosity }} # dotnet CLI arguments for build/test/pack commands - name: buildArguments value: >- $(commonArguments) --configuration ${{ parameters.buildConfiguration }} + -p:ForceMdsAssemblyNameSuffix=true -p:AbstractionsPackageVersion=${{ parameters.abstractionsPackageVersion }} # Explicitly unset the $PLATFORM environment variable that is set by the @@ -102,6 +108,11 @@ jobs: 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 9.0 SDK. - task: UseDotNet@2 displayName: Install .NET 9.0 SDK @@ -112,18 +123,18 @@ jobs: # We use the 'custom' command because the DotNetCoreCLI@2 task doesn't # support all of our argument combinations for the different build steps. - # Restore the solution. + # Restore the project. - task: DotNetCoreCLI@2 - displayName: Restore Solution + displayName: Restore Project inputs: command: custom custom: restore projects: $(project) arguments: $(commonArguments) - # Build the solution. + # Build the project. - task: DotNetCoreCLI@2 - displayName: Build Solution + displayName: Build Project inputs: command: custom custom: build @@ -144,5 +155,5 @@ jobs: displayName: Publish Pipeline Artifact inputs: targetPath: $(dotnetPackagesDir) - artifactName: ${{ parameters.artifactName }} + 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 index 9cc33eadb1..d0551c0960 100644 --- a/eng/pipelines/jobs/pack-azure-package-ci-job.yml +++ b/eng/pipelines/jobs/pack-azure-package-ci-job.yml @@ -40,12 +40,28 @@ parameters: 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. @@ -57,17 +73,6 @@ parameters: - Package - Project - # The verbosity level for the dotnet CLI commands. - - name: verbosity - type: string - default: normal - values: - - quiet - - minimal - - normal - - detailed - - diagnostic - jobs: - job: pack_azure_package_job @@ -93,8 +98,9 @@ jobs: # dotnet CLI arguments common to all commands. - name: commonArguments value: >- - --verbosity ${{ parameters.verbosity }} + --verbosity ${{ parameters.dotnetVerbosity }} -p:ReferenceType=${{ parameters.referenceType }} + -p:ForceMdsAssemblyNameSuffix=true -p:AbstractionsPackageVersion=${{ parameters.abstractionsPackageVersion }} # dotnet CLI arguments for build/test/pack commands @@ -128,6 +134,11 @@ jobs: 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') }}: @@ -152,18 +163,18 @@ jobs: # We use the 'custom' command because the DotNetCoreCLI@2 task doesn't # support all of our argument combinations for the different build steps. - # Restore the solution. + # Restore the project. - task: DotNetCoreCLI@2 - displayName: Restore Solution + displayName: Restore Project inputs: command: custom custom: restore projects: $(project) arguments: $(commonArguments) - # Build the solution. + # Build the project. - task: DotNetCoreCLI@2 - displayName: Build Solution + displayName: Build Project inputs: command: custom custom: build diff --git a/eng/pipelines/jobs/test-abstractions-package-ci-job.yml b/eng/pipelines/jobs/test-abstractions-package-ci-job.yml index a5e0f98978..e908f71562 100644 --- a/eng/pipelines/jobs/test-abstractions-package-ci-job.yml +++ b/eng/pipelines/jobs/test-abstractions-package-ci-job.yml @@ -19,6 +19,11 @@ parameters: - 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 @@ -26,6 +31,17 @@ parameters: - 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 @@ -44,17 +60,6 @@ parameters: - name: poolName type: string - # The verbosity level for the dotnet CLI commands. - - name: verbosity - type: string - default: normal - values: - - quiet - - minimal - - normal - - detailed - - diagnostic - # The pool VM image to use. - name: vmImage type: string @@ -83,13 +88,14 @@ jobs: # dotnet CLI arguments common to all commands. - name: commonArguments value: >- - --verbosity ${{ parameters.verbosity }} + --verbosity ${{ parameters.dotnetVerbosity }} # dotnet CLI arguments for build/test/pack commands - name: buildArguments value: >- $(commonArguments) --configuration ${{ parameters.buildConfiguration }} + -p:ForceMdsAssemblyNameSuffix=true # Explicitly unset the $PLATFORM environment variable that is set by the # 'ADO Build properties' Library in the ADO SqlClientDrivers public project. @@ -115,6 +121,11 @@ jobs: 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 9.0 SDK. - task: UseDotNet@2 displayName: Install .NET 9.0 SDK @@ -135,18 +146,18 @@ jobs: # We use the 'custom' command because the DotNetCoreCLI@2 task doesn't # support all of our argument combinations for the different build steps. - # Restore the solution. + # Restore the project. - task: DotNetCoreCLI@2 - displayName: Restore Solution + displayName: Restore Project inputs: command: custom custom: restore projects: $(project) arguments: $(commonArguments) - # Build the solution. + # Build the project. - task: DotNetCoreCLI@2 - displayName: Build Solution + displayName: Build Project inputs: command: custom custom: build diff --git a/eng/pipelines/jobs/test-azure-package-ci-job.yml b/eng/pipelines/jobs/test-azure-package-ci-job.yml index 8e1b8fc6db..f9b8947a24 100644 --- a/eng/pipelines/jobs/test-azure-package-ci-job.yml +++ b/eng/pipelines/jobs/test-azure-package-ci-job.yml @@ -32,6 +32,11 @@ parameters: - 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 @@ -39,10 +44,34 @@ parameters: - 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 @@ -68,17 +97,6 @@ parameters: - Package - Project - # The verbosity level for the dotnet CLI commands. - - name: verbosity - type: string - default: normal - values: - - quiet - - minimal - - normal - - detailed - - diagnostic - # The pool VM image to use. - name: vmImage type: string @@ -107,9 +125,11 @@ jobs: # dotnet CLI arguments common to all commands. - name: commonArguments value: >- - --verbosity ${{ parameters.verbosity }} + --verbosity ${{ parameters.dotnetVerbosity }} -p:ReferenceType=${{ parameters.referenceType }} + -p:ForceMdsAssemblyNameSuffix=true -p:AbstractionsPackageVersion=${{ parameters.abstractionsPackageVersion }} + -p:MdsPackageVersion=${{ parameters.mdsPackageVersion }} # dotnet CLI arguments for build/test/pack commands - name: buildArguments @@ -141,6 +161,11 @@ jobs: 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') }}: @@ -151,6 +176,15 @@ jobs: 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 @@ -172,32 +206,87 @@ jobs: # 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: ../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: ${{ eq(variables['SupportsIntegratedSecurity'], 'true') }} + TCPConnectionString: $(AZURE_DB_TCP_CONN_STRING) + UserManagedIdentityClientId: $(UserManagedIdentityClientId) + WorkloadIdentityFederationServiceConnectionId: $(WorkloadIdentityFederationServiceConnectionId) + # Note: Using the isFork variable to determine if secrets are + # available is not ideal since it's an indirect association. But + # everything else (referencing secret variables various 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) + AADServicePrincipalSecret: $(AADServicePrincipalSecret) + # We use the 'custom' command because the DotNetCoreCLI@2 task doesn't # support all of our argument combinations for the different build steps. - # Restore the solution. + # Restore the project. - task: DotNetCoreCLI@2 - displayName: Restore Solution + displayName: Restore Project inputs: command: custom custom: restore projects: $(project) arguments: $(commonArguments) - # Build the solution. + # Build the project. - task: DotNetCoreCLI@2 - displayName: Build Solution + 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 startsWith(parameters.poolName, 'ADO-') }}: + 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) @@ -207,7 +296,20 @@ jobs: - ${{ each runtime in parameters.netFrameworkRuntimes }}: - 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-CI 1ES + # images in our ADO pool. + ${{ if startsWith(parameters.poolName, 'ADO-CI') }}: + 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: + connectedServiceName: dotnetMSI-managed-identity command: custom custom: test projects: $(project) diff --git a/eng/pipelines/sqlclient-pr-package-ref-pipeline.yml b/eng/pipelines/sqlclient-pr-package-ref-pipeline.yml index f44aaf9910..41786ed2d8 100644 --- a/eng/pipelines/sqlclient-pr-package-ref-pipeline.yml +++ b/eng/pipelines/sqlclient-pr-package-ref-pipeline.yml @@ -111,7 +111,7 @@ extends: 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 e835a5db78..3e4eb85a9e 100644 --- a/eng/pipelines/sqlclient-pr-project-ref-pipeline.yml +++ b/eng/pipelines/sqlclient-pr-project-ref-pipeline.yml @@ -111,7 +111,7 @@ extends: 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 index 0d84dc6ccb..201840fac1 100644 --- a/eng/pipelines/stages/build-abstractions-package-ci-stage.yml +++ b/eng/pipelines/stages/build-abstractions-package-ci-stage.yml @@ -25,15 +25,15 @@ parameters: - # The version to apply to the NuGet package and DLLs. - - name: abstractionsPackageVersion - type: string - # The name of the pipeline artifact to publish. - - name: artifactName + - 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 @@ -42,8 +42,13 @@ parameters: - 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: verbosity + - name: dotnetVerbosity type: string default: normal values: @@ -65,55 +70,59 @@ stages: - template: ../jobs/test-abstractions-package-ci-job.yml@self parameters: - jobNameSuffix: linux + buildConfiguration: ${{ parameters.buildConfiguration }} + debug: ${{ parameters.debug }} displayNamePrefix: Linux + dotnetVerbosity: ${{ parameters.dotnetVerbosity }} + jobNameSuffix: linux + netFrameworkRuntimes: [] + netRuntimes: [net8.0, net9.0] poolName: Azure Pipelines vmImage: ubuntu-latest - buildConfiguration: ${{ parameters.buildConfiguration }} - netRuntimes: [net8.0, net9.0] - netFrameworkRuntimes: [] - verbosity: ${{ parameters.verbosity }} # ------------------------------------------------------------------------ # Build and test on Windows - template: ../jobs/test-abstractions-package-ci-job.yml@self parameters: - jobNameSuffix: windows + buildConfiguration: ${{ parameters.buildConfiguration }} + debug: ${{ parameters.debug }} displayNamePrefix: Win + dotnetVerbosity: ${{ parameters.dotnetVerbosity }} + jobNameSuffix: windows + netFrameworkRuntimes: [net462] + netRuntimes: [net8.0, net9.0] poolName: Azure Pipelines vmImage: windows-latest - buildConfiguration: ${{ parameters.buildConfiguration }} - netRuntimes: [net8.0, net9.0] - netFrameworkRuntimes: [net462] - verbosity: ${{ parameters.verbosity }} # ------------------------------------------------------------------------ # Build and test on macOS. - template: ../jobs/test-abstractions-package-ci-job.yml parameters: - jobNameSuffix: macos + buildConfiguration: ${{ parameters.buildConfiguration }} + debug: ${{ parameters.debug }} displayNamePrefix: macOS + dotnetVerbosity: ${{ parameters.dotnetVerbosity }} + jobNameSuffix: macos + netFrameworkRuntimes: [] + netRuntimes: [net8.0, net9.0] poolName: Azure Pipelines vmImage: macos-latest - buildConfiguration: ${{ parameters.buildConfiguration }} - netRuntimes: [net8.0, net9.0] - netFrameworkRuntimes: [] - verbosity: ${{ parameters.verbosity }} # ------------------------------------------------------------------------ # Create and publish the NuGet package. - template: ../jobs/pack-abstractions-package-ci-job.yml@self parameters: - artifactName: ${{ parameters.artifactName }} - buildConfiguration: ${{ parameters.buildConfiguration }} + abstractionsArtifactName: ${{ parameters.abstractionsArtifactName }} abstractionsPackageVersion: ${{ parameters.abstractionsPackageVersion }} - verbosity: ${{ parameters.verbosity }} + 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 index 90beb30caf..87d99b6ae2 100644 --- a/eng/pipelines/stages/build-azure-package-ci-stage.yml +++ b/eng/pipelines/stages/build-azure-package-ci-stage.yml @@ -38,6 +38,19 @@ parameters: - 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-UB22-SQL22 + # - ADO-CI-Win11 + # + default: $(ci_var_defaultPoolName) + # The name of the pipeline artifact to publish. - name: azureArtifactName type: string @@ -47,6 +60,12 @@ parameters: - 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 @@ -55,10 +74,39 @@ parameters: - 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: # @@ -71,17 +119,6 @@ parameters: - Package - Project - # The dotnet CLI verbosity to use. - - name: verbosity - type: string - default: normal - values: - - quiet - - minimal - - normal - - detailed - - diagnostic - stages: - stage: build_azure_package_stage @@ -94,54 +131,104 @@ stages: # ------------------------------------------------------------------------ # Build and test on Linux. + # Use the Azure Pipelines pool for basic testing. - template: ../jobs/test-azure-package-ci-job.yml@self parameters: abstractionsArtifactName: ${{ parameters.abstractionsArtifactName }} abstractionsPackageVersion: ${{ parameters.abstractionsPackageVersion }} buildConfiguration: ${{ parameters.buildConfiguration }} + debug: ${{ parameters.debug }} displayNamePrefix: Linux - jobNameSuffix: linux + dotnetVerbosity: ${{ parameters.dotnetVerbosity }} + jobNameSuffix: linux_basic + mdsArtifactName: MDS.Artifact + mdsPackageVersion: ${{ parameters.mdsPackageVersion }} netFrameworkRuntimes: [] netRuntimes: [net8.0, net9.0] - poolName: Azure Pipelines + poolName: ${{ parameters.azurePoolName }} referenceType: ${{ parameters.referenceType }} - verbosity: ${{ parameters.verbosity }} vmImage: ubuntu-latest + # Use our 1ES ADO pool for comprehensive testing. + - template: ../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_comprehensive + mdsArtifactName: MDS.Artifact + mdsPackageVersion: ${{ parameters.mdsPackageVersion }} + netFrameworkRuntimes: [] + netRuntimes: [net8.0, net9.0] + poolName: ${{ parameters.adoPoolName }} + referenceType: ${{ parameters.referenceType }} + vmImage: ADO-UB22-SQL22 + # ------------------------------------------------------------------------ # Build and test on Windows + # Use the Azure Pipelines pool for basic testing. - template: ../jobs/test-azure-package-ci-job.yml@self parameters: abstractionsArtifactName: ${{ parameters.abstractionsArtifactName }} abstractionsPackageVersion: ${{ parameters.abstractionsPackageVersion }} buildConfiguration: ${{ parameters.buildConfiguration }} + debug: ${{ parameters.debug }} displayNamePrefix: Win - jobNameSuffix: windows + dotnetVerbosity: ${{ parameters.dotnetVerbosity }} + jobNameSuffix: windows_basic + mdsArtifactName: MDS.Artifact + mdsPackageVersion: ${{ parameters.mdsPackageVersion }} netFrameworkRuntimes: [net462] netRuntimes: [net8.0, net9.0] - poolName: Azure Pipelines + poolName: ${{ parameters.azurePoolName }} referenceType: ${{ parameters.referenceType }} - verbosity: ${{ parameters.verbosity }} vmImage: windows-latest + # Use our 1ES ADO pool for comprehensive testing. + - template: ../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_comprehensive + mdsArtifactName: MDS.Artifact + mdsPackageVersion: ${{ parameters.mdsPackageVersion }} + netFrameworkRuntimes: [net462] + netRuntimes: [net8.0, net9.0] + poolName: ${{ parameters.adoPoolName }} + referenceType: ${{ parameters.referenceType }} + vmImage: ADO-CI-Win11 + # ------------------------------------------------------------------------ # Build and test on macOS. + # Use the Azure Pipelines pool for basic testing. - template: ../jobs/test-azure-package-ci-job.yml 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] - poolName: Azure Pipelines + poolName: ${{ parameters.azurePoolName }} referenceType: ${{ parameters.referenceType }} - verbosity: ${{ parameters.verbosity }} vmImage: macos-latest + # We do not currently have any images in our 1ES ADO pools for macOS. + # ------------------------------------------------------------------------ # Create and publish the NuGet package. @@ -152,11 +239,14 @@ stages: 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_windows + - test_azure_package_job_linux_basic + - test_azure_package_job_linux_comprehensive + - test_azure_package_job_windows_basic + - test_azure_package_job_windows_comprehensive - test_azure_package_job_macos + dotnetVerbosity: ${{ parameters.dotnetVerbosity }} referenceType: ${{ parameters.referenceType }} - verbosity: ${{ parameters.verbosity }} diff --git a/eng/pipelines/stages/stress-tests-ci-stage.yml b/eng/pipelines/stages/stress-tests-ci-stage.yml index b7cea84b82..1204e0b4bd 100644 --- a/eng/pipelines/stages/stress-tests-ci-stage.yml +++ b/eng/pipelines/stages/stress-tests-ci-stage.yml @@ -36,12 +36,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 +55,24 @@ parameters: type: string default: '' - # 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, net47, net471, net472, net48, net481] - # The verbosity level for the dotnet CLI commands. - - name: verbosity - displayName: Dotnet CLI verbosity + # The list of .NET runtimes to test against. + - name: netTestRuntimes + displayName: .NET Test Runtimes + type: object + default: [net8.0, net9.0] + + # 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,7 +96,7 @@ stages: # dotnet CLI arguments common to all commands. - name: commonArguments value: >- - --verbosity ${{parameters.verbosity}} + --verbosity ${{parameters.dotnetVerbosity}} --artifacts-path $(dotnetArtifactsDir) -p:MdsPackageVersion=${{parameters.mdsPackageVersion}} diff --git a/src/Directory.Build.props b/src/Directory.Build.props index d79b9a547f..2ac32b09f9 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -17,7 +17,7 @@ See the BUILDGUIDE.md for more details. --> - Project + Project $([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)')) @@ -153,4 +153,43 @@ 13 + + + + false + true + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Abstractions.csproj b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Abstractions.csproj index 9436e9067b..ba7dfd2a48 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Abstractions.csproj +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Abstractions.csproj @@ -42,6 +42,12 @@ $(AbstractionsPackageVersion) $(Artifacts)/doc/$(TargetFramework)/$(AssemblyName).xml + + + $(DefineConstants);APPLY_MDS_ASSEMBLY_NAME_SUFFIX @@ -82,5 +88,5 @@ - + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProviderInternal.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProviderInternal.cs index c14a758f76..7e4c5b0645 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProviderInternal.cs +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProviderInternal.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Reflection; +using System.Runtime.InteropServices; namespace Microsoft.Data.SqlClient; @@ -31,10 +32,23 @@ private static class Internal /// static Internal() { + // Choose the MDS assembly name based on the build configuration and + // runtime environment. See the top-level Directory.Build.props for + // more information. + string assemblyName = "Microsoft.Data.SqlClient"; + #if (APPLY_MDS_ASSEMBLY_NAME_SUFFIX) + if (RuntimeInformation.FrameworkDescription.StartsWith(".NET Framework")) + { + assemblyName += ".NetFx"; + } + else + { + assemblyName += ".NetCore"; + } + #endif + // If the MDS package is present, load its // SqlAuthenticationProviderManager class and get/set methods. - const string assemblyName = "Microsoft.Data.SqlClient"; - try { // Try to load the MDS assembly. @@ -42,35 +56,21 @@ static Internal() if (assembly is null) { - // TODO: Logging - // SqlClientEventSource.Log.TryTraceEvent( - // nameof(SqlAuthenticationProviderManager) + - // $": Azure extension assembly={assemblyName} not found; " + - // "no default provider installed"); + Log($"MDS assembly={assemblyName} not found; " + + "Get/SetProvider() will not function"); return; } // TODO(ADO-39845): Verify the assembly is signed by us? - // TODO: Logging - // 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 manager class. const string className = "Microsoft.Data.SqlClient.SqlAuthenticationProviderManager"; var manager = assembly.GetType(className); if (manager is null) { - // TODO: Logging - // SqlClientEventSource.Log.TryTraceEvent( - // nameof(SqlAuthenticationProviderManager) + - // $": Azure extension does not contain class={className}; " + - // "no default Active Directory provider installed"); - + Log($"MDS auth manager manager class={className} not found; " + + "Get/SetProvider() will not function"); return; } @@ -78,15 +78,22 @@ static Internal() _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); - - // TODO: Logging - // SqlClientEventSource.Log.TryTraceEvent( - // nameof(SqlAuthenticationProviderManager) + - // $": Azure extension class={className} installed as " + - // "provider for all Active Directory authentication methods"); + + 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. @@ -96,12 +103,8 @@ ex is BadImageFormatException || ex is FileLoadException || ex is FileNotFoundException) { - // TODO: Logging - // SqlClientEventSource.Log.TryTraceEvent( - // nameof(SqlAuthenticationProviderManager) + - // $": Azure extension assembly={assemblyName} not found or " + - // "not usable; no default provider installed; " + - // $"{ex.GetType().Name}: {ex.Message}"); + Log($"MDS assembly={assemblyName} not found or not usable; " + + $"Get/SetProvider() will not function: {ex} "); } // Any other exceptions are fatal. } @@ -136,6 +139,8 @@ ex is MethodAccessException || ex is NotSupportedException || ex is TargetInvocationException) { + Log($"GetProvider() invocation failed: " + + $"{ex.GetType().Name}: {ex.Message}"); return null; } } @@ -171,6 +176,8 @@ internal static bool SetProvider( if (!result.HasValue) { + Log($"SetProvider() invocation returned null; " + + "translating to false"); return false; } @@ -183,8 +190,16 @@ 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(ADO-39080): Convert to proper logging. + Console.WriteLine($"SqlAuthenticationProvider.Internal(): {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 index 053804342e..2f7c023e95 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/Abstractions.Test.csproj +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/Abstractions.Test.csproj @@ -9,6 +9,14 @@ Microsoft.Data.SqlClient.Extensions.Abstractions.Test + + + $(DefineConstants);APPLY_MDS_ASSEMBLY_NAME_SUFFIX + + @@ -24,4 +32,11 @@ + + + PreserveNewest + xunit.runner.json + + + diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/SqlAuthenticationProviderTest.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/SqlAuthenticationProviderTest.cs index 07e2b40078..7cd067aca3 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/SqlAuthenticationProviderTest.cs +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/SqlAuthenticationProviderTest.cs @@ -8,14 +8,23 @@ namespace Microsoft.Data.SqlClient.Extensions.Abstractions.Test; public class SqlAuthenticationProviderTest { + // Choose the MDS assembly name based on the build environment. + // See the top-level Directory.Build.props for more information. + #if (APPLY_MDS_ASSEMBLY_NAME_SUFFIX && NET) + const string assemblyName = "Microsoft.Data.SqlClient.NetCore"; + #elif (APPLY_MDS_ASSEMBLY_NAME_SUFFIX && NETFRAMEWORK) + const string assemblyName = "Microsoft.Data.SqlClient.NetFx"; + #else + const string assemblyName = "Microsoft.Data.SqlClient"; + #endif + /// /// Construct to confirm preconditions. /// public SqlAuthenticationProviderTest() { // Confirm that the MDS assembly is indeed not present. - Assert.Throws( - () => Assembly.Load("Microsoft.Data.SqlClient")); + Assert.Throws(() => Assembly.Load(assemblyName)); } #region Tests @@ -74,6 +83,8 @@ public void SetProvider_NoMdsAssembly(SqlAuthenticationMethod method) /// /// A dummy provider that supports all authentication methods. /// + + // A dummy provider that supports all authentication methods. private sealed class Provider : SqlAuthenticationProvider { /// 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..19d22fbce9 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/AADConnectionTest.cs @@ -0,0 +1,377 @@ +// 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 certainity 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); + } + + [ConditionalFact( + typeof(Config), + nameof(Config.HasIntegratedSecurityConnectionString), + 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.HasPasswordConnectionString), + nameof(Config.SupportsManagedIdentity), + nameof(Config.SupportsSystemAssignedManagedIdentity))] + 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.HasTcpConnectionString), + nameof(Config.SupportsManagedIdentity), + nameof(Config.SupportsSystemAssignedManagedIdentity), + 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.HasTcpConnectionString), + nameof(Config.HasUserManagedIdentityClientId), + nameof(Config.SupportsManagedIdentity), + nameof(Config.SupportsSystemAssignedManagedIdentity), + 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 index ad94e9b4b1..80fb2b8b5a 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Azure/test/Azure.Test.csproj +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/Azure.Test.csproj @@ -18,10 +18,50 @@ + + + + + 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..2e12cc9941 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/Config.cs @@ -0,0 +1,240 @@ +// 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. +// +// 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 HasIntegratedSecurityConnectionString() => + !TcpConnectionString.Empty() && IntegratedSecuritySupported; + 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 SupportsIntegratedSecurity() => IntegratedSecuritySupported; + internal static bool SupportsManagedIdentity() => ManagedIdentitySupported; + internal static bool SupportsSystemAssignedManagedIdentity() => SystemAssignedManagedIdentitySupported; + + 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(); + + #endregion + + #region Static Construction + + static Config() + { + // Read from the config.json file. + 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}"); + } + + // 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( + $" 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 + + 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; + } + 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; + } + + private static bool GetEnvFlag(string name) + { + return Environment.GetEnvironmentVariable(name) is not null; + } + + 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..18c09e4aa1 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/DefaultAuthProviderTests.cs @@ -0,0 +1,60 @@ +// 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 SqlAuthenticationProviderManager class in + // the MDS package and the static GetProvider() and SetProvider() methods of + // the SqlAuthenticationProvider class in the Abstractions package. + [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..acc7f10310 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/StringExtensions.cs @@ -0,0 +1,13 @@ +// 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..9f63f80e2f --- /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 if 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. See: + // + // 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/netcore/ref/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.csproj index 52812d835b..5fb084943d 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,22 @@  - false + + Microsoft.Data.SqlClient + $(AssemblyName).NetCore + 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 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 91c8dce015..143647ee2e 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -1,7 +1,14 @@  + Microsoft.Data.SqlClient + $(AssemblyName).NetCore + net8.0;net9.0 + Microsoft.Data.SqlClient is not supported on this platform. $(OS) true @@ -13,7 +20,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 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 e8131c3022..765059c76f 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,18 @@  - false + + Microsoft.Data.SqlClient + $(AssemblyName).NetFx + net462 + + false $(ObjFolder)$(Configuration)\$(AssemblyName)\ref\ $(BinFolder)$(Configuration)\$(AssemblyName)\ref\ - $(OutputPath)\Microsoft.Data.SqlClient.xml + $(OutputPath)\$(AssemblyName).xml Framework $(BaseProduct) Debug;Release 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 c1775fc530..b340993a7c 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -1,15 +1,22 @@  + + Microsoft.Data.SqlClient + $(AssemblyName).NetFx + + net462 + {407890AC-9876-4FEF-A6F1-F36A876BAADE} - net462 true - Microsoft.Data.SqlClient AnyCPU $(BinFolder)$(Configuration).$(OutputPlatform)\ $(ObjFolder)$(Configuration).$(OutputPlatform)\ $(BinPath)$(AssemblyName)\netfx\ - $(OutputPath)\Microsoft.Data.SqlClient.xml + $(OutputPath)\$(AssemblyName).xml $(ObjPath)$(AssemblyName)\netfx\ Framework $(BaseProduct) false diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Common.csproj b/src/Microsoft.Data.SqlClient/tests/Common/Common.csproj index 4133622d20..001a7410a9 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/Common.csproj +++ b/src/Microsoft.Data.SqlClient/tests/Common/Common.csproj @@ -34,10 +34,10 @@ - + PreserveNewest xunit.runner.json - + @@ -45,12 +45,11 @@ - - PreserveNewest - %(Filename)%(Extension) - + + + diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AADAuthenticationTests.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AADAuthenticationTests.cs index 32050a9716..d86b6684d1 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AADAuthenticationTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AADAuthenticationTests.cs @@ -68,34 +68,10 @@ 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); } - - [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/tests/FunctionalTests/Microsoft.Data.SqlClient.FunctionalTests.csproj b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.FunctionalTests.csproj index 7c54891ff4..2766a8a243 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.FunctionalTests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.FunctionalTests.csproj @@ -30,7 +30,6 @@ - @@ -144,27 +143,22 @@ - - + + + - - PreserveNewest - %(Filename)%(Extension) - - - - + PreserveNewest xunit.runner.json - + 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 c0315ce2f0..0000000000 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlAuthenticationProviderTest.cs +++ /dev/null @@ -1,36 +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)); - } - - // Overridden by app.config in this project - [ConditionalTheory(typeof(TestUtility), nameof(TestUtility.IsNetFramework))] - [InlineData(SqlAuthenticationMethod.ActiveDirectoryInteractive)] - public void DefaultAuthenticationProviders_Interactive(SqlAuthenticationMethod method) - { - Assert.IsType(SqlAuthenticationProvider.GetProvider(method)); - } - } -} diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs index 916b9ac04d..3004065fba 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs @@ -91,7 +91,7 @@ public static class DataTestUtility //SQL Server EngineEdition private static string s_sqlServerEngineEdition; - + // Azure Synapse EngineEditionId == 6 // More could be read at https://learn.microsoft.com/en-us/sql/t-sql/functions/serverproperty-transact-sql?view=sql-server-ver16#propertyname public static bool IsAzureSynapse @@ -224,6 +224,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..9c4ca9828e --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/UsernamePasswordProvider.cs @@ -0,0 +1,75 @@ +// 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 +{ + 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; + + var cts = new CancellationTokenSource(); + cts.CancelAfter(parameters.ConnectionTimeout * 1000); + + string[] scopes = new string[] { scope }; + SecureString password = new SecureString(); + + AuthenticationResult result = + #pragma warning disable CS0618 // Type or member is obsolete + await PublicClientApplicationBuilder.Create(_appClientId) + .WithAuthority(parameters.Authority) + .Build() + .AcquireTokenByUsernamePassword(scopes, 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 ManagedIdentity " + + $"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 697529f6f7..da19139ff3 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 @@ + + @@ -329,14 +331,12 @@ - - @@ -372,11 +372,11 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - - PreserveNewest - %(Filename)%(Extension) - + + + + PreserveNewest @@ -390,9 +390,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 027acfde23..e99192732d 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AADFedAuthTokenRefreshTest/AADFedAuthTokenRefreshTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AADFedAuthTokenRefreshTest/AADFedAuthTokenRefreshTest.cs @@ -2,7 +2,7 @@ // 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; using System.Diagnostics; using Microsoft.Data.SqlClient.ManualTesting.Tests.SQL.Common.SystemDataInternals; using Xunit; @@ -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 2c46c2598d..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,59 +5,14 @@ using System; using System.Diagnostics; using System.Security; -using System.Threading; using System.Threading.Tasks; using Azure.Core; -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)) @@ -79,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() @@ -213,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 } } } @@ -262,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))] @@ -320,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() { @@ -496,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() { @@ -653,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() { @@ -760,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/StressTests/Directory.Build.props b/src/Microsoft.Data.SqlClient/tests/StressTests/Directory.Build.props index 26de699227..bc5b40518d 100644 --- a/src/Microsoft.Data.SqlClient/tests/StressTests/Directory.Build.props +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/Directory.Build.props @@ -3,6 +3,12 @@ + + + diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/Directory.Packages.props b/src/Microsoft.Data.SqlClient/tests/StressTests/Directory.Packages.props index 45b1a5018f..12b1aecc30 100644 --- a/src/Microsoft.Data.SqlClient/tests/StressTests/Directory.Packages.props +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/Directory.Packages.props @@ -25,14 +25,31 @@ - - 6.1.0-preview2.25178.5 + + + + 6.1.2 + + + + + + + + + $(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.UnitTests.csproj b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft.Data.SqlClient.UnitTests.csproj index a105ccdf29..a60240475a 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft.Data.SqlClient.UnitTests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft.Data.SqlClient.UnitTests.csproj @@ -43,17 +43,15 @@ + + + + - - PreserveNewest - %(Filename)%(Extension) - - - - + PreserveNewest xunit.runner.json - + 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..60e094a725 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,17 @@ { - "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", - "diagnosticMessages": true, - "parallelizeAssembly": true, - "shadowCopy": false, - "printMaxEnumerableLength": 0, - "printMaxStringLength": 0, - "showLiveOutput": false + "_comment": "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..7f010f3a2c 100644 --- a/src/Microsoft.SqlServer.Server/StringsHelper.cs +++ b/src/Microsoft.SqlServer.Server/StringsHelper.cs @@ -48,14 +48,18 @@ 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; } public static string GetString(string res, params object[] args) diff --git a/tools/specs/Microsoft.Data.SqlClient.nuspec b/tools/specs/Microsoft.Data.SqlClient.nuspec index fe8417e554..27f4db08fb 100644 --- a/tools/specs/Microsoft.Data.SqlClient.nuspec +++ b/tools/specs/Microsoft.Data.SqlClient.nuspec @@ -91,20 +91,20 @@ - - + + - - + + - - + + - - + + @@ -115,105 +115,110 @@ - - + + - - + + - - + + - - + + - - - - - - - - - - - - - + + + + + + + + + + + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + - - + + - - - - + + + + - - - - + + + + diff --git a/tools/targets/CopySniDllsForNetFxProjectReferenceBuilds.targets b/tools/targets/CopySniDllsForNetFxProjectReferenceBuilds.targets new file mode 100644 index 0000000000..e24a40cb01 --- /dev/null +++ b/tools/targets/CopySniDllsForNetFxProjectReferenceBuilds.targets @@ -0,0 +1,51 @@ + + + + + + + + .NetFx + + + + + + + + + + + diff --git a/tools/targets/GenerateMdsPackage.targets b/tools/targets/GenerateMdsPackage.targets index 1c90310770..1b3418860a 100644 --- a/tools/targets/GenerateMdsPackage.targets +++ b/tools/targets/GenerateMdsPackage.targets @@ -1,5 +1,11 @@ + + + .NetFx + .NetCore + + @@ -8,6 +14,6 @@ - +