diff --git a/source/pdo_sqlsrv/pdo_dbh.cpp b/source/pdo_sqlsrv/pdo_dbh.cpp index 0f87c7c2..f0238fb6 100644 --- a/source/pdo_sqlsrv/pdo_dbh.cpp +++ b/source/pdo_sqlsrv/pdo_dbh.cpp @@ -87,7 +87,8 @@ enum PDO_STMT_OPTIONS { PDO_STMT_OPTION_FETCHES_NUMERIC_TYPE, PDO_STMT_OPTION_FETCHES_DATETIME_TYPE, PDO_STMT_OPTION_FORMAT_DECIMALS, - PDO_STMT_OPTION_DECIMAL_PLACES + PDO_STMT_OPTION_DECIMAL_PLACES, + PDO_STMT_OPTION_DATA_CLASSIFICATION }; // List of all the statement options supported by this driver. @@ -104,6 +105,7 @@ const stmt_option PDO_STMT_OPTS[] = { { NULL, 0, PDO_STMT_OPTION_FETCHES_DATETIME_TYPE, std::unique_ptr( new stmt_option_fetch_datetime ) }, { NULL, 0, PDO_STMT_OPTION_FORMAT_DECIMALS, std::unique_ptr( new stmt_option_format_decimals ) }, { NULL, 0, PDO_STMT_OPTION_DECIMAL_PLACES, std::unique_ptr( new stmt_option_decimal_places ) }, + { NULL, 0, PDO_STMT_OPTION_DATA_CLASSIFICATION, std::unique_ptr( new stmt_option_data_classification ) }, { NULL, 0, SQLSRV_STMT_OPTION_INVALID, std::unique_ptr{} }, }; @@ -1136,6 +1138,7 @@ int pdo_sqlsrv_dbh_set_attr( _Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout case PDO_ATTR_EMULATE_PREPARES: case PDO_ATTR_CURSOR: case SQLSRV_ATTR_CURSOR_SCROLL_TYPE: + case SQLSRV_ATTR_DATA_CLASSIFICATION: { THROW_PDO_ERROR( driver_dbh, PDO_SQLSRV_ERROR_STMT_LEVEL_ATTR ); } @@ -1193,7 +1196,8 @@ int pdo_sqlsrv_dbh_get_attr( _Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout // Statement level only case PDO_ATTR_EMULATE_PREPARES: case PDO_ATTR_CURSOR: - case SQLSRV_ATTR_CURSOR_SCROLL_TYPE: + case SQLSRV_ATTR_CURSOR_SCROLL_TYPE: + case SQLSRV_ATTR_DATA_CLASSIFICATION: { THROW_PDO_ERROR( driver_dbh, PDO_SQLSRV_ERROR_STMT_LEVEL_ATTR ); } @@ -1594,70 +1598,75 @@ namespace { // Maps the PDO driver specific statement option/attribute constants to the core layer // statement option/attribute constants. -void add_stmt_option_key( _Inout_ sqlsrv_context& ctx, _In_ size_t key, _Inout_ HashTable* options_ht, - _Inout_ zval* data TSRMLS_DC ) +void add_stmt_option_key(_Inout_ sqlsrv_context& ctx, _In_ size_t key, _Inout_ HashTable* options_ht, + _Inout_ zval* data TSRMLS_DC) { - zend_ulong option_key = -1; - switch( key ) { - - case PDO_ATTR_CURSOR: - option_key = SQLSRV_STMT_OPTION_SCROLLABLE; - break; - - case SQLSRV_ATTR_ENCODING: - option_key = PDO_STMT_OPTION_ENCODING; - break; + zend_ulong option_key = -1; + switch (key) { - case SQLSRV_ATTR_QUERY_TIMEOUT: - option_key = SQLSRV_STMT_OPTION_QUERY_TIMEOUT; - break; + case PDO_ATTR_CURSOR: + option_key = SQLSRV_STMT_OPTION_SCROLLABLE; + break; - case PDO_ATTR_STATEMENT_CLASS: - break; + case SQLSRV_ATTR_ENCODING: + option_key = PDO_STMT_OPTION_ENCODING; + break; - case SQLSRV_ATTR_DIRECT_QUERY: - option_key = PDO_STMT_OPTION_DIRECT_QUERY; - break; + case SQLSRV_ATTR_QUERY_TIMEOUT: + option_key = SQLSRV_STMT_OPTION_QUERY_TIMEOUT; + break; - case SQLSRV_ATTR_CURSOR_SCROLL_TYPE: - option_key = PDO_STMT_OPTION_CURSOR_SCROLL_TYPE; - break; + case PDO_ATTR_STATEMENT_CLASS: + break; - case SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE: - option_key = PDO_STMT_OPTION_CLIENT_BUFFER_MAX_KB_SIZE; - break; + case SQLSRV_ATTR_DIRECT_QUERY: + option_key = PDO_STMT_OPTION_DIRECT_QUERY; + break; - case PDO_ATTR_EMULATE_PREPARES: - option_key = PDO_STMT_OPTION_EMULATE_PREPARES; - break; + case SQLSRV_ATTR_CURSOR_SCROLL_TYPE: + option_key = PDO_STMT_OPTION_CURSOR_SCROLL_TYPE; + break; - case SQLSRV_ATTR_FETCHES_NUMERIC_TYPE: - option_key = PDO_STMT_OPTION_FETCHES_NUMERIC_TYPE; - break; + case SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE: + option_key = PDO_STMT_OPTION_CLIENT_BUFFER_MAX_KB_SIZE; + break; - case SQLSRV_ATTR_FETCHES_DATETIME_TYPE: - option_key = PDO_STMT_OPTION_FETCHES_DATETIME_TYPE; - break; + case PDO_ATTR_EMULATE_PREPARES: + option_key = PDO_STMT_OPTION_EMULATE_PREPARES; + break; - case SQLSRV_ATTR_FORMAT_DECIMALS: - option_key = PDO_STMT_OPTION_FORMAT_DECIMALS; - break; + case SQLSRV_ATTR_FETCHES_NUMERIC_TYPE: + option_key = PDO_STMT_OPTION_FETCHES_NUMERIC_TYPE; + break; - case SQLSRV_ATTR_DECIMAL_PLACES: - option_key = PDO_STMT_OPTION_DECIMAL_PLACES; - break; + case SQLSRV_ATTR_FETCHES_DATETIME_TYPE: + option_key = PDO_STMT_OPTION_FETCHES_DATETIME_TYPE; + break; - default: - CHECK_CUSTOM_ERROR( true, ctx, PDO_SQLSRV_ERROR_INVALID_STMT_OPTION ) { - throw core::CoreException(); - } - break; + case SQLSRV_ATTR_FORMAT_DECIMALS: + option_key = PDO_STMT_OPTION_FORMAT_DECIMALS; + break; + + case SQLSRV_ATTR_DECIMAL_PLACES: + option_key = PDO_STMT_OPTION_DECIMAL_PLACES; + break; + + case SQLSRV_ATTR_DATA_CLASSIFICATION: + option_key = PDO_STMT_OPTION_DATA_CLASSIFICATION; + break; + + default: + CHECK_CUSTOM_ERROR(true, ctx, PDO_SQLSRV_ERROR_INVALID_STMT_OPTION) + { + throw core::CoreException(); + } + break; } // if a PDO handled option makes it through (such as PDO_ATTR_STATEMENT_CLASS, just skip it - if( option_key != -1 ) { - zval_add_ref( data ); - core::sqlsrv_zend_hash_index_update(ctx, options_ht, option_key, data TSRMLS_CC ); + if (option_key != -1) { + zval_add_ref(data); + core::sqlsrv_zend_hash_index_update(ctx, options_ht, option_key, data TSRMLS_CC); } } diff --git a/source/pdo_sqlsrv/pdo_init.cpp b/source/pdo_sqlsrv/pdo_init.cpp index bee4cfc5..e913ca8f 100644 --- a/source/pdo_sqlsrv/pdo_init.cpp +++ b/source/pdo_sqlsrv/pdo_init.cpp @@ -294,6 +294,7 @@ namespace { { "SQLSRV_ATTR_FETCHES_DATETIME_TYPE", SQLSRV_ATTR_FETCHES_DATETIME_TYPE }, { "SQLSRV_ATTR_FORMAT_DECIMALS" , SQLSRV_ATTR_FORMAT_DECIMALS }, { "SQLSRV_ATTR_DECIMAL_PLACES" , SQLSRV_ATTR_DECIMAL_PLACES }, + { "SQLSRV_ATTR_DATA_CLASSIFICATION" , SQLSRV_ATTR_DATA_CLASSIFICATION }, // used for the size for output parameters: PDO::PARAM_INT and PDO::PARAM_BOOL use the default size of int, // PDO::PARAM_STR uses the size of the string in the variable diff --git a/source/pdo_sqlsrv/pdo_stmt.cpp b/source/pdo_sqlsrv/pdo_stmt.cpp index 6aa36e5e..edc5284d 100644 --- a/source/pdo_sqlsrv/pdo_stmt.cpp +++ b/source/pdo_sqlsrv/pdo_stmt.cpp @@ -912,6 +912,10 @@ int pdo_sqlsrv_stmt_set_attr( _Inout_ pdo_stmt_t *stmt, _In_ zend_long attr, _In core_sqlsrv_set_decimal_places(driver_stmt, val TSRMLS_CC); break; + case SQLSRV_ATTR_DATA_CLASSIFICATION: + driver_stmt->data_classification = (zend_is_true(val)) ? true : false; + break; + default: THROW_PDO_ERROR( driver_stmt, PDO_SQLSRV_ERROR_INVALID_STMT_ATTR ); break; @@ -1011,6 +1015,12 @@ int pdo_sqlsrv_stmt_get_attr( _Inout_ pdo_stmt_t *stmt, _In_ zend_long attr, _In break; } + case SQLSRV_ATTR_DATA_CLASSIFICATION: + { + ZVAL_BOOL(return_value, driver_stmt->data_classification); + break; + } + default: THROW_PDO_ERROR( driver_stmt, PDO_SQLSRV_ERROR_INVALID_STMT_ATTR ); break; @@ -1072,7 +1082,21 @@ int pdo_sqlsrv_stmt_get_col_meta( _Inout_ pdo_stmt_t *stmt, _In_ zend_long colno core_meta_data = driver_stmt->current_meta_data[colno]; // add the following fields: flags, native_type, driver:decl_type, table - add_assoc_long( return_value, "flags", 0 ); + if (driver_stmt->data_classification) { + core_sqlsrv_sensitivity_metadata(driver_stmt); + + // initialize the column data classification array + zval data_classification; + ZVAL_UNDEF(&data_classification); + core::sqlsrv_array_init(*driver_stmt, &data_classification TSRMLS_CC ); + + data_classification::fill_column_sensitivity_array(driver_stmt, (SQLSMALLINT)colno, &data_classification); + + add_assoc_zval(return_value, "flags", &data_classification); + } + else { + add_assoc_long(return_value, "flags", 0); + } // get the name of the data type char field_type_name[SQL_SERVER_IDENT_SIZE_MAX] = {'\0'}; diff --git a/source/pdo_sqlsrv/pdo_util.cpp b/source/pdo_sqlsrv/pdo_util.cpp index e399fda1..b6447740 100644 --- a/source/pdo_sqlsrv/pdo_util.cpp +++ b/source/pdo_sqlsrv/pdo_util.cpp @@ -449,6 +449,18 @@ pdo_error PDO_ERRORS[] = { SQLSRV_ERROR_AAD_MSI_UID_PWD_NOT_NULL, { IMSSP, (SQLCHAR*) "When using ActiveDirectoryMsi Authentication, PWD must be NULL. UID can be NULL, but if not, an empty string is not accepted.", -93, false} }, + { + SQLSRV_ERROR_DATA_CLASSIFICATION_PRE_EXECUTION, + { IMSSP, (SQLCHAR*) "The statement must be executed to retrieve Data Classification Sensitivity Metadata.", -94, false} + }, + { + SQLSRV_ERROR_DATA_CLASSIFICATION_NOT_AVAILABLE, + { IMSSP, (SQLCHAR*) "Failed to retrieve Data Classification Sensitivity Metadata. If the driver and the server both support the Data Classification feature, check whether the query returns columns with classification information.", -95, false} + }, + { + SQLSRV_ERROR_DATA_CLASSIFICATION_FAILED, + { IMSSP, (SQLCHAR*) "Failed to retrieve Data Classification Sensitivity Metadata: %1!s!", -96, true} + }, { UINT_MAX, {} } }; diff --git a/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h b/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h index 4b50bbc7..841b9e43 100644 --- a/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h +++ b/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h @@ -79,7 +79,8 @@ enum PDO_SQLSRV_ATTR { SQLSRV_ATTR_FETCHES_NUMERIC_TYPE, SQLSRV_ATTR_FETCHES_DATETIME_TYPE, SQLSRV_ATTR_FORMAT_DECIMALS, - SQLSRV_ATTR_DECIMAL_PLACES + SQLSRV_ATTR_DECIMAL_PLACES, + SQLSRV_ATTR_DATA_CLASSIFICATION }; // valid set of values for TransactionIsolation connection option diff --git a/source/shared/core_sqlsrv.h b/source/shared/core_sqlsrv.h index 347a89d7..e573f953 100644 --- a/source/shared/core_sqlsrv.h +++ b/source/shared/core_sqlsrv.h @@ -1120,6 +1120,7 @@ enum SQLSRV_STMT_OPTIONS { SQLSRV_STMT_OPTION_DATE_AS_STRING, SQLSRV_STMT_OPTION_FORMAT_DECIMALS, SQLSRV_STMT_OPTION_DECIMAL_PLACES, + SQLSRV_STMT_OPTION_DATA_CLASSIFICATION, // Driver specific connection options SQLSRV_STMT_OPTION_DRIVER_SPECIFIC = 1000, @@ -1321,6 +1322,11 @@ struct stmt_option_decimal_places : public stmt_option_functor { virtual void operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* opt, _In_ zval* value_z TSRMLS_DC ); }; +struct stmt_option_data_classification : public stmt_option_functor { + + virtual void operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* opt, _In_ zval* value_z TSRMLS_DC ); +}; + // used to hold the table for statment options struct stmt_option { @@ -1424,6 +1430,75 @@ struct sqlsrv_output_param { } }; +namespace data_classification { + // *** data classficiation metadata structures and helper methods -- to store and/or process the sensitivity classification data *** + struct name_id_pair; + struct sensitivity_metadata; + + void name_id_pair_free(name_id_pair * pair); + void parse_sensitivity_name_id_pairs(_Inout_ sqlsrv_stmt* stmt, _Inout_ USHORT& numpairs, _Inout_ std::vector>& pairs, _Inout_ unsigned char **pptr TSRMLS_CC); + void parse_column_sensitivity_props(_Inout_ sensitivity_metadata* meta, _Inout_ unsigned char **pptr); + USHORT fill_column_sensitivity_array(_Inout_ sqlsrv_stmt* stmt, _In_ SQLSMALLINT colno, _Inout_ zval *column_data TSRMLS_CC); + + struct name_id_pair { + UCHAR name_len; + sqlsrv_malloc_auto_ptr name; + UCHAR id_len; + sqlsrv_malloc_auto_ptr id; + + name_id_pair() : name_len(0), id_len(0) + { + } + + ~name_id_pair() + { + } + }; + + struct label_infotype_pair { + USHORT label_idx; + USHORT infotype_idx; + + label_infotype_pair() : label_idx(0), infotype_idx(0) + { + } + }; + + struct column_sensitivity { + USHORT num_pairs; + std::vector label_info_pairs; + + column_sensitivity() : num_pairs(0) + { + } + + ~column_sensitivity() + { + label_info_pairs.clear(); + } + }; + + struct sensitivity_metadata { + USHORT num_labels; + std::vector> labels; + USHORT num_infotypes; + std::vector> infotypes; + USHORT num_columns; + std::vector columns_sensitivity; + + sensitivity_metadata() : num_labels(0), num_infotypes(0), num_columns(0) + { + } + + ~sensitivity_metadata() + { + reset(); + } + + void reset(); + }; +} // namespace data_classification + // forward decls struct sqlsrv_result_set; struct field_meta_data; @@ -1434,6 +1509,9 @@ struct sqlsrv_stmt : public sqlsrv_context { void free_param_data( TSRMLS_D ); virtual void new_result_set( TSRMLS_D ); + // free sensitivity classification metadata + void clean_up_sensitivity_metadata(); + sqlsrv_conn* conn; // Connection that created this statement bool executed; // Whether the statement has been executed yet (used for error messages) @@ -1451,6 +1529,7 @@ struct sqlsrv_stmt : public sqlsrv_context { bool date_as_string; // false by default but the user can set this to true to retrieve datetime values as strings bool format_decimals; // false by default but the user can set this to true to add the missing leading zeroes and/or control number of decimal digits to show short decimal_places; // indicates number of decimals shown in fetched results (-1 by default, which means no change to number of decimal digits) + bool data_classification; // false by default but the user can set this to true to retrieve data classification sensitivity metadata // holds output pointers for SQLBindParameter // We use a deque because it 1) provides the at/[] access in constant time, and 2) grows dynamically without moving @@ -1473,6 +1552,9 @@ struct sqlsrv_stmt : public sqlsrv_context { // meta data for current result set std::vector> current_meta_data; + // meta data for data classification + sqlsrv_malloc_auto_ptr current_sensitivity_metadata; + sqlsrv_stmt( _In_ sqlsrv_conn* c, _In_ SQLHANDLE handle, _In_ error_callback e, _In_opt_ void* drv TSRMLS_DC ); virtual ~sqlsrv_stmt( void ); @@ -1544,6 +1626,7 @@ bool core_sqlsrv_send_stream_packet( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC ); void core_sqlsrv_set_buffered_query_limit( _Inout_ sqlsrv_stmt* stmt, _In_ zval* value_z TSRMLS_DC ); void core_sqlsrv_set_buffered_query_limit( _Inout_ sqlsrv_stmt* stmt, _In_ SQLLEN limit TSRMLS_DC ); void core_sqlsrv_set_decimal_places(_Inout_ sqlsrv_stmt* stmt, _In_ zval* value_z TSRMLS_DC); +void core_sqlsrv_sensitivity_metadata( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC ); //********************************************************************************************************************************* // Result Set @@ -1787,6 +1870,9 @@ enum SQLSRV_ERROR_CODES { SQLSRV_ERROR_EMPTY_ACCESS_TOKEN, SQLSRV_ERROR_INVALID_DECIMAL_PLACES, SQLSRV_ERROR_AAD_MSI_UID_PWD_NOT_NULL, + SQLSRV_ERROR_DATA_CLASSIFICATION_PRE_EXECUTION, + SQLSRV_ERROR_DATA_CLASSIFICATION_NOT_AVAILABLE, + SQLSRV_ERROR_DATA_CLASSIFICATION_FAILED, // Driver specific error codes starts from here. SQLSRV_ERROR_DRIVER_SPECIFIC = 1000, @@ -2451,6 +2537,14 @@ namespace core { } } + inline void sqlsrv_add_assoc_zval( _Inout_ sqlsrv_context& ctx, _Inout_ zval* array_z, _In_ const char* key, _In_ zval* val TSRMLS_DC ) + { + int zr = ::add_assoc_zval(array_z, key, val); + CHECK_ZEND_ERROR (zr, ctx, SQLSRV_ERROR_ZEND_HASH ) { + throw CoreException(); + } + } + inline void sqlsrv_array_init( _Inout_ sqlsrv_context& ctx, _Out_ zval* new_array TSRMLS_DC) { #if PHP_VERSION_ID < 70300 diff --git a/source/shared/core_stmt.cpp b/source/shared/core_stmt.cpp index 19c448e5..f3d3e6fb 100644 --- a/source/shared/core_stmt.cpp +++ b/source/shared/core_stmt.cpp @@ -146,6 +146,7 @@ sqlsrv_stmt::sqlsrv_stmt( _In_ sqlsrv_conn* c, _In_ SQLHANDLE handle, _In_ error date_as_string(false), format_decimals(false), // no formatting needed decimal_places(NO_CHANGE_DECIMAL_PLACES), // the default is no formatting to resultset required + data_classification(false), buffered_query_limit( sqlsrv_buffered_result_set::BUFFERED_QUERY_LIMIT_INVALID ), param_ind_ptrs( 10 ), // initially hold 10 elements, which should cover 90% of the cases and only take < 100 byte send_streams_at_exec( true ), @@ -191,6 +192,9 @@ sqlsrv_stmt::~sqlsrv_stmt( void ) current_results = NULL; } + // delete sensivity data + clean_up_sensitivity_metadata(); + invalidate(); zval_ptr_dtor( ¶m_input_strings ); zval_ptr_dtor( &output_params ); @@ -237,6 +241,9 @@ void sqlsrv_stmt::new_result_set( TSRMLS_D ) current_results = NULL; } + // delete sensivity data + clean_up_sensitivity_metadata(); + // create a new result set if( cursor_type == SQLSRV_CURSOR_BUFFERED ) { sqlsrv_malloc_auto_ptr result; @@ -250,6 +257,16 @@ void sqlsrv_stmt::new_result_set( TSRMLS_D ) } } +// free sensitivity classification metadata +void sqlsrv_stmt::clean_up_sensitivity_metadata() +{ + if (current_sensitivity_metadata) { + current_sensitivity_metadata->~sensitivity_metadata(); + sqlsrv_free(current_sensitivity_metadata); + current_sensitivity_metadata = NULL; + } +} + // core_sqlsrv_create_stmt // Common code to allocate a statement from either driver. Returns a valid driver statement object or // throws an exception if an error occurs. @@ -959,6 +976,101 @@ field_meta_data* core_sqlsrv_field_metadata( _Inout_ sqlsrv_stmt* stmt, _In_ SQL return result_field_meta_data; } +void core_sqlsrv_sensitivity_metadata( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC ) +{ + sqlsrv_malloc_auto_ptr dcbuf; + SQLINTEGER dclen = 0; + SQLINTEGER dclenout = 0; + SQLHANDLE ird; + SQLRETURN r; + + try { + if (!stmt->data_classification) { + return; + } + + if (stmt->current_sensitivity_metadata != NULL) { + // Already cached, so return + return; + } + + CHECK_CUSTOM_ERROR(!stmt->executed, stmt, SQLSRV_ERROR_DATA_CLASSIFICATION_PRE_EXECUTION) { + throw core::CoreException(); + } + + // Reference: https://docs.microsoft.com/sql/connect/odbc/data-classification + // To retrieve sensitivity classfication data, the first step is to retrieve the IRD(Implementation Row Descriptor) handle by + // calling SQLGetStmtAttr with SQL_ATTR_IMP_ROW_DESC statement attribute + r = ::SQLGetStmtAttr(stmt->handle(), SQL_ATTR_IMP_ROW_DESC, (SQLPOINTER)&ird, SQL_IS_POINTER, 0); + CHECK_SQL_ERROR_OR_WARNING(r, stmt) { + LOG(SEV_ERROR, "core_sqlsrv_sensitivity_metadata: failed in getting Implementation Row Descriptor handle." ); + throw core::CoreException(); + } + + // First call to get dclen + r = ::SQLGetDescFieldW(ird, 0, SQL_CA_SS_DATA_CLASSIFICATION, dcbuf, 0, &dclen); + if (r != SQL_SUCCESS || dclen == 0) { + // log the error first + LOG(SEV_ERROR, "core_sqlsrv_sensitivity_metadata: failed in calling SQLGetDescFieldW first time." ); + + // If this fails, check if it is the "Invalid Descriptor Field error" + SQLRETURN rc; + SQLCHAR state[SQL_SQLSTATE_BUFSIZE] = {'\0'}; + SQLSMALLINT len; + rc = ::SQLGetDiagField(SQL_HANDLE_DESC, ird, 1, SQL_DIAG_SQLSTATE, state, SQL_SQLSTATE_BUFSIZE, &len TSRMLS_CC); + + CHECK_SQL_ERROR_OR_WARNING(rc, stmt) { + throw core::CoreException(); + } + + CHECK_CUSTOM_ERROR(!strcmp("HY091", reinterpret_cast(state)), stmt, SQLSRV_ERROR_DATA_CLASSIFICATION_NOT_AVAILABLE) { + throw core::CoreException(); + } + + CHECK_CUSTOM_ERROR(true, stmt, SQLSRV_ERROR_DATA_CLASSIFICATION_FAILED, "Unexpected SQL Error state") { + throw core::CoreException(); + } + } + + // Call again to read SQL_CA_SS_DATA_CLASSIFICATION data + dcbuf = static_cast(sqlsrv_malloc(dclen * sizeof(char))); + + r = ::SQLGetDescFieldW(ird, 0, SQL_CA_SS_DATA_CLASSIFICATION, dcbuf, dclen, &dclenout); + if (r != SQL_SUCCESS) { + LOG(SEV_ERROR, "core_sqlsrv_sensitivity_metadata: failed in calling SQLGetDescFieldW again." ); + + CHECK_CUSTOM_ERROR(true, stmt, SQLSRV_ERROR_DATA_CLASSIFICATION_FAILED, "SQLGetDescFieldW failed unexpectedly") { + throw core::CoreException(); + } + } + + // Start parsing the data (blob) + using namespace data_classification; + unsigned char *dcptr = dcbuf; + + sqlsrv_malloc_auto_ptr sensitivity_meta; + sensitivity_meta = new (sqlsrv_malloc(sizeof(sensitivity_metadata))) sensitivity_metadata(); + + // Parse the name id pairs for labels first then info types + parse_sensitivity_name_id_pairs(stmt, sensitivity_meta->num_labels, sensitivity_meta->labels, &dcptr); + parse_sensitivity_name_id_pairs(stmt, sensitivity_meta->num_infotypes, sensitivity_meta->infotypes, &dcptr); + + // Next parse the sensitivity properties + parse_column_sensitivity_props(sensitivity_meta, &dcptr); + + unsigned char *dcend = dcbuf; + dcend += dclen; + + CHECK_CUSTOM_ERROR(dcptr != dcend, stmt, SQLSRV_ERROR_DATA_CLASSIFICATION_FAILED, "Metadata parsing ends unexpectedly") { + throw core::CoreException(); + } + + stmt->current_sensitivity_metadata = sensitivity_meta; + sensitivity_meta.transferred(); + } catch (core::CoreException& e) { + throw e; + } +} // core_sqlsrv_get_field // Return the value of a column from ODBC @@ -1504,6 +1616,16 @@ void stmt_option_decimal_places:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_op core_sqlsrv_set_decimal_places(stmt, value_z TSRMLS_CC); } +void stmt_option_data_classification:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /**/, _In_ zval* value_z TSRMLS_DC ) +{ + if (zend_is_true(value_z)) { + stmt->data_classification = true; + } + else { + stmt->data_classification = false; + } +} + // internal function to release the active stream. Called by each main API function // that will alter the statement and cancel any retrieval of data from a stream. void close_active_stream( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC ) diff --git a/source/shared/core_util.cpp b/source/shared/core_util.cpp index 515eb38b..5db20c3f 100644 --- a/source/shared/core_util.cpp +++ b/source/shared/core_util.cpp @@ -414,3 +414,192 @@ unsigned int convert_string_from_default_encoding( _In_ unsigned int php_encodin } } + + +namespace data_classification { + const char* DATA_CLASS = "Data Classification"; + const char* LABEL = "Label"; + const char* INFOTYPE = "Information Type"; + const char* NAME = "name"; + const char* ID = "id"; + + void convert_sensivity_field(_Inout_ sqlsrv_stmt* stmt, _In_ SQLSRV_ENCODING encoding, _In_ unsigned char *ptr, _In_ int len, _Inout_updates_bytes_(cchOutLen) char** field_name) + { + sqlsrv_malloc_auto_ptr temp_field_name; + int temp_field_len = len * 2; + SQLLEN field_name_len = 0; + + temp_field_name = static_cast(sqlsrv_malloc((len + 1) * sizeof(SQLWCHAR))); + memcpy_s(temp_field_name, temp_field_len, ptr, temp_field_len); + temp_field_name[temp_field_len] = '\0'; + + bool converted = convert_string_from_utf16(encoding, temp_field_name, len, field_name, field_name_len); + + CHECK_CUSTOM_ERROR(!converted, stmt, SQLSRV_ERROR_FIELD_ENCODING_TRANSLATE, get_last_error_message()) { + throw core::CoreException(); + } + } + + void name_id_pair_free(_Inout_ name_id_pair* pair) + { + if (pair->name) { + pair->name.reset(); + } + if (pair->id) { + pair->id.reset(); + } + sqlsrv_free(pair); + } + + void parse_sensitivity_name_id_pairs(_Inout_ sqlsrv_stmt* stmt, _Inout_ USHORT& numpairs, _Inout_ std::vector>& pairs, _Inout_ unsigned char **pptr) + { + unsigned char *ptr = *pptr; + unsigned short npairs; + numpairs = npairs = *(unsigned short*)ptr; + SQLSRV_ENCODING encoding = ((stmt->encoding() == SQLSRV_ENCODING_DEFAULT ) ? stmt->conn->encoding() : stmt->encoding()); + + ptr += sizeof(unsigned short); + while (npairs--) { + int namelen, idlen; + unsigned char *nameptr, *idptr; + + sqlsrv_malloc_auto_ptr pair; + pair = new(sqlsrv_malloc(sizeof(name_id_pair))) name_id_pair(); + + sqlsrv_malloc_auto_ptr name; + sqlsrv_malloc_auto_ptr id; + + namelen = *ptr++; + nameptr = ptr; + + pair->name_len = namelen; + convert_sensivity_field(stmt, encoding, nameptr, namelen, (char**)&name); + pair->name = name; + + ptr += namelen * 2; + idlen = *ptr++; + idptr = ptr; + ptr += idlen * 2; + + pair->id_len = idlen; + convert_sensivity_field(stmt, encoding, idptr, idlen, (char**)&id); + pair->id = id; + + pairs.push_back(pair.get()); + pair.transferred(); + } + *pptr = ptr; + } + + void parse_column_sensitivity_props(_Inout_ sensitivity_metadata* meta, _Inout_ unsigned char **pptr) + { + unsigned char *ptr = *pptr; + unsigned short ncols; + + // Get number of columns + meta->num_columns = ncols = *(reinterpret_cast(ptr)); + + // Move forward + ptr += sizeof(unsigned short); + + while (ncols--) { + unsigned short npairs = *(reinterpret_cast(ptr)); + + ptr += sizeof(unsigned short); + + column_sensitivity column; + column.num_pairs = npairs; + + while (npairs--) { + label_infotype_pair pair; + + unsigned short labelidx, typeidx; + labelidx = *(reinterpret_cast(ptr)); + ptr += sizeof(unsigned short); + typeidx = *(reinterpret_cast(ptr)); + ptr += sizeof(unsigned short); + + pair.label_idx = labelidx; + pair.infotype_idx = typeidx; + + column.label_info_pairs.push_back(pair); + } + + meta->columns_sensitivity.push_back(column); + } + + *pptr = ptr; + } + + USHORT fill_column_sensitivity_array(_Inout_ sqlsrv_stmt* stmt, _In_ SQLSMALLINT colno, _Inout_ zval *return_array TSRMLS_CC) + { + sensitivity_metadata* meta = stmt->current_sensitivity_metadata; + if (meta == NULL) { + return 0; + } + + SQLSRV_ASSERT(colno >= 0 && colno < meta->num_columns, "fill_column_sensitivity_array: column number out of bounds"); + + zval data_classification; + ZVAL_UNDEF(&data_classification); + core::sqlsrv_array_init(*stmt, &data_classification TSRMLS_CC ); + + USHORT num_pairs = meta->columns_sensitivity[colno].num_pairs; + + if (num_pairs == 0) { + core::sqlsrv_add_assoc_zval(*stmt, return_array, DATA_CLASS, &data_classification TSRMLS_CC); + + return 0; + } + + zval sensitivity_properties; + ZVAL_UNDEF(&sensitivity_properties); + core::sqlsrv_array_init(*stmt, &sensitivity_properties TSRMLS_CC); + + for (USHORT j = 0; j < num_pairs; j++) { + zval label_array, infotype_array; + ZVAL_UNDEF(&label_array); + ZVAL_UNDEF(&infotype_array); + + core::sqlsrv_array_init(*stmt, &label_array TSRMLS_CC); + core::sqlsrv_array_init(*stmt, &infotype_array TSRMLS_CC); + + USHORT labelidx = meta->columns_sensitivity[colno].label_info_pairs[j].label_idx; + USHORT typeidx = meta->columns_sensitivity[colno].label_info_pairs[j].infotype_idx; + + char *label = meta->labels[labelidx]->name; + char *label_id = meta->labels[labelidx]->id; + char *infotype = meta->infotypes[typeidx]->name; + char *infotype_id = meta->infotypes[typeidx]->id; + + core::sqlsrv_add_assoc_string(*stmt, &label_array, NAME, label, 1 TSRMLS_CC); + core::sqlsrv_add_assoc_string(*stmt, &label_array, ID, label_id, 1 TSRMLS_CC); + + core::sqlsrv_add_assoc_zval(*stmt, &sensitivity_properties, LABEL, &label_array TSRMLS_CC); + + core::sqlsrv_add_assoc_string(*stmt, &infotype_array, NAME, infotype, 1 TSRMLS_CC); + core::sqlsrv_add_assoc_string(*stmt, &infotype_array, ID, infotype_id, 1 TSRMLS_CC); + + core::sqlsrv_add_assoc_zval(*stmt, &sensitivity_properties, INFOTYPE, &infotype_array TSRMLS_CC); + + // add the pair of sensitivity properties to data_classification + core::sqlsrv_add_next_index_zval(*stmt, &data_classification, &sensitivity_properties TSRMLS_CC ); + } + + // add data classfication as associative array + core::sqlsrv_add_assoc_zval(*stmt, return_array, DATA_CLASS, &data_classification TSRMLS_CC); + + return num_pairs; + } + + void sensitivity_metadata::reset() + { + std::for_each(labels.begin(), labels.end(), name_id_pair_free); + labels.clear(); + + std::for_each(infotypes.begin(), infotypes.end(), name_id_pair_free); + infotypes.clear(); + + columns_sensitivity.clear(); + } +} // namespace data_classification \ No newline at end of file diff --git a/source/shared/msodbcsql.h b/source/shared/msodbcsql.h index 2392c5e3..3a1555e8 100644 --- a/source/shared/msodbcsql.h +++ b/source/shared/msodbcsql.h @@ -144,7 +144,11 @@ // force column encryption #define SQL_CA_SS_FORCE_ENCRYPT (SQL_CA_SS_BASE+36) // indicate mandatory encryption for this parameter -#define SQL_CA_SS_MAX_USED (SQL_CA_SS_BASE+37) +// Data Classification +#define SQL_CA_SS_DATA_CLASSIFICATION (SQL_CA_SS_BASE+37) // retrieve data classification information + +#define SQL_CA_SS_MAX_USED (SQL_CA_SS_BASE+38) + // Defines for use with SQL_COPT_SS_INTEGRATED_SECURITY - Pre-Connect Option only #define SQL_IS_OFF 0L // Integrated security isn't used #define SQL_IS_ON 1L // Integrated security is used diff --git a/source/sqlsrv/conn.cpp b/source/sqlsrv/conn.cpp index f4827863..1275e723 100644 --- a/source/sqlsrv/conn.cpp +++ b/source/sqlsrv/conn.cpp @@ -219,6 +219,7 @@ namespace SSStmtOptionNames { const char DATE_AS_STRING[] = "ReturnDatesAsStrings"; const char FORMAT_DECIMALS[] = "FormatDecimals"; const char DECIMAL_PLACES[] = "DecimalPlaces"; + const char DATA_CLASSIFICATION[] = "DataClassification"; } namespace SSConnOptionNames { @@ -312,6 +313,12 @@ const stmt_option SS_STMT_OPTS[] = { SQLSRV_STMT_OPTION_DECIMAL_PLACES, std::unique_ptr( new stmt_option_decimal_places ) }, + { + SSStmtOptionNames::DATA_CLASSIFICATION, + sizeof( SSStmtOptionNames::DATA_CLASSIFICATION ), + SQLSRV_STMT_OPTION_DATA_CLASSIFICATION, + std::unique_ptr( new stmt_option_data_classification ) + }, { NULL, 0, SQLSRV_STMT_OPTION_INVALID, std::unique_ptr{} }, }; diff --git a/source/sqlsrv/stmt.cpp b/source/sqlsrv/stmt.cpp index 9d16d24e..b6f56b6e 100644 --- a/source/sqlsrv/stmt.cpp +++ b/source/sqlsrv/stmt.cpp @@ -481,6 +481,10 @@ PHP_FUNCTION( sqlsrv_field_metadata ) // get the number of fields in the resultset and its metadata if not exists SQLSMALLINT num_cols = get_resultset_meta_data(stmt); + if (stmt->data_classification) { + core_sqlsrv_sensitivity_metadata(stmt); + } + zval result_meta_data; ZVAL_UNDEF( &result_meta_data ); core::sqlsrv_array_init( *stmt, &result_meta_data TSRMLS_CC ); @@ -533,6 +537,10 @@ PHP_FUNCTION( sqlsrv_field_metadata ) core::sqlsrv_add_assoc_long( *stmt, &field_array, FieldMetaData::NULLABLE, core_meta_data->field_is_nullable TSRMLS_CC ); + if (stmt->data_classification) { + data_classification::fill_column_sensitivity_array(stmt, f, &field_array); + } + // add this field's meta data to the result set meta data core::sqlsrv_add_next_index_zval( *stmt, &result_meta_data, &field_array TSRMLS_CC ); } diff --git a/source/sqlsrv/util.cpp b/source/sqlsrv/util.cpp index e4ea3472..5f69c5a3 100644 --- a/source/sqlsrv/util.cpp +++ b/source/sqlsrv/util.cpp @@ -440,6 +440,18 @@ ss_error SS_ERRORS[] = { SQLSRV_ERROR_AAD_MSI_UID_PWD_NOT_NULL, { IMSSP, (SQLCHAR*) "When using ActiveDirectoryMsi Authentication, PWD must be NULL. UID can be NULL, but if not, an empty string is not accepted.", -118, false} }, + { + SQLSRV_ERROR_DATA_CLASSIFICATION_PRE_EXECUTION, + { IMSSP, (SQLCHAR*) "The statement must be executed to retrieve Data Classification Sensitivity Metadata.", -119, false} + }, + { + SQLSRV_ERROR_DATA_CLASSIFICATION_NOT_AVAILABLE, + { IMSSP, (SQLCHAR*) "Failed to retrieve Data Classification Sensitivity Metadata. If the driver and the server both support the Data Classification feature, check whether the query returns columns with classification information.", -120, false} + }, + { + SQLSRV_ERROR_DATA_CLASSIFICATION_FAILED, + { IMSSP, (SQLCHAR*) "Failed to retrieve Data Classification Sensitivity Metadata: %1!s!", -121, true} + }, // terminate the list of errors/warnings { UINT_MAX, {} } diff --git a/test/functional/pdo_sqlsrv/pdo_data_classification.phpt b/test/functional/pdo_sqlsrv/pdo_data_classification.phpt new file mode 100644 index 00000000..1b29c604 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_data_classification.phpt @@ -0,0 +1,270 @@ +--TEST-- +Test data classification feature - retrieving sensitivity metadata if supported +--DESCRIPTION-- +If both ODBC and server support this feature, this test verifies that sensitivity metadata can be added and correctly retrieved. If not, it will at least test the new statement attribute and some error cases. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- + PDO::ERRMODE_EXCEPTION, PDO::SQLSRV_ATTR_DATA_CLASSIFICATION => true); + $conn = new PDO($dsn, $uid, $pwd, $attr); + } catch (PDOException $e) { + if (!fnmatch($stmtErr, $e->getMessage())) { + echo "Connection attribute test (1) unexpected\n"; + var_dump($e->getMessage()); + } + } + + try { + $dsn = getDSN($server, $databaseName, $driver); + $attr = array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION); + $conn = new PDO($dsn, $uid, $pwd, $attr); + $conn->setAttribute(PDO::SQLSRV_ATTR_DATA_CLASSIFICATION, true); + } catch (PDOException $e) { + if (!fnmatch($stmtErr, $e->getMessage())) { + echo "Connection attribute test (2) unexpected\n"; + var_dump($e->getMessage()); + } + } + + try { + $dsn = getDSN($server, $databaseName, $driver); + $attr = array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION); + $conn = new PDO($dsn, $uid, $pwd, $attr); + $conn->getAttribute(PDO::SQLSRV_ATTR_DATA_CLASSIFICATION); + } catch (PDOException $e) { + if (!fnmatch($noSupportErr, $e->getMessage())) { + echo "Connection attribute test (3) unexpected\n"; + var_dump($e->getMessage()); + } + } +} + +function testNotAvailable($conn, $tableName, $isSupported) +{ + // If supported, the query should return a column with no classification + $options = array(PDO::SQLSRV_ATTR_DATA_CLASSIFICATION => true); + $tsql = ($isSupported)? "SELECT PatientId FROM $tableName" : "SELECT * FROM $tableName"; + $stmt = $conn->prepare($tsql, $options); + $stmt->execute(); + + $notAvailableErr = '*Failed to retrieve Data Classification Sensitivity Metadata. If the driver and the server both support the Data Classification feature, check whether the query returns columns with classification information.'; + try { + $metadata = $stmt->getColumnMeta(0); + echo "testNotAvailable: expected getColumnMeta to fail\n"; + } catch (PDOException $e) { + if (!fnmatch($notAvailableErr, $e->getMessage())) { + echo "testNotAvailable: exception unexpected\n"; + var_dump($e->getMessage()); + } + } +} + +function isDataClassSupported($conn) +{ + // Check both SQL Server version and ODBC driver version + $msodbcsqlVer = $conn->getAttribute(PDO::ATTR_CLIENT_VERSION)["DriverVer"]; + $version = explode(".", $msodbcsqlVer); + + // ODBC Driver must be 17.2 or above + if ($version[0] < 17 || $version[1] < 2) { + return false; + } + + // SQL Server must be SQL Server 2019 or above + $serverVer = $conn->getAttribute(PDO::ATTR_SERVER_VERSION); + if (explode('.', $serverVer)[0] < 15) + return false; + + return true; +} + +function getRegularMetadata($conn, $tsql) +{ + // Run the query without data classification metadata + $stmt1 = $conn->query($tsql); + + // Run the query with the attribute set to false + $options = array(PDO::SQLSRV_ATTR_DATA_CLASSIFICATION => false); + $stmt2 = $conn->prepare($tsql, $options); + $stmt2->execute(); + + // The metadata for each column should be identical + $numCol = $stmt1->columnCount(); + for ($i = 0; $i < $numCol; $i++) { + $metadata1 = $stmt1->getColumnMeta($i); + $metadata2 = $stmt2->getColumnMeta($i); + + $diff = array_diff($metadata1, $metadata2); + if (!empty($diff)) { + print_r($diff); + } + } + + return $stmt1; +} + +function verifyClassInfo($input, $actual) +{ + // For simplicity of this test, only one set of sensitivity data (Label, Information Type) + if (count($actual) != 1) { + echo "Expected an array with only one element\n"; + return false; + } + + if (count($actual[0]) != 2) { + echo "Expected a Label pair and Information Type pair\n"; + return false; + } + + // Label should be name and id pair (id should be empty) + if (count($actual[0]['Label']) != 2) { + echo "Expected only two elements for the label\n"; + return false; + } + $label = $input[0]; + if ($actual[0]['Label']['name'] !== $label || !empty($actual[0]['Label']['id'])){ + return false; + } + + // Like Label, Information Type should also be name and id pair (id should be empty) + if (count($actual[0]['Information Type']) != 2) { + echo "Expected only two elements for the information type\n"; + return false; + } + $info = $input[1]; + if ($actual[0]['Information Type']['name'] !== $info || !empty($actual[0]['Information Type']['id'])){ + return false; + } + + return true; +} + +function compareDataClassification($stmt1, $stmt2, $classData) +{ + $numCol = $stmt1->columnCount(); + $noClassInfo = array('Data Classification' => array()); + + for ($i = 0; $i < $numCol; $i++) { + $metadata1 = $stmt1->getColumnMeta($i); + $metadata2 = $stmt2->getColumnMeta($i); + + // If classification sensitivity data exists, only the + // 'flags' field should be different + foreach ($metadata2 as $key => $value) { + if ($key == 'flags') { + // Is classification input data empty? + if (empty($classData[$i])) { + // Then it should be equivalent to $noClassInfo + if ($value !== $noClassInfo) { + var_dump($value); + } + } else { + // Verify the classification metadata + if (!verifyClassInfo($classData[$i], $value['Data Classification'])) { + var_dump($value); + } + } + } else { + // The other fields should be identical + if ($metadata1[$key] !== $value) { + var_dump($value); + } + } + } + } +} + +/////////////////////////////////////////////////////////////////////////////////////// +try { + testConnAttrCases(); + + $conn = connect(); + $isSupported = isDataClassSupported($conn); + + // Create a test table + $tableName = 'pdoPatients'; + $colMeta = array(new ColumnMeta('INT', 'PatientId', 'IDENTITY NOT NULL'), + new ColumnMeta('CHAR(11)', 'SSN'), + new ColumnMeta('NVARCHAR(50)', 'FirstName'), + new ColumnMeta('NVARCHAR(50)', 'LastName'), + new ColumnMeta('DATE', 'BirthDate')); + createTable($conn, $tableName, $colMeta); + + // If data classification is supported, then add sensitivity classification metadata + // to columns SSN and Birthdate + $classData = [ + array(), + array('Highly Confidential - GDPR', 'Credentials'), + array(), + array(), + array('Confidential Personal Data', 'Birthdays') + ]; + + if ($isSupported) { + // column SSN + $label = $classData[1][0]; + $infoType = $classData[1][1]; + $sql = "ADD SENSITIVITY CLASSIFICATION TO [$tableName].SSN WITH (LABEL = '$label', INFORMATION_TYPE = '$infoType')"; + $conn->query($sql); + + // column BirthDate + $label = $classData[4][0]; + $infoType = $classData[4][1]; + $sql = "ADD SENSITIVITY CLASSIFICATION TO [$tableName].BirthDate WITH (LABEL = '$label', INFORMATION_TYPE = '$infoType')"; + $conn->query($sql); + } + + // Test another error condition + testNotAvailable($conn, $tableName, $isSupported); + + // Run the query without data classification metadata + $tsql = "SELECT * FROM $tableName"; + $stmt = getRegularMetadata($conn, $tsql); + + // Proceeed to retrieve sensitivity metadata, if supported + if ($isSupported) { + $options = array(PDO::SQLSRV_ATTR_DATA_CLASSIFICATION => true); + $stmt1 = $conn->prepare($tsql, $options); + $stmt1->execute(); + + compareDataClassification($stmt, $stmt1, $classData); + + // $stmt2 should produce the same result as the previous $stmt1 + $stmt2 = $conn->prepare($tsql); + $stmt2->execute(); + $stmt2->setAttribute(PDO::SQLSRV_ATTR_DATA_CLASSIFICATION, true); + + compareDataClassification($stmt, $stmt2, $classData); + + unset($stmt1); + unset($stmt2); + } + + dropTable($conn, $tableName); + + unset($stmt); + unset($conn); + + echo "Done\n"; +} catch (PDOException $e) { + var_dump($e->getMessage()); +} +?> +--EXPECT-- +Done \ No newline at end of file diff --git a/test/functional/sqlsrv/sqlsrv_data_classification.phpt b/test/functional/sqlsrv/sqlsrv_data_classification.phpt new file mode 100644 index 00000000..4ea83cbb --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_data_classification.phpt @@ -0,0 +1,261 @@ +--TEST-- +Test data classification feature - retrieving sensitivity metadata if supported +--DESCRIPTION-- +If both ODBC and server support this feature, this test verifies that sensitivity metadata can be added and correctly retrieved. If not, it will at least test the new statement attribute and some error cases. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- + true); + $tsql = ($isSupported)? "SELECT PatientId FROM $tableName" : "SELECT * FROM $tableName"; + $stmt = sqlsrv_query($conn, $tsql, array(), $options); + if (!$stmt) { + fatalError("testErrorCases (1): failed with sqlsrv_query '$tsql'.\n"); + } + + $notAvailableErr = '*Failed to retrieve Data Classification Sensitivity Metadata. If the driver and the server both support the Data Classification feature, check whether the query returns columns with classification information.'; + + $metadata = sqlsrv_field_metadata($stmt); + if ($metadata) { + echo "testErrorCases (1): expected sqlsrv_field_metadata to fail\n"; + } + + if (!fnmatch($notAvailableErr, sqlsrv_errors()[0]['message'])) { + var_dump(sqlsrv_errors()); + } + + // (2) call sqlsrv_prepare() with DataClassification but do not execute the stmt + $stmt = sqlsrv_prepare($conn, $tsql, array(), $options); + if (!$stmt) { + fatalError("testErrorCases (2): failed with sqlsrv_prepare '$tsql'.\n"); + } + + $executeFirstErr = '*The statement must be executed to retrieve Data Classification Sensitivity Metadata.'; + $metadata = sqlsrv_field_metadata($stmt); + if ($metadata) { + echo "testErrorCases (2): expected sqlsrv_field_metadata to fail\n"; + } + + if (!fnmatch($executeFirstErr, sqlsrv_errors()[0]['message'])) { + var_dump(sqlsrv_errors()); + } +} + +function isDataClassSupported($conn) +{ + // Check both SQL Server version and ODBC driver version + $msodbcsqlVer = sqlsrv_client_info($conn)['DriverVer']; + $version = explode(".", $msodbcsqlVer); + + // ODBC Driver must be 17.2 or above + if ($version[0] < 17 || $version[1] < 2) { + return false; + } + + // SQL Server must be SQL Server 2019 or above + $serverVer = sqlsrv_server_info($conn)['SQLServerVersion']; + if (explode('.', $serverVer)[0] < 15) { + return false; + } + + return true; +} + +function getRegularMetadata($conn, $tsql) +{ + // Run the query without data classification metadata + $stmt1 = sqlsrv_query($conn, $tsql); + if (!$stmt1) { + fatalError("getRegularMetadata (1): failed in sqlsrv_query.\n"); + } + + // Run the query with the attribute set to false + $options = array('DataClassification' => false); + $stmt2 = sqlsrv_query($conn, $tsql, array(), $options); + if (!$stmt2) { + fatalError("getRegularMetadata (2): failed in sqlsrv_query.\n"); + } + + // The metadata for each statement, column by column, should be identical + $numCol = sqlsrv_num_fields($stmt1); + $metadata1 = sqlsrv_field_metadata($stmt1); + $metadata2 = sqlsrv_field_metadata($stmt2); + + for ($i = 0; $i < $numCol; $i++) { + $diff = array_diff($metadata1[$i], $metadata2[$i]); + if (!empty($diff)) { + print_r($diff); + } + } + + return $stmt1; +} + +function verifyClassInfo($input, $actual) +{ + // For simplicity of this test, only one set of sensitivity data. Namely, + // an array with one set of Label (name, id) and Information Type (name, id) + if (count($actual) != 1) { + echo "Expected an array with only one element\n"; + return false; + } + + if (count($actual[0]) != 2) { + echo "Expected a Label pair and Information Type pair\n"; + return false; + } + + // Label should be name and id pair (id should be empty) + if (count($actual[0]['Label']) != 2) { + echo "Expected only two elements for the label\n"; + return false; + } + $label = $input[0]; + if ($actual[0]['Label']['name'] !== $label || !empty($actual[0]['Label']['id'])){ + return false; + } + + // Like Label, Information Type should also be name and id pair (id should be empty) + if (count($actual[0]['Information Type']) != 2) { + echo "Expected only two elements for the information type\n"; + return false; + } + $info = $input[1]; + if ($actual[0]['Information Type']['name'] !== $info || !empty($actual[0]['Information Type']['id'])){ + return false; + } + + return true; +} + +function compareDataClassification($stmt1, $stmt2, $classData) +{ + $numCol = sqlsrv_num_fields($stmt1); + + $metadata1 = sqlsrv_field_metadata($stmt1); + $metadata2 = sqlsrv_field_metadata($stmt2); + + // The built-in array_diff_assoc() function compares the keys and values + // of two (or more) arrays, and returns an array that contains the entries + // from array1 that are not present in array2 or array3, etc. + // + // For this test, $metadata2 should have one extra key 'Data Classification', + // which should not be present in $metadata1 + // + // If the column does not have sensitivity metadata, the value should be an + // empty array. Otherwise, it should contain an array with one set of + // Label (name, id) and Information Type (name, id) + + $noClassInfo = array('Data Classification' => array()); + for ($i = 0; $i < $numCol; $i++) { + $diff = array_diff_assoc($metadata2[$i], $metadata1[$i]); + + // Is classification input data empty? + if (empty($classData[$i])) { + // Then it should be equivalent to $noClassInfo + if ($diff !== $noClassInfo) { + var_dump($diff); + } + } else { + // Verify the classification metadata + if (!verifyClassInfo($classData[$i], $diff['Data Classification'])) { + var_dump($diff); + } + } + } +} + +/////////////////////////////////////////////////////////////////////////////////////// +require_once('MsCommon.inc'); + +$conn = AE\connect(); +if (!$conn) { + fatalError("Failed to connect.\n"); +} + +$isSupported = isDataClassSupported($conn); + +// Create a test table +$tableName = 'srvPatients'; +$colMeta = array(new AE\ColumnMeta('INT', 'PatientId', 'IDENTITY NOT NULL'), + new AE\ColumnMeta('CHAR(11)', 'SSN'), + new AE\ColumnMeta('NVARCHAR(50)', 'FirstName'), + new AE\ColumnMeta('NVARCHAR(50)', 'LastName'), + new AE\ColumnMeta('DATE', 'BirthDate')); +AE\createTable($conn, $tableName, $colMeta); + +// If data classification is supported, then add sensitivity classification metadata +// to columns SSN and Birthdate +$classData = [ + array(), + array('Highly Confidential - GDPR', 'Credentials'), + array(), + array(), + array('Confidential Personal Data', 'Birthdays') + ]; + +if ($isSupported) { + // column SSN + $label = $classData[1][0]; + $infoType = $classData[1][1]; + $sql = "ADD SENSITIVITY CLASSIFICATION TO [$tableName].SSN WITH (LABEL = '$label', INFORMATION_TYPE = '$infoType')"; + $stmt = sqlsrv_query($conn, $sql); + if (!$stmt) { + fatalError("SSN: Add sensitivity $label and $infoType failed.\n"); + } + + // column BirthDate + $label = $classData[4][0]; + $infoType = $classData[4][1]; + $sql = "ADD SENSITIVITY CLASSIFICATION TO [$tableName].BirthDate WITH (LABEL = '$label', INFORMATION_TYPE = '$infoType')"; + $stmt = sqlsrv_query($conn, $sql); + if (!$stmt) { + fatalError("BirthDate: Add sensitivity $label and $infoType failed.\n"); + } +} + +testErrorCases($conn, $tableName, $isSupported); + +// Run the query without data classification metadata +$tsql = "SELECT * FROM $tableName"; +$stmt = getRegularMetadata($conn, $tsql); + +// Proceeed to retrieve sensitivity metadata, if supported +if ($isSupported) { + $options = array('DataClassification' => true); + $stmt1 = sqlsrv_prepare($conn, $tsql, array(), $options); + if (!$stmt1) { + fatalError("Error when calling sqlsrv_prepare '$tsql'.\n"); + } + if (!sqlsrv_execute($stmt1)) { + fatalError("Error in executing statement.\n"); + } + + compareDataClassification($stmt, $stmt1, $classData); + sqlsrv_free_stmt($stmt1); + + // $stmt2 should produce the same result as the previous $stmt1 + $stmt2 = sqlsrv_query($conn, $tsql, array(), $options); + if (!$stmt2) { + fatalError("Error when calling sqlsrv_query '$tsql'.\n"); + } + + compareDataClassification($stmt, $stmt2, $classData); + sqlsrv_free_stmt($stmt2); +} + +sqlsrv_free_stmt($stmt); + +dropTable($conn, $tableName); +sqlsrv_close($conn); + +echo "Done\n"; +?> +--EXPECT-- +Done