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 | Fix unit test for SPN to include port number with Managed SNI #2281

Merged
merged 21 commits into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9c776f3
Remove ActiveIssue to test pipeline.
arellegue Dec 18, 2023
f84884c
Fix SPN port number Unit Test to use TCP and NP connection strings an…
arellegue Dec 20, 2023
36c235f
Try adding retry and delay in creating and using the connection objec…
arellegue Dec 20, 2023
0caa4ce
Added IsUsingManagedSNI annotation since reflection is using ManagedS…
arellegue Dec 20, 2023
c47a16e
Add wrapper for all required annotations for the unit test as it is t…
arellegue Dec 20, 2023
64c0103
Removed all Sleeps as they are not needed.
arellegue Dec 20, 2023
af685d3
Add annotation to skip .net framework for SPN port unit test.
arellegue Dec 20, 2023
6076e8e
Put [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] so …
arellegue Dec 20, 2023
bea5513
Removed SkipOnTargetFramework as it did not stop the unit test from r…
arellegue Dec 20, 2023
fa556f5
Add validation of InstanceName within the ConditionalThreory annotation.
arellegue Dec 21, 2023
6ace809
Removed and sorted using references.
arellegue Dec 21, 2023
660d992
Fix ParseDataSource named pipe data source detection to be case insen…
arellegue Dec 22, 2023
192a1e2
Applied PR review suggesstions.
arellegue Jan 5, 2024
8a93097
Moved named pipe protocol port usage assert message to line 138 and u…
arellegue Jan 11, 2024
00d9a3c
Removed all source codes that handle named pipe protocol.
arellegue Jan 19, 2024
41366f6
Removed unwanted comments as they cause code bloat.
arellegue Jan 24, 2024
baa3ab9
Add SPN pattern validation and use Assert.Equal to compare expected p…
arellegue Jan 24, 2024
53a13e6
Merge branch 'main' into FixUnitTestForSPNPortNumber
arellegue Feb 1, 2024
2bc3f7c
Added validation for DataSource.
arellegue Feb 1, 2024
c131562
Added assertion if DataSource is valid.
arellegue Feb 1, 2024
0c5ed6f
Replace If (port > 0) with Assert( port > 0, ...)
arellegue Feb 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -653,10 +653,10 @@ private bool InferConnectionDetails()

