diff --git a/source/pdo_sqlsrv/pdo_util.cpp b/source/pdo_sqlsrv/pdo_util.cpp index 1e27ea4c..cf9aa029 100644 --- a/source/pdo_sqlsrv/pdo_util.cpp +++ b/source/pdo_sqlsrv/pdo_util.cpp @@ -383,7 +383,7 @@ pdo_error PDO_ERRORS[] = { }, { PDO_SQLSRV_ERROR_INVALID_AUTHENTICATION_OPTION, - { IMSSP, (SQLCHAR*) "Invalid option for the Authentication keyword. Only SqlPassword or ActiveDirectoryPassword is supported.", -73, false } + { IMSSP, (SQLCHAR*) "Invalid option for the Authentication keyword. Only SqlPassword, ActiveDirectoryPassword, or ActiveDirectoryMsi is supported.", -73, false } }, { SQLSRV_ERROR_CE_DRIVER_REQUIRED, @@ -445,6 +445,10 @@ pdo_error PDO_ERRORS[] = { SQLSRV_ERROR_INVALID_DECIMAL_PLACES, { IMSSP, (SQLCHAR*) "Expected an integer to specify number of decimals to format the output values of decimal data types.", -92, false} }, + { + 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} + }, { UINT_MAX, {} } }; diff --git a/source/shared/core_conn.cpp b/source/shared/core_conn.cpp index 8ec79e0c..1b0a8b16 100644 --- a/source/shared/core_conn.cpp +++ b/source/shared/core_conn.cpp @@ -730,7 +730,7 @@ bool core_is_authentication_option_valid( _In_z_ const char* value, _In_ size_t if (value_len <= 0) return false; - if( ! stricmp( value, AzureADOptions::AZURE_AUTH_SQL_PASSWORD ) || ! stricmp( value, AzureADOptions::AZURE_AUTH_AD_PASSWORD ) ) { + if (!stricmp(value, AzureADOptions::AZURE_AUTH_SQL_PASSWORD) || !stricmp(value, AzureADOptions::AZURE_AUTH_AD_PASSWORD) || !stricmp(value, AzureADOptions::AZURE_AUTH_AD_MSI)) { return true; } @@ -769,16 +769,18 @@ 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; + bool authentication_option_used = zend_hash_index_exists(options, SQLSRV_CONN_OPTION_AUTHENTICATION); try { - // First of all, check if access token is specified. If so, check if UID, PWD, Authentication exist + // Since connection options access token and authentication cannot coexist, check if both of them are used. + // If access token is specified, check UID and PWD as well. // 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; // 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)) { + if (uid != NULL || pwd != NULL || authentication_option_used) { invalidOptions = true; } @@ -789,11 +791,44 @@ void build_connection_string_and_set_conn_attr( _Inout_ sqlsrv_conn* conn, _Inou access_token_used = true; } + // Check if Authentication is ActiveDirectoryMSI + // https://docs.microsoft.com/en-ca/azure/active-directory/managed-identities-azure-resources/overview + bool activeDirectoryMSI = false; + if (authentication_option_used) { + zval* auth_option = NULL; + auth_option = zend_hash_index_find(options, SQLSRV_CONN_OPTION_AUTHENTICATION); + + char* option = Z_STRVAL_P(auth_option); + + if (!stricmp(option, AzureADOptions::AZURE_AUTH_AD_MSI)) { + activeDirectoryMSI = true; + + // There are two types of managed identities: + // (1) A system-assigned managed identity: UID must be NULL + // (2) A user-assigned managed identity: UID defined but must not be an empty string + // In both cases, PWD must be NULL + + bool invalid = false; + if (pwd != NULL) { + invalid = true; + } else { + if (uid != NULL && strnlen_s(uid) == 0) { + invalid = true; + } + } + + CHECK_CUSTOM_ERROR(invalid, conn, SQLSRV_ERROR_AAD_MSI_UID_PWD_NOT_NULL ) { + throw core::CoreException(); + } + } + } + // 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 is not present then we use trusted connection -- but not when access token or ActiveDirectoryMSI is used, + // because they are incompatible + if (!access_token_used && !activeDirectoryMSI) { if (uid == NULL || strnlen_s(uid) == 0) { connection_string += CONNECTION_OPTION_NO_CREDENTIALS; // "Trusted_Connection={Yes};" } diff --git a/source/shared/core_sqlsrv.h b/source/shared/core_sqlsrv.h index 400e9ea2..6149429c 100644 --- a/source/shared/core_sqlsrv.h +++ b/source/shared/core_sqlsrv.h @@ -191,6 +191,7 @@ const int SQL_SERVER_2008_DEFAULT_DATETIME_SCALE = 7; namespace AzureADOptions { const char AZURE_AUTH_SQL_PASSWORD[] = "SqlPassword"; const char AZURE_AUTH_AD_PASSWORD[] = "ActiveDirectoryPassword"; + const char AZURE_AUTH_AD_MSI[] = "ActiveDirectoryMsi"; } // the message returned by ODBC Driver for SQL Server @@ -1777,6 +1778,7 @@ enum SQLSRV_ERROR_CODES { SQLSRV_ERROR_INVALID_OPTION_WITH_ACCESS_TOKEN, SQLSRV_ERROR_EMPTY_ACCESS_TOKEN, SQLSRV_ERROR_INVALID_DECIMAL_PLACES, + SQLSRV_ERROR_AAD_MSI_UID_PWD_NOT_NULL, // Driver specific error codes starts from here. SQLSRV_ERROR_DRIVER_SPECIFIC = 1000, diff --git a/source/sqlsrv/util.cpp b/source/sqlsrv/util.cpp index 21be0978..591829b2 100644 --- a/source/sqlsrv/util.cpp +++ b/source/sqlsrv/util.cpp @@ -372,7 +372,7 @@ ss_error SS_ERRORS[] = { }, { SS_SQLSRV_ERROR_INVALID_AUTHENTICATION_OPTION, - { IMSSP, (SQLCHAR*)"Invalid option for the Authentication keyword. Only SqlPassword or ActiveDirectoryPassword is supported.", -62, false } + { IMSSP, (SQLCHAR*)"Invalid option for the Authentication keyword. Only SqlPassword, ActiveDirectoryPassword, or ActiveDirectoryMsi is supported.", -62, false } }, { SS_SQLSRV_ERROR_AE_QUERY_SQLTYPE_REQUIRED, @@ -436,6 +436,10 @@ ss_error SS_ERRORS[] = { SQLSRV_ERROR_INVALID_DECIMAL_PLACES, { IMSSP, (SQLCHAR*) "Expected an integer to specify number of decimals to format the output values of decimal data types.", -117, false} }, + { + 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} + }, // terminate the list of errors/warnings { UINT_MAX, {} } diff --git a/test/functional/pdo_sqlsrv/pdo_azure_ad_authentication.phpt b/test/functional/pdo_sqlsrv/pdo_azure_ad_authentication.phpt index e2d85dd4..105f6395 100644 --- a/test/functional/pdo_sqlsrv/pdo_azure_ad_authentication.phpt +++ b/test/functional/pdo_sqlsrv/pdo_azure_ad_authentication.phpt @@ -78,5 +78,5 @@ if ($azureServer != 'TARGET_AD_SERVER') { Connected successfully with Authentication=SqlPassword. string(1) "%d" Could not connect with Authentication=ActiveDirectoryIntegrated. -SQLSTATE[IMSSP]: Invalid option for the Authentication keyword. Only SqlPassword or ActiveDirectoryPassword is supported. +SQLSTATE[IMSSP]: Invalid option for the Authentication keyword. Only SqlPassword, ActiveDirectoryPassword, or ActiveDirectoryMsi is supported. %s with Authentication=ActiveDirectoryPassword. diff --git a/test/functional/pdo_sqlsrv/pdo_azure_ad_managed_identity.phpt b/test/functional/pdo_sqlsrv/pdo_azure_ad_managed_identity.phpt new file mode 100644 index 00000000..00a0c950 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_azure_ad_managed_identity.phpt @@ -0,0 +1,114 @@ +--TEST-- +Test some error conditions of Azure AD Managed Identity support +--DESCRIPTION-- +This test expects certain exceptions to be thrown under some conditions. +--SKIPIF-- + +--FILE-- +getMessage(), $expectedError) === false) { + echo "AzureAD Managed Identity test: expected to fail with $msg\n"; + + print_r($exception->getMessage()); + echo "\n"; + } +} + +function connectWithInvalidOptions() +{ + global $server; + + $message = 'AzureAD Managed Identity test: expected to fail with '; + $expectedError = 'When using ActiveDirectoryMsi Authentication, PWD must be NULL. UID can be NULL, but if not, an empty string is not accepted'; + + $uid = ''; + $connectionInfo = "Authentication = ActiveDirectoryMsi;"; + $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 = "Authentication = ActiveDirectoryMsi;"; + $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); + + $pwd = 'dummy'; + $connectionInfo = "Authentication = ActiveDirectoryMsi;"; + $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); + + $expectedError = 'When using Azure AD Access Token, the connection string must not contain UID, PWD, or Authentication keywords.'; + $connectionInfo = "Authentication = ActiveDirectoryMsi; AccessToken = '123';"; + $testCase = 'AccessToken option'; + try { + $conn = new PDO("sqlsrv:server = $server; $connectionInfo"); + echo $message . $testCase . PHP_EOL; + } catch(PDOException $e) { + verifyErrorMessage($e, $expectedError, $testCase); + } + unset($connectionInfo); +} + +function connectInvalidServer() +{ + global $server, $driver, $uid, $pwd; + + try { + $conn = new PDO("sqlsrv:server = $server; driver=$driver;", $uid, $pwd); + + $msodbcsqlVer = $conn->getAttribute(PDO::ATTR_CLIENT_VERSION)["DriverVer"]; + $version = explode(".", $msodbcsqlVer); + + if ($version[0] < 17 || $version[1] < 3) { + //skip the rest of this test, which requires ODBC driver 17.3 or above + return; + } + unset($conn); + + // Try connecting to an invalid server, should get an exception from ODBC + $connectionInfo = "Authentication = ActiveDirectoryMsi;"; + $testCase = 'invalidServer'; + try { + $conn = new PDO("sqlsrv:server = invalidServer; $connectionInfo", null, null); + echo $message . $testCase . PHP_EOL; + } catch(PDOException $e) { + // TODO: check the exception message here + } + } catch(PDOException $e) { + print_r($e->getMessage()); + } +} + +require_once('MsSetup.inc'); + +// Test some error conditions +connectWithInvalidOptions(); + +// Make a connection to an invalid server +connectInvalidServer(); + +echo "Done\n"; +?> +--EXPECT-- +Done \ No newline at end of file diff --git a/test/functional/sqlsrv/sqlsrv_azure_ad_authentication.phpt b/test/functional/sqlsrv/sqlsrv_azure_ad_authentication.phpt index af582dcb..51f61b27 100644 --- a/test/functional/sqlsrv/sqlsrv_azure_ad_authentication.phpt +++ b/test/functional/sqlsrv/sqlsrv_azure_ad_authentication.phpt @@ -85,7 +85,7 @@ Array [SQLSTATE] => IMSSP [1] => -62 [code] => -62 - [2] => Invalid option for the Authentication keyword. Only SqlPassword or ActiveDirectoryPassword is supported. - [message] => Invalid option for the Authentication keyword. Only SqlPassword or ActiveDirectoryPassword is supported. + [2] => Invalid option for the Authentication keyword. Only SqlPassword, ActiveDirectoryPassword, or ActiveDirectoryMsi is supported. + [message] => Invalid option for the Authentication keyword. Only SqlPassword, ActiveDirectoryPassword, or ActiveDirectoryMsi is supported. ) %s with Authentication=ActiveDirectoryPassword. diff --git a/test/functional/sqlsrv/sqlsrv_azure_ad_managed_identity.phpt b/test/functional/sqlsrv/sqlsrv_azure_ad_managed_identity.phpt new file mode 100644 index 00000000..644731eb --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_azure_ad_managed_identity.phpt @@ -0,0 +1,88 @@ +--TEST-- +Test some error conditions of Azure AD Managed Identity support +--DESCRIPTION-- +This test expects certain exceptions to be thrown under some conditions. +--SKIPIF-- + +--FILE-- +"", "Authentication" => "ActiveDirectoryMsi"); + $conn = sqlsrv_connect($server, $connectionInfo); + verifyErrorMessage($conn, $expectedError, 'empty UID provided'); + unset($connectionInfo); + + $connectionInfo = array("PWD"=>"", "Authentication" => "ActiveDirectoryMsi"); + $conn = sqlsrv_connect($server, $connectionInfo); + verifyErrorMessage($conn, $expectedError, 'empty PWD provided'); + unset($connectionInfo); + + $connectionInfo = array("PWD"=>"pwd", "Authentication" => "ActiveDirectoryMsi"); + $conn = sqlsrv_connect($server, $connectionInfo); + verifyErrorMessage($conn, $expectedError, 'PWD provided'); + unset($connectionInfo); + + $expectedError = 'When using Azure AD Access Token, the connection string must not contain UID, PWD, or Authentication keywords.'; + $connectionInfo = array("Authentication"=>"ActiveDirectoryMsi", "AccessToken" => "123"); + $conn = sqlsrv_connect($server, $connectionInfo); + verifyErrorMessage($conn, $expectedError, 'AccessToken option'); + unset($connectionInfo); +} + +function connectInvalidServer() +{ + global $server, $driver, $userName, $userPassword; + + $connectionInfo = array("UID"=>$userName, "PWD"=>$userPassword, "Driver" => $driver); + $conn = sqlsrv_connect($server, $connectionInfo); + if ($conn === false) { + fatalError("Failed to connect in connectInvalidServer."); + } + + $msodbcsqlVer = sqlsrv_client_info($conn)['DriverVer']; + $version = explode(".", $msodbcsqlVer); + + if ($version[0] < 17 || $version[1] < 3) { + //skip the rest of this test, which requires ODBC driver 17.3 or above + return; + } + sqlsrv_close($conn); + + // Try connecting to an invalid server, should get an exception from ODBC + $connectionInfo = array("Authentication"=>"ActiveDirectoryMsi"); + $conn = sqlsrv_connect('invalidServer', $connectionInfo); + if ($conn) { + fatalError("AzureAD Managed Identity test: expected to fail with invalidServer\n"); + } else { + // TODO: check the exception message here, using verifyErrorMessage() + } +} + +// Test some error conditions +connectWithInvalidOptions($server); + +// Make a connection to an invalid server +connectInvalidServer(); + +echo "Done\n"; +?> +--EXPECT-- +Done \ No newline at end of file