Data Classification sensitivity metadata retrieval (#979)

This commit is contained in:
Jenny Tam 2019-05-01 08:03:33 -07:00 committed by GitHub
parent c5b6540498
commit 9e90a42d1b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 1068 additions and 54 deletions

View file

@ -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<stmt_option_fetch_datetime>( new stmt_option_fetch_datetime ) },
{ NULL, 0, PDO_STMT_OPTION_FORMAT_DECIMALS, std::unique_ptr<stmt_option_format_decimals>( new stmt_option_format_decimals ) },
{ NULL, 0, PDO_STMT_OPTION_DECIMAL_PLACES, std::unique_ptr<stmt_option_decimal_places>( new stmt_option_decimal_places ) },
{ NULL, 0, PDO_STMT_OPTION_DATA_CLASSIFICATION, std::unique_ptr<stmt_option_data_classification>( new stmt_option_data_classification ) },
{ NULL, 0, SQLSRV_STMT_OPTION_INVALID, std::unique_ptr<stmt_option_functor>{} },
};
@ -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);
}
}

View file

@ -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

View file

@ -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'};

View file

@ -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, {} }
};

View file

@ -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

View file

@ -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<name_id_pair*, sqlsrv_allocator<name_id_pair*>>& 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<char> name;
UCHAR id_len;
sqlsrv_malloc_auto_ptr<char> 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_infotype_pair> label_info_pairs;
column_sensitivity() : num_pairs(0)
{
}
~column_sensitivity()
{
label_info_pairs.clear();
}
};
struct sensitivity_metadata {
USHORT num_labels;
std::vector<name_id_pair*, sqlsrv_allocator<name_id_pair*>> labels;
USHORT num_infotypes;
std::vector<name_id_pair*, sqlsrv_allocator<name_id_pair*>> infotypes;
USHORT num_columns;
std::vector<column_sensitivity> 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<field_meta_data*, sqlsrv_allocator<field_meta_data*>> current_meta_data;
// meta data for data classification
sqlsrv_malloc_auto_ptr<data_classification::sensitivity_metadata> 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

View file

@ -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( &param_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<sqlsrv_buffered_result_set> 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<unsigned char> 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<char*>(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<unsigned char*>(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_metadata> 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 )

View file

@ -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<SQLWCHAR> temp_field_name;
int temp_field_len = len * 2;
SQLLEN field_name_len = 0;
temp_field_name = static_cast<SQLWCHAR*>(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<name_id_pair*, sqlsrv_allocator<name_id_pair*>>& 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<name_id_pair> pair;
pair = new(sqlsrv_malloc(sizeof(name_id_pair))) name_id_pair();
sqlsrv_malloc_auto_ptr<char> name;
sqlsrv_malloc_auto_ptr<char> 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<unsigned short*>(ptr));
// Move forward
ptr += sizeof(unsigned short);
while (ncols--) {
unsigned short npairs = *(reinterpret_cast<unsigned short*>(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<unsigned short*>(ptr));
ptr += sizeof(unsigned short);
typeidx = *(reinterpret_cast<unsigned short*>(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

View file

@ -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

View file

@ -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<stmt_option_decimal_places>( new stmt_option_decimal_places )
},
{
SSStmtOptionNames::DATA_CLASSIFICATION,
sizeof( SSStmtOptionNames::DATA_CLASSIFICATION ),
SQLSRV_STMT_OPTION_DATA_CLASSIFICATION,
std::unique_ptr<stmt_option_data_classification>( new stmt_option_data_classification )
},
{ NULL, 0, SQLSRV_STMT_OPTION_INVALID, std::unique_ptr<stmt_option_functor>{} },
};

View file

@ -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 );
}

View file

@ -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, {} }

View file

@ -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--
<?php require('skipif_mid-refactor.inc'); ?>
--FILE--
<?php
require_once('MsSetup.inc');
require_once('MsCommon_mid-refactor.inc');
function testConnAttrCases()
{
// Attribute PDO::SQLSRV_ATTR_DATA_CLASSIFICATION is limited to statement level only
global $server, $databaseName, $driver, $uid, $pwd;
$stmtErr = '*The given attribute is only supported on the PDOStatement object.';
$noSupportErr = '*driver does not support that attribute';
try {
$dsn = getDSN($server, $databaseName, $driver);
$attr = array(PDO::ATTR_ERRMODE => 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

View file

@ -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--
<?php require('skipif_versions_old.inc'); ?>
--FILE--
<?php
function testErrorCases($conn, $tableName, $isSupported)
{
// This function will check two error cases:
// (1) if supported, the query should return a column with no classification
$options = array('DataClassification' => 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