Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FIX | Adding support for sharedInstances to managed SNI #1237

Merged
merged 14 commits into from
Sep 20, 2021
1 change: 1 addition & 0 deletions BUILDGUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ Manual Tests require the below setup to run:
|AzureKeyVaultClientId | (Optional) "Application (client) ID" of an Active Directory registered application, granted access to the Azure Key Vault specified in `AZURE_KEY_VAULT_URL`. Requires the key permissions Get, List, Import, Decrypt, Encrypt, Unwrap, Wrap, Verify, and Sign. | _{Client Application ID}_ |
|AzureKeyVaultClientSecret | (Optional) "Client Secret" of the Active Directory registered application, granted access to the Azure Key Vault specified in `AZURE_KEY_VAULT_URL` | _{Client Application Secret}_ |
|LocalDbAppName | (Optional) If Local Db Testing is supported, this property configures the name of Local DB App instance available in client environment. Empty string value disables Local Db testing. | Name of Local Db App to connect to.|
|LocalDbSharedInstanceName | (Optional) If Local Db testing is supported and an Instance of that is shared, this property configures the name of Shared Local Db Instance to connect to. | Name of Shared Local Db Instance. |
JRahnama marked this conversation as resolved.
Show resolved Hide resolved
|SupportsIntegratedSecurity | (Optional) Whether or not the USER running tests has integrated security access to the target SQL Server.| `true` OR `false`|
|FileStreamDirectory | (Optional) If File Stream is enabled on SQL Server, pass local directory path to be used for setting up File Stream enabled database. | `D:\\escaped\\absolute\\path\\to\\directory\\` |
|UseManagedSNIOnWindows | (Optional) Enables testing with Managed SNI on Windows| `true` OR `false`|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Net.Security;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;

