From e51380612db8a546c8869d3914fa54243ddbd0a3 Mon Sep 17 00:00:00 2001 From: Jenny Tam Date: Thu, 6 Sep 2018 11:32:04 -0700 Subject: [PATCH] Adding supporting for Azure AD access token (#837) * Adding supporting for Azure AD access token * Added more comments for the AD access token skipif files * Save the pointer to access token struct until after connecting * Clear the access token data before freeing the memory * Added a reference as per review --- source/pdo_sqlsrv/pdo_dbh.cpp | 10 ++ source/pdo_sqlsrv/pdo_util.cpp | 9 + source/shared/core_conn.cpp | 113 +++++++++++-- source/shared/core_sqlsrv.h | 14 +- source/shared/msodbcsql.h | 7 + source/sqlsrv/conn.cpp | 10 ++ source/sqlsrv/util.cpp | 8 + test/functional/pdo_sqlsrv/access_token.inc | 3 + .../pdo_sqlsrv/pdo_azure_ad_access_token.phpt | 157 ++++++++++++++++++ .../skipif_azure_ad_acess_token.inc | 41 +++++ .../skipif_version_less_than_2k16.inc | 6 +- test/functional/sqlsrv/access_token.inc | 3 + .../sqlsrv/skipif_azure_ad_acess_token.inc | 45 +++++ .../sqlsrv/skipif_version_less_than_2k16.inc | 6 +- .../sqlsrv/sqlsrv_azure_ad_access_token.phpt | 131 +++++++++++++++ 15 files changed, 535 insertions(+), 28 deletions(-) create mode 100644 test/functional/pdo_sqlsrv/access_token.inc create mode 100644 test/functional/pdo_sqlsrv/pdo_azure_ad_access_token.phpt create mode 100644 test/functional/pdo_sqlsrv/skipif_azure_ad_acess_token.inc create mode 100644 test/functional/sqlsrv/access_token.inc create mode 100644 test/functional/sqlsrv/skipif_azure_ad_acess_token.inc create mode 100644 test/functional/sqlsrv/sqlsrv_azure_ad_access_token.phpt diff --git a/source/pdo_sqlsrv/pdo_dbh.cpp b/source/pdo_sqlsrv/pdo_dbh.cpp index 5d2141cf..55801de0 100644 --- a/source/pdo_sqlsrv/pdo_dbh.cpp +++ b/source/pdo_sqlsrv/pdo_dbh.cpp @@ -38,6 +38,7 @@ namespace PDOConnOptionNames { const char Server[] = "Server"; const char APP[] = "APP"; +const char AccessToken[] = "AccessToken"; const char ApplicationIntent[] = "ApplicationIntent"; const char AttachDBFileName[] = "AttachDbFileName"; const char Authentication[] = "Authentication"; @@ -185,6 +186,15 @@ const connection_option PDO_CONN_OPTS[] = { CONN_ATTR_STRING, conn_str_append_func::func }, + { + PDOConnOptionNames::AccessToken, + sizeof( PDOConnOptionNames::AccessToken ), + SQLSRV_CONN_OPTION_ACCESS_TOKEN, + ODBCConnOptions::AccessToken, + sizeof( ODBCConnOptions::AccessToken), + CONN_ATTR_STRING, + access_token_set_func::func + }, { PDOConnOptionNames::ApplicationIntent, sizeof( PDOConnOptionNames::ApplicationIntent ), diff --git a/source/pdo_sqlsrv/pdo_util.cpp b/source/pdo_sqlsrv/pdo_util.cpp index 617a2df1..f0497f72 100644 --- a/source/pdo_sqlsrv/pdo_util.cpp +++ b/source/pdo_sqlsrv/pdo_util.cpp @@ -429,6 +429,15 @@ pdo_error PDO_ERRORS[] = { SQLSRV_ERROR_KEYSTORE_INVALID_VALUE, { IMSSP, (SQLCHAR*) "Invalid value for loading Azure Key Vault.", -89, false} }, + { + SQLSRV_ERROR_INVALID_OPTION_WITH_ACCESS_TOKEN, + { IMSSP, (SQLCHAR*) "When using Azure AD Access Token, the connection string must not contain UID, PWD, or Authentication keywords.", -90, false} + }, + { + SQLSRV_ERROR_EMPTY_ACCESS_TOKEN, + { IMSSP, (SQLCHAR*) "The Azure AD Access Token is empty. Expected a byte string.", -91, false} + }, + { UINT_MAX, {} } }; diff --git a/source/shared/core_conn.cpp b/source/shared/core_conn.cpp index 54ec004f..d2251fc9 100644 --- a/source/shared/core_conn.cpp +++ b/source/shared/core_conn.cpp @@ -243,6 +243,12 @@ sqlsrv_conn* core_sqlsrv_connect( _In_ sqlsrv_context& henv_cp, _In_ sqlsrv_cont } // else driver_version not unknown #endif // !_WIN32 + // time to free the access token, if not null + if (conn->azure_ad_access_token != NULL) { + memset(conn->azure_ad_access_token->data, 0, conn->azure_ad_access_token->dataSize); // clear the memory + conn->azure_ad_access_token.reset(); + } + CHECK_SQL_ERROR( r, conn ) { throw core::CoreException(); } @@ -759,35 +765,53 @@ void build_connection_string_and_set_conn_attr( _Inout_ sqlsrv_conn* conn, _Inou { bool mars_mentioned = false; connection_option const* conn_opt; + bool access_token_used = false; try { + // First of all, check if access token is specified. If so, check if UID, PWD, Authentication exist + // No need to check the keyword Trusted_Connection because it is not among the acceptable options for SQLSRV drivers + if (zend_hash_index_exists(options, SQLSRV_CONN_OPTION_ACCESS_TOKEN)) { + bool invalidOptions = false; - // Add the server name - common_conn_str_append_func( ODBCConnOptions::SERVER, server, strnlen_s( server ), connection_string TSRMLS_CC ); + // UID and PWD have to be NULLs... throw an exception as long as the user has specified any of them in the connection string, + // even if they may be empty strings. Likewise if the keyword Authentication exists + if (uid != NULL || pwd != NULL || zend_hash_index_exists(options, SQLSRV_CONN_OPTION_AUTHENTICATION)) { + invalidOptions = true; + } - // if uid is not present then we use trusted connection. - if(uid == NULL || strnlen_s( uid ) == 0 ) { - - connection_string += "Trusted_Connection={Yes};"; - } - else { - - bool escaped = core_is_conn_opt_value_escaped( uid, strnlen_s( uid )); - CHECK_CUSTOM_ERROR( !escaped, conn, SQLSRV_ERROR_UID_PWD_BRACES_NOT_ESCAPED ) { + CHECK_CUSTOM_ERROR(invalidOptions, conn, SQLSRV_ERROR_INVALID_OPTION_WITH_ACCESS_TOKEN ) { throw core::CoreException(); } - common_conn_str_append_func( ODBCConnOptions::UID, uid, strnlen_s( uid ), connection_string TSRMLS_CC ); + access_token_used = true; + } - // if no password was given, then don't add a password to the connection string. Perhaps the UID - // given doesn't have a password? - if( pwd != NULL ) { - escaped = core_is_conn_opt_value_escaped( pwd, strnlen_s( pwd )); - CHECK_CUSTOM_ERROR( !escaped, conn, SQLSRV_ERROR_UID_PWD_BRACES_NOT_ESCAPED ) { + // Add the server name + common_conn_str_append_func( ODBCConnOptions::SERVER, server, strnlen_s( server ), connection_string TSRMLS_CC ); + + // if uid is not present then we use trusted connection -- but not when access token is used, because they are incompatible + if (!access_token_used) { + if (uid == NULL || strnlen_s(uid) == 0) { + connection_string += CONNECTION_OPTION_NO_CREDENTIALS; // "Trusted_Connection={Yes};" + } + else { + bool escaped = core_is_conn_opt_value_escaped(uid, strnlen_s(uid)); + CHECK_CUSTOM_ERROR(!escaped, conn, SQLSRV_ERROR_UID_PWD_BRACES_NOT_ESCAPED) { throw core::CoreException(); } - common_conn_str_append_func( ODBCConnOptions::PWD, pwd, strnlen_s( pwd ), connection_string TSRMLS_CC ); + common_conn_str_append_func(ODBCConnOptions::UID, uid, strnlen_s(uid), connection_string TSRMLS_CC); + + // if no password was given, then don't add a password to the connection string. Perhaps the UID + // given doesn't have a password? + if (pwd != NULL) { + escaped = core_is_conn_opt_value_escaped(pwd, strnlen_s(pwd)); + CHECK_CUSTOM_ERROR(!escaped, conn, SQLSRV_ERROR_UID_PWD_BRACES_NOT_ESCAPED) { + throw core::CoreException(); + } + + common_conn_str_append_func(ODBCConnOptions::PWD, pwd, strnlen_s(pwd), connection_string TSRMLS_CC); + } } } @@ -1172,3 +1196,56 @@ size_t core_str_zval_is_true( _Inout_ zval* value_z ) return 0; // false } + +void access_token_set_func::func( _In_ connection_option const* option, _In_ zval* value, _Inout_ sqlsrv_conn* conn, _Inout_ std::string& conn_str TSRMLS_DC ) +{ + SQLSRV_ASSERT(Z_TYPE_P(value) == IS_STRING, "An access token must be a byte string."); + + size_t value_len = Z_STRLEN_P(value); + + CHECK_CUSTOM_ERROR(value_len <= 0, conn, SQLSRV_ERROR_EMPTY_ACCESS_TOKEN) { + throw core::CoreException(); + } + + const char* value_str = Z_STRVAL_P( value ); + + // The SQL_COPT_SS_ACCESS_TOKEN pre-connection attribute allows the use of an access token (in the format extracted from + // an OAuth JSON response), obtained from Azure AD for authentication instead of username and password, and also + // bypasses the negotiation and obtaining of an access token by the driver. To use an access token, set the + // SQL_COPT_SS_ACCESS_TOKEN connection attribute to a pointer to an ACCESSTOKEN structure + // + // typedef struct AccessToken + // { + // unsigned int dataSize; + // char data[]; + // } ACCESSTOKEN; + // + // NOTE: The ODBC Driver version 13.1 only supports this authentication on Windows. + // + // A valid access token byte string must be expanded so that each byte is followed by a 0 padding byte, + // similar to a UCS-2 string containing only ASCII characters + // + // See https://docs.microsoft.com/sql/connect/odbc/using-azure-active-directory#authenticating-with-an-access-token + + size_t dataSize = 2 * value_len; + + sqlsrv_malloc_auto_ptr accToken; + accToken = reinterpret_cast(sqlsrv_malloc(sizeof(ACCESSTOKEN) + dataSize)); + + ACCESSTOKEN *pAccToken = accToken.get(); + SQLSRV_ASSERT(pAccToken != NULL, "Something went wrong when trying to allocate memory for the access token."); + + pAccToken->dataSize = dataSize; + + // Expand access token with padding bytes + for (size_t i = 0, j = 0; i < dataSize; i += 2, j++) { + pAccToken->data[i] = value_str[j]; + pAccToken->data[i+1] = 0; + } + + core::SQLSetConnectAttr(conn, SQL_COPT_SS_ACCESS_TOKEN, reinterpret_cast(pAccToken), SQL_IS_POINTER); + + // Save the pointer because SQLDriverConnect() will use it to make connection to the server + conn->azure_ad_access_token = pAccToken; + accToken.transferred(); +} diff --git a/source/shared/core_sqlsrv.h b/source/shared/core_sqlsrv.h index b5ae9ef6..78215f33 100644 --- a/source/shared/core_sqlsrv.h +++ b/source/shared/core_sqlsrv.h @@ -1076,6 +1076,8 @@ struct sqlsrv_conn : public sqlsrv_context { col_encryption_option ce_option; // holds the details of what are required to enable column encryption DRIVER_VERSION driver_version; // version of ODBC driver + sqlsrv_malloc_auto_ptr azure_ad_access_token; + // initialize with default values sqlsrv_conn( _In_ SQLHANDLE h, _In_ error_callback e, _In_opt_ void* drv, _In_ SQLSRV_ENCODING encoding TSRMLS_DC ) : sqlsrv_context( h, SQL_HANDLE_DBC, e, drv, encoding ) @@ -1105,6 +1107,7 @@ enum SQLSRV_STMT_OPTIONS { namespace ODBCConnOptions { const char APP[] = "APP"; +const char AccessToken[] = "AccessToken"; const char ApplicationIntent[] = "ApplicationIntent"; const char AttachDBFileName[] = "AttachDbFileName"; const char Authentication[] = "Authentication"; @@ -1140,6 +1143,7 @@ enum SQLSRV_CONN_OPTIONS { SQLSRV_CONN_OPTION_INVALID, SQLSRV_CONN_OPTION_APP, + SQLSRV_CONN_OPTION_ACCESS_TOKEN, SQLSRV_CONN_OPTION_CHARACTERSET, SQLSRV_CONN_OPTION_CONN_POOLING, SQLSRV_CONN_OPTION_DATABASE, @@ -1222,14 +1226,14 @@ struct driver_set_func { static void func( _In_ connection_option const* option, _In_ zval* value, _Inout_ sqlsrv_conn* conn, _Inout_ std::string& conn_str TSRMLS_DC ); }; -struct ce_ksp_provider_set_func { - static void func( _In_ connection_option const* option, _In_ zval* value, _Inout_ sqlsrv_conn* conn, _Inout_ std::string& conn_str TSRMLS_DC ); -}; - struct ce_akv_str_set_func { static void func( _In_ connection_option const* option, _In_ zval* value, _Inout_ sqlsrv_conn* conn, _Inout_ std::string& conn_str TSRMLS_DC ); }; +struct access_token_set_func { + static void func( _In_ connection_option const* option, _In_ zval* value, _Inout_ sqlsrv_conn* conn, _Inout_ std::string& conn_str TSRMLS_DC ); +}; + // factory to create a connection (since they are subclassed to instantiate statements) typedef sqlsrv_conn* (*driver_conn_factory)( _In_ SQLHANDLE h, _In_ error_callback e, _In_ void* drv TSRMLS_DC ); @@ -1718,6 +1722,8 @@ enum SQLSRV_ERROR_CODES { SQLSRV_ERROR_AKV_SECRET_MISSING, SQLSRV_ERROR_KEYSTORE_INVALID_VALUE, SQLSRV_ERROR_DOUBLE_CONVERSION_FAILED, + SQLSRV_ERROR_INVALID_OPTION_WITH_ACCESS_TOKEN, + SQLSRV_ERROR_EMPTY_ACCESS_TOKEN, // Driver specific error codes starts from here. SQLSRV_ERROR_DRIVER_SPECIFIC = 1000, diff --git a/source/shared/msodbcsql.h b/source/shared/msodbcsql.h index 3a759252..30f9ee37 100644 --- a/source/shared/msodbcsql.h +++ b/source/shared/msodbcsql.h @@ -92,6 +92,7 @@ #define SQL_COPT_SS_TRUSTEDCMKPATHS (SQL_COPT_SS_BASE_EX+13)// List of trusted CMK paths #define SQL_COPT_SS_CEKCACHETTL (SQL_COPT_SS_BASE_EX+14)// Symmetric Key Cache TTL #define SQL_COPT_SS_AUTHENTICATION (SQL_COPT_SS_BASE_EX+15)// The authentication method used for the connection +#define SQL_COPT_SS_ACCESS_TOKEN (SQL_COPT_SS_BASE_EX+16)// The authentication access token used for the connection // SQLColAttributes driver specific defines. // SQLSetDescField/SQLGetDescField driver specific defines. @@ -370,6 +371,12 @@ #pragma warning(disable:4200) #endif +typedef struct AccessToken +{ + unsigned int dataSize; + char data[]; +} ACCESSTOKEN; + // Keystore Provider interface definition typedef struct CEKeystoreContext { diff --git a/source/sqlsrv/conn.cpp b/source/sqlsrv/conn.cpp index 30bc6513..95672d4d 100644 --- a/source/sqlsrv/conn.cpp +++ b/source/sqlsrv/conn.cpp @@ -180,6 +180,7 @@ namespace SSConnOptionNames { // most of these strings are the same for both the sqlsrv_connect connection option // and the name put into the connection string. MARS is the only one that's different. const char APP[] = "APP"; +const char AccessToken[] = "AccessToken"; const char ApplicationIntent[] = "ApplicationIntent"; const char AttachDBFileName[] = "AttachDbFileName"; const char Authentication[] = "Authentication"; @@ -257,6 +258,15 @@ const connection_option SS_CONN_OPTS[] = { CONN_ATTR_STRING, conn_str_append_func::func }, + { + SSConnOptionNames::AccessToken, + sizeof( SSConnOptionNames::AccessToken ), + SQLSRV_CONN_OPTION_ACCESS_TOKEN, + ODBCConnOptions::AccessToken, + sizeof( ODBCConnOptions::AccessToken), + CONN_ATTR_STRING, + access_token_set_func::func + }, { SSConnOptionNames::ApplicationIntent, sizeof( SSConnOptionNames::ApplicationIntent ), diff --git a/source/sqlsrv/util.cpp b/source/sqlsrv/util.cpp index e5063f23..a1b24c2e 100644 --- a/source/sqlsrv/util.cpp +++ b/source/sqlsrv/util.cpp @@ -420,6 +420,14 @@ ss_error SS_ERRORS[] = { SQLSRV_ERROR_KEYSTORE_INVALID_VALUE, { IMSSP, (SQLCHAR*) "Invalid value for loading Azure Key Vault.", -114, false} }, + { + SQLSRV_ERROR_INVALID_OPTION_WITH_ACCESS_TOKEN, + { IMSSP, (SQLCHAR*) "When using Azure AD Access Token, the connection string must not contain UID, PWD, or Authentication keywords.", -115, false} + }, + { + SQLSRV_ERROR_EMPTY_ACCESS_TOKEN, + { IMSSP, (SQLCHAR*) "The Azure AD Access Token is empty. Expected a byte string.", -116, false} + }, // terminate the list of errors/warnings { UINT_MAX, {} } diff --git a/test/functional/pdo_sqlsrv/access_token.inc b/test/functional/pdo_sqlsrv/access_token.inc new file mode 100644 index 00000000..dbb2f778 --- /dev/null +++ b/test/functional/pdo_sqlsrv/access_token.inc @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdo_azure_ad_access_token.phpt b/test/functional/pdo_sqlsrv/pdo_azure_ad_access_token.phpt new file mode 100644 index 00000000..f468ffd6 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_azure_ad_access_token.phpt @@ -0,0 +1,157 @@ +--TEST-- +Test some basics of Azure AD Access Token support +--DESCRIPTION-- +This test also expects certain exceptions to be thrown under some conditions. +--SKIPIF-- + +--FILE-- +getMessage(), $expectedError) === false) { + echo "AzureAD access token test: expected to fail with $msg\n"; + + print_r($exception->getMessage()); + echo "\n"; + } +} + +function connectWithEmptyAccessToken($server) +{ + $dummyToken = ''; + $expectedError = 'The Azure AD Access Token is empty. Expected a byte string.'; + + $connectionInfo = "AccessToken = $dummyToken;"; + $testCase = 'empty token'; + try { + $conn = new PDO("sqlsrv:server = $server; $connectionInfo"); + echo $message . $testCase . PHP_EOL; + } catch(PDOException $e) { + verifyErrorMessage($e, $expectedError, $testCase); + } + unset($connectionInfo); +} + +function connectWithInvalidOptions($server) +{ + $dummyToken = 'abcde'; + $expectedError = 'When using Azure AD Access Token, the connection string must not contain UID, PWD, or Authentication keywords.'; + $message = 'AzureAD access token test: expected to fail with '; + + $uid = ''; + $connectionInfo = "AccessToken = $dummyToken;"; + $testCase = 'empty UID provided'; + try { + $conn = new PDO("sqlsrv:server = $server; $connectionInfo", $uid); + echo $message . $testCase . PHP_EOL; + } catch(PDOException $e) { + verifyErrorMessage($e, $expectedError, $testCase); + } + unset($connectionInfo); + + $pwd = ''; + $connectionInfo = "AccessToken = $dummyToken;"; + $testCase = 'empty PWD provided'; + try { + $conn = new PDO("sqlsrv:server = $server; $connectionInfo", null, $pwd); + echo $message . $testCase . PHP_EOL; + } catch(PDOException $e) { + verifyErrorMessage($e, $expectedError, $testCase); + } + unset($connectionInfo); + + $uid = 'uid'; + $connectionInfo = "AccessToken = $dummyToken;"; + $testCase = 'UID provided'; + try { + $conn = new PDO("sqlsrv:server = $server; $connectionInfo", $uid); + echo $message . $testCase . PHP_EOL; + } catch(PDOException $e) { + verifyErrorMessage($e, $expectedError, $testCase); + } + unset($connectionInfo); + + $pwd = ''; + $connectionInfo = "AccessToken = $dummyToken;"; + $testCase = 'PWD provided'; + try { + $conn = new PDO("sqlsrv:server = $server; $connectionInfo", null, $pwd); + echo $message . $testCase . PHP_EOL; + } catch(PDOException $e) { + verifyErrorMessage($e, $expectedError, $testCase); + } + unset($connectionInfo); + + $connectionInfo = "Authentication = SqlPassword; AccessToken = $dummyToken;"; + $testCase = 'Authentication keyword'; + try { + $conn = new PDO("sqlsrv:server = $server; $connectionInfo"); + echo $message . $testCase . PHP_EOL; + } catch(PDOException $e) { + verifyErrorMessage($e, $expectedError, $testCase); + } + unset($connectionInfo); +} + +function simpleTest($conn) +{ + // Create table + $tableName = 'Simple'; + $col1 = 'Some simple string value'; + + dropTable($conn, $tableName); + + $query = "CREATE TABLE $tableName(ID INT IDENTITY(1,1), COL1 VARCHAR(25))"; + $stmt = $conn->query($query); + + // Insert one row + $query = "INSERT INTO $tableName VALUES ('$col1')"; + $stmt = $conn->query($query); + + // Fetch data + $query = "SELECT * FROM $tableName"; + $stmt = $conn->query($query); + + $result = $stmt->fetch(PDO::FETCH_NUM); + $id = $result[0]; + if ($id != 1) { + echo "AzureAD access token test: fetched id $id unexpected\n"; + } + + $field = $result[1]; + if ($field !== $col1) { + echo "AzureAD access token test: fetched value $field unexpected\n"; + } + + dropTable($conn, $tableName); +} + +// First test some error conditions +require_once('MsSetup.inc'); +connectWithInvalidOptions($server); + +// Then, test with an empty access token +connectWithEmptyAccessToken($server); + +// Next, test with a valid access token and perform some simple tasks +require_once('access_token.inc'); +try { + if ($adServer != 'TARGET_AD_SERVER' && $accToken != 'TARGET_ACCESS_TOKEN') { + $connectionInfo = "Database = $adDatabase; AccessToken = $accToken;"; + $conn = new PDO("sqlsrv:server = $adServer; $connectionInfo"); + $conn->setAttribute(PDO::SQLSRV_ATTR_FETCHES_NUMERIC_TYPE, true); + simpleTest($conn); + unset($conn); + } +} catch(PDOException $e) { + print_r( $e->getMessage() ); + echo PHP_EOL; +} + +echo "Done\n"; +?> +--EXPECT-- +Done \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/skipif_azure_ad_acess_token.inc b/test/functional/pdo_sqlsrv/skipif_azure_ad_acess_token.inc new file mode 100644 index 00000000..d59c02bf --- /dev/null +++ b/test/functional/pdo_sqlsrv/skipif_azure_ad_acess_token.inc @@ -0,0 +1,41 @@ +getAttribute(PDO::ATTR_CLIENT_VERSION)["DriverVer"]; +$msodbcsqlMaj = explode(".", $msodbcsqlVer)[0]; + +$isWin = (strtoupper(substr(php_uname('s'), 0, 3)) === 'WIN'); +if (!$isWin && $msodbcsqlMaj < 17) { + die("skip: Unsupported ODBC driver version"); +} + +// Now check SQL Server version - exclude this check if running on Azure +if (!$daasMode) { + $stmt = $conn->query("SELECT @@VERSION"); + if ($stmt) { + $ver_string = $stmt->fetch(PDO::FETCH_NUM)[0]; + } else { + die("skip Could not fetch SQL Server version during SKIPIF."); + } + + $version = explode(' ', $ver_string); + + if ($version[3] < '2016') { + die("skip: Wrong version of SQL Server, 2016 or later required"); + } +} + +?> diff --git a/test/functional/pdo_sqlsrv/skipif_version_less_than_2k16.inc b/test/functional/pdo_sqlsrv/skipif_version_less_than_2k16.inc index 72553974..37656773 100644 --- a/test/functional/pdo_sqlsrv/skipif_version_less_than_2k16.inc +++ b/test/functional/pdo_sqlsrv/skipif_version_less_than_2k16.inc @@ -7,11 +7,11 @@ if (!extension_loaded("pdo_sqlsrv")) { die("skip Extension not loaded"); } -$is_win = ( strtoupper( substr( php_uname( 's' ),0,3 ) ) === 'WIN' ); +$is_win = (strtoupper(substr(php_uname('s'),0,3)) === 'WIN'); -require_once( "MsSetup.inc" ); +require_once("MsSetup.inc"); -$conn = new PDO( "sqlsrv:server = $server ;", $uid, $pwd ); +$conn = new PDO("sqlsrv:server = $server; driver=$driver;", $uid, $pwd); if ($conn === false) { die( "skip Could not connect during SKIPIF." ); } diff --git a/test/functional/sqlsrv/access_token.inc b/test/functional/sqlsrv/access_token.inc new file mode 100644 index 00000000..dbb2f778 --- /dev/null +++ b/test/functional/sqlsrv/access_token.inc @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/test/functional/sqlsrv/skipif_azure_ad_acess_token.inc b/test/functional/sqlsrv/skipif_azure_ad_acess_token.inc new file mode 100644 index 00000000..3c7c447c --- /dev/null +++ b/test/functional/sqlsrv/skipif_azure_ad_acess_token.inc @@ -0,0 +1,45 @@ +$userName, "PWD"=>$userPassword, "Driver" => $driver); + +$conn = sqlsrv_connect($server, $connectionInfo); +if ($conn === false) { + die("skip: Could not connect during SKIPIF."); +} + +$msodbcsqlVer = sqlsrv_client_info($conn)['DriverVer']; +$msodbcsqlMaj = explode(".", $msodbcsqlVer)[0]; + +$isWin = (strtoupper(substr(php_uname('s'), 0, 3)) === 'WIN'); + +if (!$isWin && $msodbcsqlMaj < 17) { + die("skip: Unsupported ODBC driver version"); +} + +// Now check SQL Server version - exclude this check if running on Azure +if (!$daasMode) { + // Get SQL Server version + $stmt = sqlsrv_query($conn, "SELECT @@VERSION"); + if (sqlsrv_fetch($stmt)) { + $verString = sqlsrv_get_field($stmt, 0); + } else { + die("skip Could not fetch SQL Server version."); + } + + $version = explode(' ', $verString); + + if ($version[3] < '2016') { + die("skip: Wrong version of SQL Server, 2016 or later required"); + } +} + +?> diff --git a/test/functional/sqlsrv/skipif_version_less_than_2k16.inc b/test/functional/sqlsrv/skipif_version_less_than_2k16.inc index ce06258a..303a2030 100644 --- a/test/functional/sqlsrv/skipif_version_less_than_2k16.inc +++ b/test/functional/sqlsrv/skipif_version_less_than_2k16.inc @@ -7,11 +7,11 @@ if (!extension_loaded("sqlsrv")) { die("skip Extension not loaded"); } -$is_win = ( strtoupper( substr( php_uname( 's' ),0,3 ) ) === 'WIN' ); +$is_win = (strtoupper(substr(php_uname('s'),0,3)) === 'WIN'); -require_once( "MsSetup.inc" ); +require_once("MsSetup.inc"); -$connectionInfo = array( "UID"=>$userName, "PWD"=>$userPassword ); +$connectionInfo = array("UID"=>$userName, "PWD"=>$userPassword, "Driver" => $driver); $conn = sqlsrv_connect( $server, $connectionInfo ); if ($conn === false) { diff --git a/test/functional/sqlsrv/sqlsrv_azure_ad_access_token.phpt b/test/functional/sqlsrv/sqlsrv_azure_ad_access_token.phpt new file mode 100644 index 00000000..96c82d63 --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_azure_ad_access_token.phpt @@ -0,0 +1,131 @@ +--TEST-- +Test some basics of Azure AD Access Token support +--DESCRIPTION-- +This test also expects certain exceptions to be thrown under some conditions. +--SKIPIF-- + +--FILE-- + "$dummyToken"); + $conn = sqlsrv_connect($server, $connectionInfo); + verifyErrorMessage($conn, $expectedError, 'empty token'); + unset($connectionInfo); +} + +function connectWithInvalidOptions($server) +{ + $dummyToken = 'abcde'; + $expectedError = 'When using Azure AD Access Token, the connection string must not contain UID, PWD, or Authentication keywords.'; + + $connectionInfo = array("UID"=>"", "AccessToken" => "$dummyToken"); + $conn = sqlsrv_connect($server, $connectionInfo); + verifyErrorMessage($conn, $expectedError, 'empty UID provided'); + unset($connectionInfo); + + $connectionInfo = array("PWD"=>"", "AccessToken" => "$dummyToken"); + $conn = sqlsrv_connect($server, $connectionInfo); + verifyErrorMessage($conn, $expectedError, 'empty PWD provided'); + unset($connectionInfo); + + $connectionInfo = array("UID"=>"uid", "AccessToken" => "$dummyToken"); + $conn = sqlsrv_connect($server, $connectionInfo); + verifyErrorMessage($conn, $expectedError, 'UID provided'); + unset($connectionInfo); + + $connectionInfo = array("PWD"=>"pwd", "AccessToken" => "$dummyToken"); + $conn = sqlsrv_connect($server, $connectionInfo); + verifyErrorMessage($conn, $expectedError, 'PWD provided'); + unset($connectionInfo); + + $connectionInfo = array("Authentication"=>"SqlPassword", "AccessToken" => "$dummyToken"); + $conn = sqlsrv_connect($server, $connectionInfo); + verifyErrorMessage($conn, $expectedError, 'Authentication keyword'); + unset($connectionInfo); +} + +function simpleTest($conn) +{ + // Create table + $tableName = 'Simple'; + $col1 = 'Some simple string value'; + + dropTable($conn, $tableName); + + $query = "CREATE TABLE $tableName(ID INT IDENTITY(1,1), COL1 VARCHAR(25))"; + $stmt = sqlsrv_query($conn, $query); + if (!$stmt) { + fatalError("AzureAD access token test: failed to create a table\n"); + } + + // Insert one row + $query = "INSERT INTO $tableName VALUES ('$col1')"; + $stmt = sqlsrv_query($conn, $query); + if (!$stmt) { + fatalError("AzureAD access token test: failed to insert a row\n"); + } + + // Fetch data + $query = "SELECT * FROM $tableName"; + $stmt = sqlsrv_query($conn, $query); + if (!$stmt) { + fatalError("AzureAD access token test: failed to fetch a table\n"); + } + + while (sqlsrv_fetch($stmt)) { + $id = sqlsrv_get_field($stmt, 0); + if ($id != 1) { + fatalError("AzureAD access token test: fetched id $id unexpected\n"); + } + $field = sqlsrv_get_field($stmt, 1); + if ($field !== $col1) { + fatalError("AzureAD access token test: fetched value $field unexpected\n"); + } + } + + dropTable($conn, $tableName); +} + +// First test some error conditions +connectWithInvalidOptions($server); + +// Then, test with an empty access token +connectWithEmptyAccessToken($server); + +// Next, test with a valid access token and perform some simple tasks +require_once('access_token.inc'); +if ($adServer != 'TARGET_AD_SERVER' && $accToken != 'TARGET_ACCESS_TOKEN') { + $connectionInfo = array("Database"=>$adDatabase, "AccessToken"=>$accToken); + + $conn = sqlsrv_connect($adServer, $connectionInfo); + if ($conn === false) { + fatalError("Could not connect with Azure AD AccessToken.\n"); + } else { + simpleTest($conn); + + sqlsrv_close($conn); + } +} + +echo "Done\n"; +?> +--EXPECT-- +Done \ No newline at end of file