From 01a589e54afe43c85c7607aa05f8605fa6f13bb3 Mon Sep 17 00:00:00 2001 From: Javad Date: Fri, 26 Jan 2024 17:39:03 -0800 Subject: [PATCH] [5.1.5] Fix | Enable reading AE date as DateOnly (#2275) (#2324) --- .../Microsoft/Data/SqlClient/SqlDataReader.cs | 4 +- .../ManualTests/AlwaysEncrypted/ApiShould.cs | 4 + .../AlwaysEncrypted/DateOnlyReadTests.cs | 93 +++++++++++++++++++ .../TestFixtures/DatabaseHelper.cs | 29 ++++++ .../TestFixtures/SQLSetupStrategy.cs | 7 +- .../TestFixtures/Setup/DateOnlyTestTable.cs | 42 +++++++++ .../ManualTests/DataCommon/DataTestUtility.cs | 1 - ....Data.SqlClient.ManualTesting.Tests.csproj | 4 + 8 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/DateOnlyReadTests.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/DateOnlyTestTable.cs diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlDataReader.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlDataReader.cs index 965803f993..d72fbbf4c9 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlDataReader.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlDataReader.cs @@ -2844,11 +2844,11 @@ private T GetFieldValueFromSqlBufferInternal(SqlBuffer data, _SqlMetaData met return (T)(object)data.DateTime; } #if NET6_0_OR_GREATER - else if (typeof(T) == typeof(DateOnly) && dataType == typeof(DateTime) && _typeSystem > SqlConnectionString.TypeSystem.SQLServer2005 && metaData.Is2008DateTimeType) + else if (typeof(T) == typeof(DateOnly) && dataType == typeof(DateTime) && _typeSystem > SqlConnectionString.TypeSystem.SQLServer2005) { return (T)(object)data.DateOnly; } - else if (typeof(T) == typeof(TimeOnly) && dataType == typeof(TimeOnly) && _typeSystem > SqlConnectionString.TypeSystem.SQLServer2005 && metaData.Is2008DateTimeType) + else if (typeof(T) == typeof(TimeOnly) && dataType == typeof(TimeOnly) && _typeSystem > SqlConnectionString.TypeSystem.SQLServer2005) { return (T)(object)data.TimeOnly; } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs index 62db90fce0..8e7458f02e 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs @@ -3169,6 +3169,10 @@ public Customer(int id, string firstName, string lastName) public string LastName { get; set; } } +#if NET6_0_OR_GREATER + public record CustomerDateOnly(int Id, string FirstName, string LastName, DateOnly DateOfBirth, TimeOnly TimeOfDay); +#endif + internal class TestAsyncCallBackStateObject { /// diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/DateOnlyReadTests.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/DateOnlyReadTests.cs new file mode 100644 index 0000000000..a211fe6343 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/DateOnlyReadTests.cs @@ -0,0 +1,93 @@ +// 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; +using System.Collections.Generic; +using Microsoft.Data.SqlClient.ManualTesting.Tests.AlwaysEncrypted.Setup; +using Xunit; + +namespace Microsoft.Data.SqlClient.ManualTesting.Tests.AlwaysEncrypted +{ + public sealed class DateOnlyReadTests : IClassFixture, IDisposable + { + private SQLSetupStrategy fixture; + + private readonly string tableName; + + public DateOnlyReadTests(PlatformSpecificTestContext context) + { + fixture = context.Fixture; + tableName = fixture.DateOnlyTestTable.Name; + } + + // tests + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsTargetReadyForAeWithKeyStore))] + [ClassData(typeof(TestSelectOnEncryptedNonEncryptedColumnsDataDateOnly))] + public void TestSelectOnEncryptedNonEncryptedColumns(string connString, string selectQuery, int totalColumnsInSelect, string[] types) + { + Assert.False(string.IsNullOrWhiteSpace(selectQuery), "FAILED: select query should not be null or empty."); + Assert.True(totalColumnsInSelect <= 3, "FAILED: totalColumnsInSelect should <= 3."); + + using (SqlConnection sqlConn = new SqlConnection(connString)) + { + sqlConn.Open(); + + Table.DeleteData(tableName, sqlConn); + + // insert 1 row data + CustomerDateOnly customer = new CustomerDateOnly( + 45, + "Microsoft", + "Corporation", + new DateOnly(2001, 1, 31), + new TimeOnly(18, 36, 45)); + + DatabaseHelper.InsertCustomerDateOnlyData(sqlConn, null, tableName, customer); + + using (SqlCommand sqlCommand = new SqlCommand(string.Format(selectQuery, tableName), + sqlConn, null, SqlCommandColumnEncryptionSetting.Enabled)) + { + using (SqlDataReader sqlDataReader = sqlCommand.ExecuteReader()) + { + Assert.True(sqlDataReader.HasRows, "FAILED: Select statement did not return any rows."); + + while (sqlDataReader.Read()) + { + DatabaseHelper.CompareResults(sqlDataReader, types, totalColumnsInSelect); + } + } + } + } + } + + + public void Dispose() + { + foreach (string connStrAE in DataTestUtility.AEConnStringsSetup) + { + using (SqlConnection sqlConnection = new SqlConnection(connStrAE)) + { + sqlConnection.Open(); + Table.DeleteData(fixture.DateOnlyTestTable.Name, sqlConnection); + } + } + } + } + + public class TestSelectOnEncryptedNonEncryptedColumnsDataDateOnly : IEnumerable + { + public IEnumerator GetEnumerator() + { + foreach (string connStrAE in DataTestUtility.AEConnStrings) + { + yield return new object[] { connStrAE, @"select CustomerId, FirstName, LastName from [{0}] ", 3, new string[] { @"int", @"string", @"string" } }; + yield return new object[] { connStrAE, @"select CustomerId, FirstName from [{0}] ", 2, new string[] { @"int", @"string" } }; + yield return new object[] { connStrAE, @"select LastName from [{0}] ", 1, new string[] { @"string" }}; + yield return new object[] { connStrAE, @"select DateOfBirth, TimeOfDay from [{0}] ", 2, new string[] { @"DateOnly", "TimeOnly" } }; + } + } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/DatabaseHelper.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/DatabaseHelper.cs index 9cea6b8239..86fa2c38c5 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/DatabaseHelper.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/DatabaseHelper.cs @@ -31,6 +31,27 @@ internal static void InsertCustomerData(SqlConnection sqlConnection, SqlTransact sqlCommand.ExecuteNonQuery(); } +#if NET6_0_OR_GREATER + /// + /// Insert CustomerDateOnly record into table + /// + internal static void InsertCustomerDateOnlyData(SqlConnection sqlConnection, SqlTransaction transaction, string tableName, CustomerDateOnly customer) + { + using SqlCommand sqlCommand = new( + $"INSERT INTO [{tableName}] (CustomerId, FirstName, LastName, DateOfBirth, TimeOfDay) VALUES (@CustomerId, @FirstName, @LastName, @DateOfBirth, @TimeOfDay);", + connection: sqlConnection, + transaction: transaction, + columnEncryptionSetting: SqlCommandColumnEncryptionSetting.Enabled); + + sqlCommand.Parameters.AddWithValue(@"CustomerId", customer.Id); + sqlCommand.Parameters.AddWithValue(@"FirstName", customer.FirstName); + sqlCommand.Parameters.AddWithValue(@"LastName", customer.LastName); + sqlCommand.Parameters.AddWithValue(@"DateOfBirth", customer.DateOfBirth); + sqlCommand.Parameters.AddWithValue(@"TimeOfDay", customer.TimeOfDay); + sqlCommand.ExecuteNonQuery(); + } +#endif + /// /// Validates that the results are the ones expected. /// @@ -155,7 +176,15 @@ public static void CompareResults(SqlDataReader sqlDataReader, string[] paramete case "int": Assert.True(sqlDataReader.GetInt32(columnsRead) == 45, "FAILED: read int value does not match."); break; +#if NET6_0_OR_GREATER + case "DateOnly": + Assert.True(sqlDataReader.GetFieldValue(columnsRead) == new DateOnly(2001, 1, 31), "FAILED: read DateOnly value does not match."); + break; + case "TimeOnly": + Assert.True(sqlDataReader.GetFieldValue(columnsRead) == new TimeOnly(18, 36, 45), "FAILED: read TimeOnly value does not match."); + break; +#endif default: Assert.True(false, "FAILED: unexpected data type."); break; diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/SQLSetupStrategy.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/SQLSetupStrategy.cs index 0cd8436cb4..4d3c635684 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/SQLSetupStrategy.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/SQLSetupStrategy.cs @@ -1,4 +1,5 @@ -// Licensed to the .NET Foundation under one or more agreements. + +// 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; @@ -21,6 +22,7 @@ public class SQLSetupStrategy : IDisposable public Table BulkCopyAEErrorMessageTestTable { get; private set; } public Table BulkCopyAETestTable { get; private set; } public Table SqlParameterPropertiesTable { get; private set; } + public Table DateOnlyTestTable { get; private set; } public Table End2EndSmokeTable { get; private set; } public Table TrustedMasterKeyPathsTestTable { get; private set; } public Table SqlNullValuesTable { get; private set; } @@ -131,6 +133,9 @@ protected List CreateTables(IList columnEncryptionKe End2EndSmokeTable = new ApiTestTable(GenerateUniqueName("End2EndSmokeTable"), columnEncryptionKeys[0], columnEncryptionKeys[1]); tables.Add(End2EndSmokeTable); + DateOnlyTestTable = new DateOnlyTestTable(GenerateUniqueName("DateOnlyTestTable"), columnEncryptionKeys[0], columnEncryptionKeys[1]); + tables.Add(DateOnlyTestTable); + TrustedMasterKeyPathsTestTable = new ApiTestTable(GenerateUniqueName("TrustedMasterKeyPathsTestTable"), columnEncryptionKeys[0], columnEncryptionKeys[1]); tables.Add(TrustedMasterKeyPathsTestTable); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/DateOnlyTestTable.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/DateOnlyTestTable.cs new file mode 100644 index 0000000000..6cc9bb6f66 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/DateOnlyTestTable.cs @@ -0,0 +1,42 @@ +// 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.ManualTesting.Tests.AlwaysEncrypted.Setup +{ + public class DateOnlyTestTable : Table + { + private const string ColumnEncryptionAlgorithmName = @"AEAD_AES_256_CBC_HMAC_SHA_256"; + public ColumnEncryptionKey columnEncryptionKey1; + public ColumnEncryptionKey columnEncryptionKey2; + private bool useDeterministicEncryption; + + public DateOnlyTestTable(string tableName, ColumnEncryptionKey columnEncryptionKey1, ColumnEncryptionKey columnEncryptionKey2, bool useDeterministicEncryption = false) : base(tableName) + { + this.columnEncryptionKey1 = columnEncryptionKey1; + this.columnEncryptionKey2 = columnEncryptionKey2; + this.useDeterministicEncryption = useDeterministicEncryption; + } + + public override void Create(SqlConnection sqlConnection) + { + string encryptionType = useDeterministicEncryption ? "DETERMINISTIC" : DataTestUtility.EnclaveEnabled ? "RANDOMIZED" : "DETERMINISTIC"; + string sql = + $@"CREATE TABLE [dbo].[{Name}] + ( + [CustomerId] [int] ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [{columnEncryptionKey1.Name}], ENCRYPTION_TYPE = {encryptionType}, ALGORITHM = '{ColumnEncryptionAlgorithmName}'), + [FirstName] [nvarchar](50) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [{columnEncryptionKey2.Name}], ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = '{ColumnEncryptionAlgorithmName}'), + [LastName] [nvarchar](50) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [{columnEncryptionKey2.Name}], ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = '{ColumnEncryptionAlgorithmName}'), + [TimeOfDay] [time] ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [{columnEncryptionKey1.Name}], ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = '{ColumnEncryptionAlgorithmName}'), + [DateOfBirth] [date] ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [{columnEncryptionKey1.Name}], ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = '{ColumnEncryptionAlgorithmName}') + )"; + + using (SqlCommand command = sqlConnection.CreateCommand()) + { + command.CommandText = sql; + command.ExecuteNonQuery(); + } + } + + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs index 4853806246..c92425561c 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs @@ -61,7 +61,6 @@ public static class DataTestUtility public static readonly bool IsDNSCachingSupportedTR = false; // this is for the tenant ring public static readonly string UserManagedIdentityClientId = null; - public static readonly string EnclaveAzureDatabaseConnString = null; public static bool ManagedIdentitySupported = true; public static string AADAccessToken = null; 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 5172452626..9f0cb5a11b 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 @@ -57,6 +57,7 @@ + @@ -70,6 +71,9 @@ + + +