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
This commit is contained in:
parent
ae1b413f19
commit
e51380612d
|
@ -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 ),
|
||||
|
|
|
@ -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, {} }
|
||||
};
|
||||
|
||||
|
|
|
@ -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<ACCESSTOKEN> accToken;
|
||||
accToken = reinterpret_cast<ACCESSTOKEN*>(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<SQLPOINTER>(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();
|
||||
}
|
||||
|
|
|
@ -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<ACCESSTOKEN> 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,
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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 ),
|
||||
|
|
|
@ -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, {} }
|
||||
|
|
3
test/functional/pdo_sqlsrv/access_token.inc
Normal file
3
test/functional/pdo_sqlsrv/access_token.inc
Normal file
|
@ -0,0 +1,3 @@
|
|||
<?php
|
||||
$accToken = 'TARGET_ACCESS_TOKEN';
|
||||
?>
|
157
test/functional/pdo_sqlsrv/pdo_azure_ad_access_token.phpt
Normal file
157
test/functional/pdo_sqlsrv/pdo_azure_ad_access_token.phpt
Normal file
|
@ -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--
|
||||
<?php require('skipif.inc');
|
||||
require('skipif_azure_ad_acess_token.inc'); ?>
|
||||
--FILE--
|
||||
<?php
|
||||
require_once("MsCommon_mid-refactor.inc");
|
||||
|
||||
function verifyErrorMessage($exception, $expectedError, $msg)
|
||||
{
|
||||
if (strpos($exception->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
|
41
test/functional/pdo_sqlsrv/skipif_azure_ad_acess_token.inc
Normal file
41
test/functional/pdo_sqlsrv/skipif_azure_ad_acess_token.inc
Normal file
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
// Check if the Azure AD Access Token test should be skipped
|
||||
// From online documentation - For the ODBC Driver version 13.1,
|
||||
// the Azure Active Directory access token authentication is Windows only.
|
||||
|
||||
if (!extension_loaded("pdo_sqlsrv")) {
|
||||
die("skip Extension not loaded");
|
||||
}
|
||||
|
||||
require_once("MsSetup.inc");
|
||||
|
||||
$conn = new PDO("sqlsrv:server = $server; driver=$driver;", $uid, $pwd);
|
||||
if ($conn === false) {
|
||||
die("skip Could not connect during SKIPIF.");
|
||||
}
|
||||
|
||||
$msodbcsqlVer = $conn->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");
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
|
@ -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." );
|
||||
}
|
||||
|
|
3
test/functional/sqlsrv/access_token.inc
Normal file
3
test/functional/sqlsrv/access_token.inc
Normal file
|
@ -0,0 +1,3 @@
|
|||
<?php
|
||||
$accToken = 'TARGET_ACCESS_TOKEN';
|
||||
?>
|
45
test/functional/sqlsrv/skipif_azure_ad_acess_token.inc
Normal file
45
test/functional/sqlsrv/skipif_azure_ad_acess_token.inc
Normal file
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
// Check if the Azure AD Access Token test should be skipped
|
||||
// From online documentation - For the ODBC Driver version 13.1,
|
||||
// the Azure Active Directory access token authentication is Windows only.
|
||||
|
||||
if (!extension_loaded("sqlsrv")) {
|
||||
die("skip Extension not loaded");
|
||||
}
|
||||
|
||||
require_once("MsSetup.inc");
|
||||
|
||||
$connectionInfo = array("UID"=>$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");
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
|
@ -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) {
|
||||
|
|
131
test/functional/sqlsrv/sqlsrv_azure_ad_access_token.phpt
Normal file
131
test/functional/sqlsrv/sqlsrv_azure_ad_access_token.phpt
Normal file
|
@ -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--
|
||||
<?php require('skipif.inc');
|
||||
require('skipif_azure_ad_acess_token.inc'); ?>
|
||||
--FILE--
|
||||
<?php
|
||||
require_once("MsCommon.inc");
|
||||
|
||||
function verifyErrorMessage($conn, $expectedError, $msg)
|
||||
{
|
||||
if ($conn === false) {
|
||||
if (strpos(sqlsrv_errors($conn)[0]['message'], $expectedError) === false) {
|
||||
print_r(sqlsrv_errors());
|
||||
}
|
||||
} else {
|
||||
fatalError("AzureAD access token test: expected to fail with $msg\n");
|
||||
}
|
||||
}
|
||||
|
||||
function connectWithEmptyAccessToken($server)
|
||||
{
|
||||
$dummyToken = '';
|
||||
$expectedError = 'The Azure AD Access Token is empty. Expected a byte string.';
|
||||
|
||||
$connectionInfo = array("AccessToken" => "$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
|
Loading…
Reference in a new issue