namespace Microsoft.Data.SqlClient.SNI
{
Expand Down Expand Up @@ -142,7 +143,7 @@ private static bool IsErrorStatus(SecurityStatusPalErrorCode errorCode)
/// <param name="cachedFQDN">Used for DNS Cache</param>
/// <param name="pendingDNSInfo">Used for DNS Cache</param>
/// <returns>SNI handle</returns>
internal static SNIHandle CreateConnectionHandle(string fullServerName, bool ignoreSniOpenTimeout, long timerExpire, out byte[] instanceName, ref byte[][] spnBuffer,
internal static SNIHandle CreateConnectionHandle(string fullServerName, bool ignoreSniOpenTimeout, long timerExpire, out byte[] instanceName, ref byte[][] spnBuffer,
bool flushCache, bool async, bool parallel, bool isIntegratedSecurity, SqlConnectionIPAddressPreference ipPreference, string cachedFQDN, ref SQLDNSInfo pendingDNSInfo)
{
instanceName = new byte[1];
Expand Down Expand Up @@ -415,6 +416,7 @@ internal enum Protocol { TCP, NP, None, Admin };

private string _workingDataSource;
private string _dataSourceAfterTrimmingProtocol;

internal bool IsBadDataSource { get; private set; } = false;

internal bool IsSsrpRequired { get; private set; } = false;
Expand Down Expand Up @@ -472,31 +474,39 @@ private void PopulateProtocol()
}
}

// LocalDbInstance name always starts with (localdb)
// possible scenarios:
// (localdb)\<instance name>
// or (localdb)\. which goes to default localdb
// or (localdb)\.\<sharedInstance name>
internal static string GetLocalDBInstance(string dataSource, out bool error)
{
string instanceName = null;

string workingDataSource = dataSource.ToLowerInvariant();

string[] tokensByBackSlash = workingDataSource.Split(BackSlashCharacter);

// ReadOnlySpan is not supported in netstandard 2.0, but installing System.Memory solves the issue
ReadOnlySpan<char> input = dataSource.AsSpan().TrimStart();
error = false;

// All LocalDb endpoints are of the format host\instancename where host is always (LocalDb) (case-insensitive)
if (tokensByBackSlash.Length == 2 && LocalDbHost.Equals(tokensByBackSlash[0].TrimStart()))
// NetStandard 2.0 does not support passing a string to ReadOnlySpan<char>
if (input.StartsWith(LocalDbHost.AsSpan().Trim(), StringComparison.InvariantCultureIgnoreCase))
{
if (!string.IsNullOrWhiteSpace(tokensByBackSlash[1]))
// When netcoreapp support for netcoreapp2.1 is dropped these slice calls could be converted to System.Range\System.Index
// Such ad input = input[1..];
input = input.Slice(LocalDbHost.Length);
if (!input.IsEmpty && input[0] == BackSlashCharacter)
{
instanceName = tokensByBackSlash[1].Trim();
input = input.Slice(1);
}
if (!input.IsEmpty)
{
instanceName = input.Trim().ToString();
}
else
{
SNILoadHandle.SingletonInstance.LastError = new SNIError(SNIProviders.INVALID_PROV, 0, SNICommon.LocalDBNoInstanceName, Strings.SNI_ERROR_51);
error = true;
return null;
}
}

return instanceName;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public static class DataTestUtility
public static readonly string AKVClientId = null;
public static readonly string AKVClientSecret = null;
public static readonly string LocalDbAppName = null;
public static readonly string LocalDbSharedInstanceName = null;
public static List<string> AEConnStrings = new List<string>();
public static List<string> AEConnStringsSetup = new List<string>();
public static bool EnclaveEnabled { get; private set; } = false;
Expand Down Expand Up @@ -84,6 +85,7 @@ static DataTestUtility()
AADServicePrincipalId = c.AADServicePrincipalId;
AADServicePrincipalSecret = c.AADServicePrincipalSecret;
LocalDbAppName = c.LocalDbAppName;
LocalDbSharedInstanceName = c.LocalDbSharedInstanceName;
SupportsIntegratedSecurity = c.SupportsIntegratedSecurity;
FileStreamDirectory = c.FileStreamDirectory;
EnclaveEnabled = c.EnclaveEnabled;
Expand Down Expand Up @@ -441,7 +443,8 @@ public static void DropDatabase(SqlConnection sqlConnection, string dbName)
cmd.ExecuteNonQuery();
}

public static bool IsLocalDBInstalled() => !string.IsNullOrEmpty(LocalDbAppName?.Trim());
public static bool IsLocalDBInstalled() => !string.IsNullOrEmpty(LocalDbAppName?.Trim()) && !IsIntegratedSecuritySetup();
public static bool IsLocalDbSharedInstanceSetup() => !string.IsNullOrEmpty(LocalDbSharedInstanceName?.Trim()) && !IsIntegratedSecuritySetup();

public static bool IsIntegratedSecuritySetup() => SupportsIntegratedSecurity;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,57 +1,95 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Collections.Generic;
using Xunit;

namespace Microsoft.Data.SqlClient.ManualTesting.Tests
{
public static class LocalDBTest
{
private static bool IsLocalDBEnvironmentSet() => DataTestUtility.IsLocalDBInstalled();
private static bool IsLocalDbSharedInstanceSet() => DataTestUtility.IsLocalDbSharedInstanceSetup();
private static readonly string s_localDbConnectionString = @$"server=(localdb)\{DataTestUtility.LocalDbAppName}";
private static readonly string[] s_sharedLocalDbInstances = new string[] { @$"server=(localdb)\.\{DataTestUtility.LocalDbSharedInstanceName}", @$"server=(localdb)\." };
private static readonly string s_badConnectionString = $@"server=(localdb)\{DataTestUtility.LocalDbAppName};Database=DOES_NOT_EXIST;Pooling=false;";

static string LocalDbName = DataTestUtility.LocalDbAppName;
#region LocalDbTests
[SkipOnTargetFramework(TargetFrameworkMonikers.Uap)] // No Registry support on UAP
[ConditionalFact(nameof(IsLocalDBEnvironmentSet))]
public static void LocalDBConnectionTest()
public static void SqlLocalDbConnectionTest()
{
SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(@$"server=(localdb)\{DataTestUtility.LocalDbAppName}");
builder.IntegratedSecurity = true;
builder.ConnectTimeout = 2;
OpenConnection(builder.ConnectionString);
ConnectionTest(s_localDbConnectionString);
}

[SkipOnTargetFramework(TargetFrameworkMonikers.Uap)] // No Registry support on UAP
[ConditionalFact(nameof(IsLocalDBEnvironmentSet))]
public static void LocalDBMarsTest()
{
SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(@$"server=(localdb)\{DataTestUtility.LocalDbAppName}");
builder.IntegratedSecurity = true;
builder.MultipleActiveResultSets = true;
builder.ConnectTimeout = 2;
OpenConnection(builder.ConnectionString);
ConnectionWithMarsTest(s_localDbConnectionString);
}

[SkipOnTargetFramework(TargetFrameworkMonikers.Uap)] // No Registry support on UAP
[ConditionalFact(nameof(IsLocalDBEnvironmentSet))]
public static void InvalidDBTest()
public static void InvalidLocalDBTest()
{
using (var connection = new SqlConnection(@$"server=(localdb)\{DataTestUtility.LocalDbAppName};Database=DOES_NOT_EXIST;Pooling=false;"))
using var connection = new SqlConnection(s_badConnectionString);
DataTestUtility.AssertThrowsWrapper<SqlException>(() => connection.Open());
}
#endregion

#region SharedLocalDb tests
[SkipOnTargetFramework(TargetFrameworkMonikers.Uap)] // No Registry support on UAP
[ConditionalFact(nameof(IsLocalDbSharedInstanceSet))]
public static void SharedLocalDbMarsTest()
{
foreach (string connectionString in s_sharedLocalDbInstances)
{
DataTestUtility.AssertThrowsWrapper<SqlException>(() => connection.Open());
ConnectionWithMarsTest(connectionString);
}
}

private static void OpenConnection(string connString)
[SkipOnTargetFramework(TargetFrameworkMonikers.Uap)] // No Registry support on UAP
[ConditionalFact(nameof(IsLocalDbSharedInstanceSet))]
public static void SqlLocalDbSharedInstanceConnectionTest()
{
using (SqlConnection connection = new SqlConnection(connString))
foreach (string connectionString in s_sharedLocalDbInstances)
{
connection.Open();
using (SqlCommand command = new SqlCommand("SELECT @@SERVERNAME", connection))
{
var result = command.ExecuteScalar();
Assert.NotNull(result);
}
ConnectionTest(connectionString);
}
}
#endregion

private static void ConnectionWithMarsTest(string connectionString)
{
SqlConnectionStringBuilder builder = new(connectionString)
{
IntegratedSecurity = true,
cheenamalhotra marked this conversation as resolved.
Show resolved Hide resolved
MultipleActiveResultSets = true,
ConnectTimeout = 2,
Encrypt = false
};
OpenConnection(builder.ConnectionString);
}
private static void ConnectionTest(string connectionString)
{
SqlConnectionStringBuilder builder = new(connectionString)
{
IntegratedSecurity = true,
JRahnama marked this conversation as resolved.
Show resolved Hide resolved
ConnectTimeout = 2,
Encrypt = false
};
OpenConnection(builder.ConnectionString);
}

private static void OpenConnection(string connString)
{
using SqlConnection connection = new(connString);
connection.Open();
using SqlCommand command = new SqlCommand("SELECT @@SERVERNAME", connection);
var result = command.ExecuteScalar();
Assert.NotNull(result);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public class Config
public string AzureKeyVaultClientId = null;
public string AzureKeyVaultClientSecret = null;
public string LocalDbAppName = null;
public string LocalDbSharedInstanceName = null;
DavoudEshtehari marked this conversation as resolved.
Show resolved Hide resolved
public bool EnclaveEnabled = false;
public bool TracingEnabled = false;
public bool SupportsIntegratedSecurity = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"AzureKeyVaultClientSecret": "",
"SupportsIntegratedSecurity": true,
"LocalDbAppName": "",
"SupportsFileStream": false,
"FileStreamDirectory": "",
"UseManagedSNIOnWindows": false,
"DNSCachingConnString": "",
Expand Down