Port = port;
}
// Instance Name Handling. Only if we found a '\' and we did not find a port in the Data Source
else if (backSlashIndex > -1)
// Instance Name Handling.
if (backSlashIndex > -1)
{
// This means that there will not be any part separated by comma.
// This means that there is a part separated by '\'
InstanceName = tokensByCommaAndSlash[1].Trim();

if (string.IsNullOrWhiteSpace(InstanceName))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -979,9 +979,6 @@ public static bool ParseDataSource(string dataSource, out string hostname, out i
port = -1;
instanceName = string.Empty;

if (dataSource.Contains(",") && dataSource.Contains("\\"))
return false;

if (dataSource.Contains(":"))
{
dataSource = dataSource.Substring(dataSource.IndexOf(":", StringComparison.Ordinal) + 1);
Expand All @@ -993,7 +990,8 @@ public static bool ParseDataSource(string dataSource, out string hostname, out i
{
return false;
}
dataSource = dataSource.Substring(0, dataSource.IndexOf(",", StringComparison.Ordinal) - 1);
// IndexOf is zero-based, no need to subtract one
dataSource = dataSource.Substring(0, dataSource.IndexOf(",", StringComparison.Ordinal));
arellegue marked this conversation as resolved.
Show resolved Hide resolved
}

if (dataSource.Contains("\\"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,48 +84,96 @@ public static void ConnectManagedWithInstanceNameTest(bool useMultiSubnetFailove
}
}

// Note: This Unit test was tested in a domain-joined VM connecting to a remote
// SQL Server using Kerberos in the same domain.
[ActiveIssue("27824")] // When specifying instance name and port number, this method call always returns false
[ConditionalFact(nameof(IsKerberos))]
public static void PortNumberInSPNTest()
#if NETCOREAPP
[ActiveIssue("27981")] // DataSource.InferNamedPipesInformation is not initializing InstanceName field
arellegue marked this conversation as resolved.
Show resolved Hide resolved
[ConditionalTheory(nameof(IsSPNPortNumberTestForNP))]
[InlineData("")]
[InlineData("44444")] // Named Instance Sql Server Port will be setup in the pipeline to 44444 as well
public static void PortNumberInSPNTestForNP(string port)
arellegue marked this conversation as resolved.
Show resolved Hide resolved
{
string connStr = DataTestUtility.TCPConnectionString;
// If config.json.SupportsIntegratedSecurity = true, replace all keys defined below with Integrated Security=true
string connectionString = DataTestUtility.NPConnectionString;
SqlConnectionStringBuilder builder = new(connectionString);

if (!string.IsNullOrWhiteSpace(port))
builder.DataSource = $"{builder.DataSource},{port}";

PortNumberInSPNTest(builder.ConnectionString);
}

[ConditionalTheory(nameof(IsSPNPortNumberTestForTCP))]
[InlineData("")]
[InlineData("44444")] // Named Instance Sql Server Port will be setup in the pipeline to 44444 as well
arellegue marked this conversation as resolved.
Show resolved Hide resolved
public static void PortNumberInSPNTestForTCP(string port)
{
string connectionString = DataTestUtility.TCPConnectionString;
SqlConnectionStringBuilder builder = new(connectionString);

if (!string.IsNullOrWhiteSpace(port))
builder.DataSource = $"{builder.DataSource},{port}";

PortNumberInSPNTest(builder.ConnectionString);
}
#endif

private static void PortNumberInSPNTest(string connectionString)
{
// If config.json.Supports IntegratedSecurity = true, replace all keys defined below with Integrated Security=true
if (DataTestUtility.IsIntegratedSecuritySetup())
{
string[] removeKeys = { "Authentication", "User ID", "Password", "UID", "PWD", "Trusted_Connection" };
connStr = DataTestUtility.RemoveKeysInConnStr(DataTestUtility.TCPConnectionString, removeKeys) + $"Integrated Security=true";
connectionString = DataTestUtility.RemoveKeysInConnStr(connectionString, removeKeys) + $"Integrated Security=true";
}

SqlConnectionStringBuilder builder = new(connectionString);

string hostname = "";
string instanceName = "";
int port = -1;

// Named pipe protocol data source does not support port number
string dataSource = builder.DataSource.ToUpper();
if ((dataSource.Contains(@"\MSSQL$") && builder.DataSource.ToUpper().Contains(@"\SQL\QUERY") && dataSource.Contains(",")) ||
arellegue marked this conversation as resolved.
Show resolved Hide resolved
(dataSource.Contains(@"NP:") && dataSource.Contains(",")))
{
port = -2;
}
Assert.False(port == -2, "Named pipe protocol in data source does not support port number.");
arellegue marked this conversation as resolved.
Show resolved Hide resolved

SqlConnectionStringBuilder builder = new(connStr);
DataTestUtility.ParseDataSource(builder.DataSource, out hostname, out port, out instanceName);

Assert.False(string.IsNullOrEmpty(hostname), "Hostname must be included in the data source.");
Assert.False(string.IsNullOrEmpty(instanceName), "Instance name must be included in the data source.");

Assert.True(DataTestUtility.ParseDataSource(builder.DataSource, out string hostname, out _, out string instanceName), "Data source to be parsed must contain a host name and instance name");
bool isBrowserRunning = IsBrowserAlive(hostname);
Assert.True(isBrowserRunning, "Browser service is not running.");

bool condition = IsBrowserAlive(hostname) && IsValidInstance(hostname, instanceName);
Assert.True(condition, "Browser service is not running or instance name is invalid");
bool isInstanceExisting = IsValidInstance(hostname, instanceName);
Assert.True(isInstanceExisting, "Instance name is invalid.");
arellegue marked this conversation as resolved.
Show resolved Hide resolved

if (condition)
if (isBrowserRunning && isInstanceExisting)
arellegue marked this conversation as resolved.
Show resolved Hide resolved
{
using SqlConnection connection = new(builder.ConnectionString);
// Create a connection object to ensure SPN info is available via reflection
SqlConnection connection = new(builder.ConnectionString);
arellegue marked this conversation as resolved.
Show resolved Hide resolved
connection.Open();
using SqlCommand command = new("SELECT auth_scheme, local_tcp_port from sys.dm_exec_connections where session_id = @@spid", connection);
using SqlDataReader reader = command.ExecuteReader();
Assert.True(reader.Read(), "Expected to receive one row data");
Assert.Equal("KERBEROS", reader.GetString(0));
int localTcpPort = reader.GetInt32(1);

int spnPort = -1;
string spnInfo = GetSPNInfo(builder.DataSource, out spnPort);

// sample output to validate = MSSQLSvc/machine.domain.tld:spnPort"
Assert.Contains($"MSSQLSvc/{hostname}", spnInfo);
// the local_tcp_port should be the same as the inferred SPN port from instance name
Assert.Equal(localTcpPort, spnPort);

// Get the SPN info using reflection
string spnInfo = GetSPNInfo(builder.DataSource);

// The expected output to validate is supposed to be in the format "MSSQLSvc/machine.domain.tld:spnPort".
// So, we want to get the port number from the SPN and ensure it is a valid port number.
string[] spnStrs = spnInfo.Split(':');
int portInSPN = 0;
if (spnStrs.Length > 1)
{
int.TryParse(spnStrs[1], out portInSPN);
}
Assert.True(portInSPN > 0, "The expected SPN must include a valid port number.");
arellegue marked this conversation as resolved.
Show resolved Hide resolved

connection.Close();
}
}

private static string GetSPNInfo(string datasource, out int out_port)
private static string GetSPNInfo(string dataSource)
{
Assembly sqlConnectionAssembly = Assembly.GetAssembly(typeof(SqlConnection));

Expand Down Expand Up @@ -156,26 +204,27 @@ private static string GetSPNInfo(string datasource, out int out_port)
// Instantiate SNIProxy
object sniProxy = sniProxyCtor.Invoke(new object[] { });

// Instantiate datasource
object dataSourceObj = dataSourceCtor.Invoke(new object[] { datasource });
// Instantiate dataSource
object dataSourceObj = dataSourceCtor.Invoke(new object[] { dataSource });

// Instantiate SSRP
object ssrp = SSRPCtor.Invoke(new object[] { });
object ssrpObj = SSRPCtor.Invoke(new object[] { });

// Instantiate TimeoutTimer
object timeoutTimer = timeoutTimerCtor.Invoke(new object[] { });
object timeoutTimerObj = timeoutTimerCtor.Invoke(new object[] { });

// Get TimeoutTimer.StartSecondsTimeout Method
MethodInfo startSecondsTimeout = timeoutTimer.GetType().GetMethod("StartSecondsTimeout", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, CallingConventions.Any, startSecondsTimeoutTypesArray, null);
MethodInfo startSecondsTimeout = timeoutTimerObj.GetType().GetMethod("StartSecondsTimeout", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, CallingConventions.Any, startSecondsTimeoutTypesArray, null);

// Create a timeoutTimer that expires in 30 seconds
timeoutTimer = startSecondsTimeout.Invoke(dataSourceObj, new object[] { 30 });
timeoutTimerObj = startSecondsTimeout.Invoke(dataSourceObj, new object[] { 30 });

// Parse the datasource to separate the server name and instance name
// Parse the dataSource to separate the server name and instance name
MethodInfo ParseServerName = dataSourceObj.GetType().GetMethod("ParseServerName", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, CallingConventions.Any, dataSourceConstructorTypesArray, null);
object dataSrcInfo = ParseServerName.Invoke(dataSourceObj, new object[] { datasource });
object dataSrcInfo = ParseServerName.Invoke(dataSourceObj, new object[] { dataSource });

// Get the GetPortByInstanceName method of SSRP
MethodInfo getPortByInstanceName = ssrp.GetType().GetMethod("GetPortByInstanceName", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, CallingConventions.Any, getPortByInstanceNameTypesArray, null);
MethodInfo getPortByInstanceName = ssrpObj.GetType().GetMethod("GetPortByInstanceName", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, CallingConventions.Any, getPortByInstanceNameTypesArray, null);

// Get the server name
PropertyInfo serverInfo = dataSrcInfo.GetType().GetProperty("ServerName", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
Expand All @@ -186,9 +235,9 @@ private static string GetSPNInfo(string datasource, out int out_port)
string instanceName = instanceNameInfo.GetValue(dataSrcInfo, null).ToString();

// Get the port number using the GetPortByInstanceName method of SSRP
object port = getPortByInstanceName.Invoke(ssrp, parameters: new object[] { serverName, instanceName, timeoutTimer, false, 0 });
object port = getPortByInstanceName.Invoke(ssrpObj, parameters: new object[] { serverName, instanceName, timeoutTimerObj, false, 0 });

// Set the resolved port property of datasource
// Set the resolved port property of dataSource
PropertyInfo resolvedPortInfo = dataSrcInfo.GetType().GetProperty("ResolvedPort", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
resolvedPortInfo.SetValue(dataSrcInfo, (int)port, null);

Expand All @@ -202,20 +251,40 @@ private static string GetSPNInfo(string datasource, out int out_port)
// Example result: MSSQLSvc/machine.domain.tld:port"
string spnInfo = Encoding.Unicode.GetString(result[0]);

out_port = (int)port;

return spnInfo;
}

private static bool IsKerberos()
private static bool IsSPNPortNumberTestForTCP()
{
return (IsInstanceNameValid(DataTestUtility.TCPConnectionString)
&& DataTestUtility.AreConnStringsSetup()
&& DataTestUtility.IsUsingManagedSNI()
&& DataTestUtility.IsNotAzureServer()
&& DataTestUtility.IsNotAzureSynapse());
}

private static bool IsSPNPortNumberTestForNP()
{
return (DataTestUtility.AreConnStringsSetup()
&& DataTestUtility.IsNotLocalhost()
&& DataTestUtility.IsKerberosTest
&& DataTestUtility.IsNotAzureServer()
return (IsInstanceNameValid(DataTestUtility.NPConnectionString)
&& DataTestUtility.AreConnStringsSetup()
arellegue marked this conversation as resolved.
Show resolved Hide resolved
&& DataTestUtility.IsUsingManagedSNI()
&& DataTestUtility.IsNotAzureServer()
&& DataTestUtility.IsNotAzureSynapse());
}

private static bool IsInstanceNameValid(string connectionString)
{
string hostname = "";
string instanceName = "";
int port = -1;

SqlConnectionStringBuilder builder = new(connectionString);

DataTestUtility.ParseDataSource(builder.DataSource, out hostname, out port, out instanceName);
arellegue marked this conversation as resolved.
Show resolved Hide resolved

return !string.IsNullOrWhiteSpace(instanceName);
}

private static bool IsBrowserAlive(string browserHostname)
{
const byte ClntUcastEx = 0x03;
Expand Down