diff --git a/doc/snippets/Microsoft.Data.SqlClient/ActiveDirectoryAuthenticationProvider.xml b/doc/snippets/Microsoft.Data.SqlClient/ActiveDirectoryAuthenticationProvider.xml index 052cd0cc30..990c162acb 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/ActiveDirectoryAuthenticationProvider.xml +++ b/doc/snippets/Microsoft.Data.SqlClient/ActiveDirectoryAuthenticationProvider.xml @@ -88,6 +88,8 @@ || || || +|| +|| ## Examples The following example demonstrates providing a custom device flow callback to SqlClient for the Device Code Flow authentication method: diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationMethod.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationMethod.xml index 2ddb84b9a1..19927484fd 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationMethod.xml +++ b/doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationMethod.xml @@ -33,5 +33,13 @@ The authentication method uses Active Directory Device Code Flow. Use Active Directory Device Code Flow to connect to a SQL Database from devices and operating systems that do not provide a Web browser, using another device to perform interactive authentication. 6 + + The authentication method uses Active Directory Managed Identity. Use System Assigned or User Assigned Managed Identity to connect to SQL Database from Azure client environments that have enabled support for Managed Identity. For User Assigned Managed Identity, 'User Id' or 'UID' is required to be set to the object ID of the user identity. + 7 + + + Alias for "Active Directory Managed Identity" authentication method. Use System Assigned or User Assigned Managed Identity to connect to SQL Database from Azure client environments that have enabled support for Managed Identity. For User Assigned Managed Identity, 'User Id' or 'UID' is required to be set to the object ID of the user identity. + 8 + diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationParameters.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationParameters.xml index 32e6c6a9bf..520882eb93 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationParameters.xml +++ b/doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationParameters.xml @@ -12,8 +12,7 @@ The user login name/ID. The user password. The connection ID. - Initializes a new instance of the - class using the specified authentication method, server name, database name, resource URI, authority URI, user login name/ID, user password and connection ID. + Initializes a new instance of the class using the specified authentication method, server name, database name, resource URI, authority URI, user login name/ID, user password and connection ID. Gets the authentication method. diff --git a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs index f08e1835ed..66ad7c3679 100644 --- a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs +++ b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs @@ -93,6 +93,10 @@ public enum SqlAuthenticationMethod ActiveDirectoryServicePrincipal = 5, /// ActiveDirectoryDeviceCodeFlow = 6, + /// + ActiveDirectoryManagedIdentity = 7, + /// + ActiveDirectoryMSI = 8, /// NotSpecified = 0, /// @@ -580,12 +584,12 @@ public sealed partial class SqlConnection : System.Data.Common.DbConnection, Sys [System.ComponentModel.DesignerSerializationVisibilityAttribute(0)] public System.Guid ClientConnectionId { get { throw null; } } - /// + /// /// for internal test only /// [System.ComponentModel.DesignerSerializationVisibilityAttribute(0)] internal string SQLDNSCachingSupportedState { get { throw null; } } - /// + /// /// for internal test only /// [System.ComponentModel.DesignerSerializationVisibilityAttribute(0)] diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj index ff1db9b5e6..e63a6cce77 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -78,6 +78,9 @@ Microsoft\Data\SqlClient\ActiveDirectoryAuthenticationProvider.cs + + Microsoft\Data\SqlClient\AzureManagedIdentityAuthenticationProvider.cs + Microsoft\Data\SqlClient\Server\ExtendedClrTypeCode.cs diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/Common/DbConnectionStringCommon.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/Common/DbConnectionStringCommon.cs index 66a04e5ff5..ee9544b4ce 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/Common/DbConnectionStringCommon.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/Common/DbConnectionStringCommon.cs @@ -104,10 +104,12 @@ internal static string ConvertToString(object value) const string ActiveDirectoryInteractiveString = "Active Directory Interactive"; const string ActiveDirectoryServicePrincipalString = "Active Directory Service Principal"; const string ActiveDirectoryDeviceCodeFlowString = "Active Directory Device Code Flow"; + internal const string ActiveDirectoryManagedIdentityString = "Active Directory Managed Identity"; + internal const string ActiveDirectoryMSIString = "Active Directory MSI"; internal static bool TryConvertToAuthenticationType(string value, out SqlAuthenticationMethod result) { - Debug.Assert(Enum.GetNames(typeof(SqlAuthenticationMethod)).Length == 7, "SqlAuthenticationMethod enum has changed, update needed"); + Debug.Assert(Enum.GetNames(typeof(SqlAuthenticationMethod)).Length == 9, "SqlAuthenticationMethod enum has changed, update needed"); bool isSuccess = false; @@ -147,6 +149,18 @@ internal static bool TryConvertToAuthenticationType(string value, out SqlAuthent result = SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow; isSuccess = true; } + else if (StringComparer.InvariantCultureIgnoreCase.Equals(value, ActiveDirectoryManagedIdentityString) + || StringComparer.InvariantCultureIgnoreCase.Equals(value, Convert.ToString(SqlAuthenticationMethod.ActiveDirectoryManagedIdentity, CultureInfo.InvariantCulture))) + { + result = SqlAuthenticationMethod.ActiveDirectoryManagedIdentity; + isSuccess = true; + } + else if (StringComparer.InvariantCultureIgnoreCase.Equals(value, ActiveDirectoryMSIString) + || StringComparer.InvariantCultureIgnoreCase.Equals(value, Convert.ToString(SqlAuthenticationMethod.ActiveDirectoryMSI, CultureInfo.InvariantCulture))) + { + result = SqlAuthenticationMethod.ActiveDirectoryMSI; + isSuccess = true; + } else { result = DbConnectionStringDefaults.Authentication; @@ -367,7 +381,7 @@ internal static SqlConnectionAttestationProtocol ConvertToAttestationProtocol(st } catch (ArgumentException e) { - // to be consistent with the messages we send in case of wrong type usage, replace + // to be consistent with the messages we send in case of wrong type usage, replace // the error with our exception, and keep the original one as inner one for troubleshooting throw ADP.ConvertFailed(value.GetType(), typeof(SqlConnectionAttestationProtocol), e); } @@ -411,7 +425,7 @@ internal static string ApplicationIntentToString(ApplicationIntent value) /// * if the value is from type ApplicationIntent, it will be used as is /// * if the value is from integral type (SByte, Int16, Int32, Int64, Byte, UInt16, UInt32, or UInt64), it will be converted to enum /// * if the value is another enum or any other type, it will be blocked with an appropriate ArgumentException - /// + /// /// in any case above, if the converted value is out of valid range, the method raises ArgumentOutOfRangeException. /// /// application intent value in the valid range @@ -467,7 +481,7 @@ internal static ApplicationIntent ConvertToApplicationIntent(string keyword, obj } catch (ArgumentException e) { - // to be consistent with the messages we send in case of wrong type usage, replace + // to be consistent with the messages we send in case of wrong type usage, replace // the error with our exception, and keep the original one as inner one for troubleshooting throw ADP.ConvertFailed(value.GetType(), typeof(ApplicationIntent), e); } @@ -487,13 +501,15 @@ internal static ApplicationIntent ConvertToApplicationIntent(string keyword, obj internal static bool IsValidAuthenticationTypeValue(SqlAuthenticationMethod value) { - Debug.Assert(Enum.GetNames(typeof(SqlAuthenticationMethod)).Length == 7, "SqlAuthenticationMethod enum has changed, update needed"); + Debug.Assert(Enum.GetNames(typeof(SqlAuthenticationMethod)).Length == 9, "SqlAuthenticationMethod enum has changed, update needed"); return value == SqlAuthenticationMethod.SqlPassword || value == SqlAuthenticationMethod.ActiveDirectoryPassword || value == SqlAuthenticationMethod.ActiveDirectoryIntegrated || value == SqlAuthenticationMethod.ActiveDirectoryInteractive || value == SqlAuthenticationMethod.ActiveDirectoryServicePrincipal || value == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow + || value == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity + || value == SqlAuthenticationMethod.ActiveDirectoryMSI || value == SqlAuthenticationMethod.NotSpecified; } @@ -515,6 +531,10 @@ internal static string AuthenticationTypeToString(SqlAuthenticationMethod value) return ActiveDirectoryServicePrincipalString; case SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow: return ActiveDirectoryDeviceCodeFlowString; + case SqlAuthenticationMethod.ActiveDirectoryManagedIdentity: + return ActiveDirectoryManagedIdentityString; + case SqlAuthenticationMethod.ActiveDirectoryMSI: + return ActiveDirectoryMSIString; default: return null; } @@ -572,7 +592,7 @@ internal static SqlAuthenticationMethod ConvertToAuthenticationType(string keywo } catch (ArgumentException e) { - // to be consistent with the messages we send in case of wrong type usage, replace + // to be consistent with the messages we send in case of wrong type usage, replace // the error with our exception, and keep the original one as inner one for troubleshooting throw ADP.ConvertFailed(value.GetType(), typeof(SqlAuthenticationMethod), e); } @@ -648,7 +668,7 @@ internal static SqlConnectionColumnEncryptionSetting ConvertToColumnEncryptionSe } catch (ArgumentException e) { - // to be consistent with the messages we send in case of wrong type usage, replace + // to be consistent with the messages we send in case of wrong type usage, replace // the error with our exception, and keep the original one as inner one for troubleshooting throw ADP.ConvertFailed(value.GetType(), typeof(SqlConnectionColumnEncryptionSetting), e); } @@ -672,18 +692,19 @@ internal static partial class DbConnectionStringDefaults // all // internal const string NamedConnection = ""; + private const string _emptyString = ""; // SqlClient internal const ApplicationIntent ApplicationIntent = Microsoft.Data.SqlClient.ApplicationIntent.ReadWrite; internal const string ApplicationName = "Core Microsoft SqlClient Data Provider"; - internal const string AttachDBFilename = ""; + internal const string AttachDBFilename = _emptyString; internal const int CommandTimeout = 30; internal const int ConnectTimeout = 15; - internal const string CurrentLanguage = ""; - internal const string DataSource = ""; + internal const string CurrentLanguage = _emptyString; + internal const string DataSource = _emptyString; internal const bool Encrypt = false; internal const bool Enlist = true; - internal const string FailoverPartner = ""; - internal const string InitialCatalog = ""; + internal const string FailoverPartner = _emptyString; + internal const string InitialCatalog = _emptyString; internal const bool IntegratedSecurity = false; internal const int LoadBalanceTimeout = 0; // default of 0 means don't use internal const bool MultipleActiveResultSets = false; @@ -691,21 +712,21 @@ internal static partial class DbConnectionStringDefaults internal const int MaxPoolSize = 100; internal const int MinPoolSize = 0; internal const int PacketSize = 8000; - internal const string Password = ""; + internal const string Password = _emptyString; internal const bool PersistSecurityInfo = false; internal const bool Pooling = true; internal const bool TrustServerCertificate = false; internal const string TypeSystemVersion = "Latest"; - internal const string UserID = ""; + internal const string UserID = _emptyString; internal const bool UserInstance = false; internal const bool Replication = false; - internal const string WorkstationID = ""; + internal const string WorkstationID = _emptyString; internal const string TransactionBinding = "Implicit Unbind"; internal const int ConnectRetryCount = 1; internal const int ConnectRetryInterval = 10; internal static readonly SqlAuthenticationMethod Authentication = SqlAuthenticationMethod.NotSpecified; internal const SqlConnectionColumnEncryptionSetting ColumnEncryptionSetting = SqlConnectionColumnEncryptionSetting.Disabled; - internal const string EnclaveAttestationUrl = ""; + internal const string EnclaveAttestationUrl = _emptyString; internal const SqlConnectionAttestationProtocol AttestationProtocol = SqlConnectionAttestationProtocol.NotSpecified; } @@ -813,7 +834,7 @@ internal static class DbConnectionStringSynonyms //internal const string MultiSubnetFailover = MULTISUBNETFAILOVER; internal const string MULTISUBNETFAILOVER = "MultiSubnetFailover"; - + //internal const string NetworkLibrary = NET+","+NETWORK; internal const string NET = "net"; internal const string NETWORK = "network"; diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.NetCoreApp.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.NetCoreApp.cs index 06d9c786ce..bf44f23184 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.NetCoreApp.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.NetCoreApp.cs @@ -15,6 +15,7 @@ internal partial class SqlAuthenticationProviderManager static SqlAuthenticationProviderManager() { + var azureManagedIdentityAuthenticationProvider = new AzureManagedIdentityAuthenticationProvider(); SqlAuthenticationProviderConfigurationSection configurationSection = null; try @@ -40,6 +41,8 @@ static SqlAuthenticationProviderManager() Instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryInteractive, activeDirectoryAuthProvider); Instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryServicePrincipal, activeDirectoryAuthProvider); Instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, activeDirectoryAuthProvider); + Instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryManagedIdentity, azureManagedIdentityAuthenticationProvider); + Instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryMSI, azureManagedIdentityAuthenticationProvider); } /// @@ -153,6 +156,10 @@ private static SqlAuthenticationMethod AuthenticationEnumFromString(string authe return SqlAuthenticationMethod.ActiveDirectoryServicePrincipal; case ActiveDirectoryDeviceCodeFlow: return SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow; + case ActiveDirectoryManagedIdentity: + return SqlAuthenticationMethod.ActiveDirectoryManagedIdentity; + case ActiveDirectoryMSI: + return SqlAuthenticationMethod.ActiveDirectoryMSI; default: throw SQL.UnsupportedAuthentication(authentication); } diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.NetStandard.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.NetStandard.cs index d283f58400..71b8b23269 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.NetStandard.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.NetStandard.cs @@ -8,6 +8,8 @@ internal partial class SqlAuthenticationProviderManager { static SqlAuthenticationProviderManager() { + var azureManagedIdentityAuthenticationProvider = new AzureManagedIdentityAuthenticationProvider(); + Instance = new SqlAuthenticationProviderManager(); var activeDirectoryAuthProvider = new ActiveDirectoryAuthenticationProvider(Instance._applicationClientId); Instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryPassword, activeDirectoryAuthProvider); @@ -15,6 +17,8 @@ static SqlAuthenticationProviderManager() Instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryInteractive, activeDirectoryAuthProvider); Instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryServicePrincipal, activeDirectoryAuthProvider); Instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, activeDirectoryAuthProvider); + Instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryManagedIdentity, azureManagedIdentityAuthenticationProvider); + Instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryMSI, azureManagedIdentityAuthenticationProvider); } } } diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs index 512f3d4b04..f55bb9d8e4 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs @@ -19,6 +19,8 @@ internal partial class SqlAuthenticationProviderManager private const string ActiveDirectoryInteractive = "active directory interactive"; private const string ActiveDirectoryServicePrincipal = "active directory service principal"; private const string ActiveDirectoryDeviceCodeFlow = "active directory device code flow"; + private const string ActiveDirectoryManagedIdentity = "active directory managed identity"; + private const string ActiveDirectoryMSI = "active directory msi"; private readonly string _typeName; private readonly IReadOnlyCollection _authenticationsWithAppSpecifiedProvider; diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs index d120c08634..082bec5c21 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs @@ -65,7 +65,7 @@ private enum CultureCheckState : uint // Transient Fault handling flag. This is needed to convey to the downstream mechanism of connection establishment, if Transient Fault handling should be used or not // The downstream handling of Connection open is the same for idle connection resiliency. Currently we want to apply transient fault handling only to the connections opened - // using SqlConnection.Open() method. + // using SqlConnection.Open() method. internal bool _applyTransientFaultHandling = false; // System column encryption key store providers are added by default @@ -135,21 +135,26 @@ public SqlConnection(string connectionString, SqlCredential credential) : this() { throw ADP.InvalidMixedArgumentOfSecureCredentialAndIntegratedSecurity(); } - - if (UsesActiveDirectoryIntegrated(connectionOptions)) + else if (UsesActiveDirectoryIntegrated(connectionOptions)) { throw SQL.SettingCredentialWithIntegratedArgument(); } - - if (UsesActiveDirectoryInteractive(connectionOptions)) + else if (UsesActiveDirectoryInteractive(connectionOptions)) { throw SQL.SettingCredentialWithInteractiveArgument(); } - - if (UsesActiveDirectoryDeviceCodeFlow(connectionOptions)) + else if (UsesActiveDirectoryDeviceCodeFlow(connectionOptions)) { throw SQL.SettingCredentialWithDeviceFlowArgument(); } + else if (UsesActiveDirectoryManagedIdentity(connectionOptions)) + { + throw SQL.SettingCredentialWithManagedIdentityArgument(DbConnectionStringBuilderUtil.ActiveDirectoryManagedIdentityString); + } + else if (UsesActiveDirectoryMSI(connectionOptions)) + { + throw SQL.SettingCredentialWithManagedIdentityArgument(DbConnectionStringBuilderUtil.ActiveDirectoryMSIString); + } Credential = credential; } @@ -313,7 +318,7 @@ internal SqlConnectionAttestationProtocol AttestationProtocol } } - // This method will be called once connection string is set or changed. + // This method will be called once connection string is set or changed. private void CacheConnectionStringProperties() { SqlConnectionString connString = ConnectionOptions as SqlConnectionString; @@ -321,7 +326,7 @@ private void CacheConnectionStringProperties() { _connectRetryCount = connString.ConnectRetryCount; // For Azure SQL connection, set _connectRetryCount to 2 instead of 1 will greatly improve recovery - // success rate + // success rate if (_connectRetryCount == 1 && ADP.IsAzureSqlServerEndpoint(connString.DataSource)) { _connectRetryCount = 2; @@ -397,28 +402,38 @@ internal bool AsyncCommandInProgress private bool UsesActiveDirectoryIntegrated(SqlConnectionString opt) { - return opt != null ? opt.Authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated : false; + return opt != null && opt.Authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated; } private bool UsesActiveDirectoryInteractive(SqlConnectionString opt) { - return opt != null ? opt.Authentication == SqlAuthenticationMethod.ActiveDirectoryInteractive : false; + return opt != null && opt.Authentication == SqlAuthenticationMethod.ActiveDirectoryInteractive; } private bool UsesActiveDirectoryDeviceCodeFlow(SqlConnectionString opt) { - return opt != null ? opt.Authentication == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow : false; + return opt != null && opt.Authentication == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow; + } + + private bool UsesActiveDirectoryManagedIdentity(SqlConnectionString opt) + { + return opt != null && opt.Authentication == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity; + } + + private bool UsesActiveDirectoryMSI(SqlConnectionString opt) + { + return opt != null && opt.Authentication == SqlAuthenticationMethod.ActiveDirectoryMSI; } private bool UsesAuthentication(SqlConnectionString opt) { - return opt != null ? opt.Authentication != SqlAuthenticationMethod.NotSpecified : false; + return opt != null && opt.Authentication != SqlAuthenticationMethod.NotSpecified; } // Does this connection use Integrated Security? private bool UsesIntegratedSecurity(SqlConnectionString opt) { - return opt != null ? opt.IntegratedSecurity : false; + return opt != null && opt.IntegratedSecurity; } // Does this connection use old style of clear userID or Password in connection string? @@ -466,7 +481,8 @@ public override string ConnectionString SqlConnectionString connectionOptions = new SqlConnectionString(value); if (_credential != null) { - // Check for Credential being used with Authentication=ActiveDirectoryIntegrated/ActiveDirectoryInteractive/ActiveDirectoryDeviceCodeFlow. Since a different error string is used + // Check for Credential being used with Authentication=ActiveDirectoryIntegrated | ActiveDirectoryInteractive | + // ActiveDirectoryDeviceCodeFlow | ActiveDirectoryManagedIdentity/ActiveDirectoryMSI. Since a different error string is used // for this case in ConnectionString setter vs in Credential setter, check for this error case before calling // CheckAndThrowOnInvalidCombinationOfConnectionStringAndSqlCredential, which is common to both setters. if (UsesActiveDirectoryIntegrated(connectionOptions)) @@ -481,6 +497,14 @@ public override string ConnectionString { throw SQL.SettingDeviceFlowWithCredential(); } + else if (UsesActiveDirectoryManagedIdentity(connectionOptions)) + { + throw SQL.SettingManagedIdentityWithCredential(DbConnectionStringBuilderUtil.ActiveDirectoryManagedIdentityString); + } + else if (UsesActiveDirectoryMSI(connectionOptions)) + { + throw SQL.SettingManagedIdentityWithCredential(DbConnectionStringBuilderUtil.ActiveDirectoryMSIString); + } CheckAndThrowOnInvalidCombinationOfConnectionStringAndSqlCredential(connectionOptions); } @@ -571,9 +595,9 @@ public override string Database } } - /// + /// /// To indicate the IsSupported flag sent by the server for DNS Caching. This property is for internal testing only. - /// + /// internal string SQLDNSCachingSupportedState { get @@ -594,9 +618,9 @@ internal string SQLDNSCachingSupportedState } } - /// + /// /// To indicate the IsSupported flag sent by the server for DNS Caching before redirection. This property is for internal testing only. - /// + /// internal string SQLDNSCachingSupportedStateBeforeRedirect { get @@ -763,23 +787,33 @@ public SqlCredential Credential // check if the usage of credential has any conflict with the keys used in connection string if (value != null) { - // Check for Credential being used with Authentication=ActiveDirectoryIntegrated/ActiveDirectoryInteractive. Since a different error string is used + var connectionOptions = (SqlConnectionString)ConnectionOptions; + // Check for Credential being used with Authentication=ActiveDirectoryIntegrated | ActiveDirectoryInteractive | + // ActiveDirectoryDeviceCodeFlow | ActiveDirectoryManagedIdentity/ActiveDirectoryMSI. Since a different error string is used // for this case in ConnectionString setter vs in Credential setter, check for this error case before calling // CheckAndThrowOnInvalidCombinationOfConnectionStringAndSqlCredential, which is common to both setters. - if (UsesActiveDirectoryIntegrated((SqlConnectionString)ConnectionOptions)) + if (UsesActiveDirectoryIntegrated(connectionOptions)) { throw SQL.SettingCredentialWithIntegratedInvalid(); } - else if (UsesActiveDirectoryInteractive((SqlConnectionString)ConnectionOptions)) + else if (UsesActiveDirectoryInteractive(connectionOptions)) { throw SQL.SettingCredentialWithInteractiveInvalid(); } - else if (UsesActiveDirectoryDeviceCodeFlow((SqlConnectionString)ConnectionOptions)) + else if (UsesActiveDirectoryDeviceCodeFlow(connectionOptions)) { throw SQL.SettingCredentialWithDeviceFlowInvalid(); } + else if (UsesActiveDirectoryManagedIdentity(connectionOptions)) + { + throw SQL.SettingCredentialWithManagedIdentityInvalid(DbConnectionStringBuilderUtil.ActiveDirectoryManagedIdentityString); + } + else if (UsesActiveDirectoryMSI(connectionOptions)) + { + throw SQL.SettingCredentialWithManagedIdentityInvalid(DbConnectionStringBuilderUtil.ActiveDirectoryMSIString); + } - CheckAndThrowOnInvalidCombinationOfConnectionStringAndSqlCredential((SqlConnectionString)ConnectionOptions); + CheckAndThrowOnInvalidCombinationOfConnectionStringAndSqlCredential(connectionOptions); if (_accessToken != null) { throw ADP.InvalidMixedUsageOfCredentialAndAccessToken(); @@ -913,7 +947,7 @@ override protected DbTransaction BeginDbTransaction(System.Data.IsolationLevel i { DbTransaction transaction = BeginTransaction(isolationLevel); - // InnerConnection doesn't maintain a ref on the outer connection (this) and + // InnerConnection doesn't maintain a ref on the outer connection (this) and // subsequently leaves open the possibility that the outer connection could be GC'ed before the SqlTransaction // is fully hooked up (leaving a DbTransaction with a null connection property). Ensure that this is reachable // until the completion of BeginTransaction with KeepAlive @@ -1001,7 +1035,7 @@ private void CloseInnerConnection() { // CloseConnection() now handles the lock - // The SqlInternalConnectionTds is set to OpenBusy during close, once this happens the cast below will fail and + // The SqlInternalConnectionTds is set to OpenBusy during close, once this happens the cast below will fail and // the command will no longer be cancelable. It might be desirable to be able to cancel the close operation, but this is // outside of the scope of Whidbey RTM. See (SqlCommand::Cancel) for other lock. _originalConnectionId = ClientConnectionId; @@ -1019,15 +1053,15 @@ public override void Close() Guid operationId = default(Guid); Guid clientConnectionId = default(Guid); - // during the call to Dispose() there is a redundant call to - // Close(). because of this, the second time Close() is invoked the - // connection is already in a closed state. this doesn't seem to be a + // during the call to Dispose() there is a redundant call to + // Close(). because of this, the second time Close() is invoked the + // connection is already in a closed state. this doesn't seem to be a // problem except for logging, as we'll get duplicate Before/After/Error // log entries if (previousState != ConnectionState.Closed) { operationId = s_diagnosticListener.WriteConnectionCloseBefore(this); - // we want to cache the ClientConnectionId for After/Error logging, as when the connection + // we want to cache the ClientConnectionId for After/Error logging, as when the connection // is closed then we will lose this identifier // // note: caching this is only for diagnostics logging purposes @@ -1051,7 +1085,7 @@ public override void Close() } AsyncHelper.WaitForCompletion(reconnectTask, 0, null, rethrowExceptions: false); // we do not need to deal with possible exceptions in reconnection if (State != ConnectionState.Open) - {// if we cancelled before the connection was opened + {// if we cancelled before the connection was opened OnStateChange(DbConnectionInternal.StateChangeClosed); } } @@ -1073,7 +1107,7 @@ public override void Close() { SqlStatistics.StopTimer(statistics); - // we only want to log this if the previous state of the + // we only want to log this if the previous state of the // connection is open, as that's the valid use-case if (previousState != ConnectionState.Closed) { @@ -1107,7 +1141,7 @@ private void DisposeMe(bool disposing) if (!disposing) { - // For non-pooled connections we need to make sure that if the SqlConnection was not closed, + // For non-pooled connections we need to make sure that if the SqlConnection was not closed, // then we release the GCHandle on the stateObject to allow it to be GCed // For pooled connections, we will rely on the pool reclaiming the connection var innerConnection = (InnerConnection as SqlInternalConnectionTds); @@ -1184,7 +1218,7 @@ internal void RegisterWaitingForReconnect(Task waitingTask) } Interlocked.CompareExchange(ref _asyncWaitingForReconnection, waitingTask, null); if (_asyncWaitingForReconnection != waitingTask) - { // somebody else managed to register + { // somebody else managed to register throw SQL.MARSUnsupportedOnConnection(); } } @@ -1277,7 +1311,7 @@ internal Task ValidateAndReconnect(Action beforeDisconnect, int timeout) { if (tdsConn.Parser._sessionPool.ActiveSessionsCount > 0) { - // >1 MARS session + // >1 MARS session if (beforeDisconnect != null) { beforeDisconnect(); diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionString.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionString.cs index 9a87f2feed..7e58eaf4a2 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionString.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionString.cs @@ -20,17 +20,18 @@ internal sealed partial class SqlConnectionString : DbConnectionOptions internal static partial class DEFAULT { + private const string _emptyString = ""; internal const ApplicationIntent ApplicationIntent = DbConnectionStringDefaults.ApplicationIntent; internal const string Application_Name = TdsEnums.SQL_PROVIDER_NAME; - internal const string AttachDBFilename = ""; + internal const string AttachDBFilename = _emptyString; internal const int Command_Timeout = ADP.DefaultCommandTimeout; internal const int Connect_Timeout = ADP.DefaultConnectionTimeout; - internal const string Current_Language = ""; - internal const string Data_Source = ""; + internal const string Current_Language = _emptyString; + internal const string Data_Source = _emptyString; internal const bool Encrypt = false; internal const bool Enlist = true; - internal const string FailoverPartner = ""; - internal const string Initial_Catalog = ""; + internal const string FailoverPartner = _emptyString; + internal const string Initial_Catalog = _emptyString; internal const bool Integrated_Security = false; internal const int Load_Balance_Timeout = 0; // default of 0 means don't use internal const bool MARS = false; @@ -38,19 +39,19 @@ internal static partial class DEFAULT internal const int Min_Pool_Size = 0; internal const bool MultiSubnetFailover = DbConnectionStringDefaults.MultiSubnetFailover; internal const int Packet_Size = 8000; - internal const string Password = ""; + internal const string Password = _emptyString; internal const bool Persist_Security_Info = false; internal const bool Pooling = true; internal const bool TrustServerCertificate = false; - internal const string Type_System_Version = ""; - internal const string User_ID = ""; + internal const string Type_System_Version = _emptyString; + internal const string User_ID = _emptyString; internal const bool User_Instance = false; internal const bool Replication = false; internal const int Connect_Retry_Count = 1; internal const int Connect_Retry_Interval = 10; internal static readonly SqlAuthenticationMethod Authentication = SqlAuthenticationMethod.NotSpecified; internal const SqlConnectionColumnEncryptionSetting ColumnEncryptionSetting = SqlConnectionColumnEncryptionSetting.Disabled; - internal const string EnclaveAttestationUrl = ""; + internal const string EnclaveAttestationUrl = _emptyString; internal static readonly SqlConnectionAttestationProtocol AttestationProtocol = SqlConnectionAttestationProtocol.NotSpecified; } @@ -227,7 +228,7 @@ internal static class TRANSACTIONBINDING private readonly string _attachDBFileName; private readonly string _currentLanguage; private readonly string _dataSource; - private readonly string _localDBInstance; // created based on datasource, set to NULL if datasource is not LocalDB + private readonly string _localDBInstance; // created based on datasource, set to NULL if datasource is not LocalDB private readonly string _failoverPartner; private readonly string _initialCatalog; private readonly string _password; @@ -300,8 +301,6 @@ internal SqlConnectionString(string connectionString) : base(connectionString, G _userID = ConvertValueToString(KEY.User_ID, DEFAULT.User_ID); _workstationId = ConvertValueToString(KEY.Workstation_Id, null); - - if (_loadBalanceTimeout < 0) { throw ADP.InvalidConnectionOptionValue(KEY.Load_Balance_Timeout); @@ -336,7 +335,6 @@ internal SqlConnectionString(string connectionString) : base(connectionString, G throw SQL.InvalidPacketSizeValue(); } - ValidateValueLength(_applicationName, TdsEnums.MAXLEN_APPNAME, KEY.Application_Name); ValidateValueLength(_currentLanguage, TdsEnums.MAXLEN_LANGUAGE, KEY.Current_Language); ValidateValueLength(_dataSource, TdsEnums.MAXLEN_SERVERNAME, KEY.Data_Source); @@ -477,6 +475,16 @@ internal SqlConnectionString(string connectionString) : base(connectionString, G { throw SQL.DeviceFlowWithUsernamePassword(); } + + if (Authentication == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity && HasPasswordKeyword) + { + throw SQL.ManagedIdentityWithPassword(DbConnectionStringBuilderUtil.ActiveDirectoryManagedIdentityString); + } + + if (Authentication == SqlAuthenticationMethod.ActiveDirectoryMSI && HasPasswordKeyword) + { + throw SQL.ManagedIdentityWithPassword(DbConnectionStringBuilderUtil.ActiveDirectoryMSIString); + } } // This c-tor is used to create SSE and user instance connection strings when user instance is set to true diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionStringBuilder.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionStringBuilder.cs index a2a327ea51..2c4d110254 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionStringBuilder.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionStringBuilder.cs @@ -29,7 +29,6 @@ private enum Keywords PersistSecurityInfo, UserID, Password, - Enlist, Pooling, MinPoolSize, @@ -37,10 +36,8 @@ private enum Keywords #if netcoreapp PoolBlockingPeriod, #endif - MultipleActiveResultSets, Replication, - ConnectTimeout, Encrypt, TrustServerCertificate, @@ -48,23 +45,15 @@ private enum Keywords PacketSize, TypeSystemVersion, Authentication, - ApplicationName, CurrentLanguage, WorkstationID, - UserInstance, - TransactionBinding, - ApplicationIntent, - MultiSubnetFailover, - ConnectRetryCount, - ConnectRetryInterval, - ColumnEncryptionSetting, EnclaveAttestationUrl, AttestationProtocol, @@ -991,7 +980,6 @@ private object GetAt(Keywords index) return EnclaveAttestationUrl; case Keywords.AttestationProtocol: return AttestationProtocol; - default: Debug.Fail("unexpected keyword"); throw UnsupportedKeyword(s_validKeywords[(int)index]); diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 04debd8d09..bc8c0655ff 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -253,21 +253,21 @@ internal bool IsDNSCachingBeforeRedirectSupported 10928, // SQL Error Code: 10929 - // Resource ID: %d. The %s minimum guarantee is %d, maximum limit is %d and the current usage for the database is %d. + // Resource ID: %d. The %s minimum guarantee is %d, maximum limit is %d and the current usage for the database is %d. // However, the server is currently too busy to support requests greater than %d for this database. 10929, // SQL Error Code: 40197 - // You will receive this error, when the service is down due to software or hardware upgrades, hardware failures, - // or any other failover problems. The error code (%d) embedded within the message of error 40197 provides - // additional information about the kind of failure or failover that occurred. Some examples of the error codes are + // You will receive this error, when the service is down due to software or hardware upgrades, hardware failures, + // or any other failover problems. The error code (%d) embedded within the message of error 40197 provides + // additional information about the kind of failure or failover that occurred. Some examples of the error codes are // embedded within the message of error 40197 are 40020, 40143, 40166, and 40540. 40197, // The service is currently busy. Retry the request after 10 seconds. Incident ID: %ls. Code: %d. 40501, - // Database '%.*ls' on server '%.*ls' is not currently available. Please retry the connection later. + // Database '%.*ls' on server '%.*ls' is not currently available. Please retry the connection later. // If the problem persists, contact customer support, and provide them the session tracing ID of '%.*ls'. 40613 }; @@ -378,7 +378,7 @@ internal void Wait(bool canReleaseFromAnyThread, int timeout, ref bool lockTaken internal void Release() { if (_semaphore.CurrentCount == 0) - { // semaphore methods were used for locking + { // semaphore methods were used for locking _semaphore.Release(); } else @@ -396,7 +396,7 @@ internal bool CanBeReleasedFromAnyThread } } - // Necessary but not sufficient condition for thread to have lock (since semaphore may be obtained by any thread) + // Necessary but not sufficient condition for thread to have lock (since semaphore may be obtained by any thread) internal bool ThreadMayHaveLock() { return Monitor.IsEntered(_semaphore) || _semaphore.CurrentCount == 0; @@ -798,15 +798,15 @@ internal override void ValidateConnectionForExecute(SqlCommand command) /// /// /// This method must be called while holding a lock on the SqlInternalConnection instance, - /// to ensure we don't accidentally execute after the transaction has completed on a different thread, + /// to ensure we don't accidentally execute after the transaction has completed on a different thread, /// causing us to unwittingly execute in auto-commit mode. /// - /// + /// /// - /// When using Explicit transaction unbinding, + /// When using Explicit transaction unbinding, /// verify that the enlisted transaction is active and equal to the current ambient transaction. /// - /// + /// /// /// When using Implicit transaction unbinding, /// verify that the enlisted transaction is active. @@ -895,7 +895,7 @@ protected override void InternalDeactivate() DoomThisConnection(); } - // If we're deactivating with a delegated transaction, we + // If we're deactivating with a delegated transaction, we // should not be cleaning up the parser just yet, that will // cause our transaction to be rolled back and the connection // to be reset. We'll get called again once the delegated @@ -1095,17 +1095,17 @@ bool isDelegateControlRequest // SQLBUDT #20010853 - Promote, Commit and Rollback requests for // delegated transactions often happen while there is an open result - // set, so we need to handle them by using a different MARS session, + // set, so we need to handle them by using a different MARS session, // otherwise we'll write on the physical state objects while someone - // else is using it. When we don't have MARS enabled, we need to - // lock the physical state object to synchronize it's use at least - // until we increment the open results count. Once it's been + // else is using it. When we don't have MARS enabled, we need to + // lock the physical state object to synchronize it's use at least + // until we increment the open results count. Once it's been // incremented the delegated transaction requests will fail, so they // won't stomp on anything. - // + // // We need to keep this lock through the duration of the TM request // so that we won't hijack a different request's data stream and a - // different request won't hijack ours, so we have a lock here on + // different request won't hijack ours, so we have a lock here on // an object that the ExecTMReq will also lock, but since we're on // the same thread, the lock is a no-op. @@ -1272,7 +1272,7 @@ private void Login(ServerInfo server, TimeoutTimer timeout, string newPassword, } // VSTS#795621 - Ensure ServerName is Sent During TdsLogin To Enable Sql Azure Connectivity. - // Using server.UserServerName (versus ConnectionOptions.DataSource) since TdsLogin requires + // Using server.UserServerName (versus ConnectionOptions.DataSource) since TdsLogin requires // serverName to always be non-null. login.serverName = server.UserServerName; @@ -1295,13 +1295,15 @@ private void Login(ServerInfo server, TimeoutTimer timeout, string newPassword, _sessionRecoveryRequested = true; } - // If the workflow being used is Active Directory Password/Integrated/Interactive/Service Principal and server's prelogin response + // If the workflow being used is Active Directory Authentication and server's prelogin response // for FEDAUTHREQUIRED option indicates Federated Authentication is required, we have to insert FedAuth Feature Extension // in Login7, indicating the intent to use Active Directory Authentication for SQL Server. if (ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryPassword || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryInteractive || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryServicePrincipal + || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity + || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryMSI // Since AD Integrated may be acting like Windows integrated, additionally check _fedAuthRequired || (ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated && _fedAuthRequired)) { @@ -1491,7 +1493,7 @@ private bool IsDoNotRetryConnectError(SqlException exc) if (connectionOptions.MultiSubnetFailover) { attemptNumber++; - // Set timeout for this attempt, but don't exceed original timer + // Set timeout for this attempt, but don't exceed original timer long nextTimeoutInterval = checked(timeoutUnitInterval * attemptNumber); long milliseconds = timeout.MillisecondsRemaining; if (nextTimeoutInterval > milliseconds) @@ -1514,7 +1516,7 @@ private bool IsDoNotRetryConnectError(SqlException exc) AttemptOneLogin(serverInfo, newPassword, newSecurePassword, - !connectionOptions.MultiSubnetFailover, // ignore timeout for SniOpen call unless MSF + !connectionOptions.MultiSubnetFailover, // ignore timeout for SniOpen call unless MSF connectionOptions.MultiSubnetFailover ? intervalTimer : timeout); if (connectionOptions.MultiSubnetFailover && null != ServerProvidedFailOverPartner) @@ -1609,7 +1611,7 @@ private bool IsDoNotRetryConnectError(SqlException exc) return; // LoginWithFailover successfully connected and handled entire connection setup } - // Sleep for a bit to prevent clogging the network with requests, + // Sleep for a bit to prevent clogging the network with requests, // then update sleep interval for next iteration (max 1 second interval) SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, sleeping {1}[milisec]", ObjectID, sleepInterval); Thread.Sleep(sleepInterval); @@ -1620,7 +1622,7 @@ private bool IsDoNotRetryConnectError(SqlException exc) if (null != PoolGroupProviderInfo) { // We must wait for CompleteLogin to finish for to have the - // env change from the server to know its designated failover + // env change from the server to know its designated failover // partner; save this information in _currentFailoverPartner. PoolGroupProviderInfo.FailoverCheck(this, false, connectionOptions, ServerProvidedFailOverPartner); } @@ -1706,7 +1708,7 @@ TimeoutTimer timeout // 2) Parser threw exception while main timer was expired // 3) Parser threw logon failure-related exception (LOGON_FAILED, PASSWORD_EXPIRED, etc) // - // Of these methods, only #1 exits normally. This preserves the call stack on the exception + // Of these methods, only #1 exits normally. This preserves the call stack on the exception // back into the parser for the error cases. while (true) { @@ -1762,7 +1764,7 @@ TimeoutTimer timeout { // We are in login with failover scenation and server sent routing information // If it is read-only routing - we did not supply AppIntent=RO (it should be checked before) - // If it is something else, not known yet (future server) - this client is not designed to support this. + // If it is something else, not known yet (future server) - this client is not designed to support this. // In any case, server should not have sent the routing info. SqlClientEventSource.Log.TryTraceEvent(" Routed to {0}", RoutingInfo.ServerName); throw SQL.ROR_UnexpectedRoutingInfo(this); @@ -2108,6 +2110,8 @@ internal void OnFedAuthInfo(SqlFedAuthInfo fedAuthInfo) || _credential != null || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryInteractive || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow + || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity + || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryMSI || (ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated && _fedAuthRequired), "Credentials aren't provided for calling MSAL"); Debug.Assert(fedAuthInfo != null, "info should not be null."); @@ -2348,6 +2352,8 @@ internal SqlFedAuthToken GetFedAuthToken(SqlFedAuthInfo fedAuthInfo) break; case SqlAuthenticationMethod.ActiveDirectoryInteractive: case SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow: + case SqlAuthenticationMethod.ActiveDirectoryManagedIdentity: + case SqlAuthenticationMethod.ActiveDirectoryMSI: if (_activeDirectoryAuthTimeoutRetryHelper.State == ActiveDirectoryAuthenticationTimeoutRetryState.Retrying) { _fedAuthToken = _activeDirectoryAuthTimeoutRetryHelper.CachedToken; @@ -2694,7 +2700,7 @@ internal void OnFeatureExtAck(int featureId, byte[] data) } // need to add more steps for phase 2 - // get IPv4 + IPv6 + Port number + // get IPv4 + IPv6 + Port number // not put them in the DNS cache at this point but need to store them somewhere // generate pendingSQLDNSObject and turn on IsSQLDNSRetryEnabled flag @@ -2703,7 +2709,7 @@ internal void OnFeatureExtAck(int featureId, byte[] data) default: { - // Unknown feature ack + // Unknown feature ack throw SQL.ParsingError(); } } diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlUtil.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlUtil.cs index 744ab20402..606f20deba 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlUtil.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlUtil.cs @@ -281,6 +281,10 @@ internal static Exception DeviceFlowWithUsernamePassword() { return ADP.Argument(System.StringsHelper.GetString(Strings.SQL_DeviceFlowWithUsernamePassword)); } + internal static Exception ManagedIdentityWithPassword(string authenticationMode) + { + return ADP.Argument(System.StringsHelper.GetString(Strings.SQL_ManagedIdentityWithPassword, authenticationMode)); + } static internal Exception SettingIntegratedWithCredential() { return ADP.InvalidOperation(System.StringsHelper.GetString(Strings.SQL_SettingIntegratedWithCredential)); @@ -293,6 +297,10 @@ static internal Exception SettingDeviceFlowWithCredential() { return ADP.InvalidOperation(System.StringsHelper.GetString(Strings.SQL_SettingDeviceFlowWithCredential)); } + static internal Exception SettingManagedIdentityWithCredential(string authenticationMode) + { + return ADP.InvalidOperation(System.StringsHelper.GetString(Strings.SQL_SettingManagedIdentityWithCredential, authenticationMode)); + } static internal Exception SettingCredentialWithIntegratedArgument() { return ADP.Argument(System.StringsHelper.GetString(Strings.SQL_SettingCredentialWithIntegrated)); @@ -305,6 +313,10 @@ static internal Exception SettingCredentialWithDeviceFlowArgument() { return ADP.Argument(System.StringsHelper.GetString(Strings.SQL_SettingCredentialWithDeviceFlow)); } + static internal Exception SettingCredentialWithManagedIdentityArgument(string authenticationMode) + { + return ADP.Argument(System.StringsHelper.GetString(Strings.SQL_SettingCredentialWithManagedIdentity, authenticationMode)); + } static internal Exception SettingCredentialWithIntegratedInvalid() { return ADP.InvalidOperation(System.StringsHelper.GetString(Strings.SQL_SettingCredentialWithIntegrated)); @@ -317,6 +329,10 @@ static internal Exception SettingCredentialWithDeviceFlowInvalid() { return ADP.InvalidOperation(System.StringsHelper.GetString(Strings.SQL_SettingCredentialWithDeviceFlow)); } + static internal Exception SettingCredentialWithManagedIdentityInvalid(string authenticationMode) + { + return ADP.InvalidOperation(System.StringsHelper.GetString(Strings.SQL_SettingCredentialWithManagedIdentity, authenticationMode)); + } internal static Exception NullEmptyTransactionName() { return ADP.Argument(System.StringsHelper.GetString(Strings.SQL_NullEmptyTransactionName)); @@ -1213,6 +1229,16 @@ internal static Exception BatchedUpdatesNotAvailableOnContextConnection() { return ADP.InvalidOperation(System.StringsHelper.GetString(Strings.SQL_BatchedUpdatesNotAvailableOnContextConnection)); } + internal static Exception Azure_ManagedIdentityException(string msg) + { + SqlErrorCollection errors = new SqlErrorCollection + { + new SqlError(0, (byte)0x00, TdsEnums.FATAL_ERROR_CLASS, null, msg, "", 0) + }; + SqlException exc = SqlException.CreateException(errors, null); + exc._doNotReconnect = true; // disable open retry logic on this error + return exc; + } #region Always Encrypted Errors diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsEnums.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsEnums.cs index 7325755d26..416ec86fd0 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsEnums.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsEnums.cs @@ -242,7 +242,7 @@ public enum FedAuthLibrary : byte { LiveId = FEDAUTHLIB_LIVEID, SecurityToken = FEDAUTHLIB_SECURITYTOKEN, - MSAL = FEDAUTHLIB_MSAL, // For later support + MSAL = FEDAUTHLIB_MSAL, Default = FEDAUTHLIB_RESERVED } @@ -251,6 +251,7 @@ public enum FedAuthLibrary : byte public const byte MSALWORKFLOW_ACTIVEDIRECTORYINTERACTIVE = 0x03; public const byte MSALWORKFLOW_ACTIVEDIRECTORYSERVICEPRINCIPAL = 0x01; // Using the Password byte as that is the closest we have public const byte MSALWORKFLOW_ACTIVEDIRECTORYDEVICECODEFLOW = 0x03; // Using the Interactive byte as that is the closest we have + public const byte MSALWORKFLOW_ACTIVEDIRECTORYMANAGEDIDENTITY = 0x03; // Using the Interactive byte as that's supported for Identity based authentication public enum ActiveDirectoryWorkflow : byte { @@ -259,6 +260,7 @@ public enum ActiveDirectoryWorkflow : byte Interactive = MSALWORKFLOW_ACTIVEDIRECTORYINTERACTIVE, ServicePrincipal = MSALWORKFLOW_ACTIVEDIRECTORYSERVICEPRINCIPAL, DeviceCodeFlow = MSALWORKFLOW_ACTIVEDIRECTORYDEVICECODEFLOW, + ManagedIdentity = MSALWORKFLOW_ACTIVEDIRECTORYMANAGEDIDENTITY, } // The string used for username in the error message when Authentication = Active Directory Integrated with FedAuth is used, if authentication fails. @@ -1135,6 +1137,12 @@ public enum SqlAuthenticationMethod /// ActiveDirectoryDeviceCodeFlow, + + /// + ActiveDirectoryManagedIdentity, + + /// + ActiveDirectoryMSI } // This enum indicates the state of TransparentNetworkIPResolution // The first attempt when TNIR is on should be sequential. If the first attempt failes next attempts should be parallel. diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs index b0b25d2e09..70cecd2e3f 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -397,6 +397,12 @@ internal void ProcessPendingAck(TdsParserStateObject stateObj) case SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow: SqlClientEventSource.Log.TryTraceEvent(" Active Directory Device Code Flow authentication"); break; + case SqlAuthenticationMethod.ActiveDirectoryManagedIdentity: + SqlClientEventSource.Log.TryTraceEvent(" Active Directory Managed Identity authentication"); + break; + case SqlAuthenticationMethod.ActiveDirectoryMSI: + SqlClientEventSource.Log.TryTraceEvent(" Active Directory MSI authentication"); + break; case SqlAuthenticationMethod.SqlPassword: SqlClientEventSource.Log.TryTraceEvent(" SQL Password authentication"); break; @@ -2907,7 +2913,7 @@ private bool TryProcessDone(SqlCommand cmd, SqlDataReader reader, ref RunBehavio int count; // This is added back since removing it from here introduces regressions in Managed SNI. - // It forces SqlDataReader.ReadAsync() method to run synchronously, + // It forces SqlDataReader.ReadAsync() method to run synchronously, // and will block the calling thread until data is fed from SQL Server. // TODO Investigate better solution to support non-blocking ReadAsync(). stateObj._syncOverAsync = true; @@ -7805,6 +7811,10 @@ internal int WriteSessionRecoveryFeatureRequest(SessionData reconnectData, bool case SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow: workflow = TdsEnums.MSALWORKFLOW_ACTIVEDIRECTORYDEVICECODEFLOW; break; + case SqlAuthenticationMethod.ActiveDirectoryManagedIdentity: + case SqlAuthenticationMethod.ActiveDirectoryMSI: + workflow = TdsEnums.MSALWORKFLOW_ACTIVEDIRECTORYMANAGEDIDENTITY; + break; default: Debug.Assert(false, "Unrecognized Authentication type for fedauth MSAL request"); break; diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Resources/Strings.Designer.cs b/src/Microsoft.Data.SqlClient/netcore/src/Resources/Strings.Designer.cs index 438423fa71..aec209d3fd 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Resources/Strings.Designer.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Resources/Strings.Designer.cs @@ -897,6 +897,60 @@ internal class Strings { } } + /// + /// Looks up a localized string similar to Access token could not be acquired.. + /// + internal static string Azure_GenericErrorMessage { + get { + return ResourceManager.GetString("Azure_GenericErrorMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unable to connect to the Managed Identity endpoint. Please check that you are running on an Azure resource that has Identity setup.. + /// + internal static string Azure_IdentityEndpointNotListening { + get { + return ResourceManager.GetString("Azure_IdentityEndpointNotListening", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Tried to get token using Managed Identity.. + /// + internal static string Azure_ManagedIdentityUsed { + get { + return ResourceManager.GetString("Azure_ManagedIdentityUsed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unable to connect to the Instance Metadata Service (IMDS). Skipping request to the Managed Identity token endpoint.. + /// + internal static string Azure_MetadataEndpointNotListening { + get { + return ResourceManager.GetString("Azure_MetadataEndpointNotListening", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Received a non-retryable error.. + /// + internal static string Azure_NonRetryableError { + get { + return ResourceManager.GetString("Azure_NonRetryableError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed after 5 retries.. + /// + internal static string Azure_RetryFailure { + get { + return ResourceManager.GetString("Azure_RetryFailure", resourceCulture); + } + } + /// /// Looks up a localized string similar to .database.chinacloudapi.cn. /// @@ -2796,6 +2850,15 @@ internal class Strings { } } + /// + /// Looks up a localized string similar to Cannot use 'Authentication={0}' with 'Password' or 'PWD' connection string keywords.. + /// + internal static string SQL_ManagedIdentityWithPassword { + get { + return ResourceManager.GetString("SQL_ManagedIdentityWithPassword", resourceCulture); + } + } + /// /// Looks up a localized string similar to The connection does not support MultipleActiveResultSets.. /// @@ -3075,6 +3138,15 @@ internal class Strings { } } + /// + /// Looks up a localized string similar to Cannot set the Credential property if 'Authentication={0}' has been specified in the connection string.. + /// + internal static string SQL_SettingCredentialWithManagedIdentity { + get { + return ResourceManager.GetString("SQL_SettingCredentialWithManagedIdentity", resourceCulture); + } + } + /// /// Looks up a localized string similar to Cannot use 'Authentication=Active Directory Device Code Flow', if the Credential property has been set.. /// @@ -3102,6 +3174,15 @@ internal class Strings { } } + /// + /// Looks up a localized string similar to Cannot use 'Authentication={0}', if the Credential property has been set.. + /// + internal static string SQL_SettingManagedIdentityWithCredential { + get { + return ResourceManager.GetString("SQL_SettingManagedIdentityWithCredential", resourceCulture); + } + } + /// /// Looks up a localized string similar to A severe error occurred on the current command. The results, if any, should be discarded.. /// diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Resources/Strings.resx b/src/Microsoft.Data.SqlClient/netcore/src/Resources/Strings.resx index b94391deac..2b01c549e7 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Resources/Strings.resx +++ b/src/Microsoft.Data.SqlClient/netcore/src/Resources/Strings.resx @@ -411,6 +411,9 @@ Cannot use 'Authentication=Active Directory Device Code Flow' with 'User ID', 'UID', 'Password' or 'PWD' connection string keywords. + + Cannot use 'Authentication={0}' with 'Password' or 'PWD' connection string keywords. + The instance of SQL Server you attempted to connect to requires encryption but this machine does not support it. @@ -1902,7 +1905,31 @@ Cannot set the Credential property if 'Authentication=Active Directory Device Code Flow' has been specified in the connection string. + + Cannot set the Credential property if 'Authentication={0}' has been specified in the connection string. + Cannot use 'Authentication=Active Directory Device Code Flow', if the Credential property has been set. - + + Cannot use 'Authentication={0}', if the Credential property has been set. + + + Access token could not be acquired. + + + Unable to connect to the Managed Identity endpoint. Please check that you are running on an Azure resource that has Identity setup. + + + Tried to get token using Managed Identity. + + + Unable to connect to the Instance Metadata Service (IMDS). Skipping request to the Managed Identity token endpoint. + + + Received a non-retryable error. + + + Failed after 5 retries. + + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs index 8fb8e1fed5..71c267a63d 100644 --- a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs +++ b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs @@ -111,6 +111,10 @@ public enum SqlAuthenticationMethod ActiveDirectoryServicePrincipal = 5, /// ActiveDirectoryDeviceCodeFlow = 6, + /// + ActiveDirectoryManagedIdentity = 7, + /// + ActiveDirectoryMSI = 8, /// NotSpecified = 0, /// @@ -959,7 +963,6 @@ public sealed partial class SqlConnectionStringBuilder : System.Data.Common.DbCo [System.ComponentModel.DisplayNameAttribute("Attestation Protocol")] [System.ComponentModel.RefreshPropertiesAttribute(System.ComponentModel.RefreshProperties.All)] public Microsoft.Data.SqlClient.SqlConnectionAttestationProtocol AttestationProtocol { get { throw null; } set { } } - /// [System.ComponentModel.DisplayNameAttribute("Encrypt")] [System.ComponentModel.RefreshPropertiesAttribute(System.ComponentModel.RefreshProperties.All)] diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj index a63ed87bce..e3db948526 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -124,6 +124,9 @@ Microsoft\Data\SqlClient\ActiveDirectoryAuthenticationTimeoutRetryHelper.cs + + Microsoft\Data\SqlClient\AzureManagedIdentityAuthenticationProvider.cs + Microsoft\Data\SqlClient\ApplicationIntent.cs @@ -503,4 +506,4 @@ - \ No newline at end of file + diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Common/DbConnectionStringCommon.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Common/DbConnectionStringCommon.cs index b3ba200bdd..71ab0deea3 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Common/DbConnectionStringCommon.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Common/DbConnectionStringCommon.cs @@ -317,7 +317,7 @@ internal static string PoolBlockingPeriodToString(PoolBlockingPeriod value) /// * if the value is from type PoolBlockingPeriod, it will be used as is /// * if the value is from integral type (SByte, Int16, Int32, Int64, Byte, UInt16, UInt32, or UInt64), it will be converted to enum /// * if the value is another enum or any other type, it will be blocked with an appropriate ArgumentException - /// + /// /// in any case above, if the converted value is out of valid range, the method raises ArgumentOutOfRangeException. /// /// PoolBlockingPeriod value in the valid range @@ -373,7 +373,7 @@ internal static PoolBlockingPeriod ConvertToPoolBlockingPeriod(string keyword, o } catch (ArgumentException e) { - // to be consistent with the messages we send in case of wrong type usage, replace + // to be consistent with the messages we send in case of wrong type usage, replace // the error with our exception, and keep the original one as inner one for troubleshooting throw ADP.ConvertFailed(value.GetType(), typeof(PoolBlockingPeriod), e); } @@ -442,7 +442,7 @@ internal static string ApplicationIntentToString(ApplicationIntent value) /// * if the value is from type ApplicationIntent, it will be used as is /// * if the value is from integral type (SByte, Int16, Int32, Int64, Byte, UInt16, UInt32, or UInt64), it will be converted to enum /// * if the value is another enum or any other type, it will be blocked with an appropriate ArgumentException - /// + /// /// in any case above, if the converted value is out of valid range, the method raises ArgumentOutOfRangeException. /// /// application intent value in the valid range @@ -498,7 +498,7 @@ internal static ApplicationIntent ConvertToApplicationIntent(string keyword, obj } catch (ArgumentException e) { - // to be consistent with the messages we send in case of wrong type usage, replace + // to be consistent with the messages we send in case of wrong type usage, replace // the error with our exception, and keep the original one as inner one for troubleshooting throw ADP.ConvertFailed(value.GetType(), typeof(ApplicationIntent), e); } @@ -522,11 +522,13 @@ internal static ApplicationIntent ConvertToApplicationIntent(string keyword, obj const string ActiveDirectoryInteractiveString = "Active Directory Interactive"; const string ActiveDirectoryServicePrincipalString = "Active Directory Service Principal"; const string ActiveDirectoryDeviceCodeFlowString = "Active Directory Device Code Flow"; + internal const string ActiveDirectoryManagedIdentityString = "Active Directory Managed Identity"; + internal const string ActiveDirectoryMSIString = "Active Directory MSI"; const string SqlCertificateString = "Sql Certificate"; internal static bool TryConvertToAuthenticationType(string value, out SqlAuthenticationMethod result) { - Debug.Assert(Enum.GetNames(typeof(SqlAuthenticationMethod)).Length == 7, "SqlAuthenticationMethod enum has changed, update needed"); + Debug.Assert(Enum.GetNames(typeof(SqlAuthenticationMethod)).Length == 9, "SqlAuthenticationMethod enum has changed, update needed"); bool isSuccess = false; @@ -566,7 +568,19 @@ internal static bool TryConvertToAuthenticationType(string value, out SqlAuthent result = SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow; isSuccess = true; } -#if ADONET_CERT_AUTH + else if (StringComparer.InvariantCultureIgnoreCase.Equals(value, ActiveDirectoryManagedIdentityString) + || StringComparer.InvariantCultureIgnoreCase.Equals(value, Convert.ToString(SqlAuthenticationMethod.ActiveDirectoryManagedIdentity, CultureInfo.InvariantCulture))) + { + result = SqlAuthenticationMethod.ActiveDirectoryManagedIdentity; + isSuccess = true; + } + else if (StringComparer.InvariantCultureIgnoreCase.Equals(value, ActiveDirectoryMSIString) + || StringComparer.InvariantCultureIgnoreCase.Equals(value, Convert.ToString(SqlAuthenticationMethod.ActiveDirectoryMSI, CultureInfo.InvariantCulture))) + { + result = SqlAuthenticationMethod.ActiveDirectoryMSI; + isSuccess = true; + } +#if ADONET_CERT_AUTH else if (StringComparer.InvariantCultureIgnoreCase.Equals(value, SqlCertificateString) || StringComparer.InvariantCultureIgnoreCase.Equals(value, Convert.ToString(SqlAuthenticationMethod.SqlCertificate, CultureInfo.InvariantCulture))) { result = SqlAuthenticationMethod.SqlCertificate; @@ -648,16 +662,18 @@ internal static string ColumnEncryptionSettingToString(SqlConnectionColumnEncryp internal static bool IsValidAuthenticationTypeValue(SqlAuthenticationMethod value) { - Debug.Assert(Enum.GetNames(typeof(SqlAuthenticationMethod)).Length == 7, "SqlAuthenticationMethod enum has changed, update needed"); + Debug.Assert(Enum.GetNames(typeof(SqlAuthenticationMethod)).Length == 9, "SqlAuthenticationMethod enum has changed, update needed"); return value == SqlAuthenticationMethod.SqlPassword || value == SqlAuthenticationMethod.ActiveDirectoryPassword || value == SqlAuthenticationMethod.ActiveDirectoryIntegrated || value == SqlAuthenticationMethod.ActiveDirectoryInteractive || value == SqlAuthenticationMethod.ActiveDirectoryServicePrincipal || value == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow -#if ADONET_CERT_AUTH + || value == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity + || value == SqlAuthenticationMethod.ActiveDirectoryMSI +#if ADONET_CERT_AUTH || value == SqlAuthenticationMethod.SqlCertificate -#endif +#endif || value == SqlAuthenticationMethod.NotSpecified; } @@ -679,11 +695,14 @@ internal static string AuthenticationTypeToString(SqlAuthenticationMethod value) return ActiveDirectoryServicePrincipalString; case SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow: return ActiveDirectoryDeviceCodeFlowString; + case SqlAuthenticationMethod.ActiveDirectoryManagedIdentity: + return ActiveDirectoryManagedIdentityString; + case SqlAuthenticationMethod.ActiveDirectoryMSI: + return ActiveDirectoryMSIString; #if ADONET_CERT_AUTH case SqlAuthenticationMethod.SqlCertificate: return SqlCertificateString; #endif - default: return null; } @@ -741,7 +760,7 @@ internal static SqlAuthenticationMethod ConvertToAuthenticationType(string keywo } catch (ArgumentException e) { - // to be consistent with the messages we send in case of wrong type usage, replace + // to be consistent with the messages we send in case of wrong type usage, replace // the error with our exception, and keep the original one as inner one for troubleshooting throw ADP.ConvertFailed(value.GetType(), typeof(SqlAuthenticationMethod), e); } @@ -817,7 +836,7 @@ internal static SqlConnectionColumnEncryptionSetting ConvertToColumnEncryptionSe } catch (ArgumentException e) { - // to be consistent with the messages we send in case of wrong type usage, replace + // to be consistent with the messages we send in case of wrong type usage, replace // the error with our exception, and keep the original one as inner one for troubleshooting throw ADP.ConvertFailed(value.GetType(), typeof(SqlConnectionColumnEncryptionSetting), e); } @@ -960,7 +979,7 @@ internal static SqlConnectionAttestationProtocol ConvertToAttestationProtocol(st } catch (ArgumentException e) { - // to be consistent with the messages we send in case of wrong type usage, replace + // to be consistent with the messages we send in case of wrong type usage, replace // the error with our exception, and keep the original one as inner one for troubleshooting throw ADP.ConvertFailed(value.GetType(), typeof(SqlConnectionAttestationProtocol), e); } @@ -991,16 +1010,16 @@ internal static class DbConnectionStringDefaults { // all // internal const string NamedConnection = ""; - + private const string _emptyString = ""; // Odbc - internal const string Driver = ""; - internal const string Dsn = ""; + internal const string Driver = _emptyString; + internal const string Dsn = _emptyString; // OleDb internal const bool AdoNetPooler = false; - internal const string FileName = ""; + internal const string FileName = _emptyString; internal const int OleDbServices = ~(/*DBPROPVAL_OS_AGR_AFTERSESSION*/0x00000008 | /*DBPROPVAL_OS_CLIENTCURSOR*/0x00000004); // -13 - internal const string Provider = ""; + internal const string Provider = _emptyString; // OracleClient internal const bool Unicode = false; @@ -1010,17 +1029,17 @@ internal static class DbConnectionStringDefaults internal const ApplicationIntent ApplicationIntent = Microsoft.Data.SqlClient.ApplicationIntent.ReadWrite; internal const string ApplicationName = "Framework Microsoft SqlClient Data Provider"; internal const bool AsynchronousProcessing = false; - internal const string AttachDBFilename = ""; + internal const string AttachDBFilename = _emptyString; internal const int CommandTimeout = 30; internal const int ConnectTimeout = 15; internal const bool ConnectionReset = true; internal const bool ContextConnection = false; - internal const string CurrentLanguage = ""; - internal const string DataSource = ""; + internal const string CurrentLanguage = _emptyString; + internal const string DataSource = _emptyString; internal const bool Encrypt = false; internal const bool Enlist = true; - internal const string FailoverPartner = ""; - internal const string InitialCatalog = ""; + internal const string FailoverPartner = _emptyString; + internal const string InitialCatalog = _emptyString; internal const bool IntegratedSecurity = false; internal const int LoadBalanceTimeout = 0; // default of 0 means don't use internal const bool MultipleActiveResultSets = false; @@ -1028,25 +1047,25 @@ internal static class DbConnectionStringDefaults internal static readonly bool TransparentNetworkIPResolution = LocalAppContextSwitches.DisableTNIRByDefault ? false : true; internal const int MaxPoolSize = 100; internal const int MinPoolSize = 0; - internal const string NetworkLibrary = ""; + internal const string NetworkLibrary = _emptyString; internal const int PacketSize = 8000; - internal const string Password = ""; + internal const string Password = _emptyString; internal const bool PersistSecurityInfo = false; internal const bool Pooling = true; internal const bool TrustServerCertificate = false; internal const string TypeSystemVersion = "Latest"; - internal const string UserID = ""; + internal const string UserID = _emptyString; internal const bool UserInstance = false; internal const bool Replication = false; - internal const string WorkstationID = ""; + internal const string WorkstationID = _emptyString; internal const string TransactionBinding = "Implicit Unbind"; internal const int ConnectRetryCount = 1; internal const int ConnectRetryInterval = 10; internal static readonly SqlAuthenticationMethod Authentication = SqlAuthenticationMethod.NotSpecified; internal static readonly SqlConnectionColumnEncryptionSetting ColumnEncryptionSetting = SqlConnectionColumnEncryptionSetting.Disabled; - internal const string EnclaveAttestationUrl = ""; + internal const string EnclaveAttestationUrl = _emptyString; internal const SqlConnectionAttestationProtocol AttestationProtocol = SqlConnectionAttestationProtocol.NotSpecified; - internal const string Certificate = ""; + internal const string Certificate = _emptyString; internal const PoolBlockingPeriod PoolBlockingPeriod = SqlClient.PoolBlockingPeriod.Auto; } @@ -1186,14 +1205,14 @@ internal static class DbConnectionStringSynonyms //internal const string MultiSubnetFailover = MULTISUBNETFAILOVER; internal const string MULTISUBNETFAILOVER = "multisubnetfailover"; - + //internal const string NetworkLibrary = NET+","+NETWORK; internal const string NET = "net"; internal const string NETWORK = "network"; //internal const string PoolBlockingPeriod = POOLBLOCKINGPERIOD; internal const string POOLBLOCKINGPERIOD = "poolblockingperiod"; - + internal const string WorkaroundOracleBug914652 = "Workaround Oracle Bug 914652"; //internal const string Password = Pwd; diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs index b875c0160d..0280954005 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs @@ -20,9 +20,12 @@ internal class SqlAuthenticationProviderManager private const string ActiveDirectoryInteractive = "active directory interactive"; private const string ActiveDirectoryServicePrincipal = "active directory service principal"; private const string ActiveDirectoryDeviceCodeFlow = "active directory device code flow"; + private const string ActiveDirectoryManagedIdentity = "active directory managed identity"; + private const string ActiveDirectoryMSI = "active directory msi"; static SqlAuthenticationProviderManager() { + var azureManagedIdentityAuthenticationProvider = new AzureManagedIdentityAuthenticationProvider(); SqlAuthenticationProviderConfigurationSection configurationSection = null; try { @@ -47,6 +50,8 @@ static SqlAuthenticationProviderManager() Instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryInteractive, activeDirectoryAuthProvider); Instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryServicePrincipal, activeDirectoryAuthProvider); Instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, activeDirectoryAuthProvider); + Instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryManagedIdentity, azureManagedIdentityAuthenticationProvider); + Instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryMSI, azureManagedIdentityAuthenticationProvider); } public static readonly SqlAuthenticationProviderManager Instance; @@ -213,6 +218,10 @@ private static SqlAuthenticationMethod AuthenticationEnumFromString(string authe return SqlAuthenticationMethod.ActiveDirectoryServicePrincipal; case ActiveDirectoryDeviceCodeFlow: return SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow; + case ActiveDirectoryManagedIdentity: + return SqlAuthenticationMethod.ActiveDirectoryManagedIdentity; + case ActiveDirectoryMSI: + return SqlAuthenticationMethod.ActiveDirectoryMSI; default: throw SQL.UnsupportedAuthentication(authentication); } diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnection.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnection.cs index 6682ccbb90..c48a540746 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnection.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnection.cs @@ -283,7 +283,7 @@ static internal List GetColumnEncryptionCustomKeyStoreProviders() // Transient Fault handling flag. This is needed to convey to the downstream mechanism of connection establishment, if Transient Fault handling should be used or not // The downstream handling of Connection open is the same for idle connection resiliency. Currently we want to apply transient fault handling only to the connections opened - // using SqlConnection.Open() method. + // using SqlConnection.Open() method. internal bool _applyTransientFaultHandling = false; /// @@ -331,6 +331,16 @@ public SqlConnection(string connectionString, SqlCredential credential) : this() throw SQL.SettingCredentialWithDeviceFlowArgument(); } + if (UsesActiveDirectoryManagedIdentity(connectionOptions)) + { + throw SQL.SettingCredentialWithManagedIdentityArgument(DbConnectionStringBuilderUtil.ActiveDirectoryManagedIdentityString); + } + + if (UsesActiveDirectoryMSI(connectionOptions)) + { + throw SQL.SettingCredentialWithManagedIdentityArgument(DbConnectionStringBuilderUtil.ActiveDirectoryMSIString); + } + Credential = credential; } // else @@ -356,7 +366,7 @@ private SqlConnection(SqlConnection connection) CacheConnectionStringProperties(); } - // This method will be called once connection string is set or changed. + // This method will be called once connection string is set or changed. private void CacheConnectionStringProperties() { SqlConnectionString connString = ConnectionOptions as SqlConnectionString; @@ -364,7 +374,7 @@ private void CacheConnectionStringProperties() { _connectRetryCount = connString.ConnectRetryCount; // For Azure SQL connection, set _connectRetryCount to 2 instead of 1 will greatly improve recovery - // success rate + // success rate if (_connectRetryCount == 1 && ADP.IsAzureSqlServerEndpoint(connString.DataSource)) { _connectRetryCount = 2; @@ -506,33 +516,43 @@ internal SqlConnectionAttestationProtocol AttestationProtocol // Is this connection is a Context Connection? private bool UsesContextConnection(SqlConnectionString opt) { - return opt != null ? opt.ContextConnection : false; + return opt != null && opt.ContextConnection; } private bool UsesActiveDirectoryIntegrated(SqlConnectionString opt) { - return opt != null ? opt.Authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated : false; + return opt != null && opt.Authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated; } private bool UsesActiveDirectoryInteractive(SqlConnectionString opt) { - return opt != null ? opt.Authentication == SqlAuthenticationMethod.ActiveDirectoryInteractive : false; + return opt != null && opt.Authentication == SqlAuthenticationMethod.ActiveDirectoryInteractive; } private bool UsesActiveDirectoryDeviceCodeFlow(SqlConnectionString opt) { - return opt != null ? opt.Authentication == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow : false; + return opt != null && opt.Authentication == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow; + } + + private bool UsesActiveDirectoryManagedIdentity(SqlConnectionString opt) + { + return opt != null && opt.Authentication == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity; + } + + private bool UsesActiveDirectoryMSI(SqlConnectionString opt) + { + return opt != null && opt.Authentication == SqlAuthenticationMethod.ActiveDirectoryMSI; } private bool UsesAuthentication(SqlConnectionString opt) { - return opt != null ? opt.Authentication != SqlAuthenticationMethod.NotSpecified : false; + return opt != null && opt.Authentication != SqlAuthenticationMethod.NotSpecified; } // Does this connection uses Integrated Security? private bool UsesIntegratedSecurity(SqlConnectionString opt) { - return opt != null ? opt.IntegratedSecurity : false; + return opt != null && opt.IntegratedSecurity; } // Does this connection uses old style of clear userID or Password in connection string? @@ -548,7 +568,7 @@ private bool UsesClearUserIdOrPassword(SqlConnectionString opt) private bool UsesCertificate(SqlConnectionString opt) { - return opt != null ? opt.UsesCertificate : false; + return opt != null && opt.UsesCertificate; } internal SqlConnectionString.TransactionBindingEnum TransactionBinding @@ -681,7 +701,8 @@ override public string ConnectionString SqlConnectionString connectionOptions = new SqlConnectionString(value); if (_credential != null) { - // Check for Credential being used with Authentication=ActiveDirectoryIntegrated/ActiveDirectoryInteractive/ActiveDirectoryDeviceCodeFlow. Since a different error string is used + // Check for Credential being used with Authentication=ActiveDirectoryIntegrated | ActiveDirectoryInteractive | + // ActiveDirectoryDeviceCodeFlow | ActiveDirectoryManagedIdentity/ActiveDirectoryMSI. Since a different error string is used // for this case in ConnectionString setter vs in Credential setter, check for this error case before calling // CheckAndThrowOnInvalidCombinationOfConnectionStringAndSqlCredential, which is common to both setters. if (UsesActiveDirectoryIntegrated(connectionOptions)) @@ -696,6 +717,14 @@ override public string ConnectionString { throw SQL.SettingDeviceFlowWithCredential(); } + else if (UsesActiveDirectoryManagedIdentity(connectionOptions)) + { + throw SQL.SettingManagedIdentityWithCredential(DbConnectionStringBuilderUtil.ActiveDirectoryManagedIdentityString); + } + else if (UsesActiveDirectoryMSI(connectionOptions)) + { + throw SQL.SettingManagedIdentityWithCredential(DbConnectionStringBuilderUtil.ActiveDirectoryMSIString); + } CheckAndThrowOnInvalidCombinationOfConnectionStringAndSqlCredential(connectionOptions); } @@ -752,9 +781,9 @@ override public string Database } } - /// + /// /// To indicate the IsSupported flag sent by the server for DNS Caching. This property is for internal testing only. - /// + /// [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] internal string SQLDNSCachingSupportedState { @@ -776,9 +805,9 @@ internal string SQLDNSCachingSupportedState } } - /// + /// /// To indicate the IsSupported flag sent by the server for DNS Caching before redirection. This property is for internal testing only. - /// + /// [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] internal string SQLDNSCachingSupportedStateBeforeRedirect { @@ -1010,23 +1039,33 @@ public SqlCredential Credential // check if the usage of credential has any conflict with the keys used in connection string if (value != null) { - // Check for Credential being used with Authentication=ActiveDirectoryIntegrated/ActiveDirectoryInteractive/ActiveDirectoryDeviceCodeFlow. Since a different error string is used + var connectionOptions = (SqlConnectionString)ConnectionOptions; + // Check for Credential being used with Authentication=ActiveDirectoryIntegrated | ActiveDirectoryInteractive | + // ActiveDirectoryDeviceCodeFlow | ActiveDirectoryManagedIdentity/ActiveDirectoryMSI. Since a different error string is used // for this case in ConnectionString setter vs in Credential setter, check for this error case before calling // CheckAndThrowOnInvalidCombinationOfConnectionStringAndSqlCredential, which is common to both setters. - if (UsesActiveDirectoryIntegrated((SqlConnectionString)ConnectionOptions)) + if (UsesActiveDirectoryIntegrated(connectionOptions)) { throw SQL.SettingCredentialWithIntegratedInvalid(); } - else if (UsesActiveDirectoryInteractive((SqlConnectionString)ConnectionOptions)) + else if (UsesActiveDirectoryInteractive(connectionOptions)) { throw SQL.SettingCredentialWithInteractiveInvalid(); } - else if (UsesActiveDirectoryDeviceCodeFlow((SqlConnectionString)ConnectionOptions)) + else if (UsesActiveDirectoryDeviceCodeFlow(connectionOptions)) { throw SQL.SettingCredentialWithDeviceFlowInvalid(); } + else if (UsesActiveDirectoryManagedIdentity(connectionOptions)) + { + throw SQL.SettingCredentialWithManagedIdentityInvalid(DbConnectionStringBuilderUtil.ActiveDirectoryManagedIdentityString); + } + else if (UsesActiveDirectoryMSI(connectionOptions)) + { + throw SQL.SettingCredentialWithManagedIdentityInvalid(DbConnectionStringBuilderUtil.ActiveDirectoryMSIString); + } - CheckAndThrowOnInvalidCombinationOfConnectionStringAndSqlCredential((SqlConnectionString)ConnectionOptions); + CheckAndThrowOnInvalidCombinationOfConnectionStringAndSqlCredential(connectionOptions); if (_accessToken != null) { throw ADP.InvalidMixedUsageOfCredentialAndAccessToken(); @@ -1216,7 +1255,7 @@ override protected DbTransaction BeginDbTransaction(IsolationLevel isolationLeve DbTransaction transaction = BeginTransaction(isolationLevel); - // VSTFDEVDIV# 560355 - InnerConnection doesn't maintain a ref on the outer connection (this) and + // VSTFDEVDIV# 560355 - InnerConnection doesn't maintain a ref on the outer connection (this) and // subsequently leaves open the possibility that the outer connection could be GC'ed before the SqlTransaction // is fully hooked up (leaving a DbTransaction with a null connection property). Ensure that this is reachable // until the completion of BeginTransaction with KeepAlive @@ -1357,7 +1396,7 @@ void CloseInnerConnection() { // CloseConnection() now handles the lock - // The SqlInternalConnectionTds is set to OpenBusy during close, once this happens the cast below will fail and + // The SqlInternalConnectionTds is set to OpenBusy during close, once this happens the cast below will fail and // the command will no longer be cancelable. It might be desirable to be able to cancel the close opperation, but this is // outside of the scope of Whidbey RTM. See (SqlCommand::Cancel) for other lock. _originalConnectionId = ClientConnectionId; @@ -1401,7 +1440,7 @@ override public void Close() } AsyncHelper.WaitForCompletion(reconnectTask, 0, null, rethrowExceptions: false); // we do not need to deal with possible exceptions in reconnection if (State != ConnectionState.Open) - {// if we cancelled before the connection was opened + {// if we cancelled before the connection was opened OnStateChange(DbConnectionInternal.StateChangeClosed); } } @@ -1559,7 +1598,7 @@ internal void RegisterWaitingForReconnect(Task waitingTask) } Interlocked.CompareExchange(ref _asyncWaitingForReconnection, waitingTask, null); if (_asyncWaitingForReconnection != waitingTask) - { // somebody else managed to register + { // somebody else managed to register throw SQL.MARSUnsupportedOnConnection(); } } @@ -1655,7 +1694,7 @@ internal Task ValidateAndReconnect(Action beforeDisconnect, int timeout) { if (tdsConn.Parser._sessionPool.ActiveSessionsCount > 0) { - // >1 MARS session + // >1 MARS session if (beforeDisconnect != null) { beforeDisconnect(); @@ -1718,7 +1757,7 @@ internal Task ValidateAndReconnect(Action beforeDisconnect, int timeout) OnError(SQL.CR_UnrecoverableServer(ClientConnectionId), true, null); } } // ValidateSNIConnection - } // sessionRecoverySupported + } // sessionRecoverySupported } // connectRetryCount>0 } else diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnectionString.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnectionString.cs index 2f24e0dabd..2b3ffa9d70 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnectionString.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnectionString.cs @@ -19,21 +19,22 @@ internal sealed class SqlConnectionString : DbConnectionOptions internal static class DEFAULT { + private const string _emptyString = ""; internal const ApplicationIntent ApplicationIntent = DbConnectionStringDefaults.ApplicationIntent; internal const string Application_Name = TdsEnums.SQL_PROVIDER_NAME; internal const bool Asynchronous = false; - internal const string AttachDBFilename = ""; + internal const string AttachDBFilename = _emptyString; internal const PoolBlockingPeriod PoolBlockingPeriod = DbConnectionStringDefaults.PoolBlockingPeriod; internal const int Command_Timeout = ADP.DefaultCommandTimeout; internal const int Connect_Timeout = ADP.DefaultConnectionTimeout; internal const bool Connection_Reset = true; internal const bool Context_Connection = false; - internal const string Current_Language = ""; - internal const string Data_Source = ""; + internal const string Current_Language = _emptyString; + internal const string Data_Source = _emptyString; internal const bool Encrypt = false; internal const bool Enlist = true; - internal const string FailoverPartner = ""; - internal const string Initial_Catalog = ""; + internal const string FailoverPartner = _emptyString; + internal const string Initial_Catalog = _emptyString; internal const bool Integrated_Security = false; internal const int Load_Balance_Timeout = 0; // default of 0 means don't use internal const bool MARS = false; @@ -41,24 +42,25 @@ internal static class DEFAULT internal const int Min_Pool_Size = 0; internal const bool MultiSubnetFailover = DbConnectionStringDefaults.MultiSubnetFailover; internal static readonly bool TransparentNetworkIPResolution = DbConnectionStringDefaults.TransparentNetworkIPResolution; - internal const string Network_Library = ""; + internal const string Network_Library = _emptyString; internal const int Packet_Size = 8000; - internal const string Password = ""; + internal const string Password = _emptyString; internal const bool Persist_Security_Info = false; internal const bool Pooling = true; internal const bool TrustServerCertificate = false; - internal const string Type_System_Version = ""; - internal const string User_ID = ""; + internal const string Type_System_Version = _emptyString; + internal const string User_ID = _emptyString; internal const bool User_Instance = false; internal const bool Replication = false; internal const int Connect_Retry_Count = 1; internal const int Connect_Retry_Interval = 10; internal static readonly SqlAuthenticationMethod Authentication = SqlAuthenticationMethod.NotSpecified; internal static readonly SqlConnectionColumnEncryptionSetting ColumnEncryptionSetting = SqlConnectionColumnEncryptionSetting.Disabled; - internal const string EnclaveAttestationUrl = ""; + internal const string EnclaveAttestationUrl = _emptyString; internal static readonly SqlConnectionAttestationProtocol AttestationProtocol = SqlConnectionAttestationProtocol.NotSpecified; + #if ADONET_CERT_AUTH - internal const string Certificate = ""; + internal const string Certificate = _emptyString; #endif } @@ -106,7 +108,8 @@ internal static class KEY internal const string Connect_Retry_Count = "connect retry count"; internal const string Connect_Retry_Interval = "connect retry interval"; internal const string Authentication = "authentication"; -#if ADONET_CERT_AUTH + +#if ADONET_CERT_AUTH internal const string Certificate = "certificate"; #endif } @@ -251,12 +254,12 @@ internal static class TRANSACIONBINDING private readonly string _attachDBFileName; private readonly string _currentLanguage; private readonly string _dataSource; - private readonly string _localDBInstance; // created based on datasource, set to NULL if datasource is not LocalDB + private readonly string _localDBInstance; // created based on datasource, set to NULL if datasource is not LocalDB private readonly string _failoverPartner; private readonly string _initialCatalog; private readonly string _password; private readonly string _userID; -#if ADONET_CERT_AUTH +#if ADONET_CERT_AUTH private readonly string _certificate; #endif private readonly string _networkLibrary; @@ -323,7 +326,7 @@ internal SqlConnectionString(string connectionString) : base(connectionString, G _enclaveAttestationUrl = ConvertValueToString(KEY.EnclaveAttestationUrl, DEFAULT.EnclaveAttestationUrl); _attestationProtocol = ConvertValueToAttestationProtocol(); -#if ADONET_CERT_AUTH +#if ADONET_CERT_AUTH _certificate = ConvertValueToString(KEY.Certificate, DEFAULT.Certificate); #endif @@ -336,7 +339,7 @@ internal SqlConnectionString(string connectionString) : base(connectionString, G if (_contextConnection) { - // We have to be running in the engine for you to request a + // We have to be running in the engine for you to request a // context connection. if (!runningInProc) @@ -344,7 +347,7 @@ internal SqlConnectionString(string connectionString) : base(connectionString, G throw SQL.ContextUnavailableOutOfProc(); } - // When using a context connection, we need to ensure that no + // When using a context connection, we need to ensure that no // other connection string keywords are specified. foreach (DictionaryEntry entry in Parsetable) @@ -565,19 +568,29 @@ internal SqlConnectionString(string connectionString) : base(connectionString, G throw SQL.DeviceFlowWithUsernamePassword(); } + if (Authentication == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity && HasPasswordKeyword) + { + throw SQL.ManagedIdentityWithPassword(DbConnectionStringBuilderUtil.ActiveDirectoryManagedIdentityString); + } + + if (Authentication == SqlAuthenticationMethod.ActiveDirectoryMSI && HasPasswordKeyword) + { + throw SQL.ManagedIdentityWithPassword(DbConnectionStringBuilderUtil.ActiveDirectoryMSIString); + } + #if ADONET_CERT_AUTH - + if (!DbConnectionStringBuilderUtil.IsValidCertificateValue(_certificate)) { throw ADP.InvalidConnectionOptionValue(KEY.Certificate); } if (!string.IsNullOrEmpty(_certificate)) { - + if (Authentication == SqlClient.SqlAuthenticationMethod.NotSpecified && !_integratedSecurity) { _authType = SqlClient.SqlAuthenticationMethod.SqlCertificate; } - if (Authentication == SqlClient.SqlAuthenticationMethod.SqlCertificate && (HasUserIdKeyword || HasPasswordKeyword || _integratedSecurity)) { + if (Authentication == SqlClient.SqlAuthenticationMethod.SqlCertificate && (HasUserIdKeyword || HasPasswordKeyword || _integratedSecurity)) { throw SQL.InvalidCertAuth(); } } @@ -641,8 +654,8 @@ internal SqlConnectionString(SqlConnectionString connectionOptions, string dataS _columnEncryptionSetting = connectionOptions._columnEncryptionSetting; _enclaveAttestationUrl = connectionOptions._enclaveAttestationUrl; _attestationProtocol = connectionOptions._attestationProtocol; -#if ADONET_CERT_AUTH - _certificate = connectionOptions._certificate; +#if ADONET_CERT_AUTH + _certificate = connectionOptions._certificate; #endif ValidateValueLength(_dataSource, TdsEnums.MAXLEN_SERVERNAME, KEY.Data_Source); } @@ -669,7 +682,7 @@ internal SqlConnectionString(SqlConnectionString connectionOptions, string dataS internal SqlConnectionColumnEncryptionSetting ColumnEncryptionSetting { get { return _columnEncryptionSetting; } } internal string EnclaveAttestationUrl { get { return _enclaveAttestationUrl; } } internal SqlConnectionAttestationProtocol AttestationProtocol { get { return _attestationProtocol; } } -#if ADONET_CERT_AUTH +#if ADONET_CERT_AUTH internal string Certificate { get { return _certificate; } } internal bool UsesCertificate { get { return _authType == SqlClient.SqlAuthenticationMethod.SqlCertificate; } } #else @@ -809,7 +822,7 @@ internal static Hashtable GetParseSynonyms() hash.Add(KEY.Connect_Retry_Count, KEY.Connect_Retry_Count); hash.Add(KEY.Connect_Retry_Interval, KEY.Connect_Retry_Interval); hash.Add(KEY.Authentication, KEY.Authentication); -#if ADONET_CERT_AUTH +#if ADONET_CERT_AUTH hash.Add(KEY.Certificate, KEY.Certificate); #endif hash.Add(SYNONYM.APPLICATIONINTENT, KEY.ApplicationIntent); diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnectionStringBuilder.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnectionStringBuilder.cs index fb17b8df03..9b44f7459a 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnectionStringBuilder.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnectionStringBuilder.cs @@ -21,11 +21,9 @@ namespace Microsoft.Data.SqlClient [System.ComponentModel.TypeConverterAttribute(typeof(SqlConnectionStringBuilder.SqlConnectionStringBuilderConverter))] public sealed class SqlConnectionStringBuilder : DbConnectionStringBuilder { - private enum Keywords { // specific ordering for ConnectionString output construction // NamedConnection, - DataSource, FailoverPartner, AttachDBFilename, @@ -34,18 +32,15 @@ private enum Keywords PersistSecurityInfo, UserID, Password, - Enlist, Pooling, MinPoolSize, MaxPoolSize, PoolBlockingPeriod, - AsynchronousProcessing, ConnectionReset, MultipleActiveResultSets, Replication, - ConnectTimeout, Encrypt, TrustServerCertificate, @@ -53,31 +48,19 @@ private enum Keywords NetworkLibrary, PacketSize, TypeSystemVersion, - Authentication, - ApplicationName, CurrentLanguage, WorkstationID, - UserInstance, - ContextConnection, - TransactionBinding, - ApplicationIntent, - MultiSubnetFailover, - TransparentNetworkIPResolution, - ConnectRetryCount, - ConnectRetryInterval, - ColumnEncryptionSetting, - EnclaveAttestationUrl, AttestationProtocol, @@ -109,7 +92,6 @@ private enum Keywords private string _typeSystemVersion = DbConnectionStringDefaults.TypeSystemVersion; private string _userID = DbConnectionStringDefaults.UserID; private string _workstationID = DbConnectionStringDefaults.WorkstationID; - private int _commandTimeout = DbConnectionStringDefaults.CommandTimeout; private int _connectTimeout = DbConnectionStringDefaults.ConnectTimeout; private int _loadBalanceTimeout = DbConnectionStringDefaults.LoadBalanceTimeout; @@ -118,7 +100,6 @@ private enum Keywords private int _packetSize = DbConnectionStringDefaults.PacketSize; private int _connectRetryCount = DbConnectionStringDefaults.ConnectRetryCount; private int _connectRetryInterval = DbConnectionStringDefaults.ConnectRetryInterval; - private bool _asynchronousProcessing = DbConnectionStringDefaults.AsynchronousProcessing; private bool _connectionReset = DbConnectionStringDefaults.ConnectionReset; private bool _contextConnection = DbConnectionStringDefaults.ContextConnection; @@ -140,7 +121,7 @@ private enum Keywords private PoolBlockingPeriod _poolBlockingPeriod = DbConnectionStringDefaults.PoolBlockingPeriod; #if ADONET_CERT_AUTH - private string _certificate = DbConnectionStringDefaults.Certificate; + private string _certificate = DbConnectionStringDefaults.Certificate; #endif static SqlConnectionStringBuilder() @@ -187,8 +168,8 @@ static SqlConnectionStringBuilder() validKeywords[(int)Keywords.ColumnEncryptionSetting] = DbConnectionStringKeywords.ColumnEncryptionSetting; validKeywords[(int)Keywords.EnclaveAttestationUrl] = DbConnectionStringKeywords.EnclaveAttestationUrl; validKeywords[(int)Keywords.AttestationProtocol] = DbConnectionStringKeywords.AttestationProtocol; -#if ADONET_CERT_AUTH - validKeywords[(int)Keywords.Certificate] = DbConnectionStringKeywords.Certificate; +#if ADONET_CERT_AUTH + validKeywords[(int)Keywords.Certificate] = DbConnectionStringKeywords.Certificate; #endif _validKeywords = validKeywords; @@ -234,8 +215,8 @@ static SqlConnectionStringBuilder() hash.Add(DbConnectionStringKeywords.ColumnEncryptionSetting, Keywords.ColumnEncryptionSetting); hash.Add(DbConnectionStringKeywords.EnclaveAttestationUrl, Keywords.EnclaveAttestationUrl); hash.Add(DbConnectionStringKeywords.AttestationProtocol, Keywords.AttestationProtocol); -#if ADONET_CERT_AUTH - hash.Add(DbConnectionStringKeywords.Certificate, Keywords.Certificate); +#if ADONET_CERT_AUTH + hash.Add(DbConnectionStringKeywords.Certificate, Keywords.Certificate); #endif hash.Add(DbConnectionStringSynonyms.APP, Keywords.ApplicationName); hash.Add(DbConnectionStringSynonyms.APPLICATIONINTENT, Keywords.ApplicationIntent); @@ -377,7 +358,9 @@ public SqlConnectionStringBuilder(string connectionString) : base() AttestationProtocol = ConvertToAttestationProtocol(keyword, value); break; #if ADONET_CERT_AUTH - case Keywords.Certificate: Certificate = ConvertToString(value); break; + case Keywords.Certificate: + Certificate = ConvertToString(value); + break; #endif case Keywords.AsynchronousProcessing: AsynchronousProcessing = ConvertToBoolean(value); diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 2d90950cef..381a87036e 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -983,7 +983,7 @@ internal override bool IsConnectionAlive(bool throwOnException) tdsReliabilitySection.Start(); #endif //DEBUG - isAlive = _parser._physicalStateObj.IsConnectionAlive(throwOnException); + isAlive = _parser._physicalStateObj.IsConnectionAlive(throwOnException); #if DEBUG } @@ -1567,13 +1567,15 @@ private void Login(ServerInfo server, TimeoutTimer timeout, string newPassword, _sessionRecoveryRequested = true; } - // If the workflow being used is Active Directory Password/Integrated/Interactive/Service Principal/Device Code Flow and server's prelogin response + // If the workflow being used is Active Directory Authentication and server's prelogin response // for FEDAUTHREQUIRED option indicates Federated Authentication is required, we have to insert FedAuth Feature Extension // in Login7, indicating the intent to use Active Directory Authentication for SQL Server. if (ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryPassword || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryInteractive || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryServicePrincipal || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow + || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity + || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryMSI // Since AD Integrated may be acting like Windows integrated, additionally check _fedAuthRequired || (ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated && _fedAuthRequired)) { @@ -1587,6 +1589,7 @@ private void Login(ServerInfo server, TimeoutTimer timeout, string newPassword, fedAuthRequiredPreLoginResponse = _fedAuthRequired }; } + if (_accessTokenInBytes != null) { requestedFeatures |= TdsEnums.FeatureExtension.FedAuth; @@ -1966,7 +1969,9 @@ private bool ShouldDisableTnir(SqlConnectionString connectionOptions) connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated || connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryInteractive || connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryServicePrincipal || - connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow; + connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow || + connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity || + connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryMSI; // Check if the user had explicitly specified the TNIR option in the connection string or the connection string builder. // If the user has specified the option in the connection string explicitly, then we shouldn't disable TNIR. @@ -2367,18 +2372,18 @@ internal bool GetSessionAndReconnectIfNeeded(SqlConnection parent, int timeout = { tdsReliabilitySection.Start(); #endif //DEBUG - Task reconnectTask = parent.ValidateAndReconnect(() => - { - ThreadHasParserLockForClose = false; - _parserLock.Release(); - releaseConnectionLock = false; - }, timeout); - if (reconnectTask != null) - { - AsyncHelper.WaitForCompletion(reconnectTask, timeout); - return true; - } - return false; + Task reconnectTask = parent.ValidateAndReconnect(() => + { + ThreadHasParserLockForClose = false; + _parserLock.Release(); + releaseConnectionLock = false; + }, timeout); + if (reconnectTask != null) + { + AsyncHelper.WaitForCompletion(reconnectTask, timeout); + return true; + } + return false; #if DEBUG } finally @@ -2552,6 +2557,8 @@ internal void OnFedAuthInfo(SqlFedAuthInfo fedAuthInfo) Debug.Assert((ConnectionOptions.HasUserIdKeyword && ConnectionOptions.HasPasswordKeyword) || _credential != null || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryInteractive + || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity + || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryMSI || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow || (ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated && _fedAuthRequired), "Credentials aren't provided for calling MSAL"); @@ -2781,6 +2788,8 @@ internal SqlFedAuthToken GetFedAuthToken(SqlFedAuthInfo fedAuthInfo) break; case SqlAuthenticationMethod.ActiveDirectoryInteractive: case SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow: + case SqlAuthenticationMethod.ActiveDirectoryManagedIdentity: + case SqlAuthenticationMethod.ActiveDirectoryMSI: if (_activeDirectoryAuthTimeoutRetryHelper.State == ActiveDirectoryAuthenticationTimeoutRetryState.Retrying) { fedAuthToken = _activeDirectoryAuthTimeoutRetryHelper.CachedToken; @@ -3154,7 +3163,7 @@ internal void OnFeatureExtAck(int featureId, byte[] data) } // need to add more steps for phrase 2 - // get IPv4 + IPv6 + Port number + // get IPv4 + IPv6 + Port number // not put them in the DNS cache at this point but need to store them somewhere // generate pendingSQLDNSObject and turn on IsSQLDNSRetryEnabled flag diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlUtil.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlUtil.cs index fee169e994..9d64e1a3d9 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlUtil.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlUtil.cs @@ -333,6 +333,10 @@ static internal Exception DeviceFlowWithUsernamePassword() { return ADP.Argument(StringsHelper.GetString(Strings.SQL_DeviceFlowWithUsernamePassword)); } + static internal Exception ManagedIdentityWithPassword(string authenticationMode) + { + return ADP.Argument(StringsHelper.GetString(Strings.SQL_ManagedIdentityWithPassword, authenticationMode)); + } static internal Exception SettingIntegratedWithCredential() { return ADP.InvalidOperation(StringsHelper.GetString(Strings.SQL_SettingIntegratedWithCredential)); @@ -345,6 +349,10 @@ static internal Exception SettingDeviceFlowWithCredential() { return ADP.InvalidOperation(StringsHelper.GetString(Strings.SQL_SettingDeviceFlowWithCredential)); } + static internal Exception SettingManagedIdentityWithCredential(string authenticationMode) + { + return ADP.InvalidOperation(StringsHelper.GetString(Strings.SQL_SettingManagedIdentityWithCredential, authenticationMode)); + } static internal Exception SettingCredentialWithIntegratedArgument() { return ADP.Argument(StringsHelper.GetString(Strings.SQL_SettingCredentialWithIntegrated)); @@ -357,6 +365,10 @@ static internal Exception SettingCredentialWithDeviceFlowArgument() { return ADP.Argument(StringsHelper.GetString(Strings.SQL_SettingCredentialWithDeviceFlow)); } + static internal Exception SettingCredentialWithManagedIdentityArgument(string authenticationMode) + { + return ADP.Argument(StringsHelper.GetString(Strings.SQL_SettingCredentialWithManagedIdentity, authenticationMode)); + } static internal Exception SettingCredentialWithIntegratedInvalid() { return ADP.InvalidOperation(StringsHelper.GetString(Strings.SQL_SettingCredentialWithIntegrated)); @@ -369,6 +381,10 @@ static internal Exception SettingCredentialWithDeviceFlowInvalid() { return ADP.InvalidOperation(StringsHelper.GetString(Strings.SQL_SettingCredentialWithDeviceFlow)); } + static internal Exception SettingCredentialWithManagedIdentityInvalid(string authenticationMode) + { + return ADP.InvalidOperation(StringsHelper.GetString(Strings.SQL_SettingCredentialWithManagedIdentity, authenticationMode)); + } static internal Exception InvalidSQLServerVersionUnknown() { return ADP.DataAdapter(StringsHelper.GetString(Strings.SQL_InvalidSQLServerVersionUnknown)); @@ -2072,6 +2088,16 @@ static internal SqlException CR_UnrecoverableClient(Guid connectionId) SqlException exc = SqlException.CreateException(errors, "", connectionId); return exc; } + internal static Exception Azure_ManagedIdentityException(string msg) + { + SqlErrorCollection errors = new SqlErrorCollection + { + new SqlError(0, (byte)0x00, TdsEnums.FATAL_ERROR_CLASS, null, msg, "", 0) + }; + SqlException exc = SqlException.CreateException(errors, null); + exc._doNotReconnect = true; // disable open retry logic on this error + return exc; + } // // Merged Provider diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsEnums.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsEnums.cs index 2ea1ce25c7..5e422fef74 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsEnums.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsEnums.cs @@ -243,6 +243,7 @@ public enum FedAuthLibrary : byte public const byte MSALWORKFLOW_ACTIVEDIRECTORYINTERACTIVE = 0x03; public const byte MSALWORKFLOW_ACTIVEDIRECTORYSERVICEPRINCIPAL = 0x01; // Using the Password byte as that is the closest we have public const byte MSALWORKFLOW_ACTIVEDIRECTORYDEVICECODEFLOW = 0x03; // Using the Interactive byte as that is the closest we have + public const byte MSALWORKFLOW_ACTIVEDIRECTORYMANAGEDIDENTITY = 0x03; // Using the Interactive byte as that's supported for Identity based authentication public enum ActiveDirectoryWorkflow : byte { @@ -251,6 +252,7 @@ public enum ActiveDirectoryWorkflow : byte Interactive = MSALWORKFLOW_ACTIVEDIRECTORYINTERACTIVE, ServicePrincipal = MSALWORKFLOW_ACTIVEDIRECTORYSERVICEPRINCIPAL, DeviceCodeFlow = MSALWORKFLOW_ACTIVEDIRECTORYDEVICECODEFLOW, + ManagedIdentity = MSALWORKFLOW_ACTIVEDIRECTORYMANAGEDIDENTITY, } // The string used for username in the error message when Authentication = Active Directory Integrated with FedAuth is used, if authentication fails. @@ -1097,6 +1099,12 @@ public enum SqlAuthenticationMethod /// ActiveDirectoryDeviceCodeFlow, + + /// + ActiveDirectoryManagedIdentity, + + /// + ActiveDirectoryMSI, #if ADONET_CERT_AUTH SqlCertificate #endif diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs index 9c7bdee4c4..bb4ab023ef 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -526,7 +526,7 @@ internal void ProcessPendingAck(TdsParserStateObject stateObj) LoadSSPILibrary(); // now allocate proper length of buffer _sniSpnBuffer = new byte[SNINativeMethodWrapper.SniMaxComposedSpnLength]; - SqlClientEventSource.Log.TryTraceEvent(" SSPI or Active Directory Authentication Library for SQL Server based integrated authentication", "SEC"); + SqlClientEventSource.Log.TryTraceEvent(" SSPI or Active Directory Authentication Library for SQL Server based integrated authentication"); } else { @@ -534,25 +534,31 @@ internal void ProcessPendingAck(TdsParserStateObject stateObj) switch (authType) { case SqlAuthenticationMethod.ActiveDirectoryPassword: - SqlClientEventSource.Log.TryTraceEvent(" Active Directory Password authentication", "SEC"); + SqlClientEventSource.Log.TryTraceEvent(" Active Directory Password authentication"); break; case SqlAuthenticationMethod.ActiveDirectoryIntegrated: - SqlClientEventSource.Log.TryTraceEvent(" Active Directory Integrated authentication", "SEC"); + SqlClientEventSource.Log.TryTraceEvent(" Active Directory Integrated authentication"); break; case SqlAuthenticationMethod.ActiveDirectoryInteractive: - SqlClientEventSource.Log.TryTraceEvent(" Active Directory Interactive authentication", "SEC"); + SqlClientEventSource.Log.TryTraceEvent(" Active Directory Interactive authentication"); break; case SqlAuthenticationMethod.ActiveDirectoryServicePrincipal: - SqlClientEventSource.Log.TryTraceEvent(" Active Directory Service Principal authentication", "SEC"); + SqlClientEventSource.Log.TryTraceEvent(" Active Directory Service Principal authentication"); break; case SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow: - SqlClientEventSource.Log.TryTraceEvent(" Active Directory Device Code Flow authentication", "SEC"); + SqlClientEventSource.Log.TryTraceEvent(" Active Directory Device Code Flow authentication"); + break; + case SqlAuthenticationMethod.ActiveDirectoryManagedIdentity: + SqlClientEventSource.Log.TryTraceEvent(" Active Directory Managed Identity authentication"); + break; + case SqlAuthenticationMethod.ActiveDirectoryMSI: + SqlClientEventSource.Log.TryTraceEvent(" Active Directory MSI authentication"); break; case SqlAuthenticationMethod.SqlPassword: - SqlClientEventSource.Log.TryTraceEvent(" SQL Password authentication", "SEC"); + SqlClientEventSource.Log.TryTraceEvent(" SQL Password authentication"); break; default: - SqlClientEventSource.Log.TryTraceEvent(" SQL authentication", "SEC"); + SqlClientEventSource.Log.TryTraceEvent(" SQL authentication"); break; } } @@ -598,7 +604,7 @@ internal void ProcessPendingAck(TdsParserStateObject stateObj) // don't, the memory for the connection object might not be accurate and thus // a bad error could be returned (as it was when it was freed to early for me). _physicalStateObj.Dispose(); - SqlClientEventSource.Log.TryTraceEvent(" Login failure", "ERR|SEC"); + SqlClientEventSource.Log.TryTraceEvent(" Login failure"); ThrowExceptionAndWarning(_physicalStateObj); Debug.Fail("SNI returned status != success, but no error thrown?"); } @@ -631,21 +637,21 @@ internal void ProcessPendingAck(TdsParserStateObject stateObj) AssignPendingDNSInfo(serverInfo.UserProtocol, FQDNforDNSCahce); // UNDONE - send "" for instance now, need to fix later - SqlClientEventSource.Log.TryTraceEvent(" Sending prelogin handshake", "SEC"); + SqlClientEventSource.Log.TryTraceEvent(" Sending prelogin handshake"); SendPreLoginHandshake(instanceName, encrypt, !string.IsNullOrEmpty(certificate), useOriginalAddressInfo); _connHandler.TimeoutErrorInternal.EndPhase(SqlConnectionTimeoutErrorPhase.SendPreLoginHandshake); _connHandler.TimeoutErrorInternal.SetAndBeginPhase(SqlConnectionTimeoutErrorPhase.ConsumePreLoginHandshake); _physicalStateObj.SniContext = SniContext.Snix_PreLogin; - SqlClientEventSource.Log.TryTraceEvent(" Consuming prelogin handshake", "SEC"); + SqlClientEventSource.Log.TryTraceEvent(" Consuming prelogin handshake"); PreLoginHandshakeStatus status = ConsumePreLoginHandshake(authType, encrypt, trustServerCert, integratedSecurity, serverCallback, clientCallback, out marsCapable, out _connHandler._fedAuthRequired); if (status == PreLoginHandshakeStatus.InstanceFailure) { - SqlClientEventSource.Log.TryTraceEvent(" Prelogin handshake unsuccessful. Reattempting prelogin handshake", "SEC"); + SqlClientEventSource.Log.TryTraceEvent(" Prelogin handshake unsuccessful. Reattempting prelogin handshake"); _physicalStateObj.Dispose(); // Close previous connection // On Instance failure re-connect and flush SNI named instance cache. @@ -655,14 +661,14 @@ internal void ProcessPendingAck(TdsParserStateObject stateObj) if (TdsEnums.SNI_SUCCESS != _physicalStateObj.Status) { _physicalStateObj.AddError(ProcessSNIError(_physicalStateObj)); - SqlClientEventSource.Log.TryTraceEvent(" Login failure", "ERR|SEC"); + SqlClientEventSource.Log.TryTraceEvent(" Login failure"); ThrowExceptionAndWarning(_physicalStateObj); } UInt32 retCode = SNINativeMethodWrapper.SniGetConnectionId(_physicalStateObj.Handle, ref _connHandler._clientConnectionId); Debug.Assert(retCode == TdsEnums.SNI_SUCCESS, "Unexpected failure state upon calling SniGetConnectionId"); - SqlClientEventSource.Log.TryTraceEvent(" Sending prelogin handshake", "SEC"); + SqlClientEventSource.Log.TryTraceEvent(" Sending prelogin handshake"); // for DNS Caching phase 1 AssignPendingDNSInfo(serverInfo.UserProtocol, FQDNforDNSCahce); @@ -675,11 +681,11 @@ internal void ProcessPendingAck(TdsParserStateObject stateObj) // one pre-login packet and know we are connecting to Shiloh. if (status == PreLoginHandshakeStatus.InstanceFailure) { - SqlClientEventSource.Log.TryTraceEvent(" Prelogin handshake unsuccessful. Login failure", "ERR|SEC"); + SqlClientEventSource.Log.TryTraceEvent(" Prelogin handshake unsuccessful. Login failure"); throw SQL.InstanceFailure(); } } - SqlClientEventSource.Log.TryTraceEvent(" Prelogin handshake successful", "SEC"); + SqlClientEventSource.Log.TryTraceEvent(" Prelogin handshake successful"); if (_fMARS && marsCapable) { @@ -695,7 +701,7 @@ internal void ProcessPendingAck(TdsParserStateObject stateObj) } // Retrieve the IP and port number from native SNI for TCP protocol. The IP information is stored temporarily in the - // pendingSQLDNSObject but not in the DNS Cache at this point. We only add items to the DNS Cache after we receive the + // pendingSQLDNSObject but not in the DNS Cache at this point. We only add items to the DNS Cache after we receive the // IsSupported flag as true in the feature ext ack from server. internal void AssignPendingDNSInfo(string userProtocol, string DNSCacheKey) { @@ -2620,7 +2626,7 @@ internal bool TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataRead } case TdsEnums.SQLLOGINACK: { - SqlClientEventSource.Log.TryTraceEvent(" Received login acknowledgement token", "SEC"); + SqlClientEventSource.Log.TryTraceEvent(" Received login acknowledgement token"); SqlLoginAck ack; if (!TryProcessLoginAck(stateObj, out ack)) @@ -2643,7 +2649,7 @@ internal bool TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataRead { _connHandler._federatedAuthenticationInfoReceived = true; SqlFedAuthInfo info; - SqlClientEventSource.Log.TryTraceEvent(" Received federated authentication info token", "SEC"); + SqlClientEventSource.Log.TryTraceEvent(" Received federated authentication info token"); if (!TryProcessFedAuthInfo(stateObj, tokenLength, out info)) { @@ -4128,7 +4134,7 @@ private bool TryProcessFedAuthInfo(TdsParserStateObject stateObj, int tokenLen, if (!stateObj.TryReadUInt32(out optionsCount)) { - SqlClientEventSource.Log.TryTraceEvent(" Failed to read CountOfInfoIDs in FEDAUTHINFO token stream.", "ERR"); + SqlClientEventSource.Log.TryTraceEvent(" Failed to read CountOfInfoIDs in FEDAUTHINFO token stream."); throw SQL.ParsingError(ParsingErrorState.FedAuthInfoFailedToReadCountOfInfoIds); } @@ -4185,7 +4191,7 @@ private bool TryProcessFedAuthInfo(TdsParserStateObject stateObj, int tokenLen, // if dataOffset points to a region within FedAuthInfoOpt or after the end of the token, throw if (dataOffset < totalOptionsSize || dataOffset >= tokenLen) { - SqlClientEventSource.Log.TryTraceEvent(" FedAuthInfoDataOffset points to an invalid location.", "ERR"); + SqlClientEventSource.Log.TryTraceEvent(" FedAuthInfoDataOffset points to an invalid location."); throw SQL.ParsingErrorOffset(ParsingErrorState.FedAuthInfoInvalidOffset, unchecked((int)dataOffset)); } @@ -4197,12 +4203,12 @@ private bool TryProcessFedAuthInfo(TdsParserStateObject stateObj, int tokenLen, } catch (ArgumentOutOfRangeException e) { - SqlClientEventSource.Log.TryTraceEvent(" Failed to read FedAuthInfoData.", "ERR"); + SqlClientEventSource.Log.TryTraceEvent(" Failed to read FedAuthInfoData."); throw SQL.ParsingError(ParsingErrorState.FedAuthInfoFailedToReadData, e); } catch (ArgumentException e) { - SqlClientEventSource.Log.TryTraceEvent(" FedAuthInfoData is not in unicode format.", "ERR"); + SqlClientEventSource.Log.TryTraceEvent(" FedAuthInfoData is not in unicode format."); throw SQL.ParsingError(ParsingErrorState.FedAuthInfoDataNotUnicode, e); } SqlClientEventSource.Log.TryAdvancedTraceEvent(" FedAuthInfoData: {0}", data); @@ -4224,7 +4230,7 @@ private bool TryProcessFedAuthInfo(TdsParserStateObject stateObj, int tokenLen, } else { - SqlClientEventSource.Log.TryTraceEvent(" FEDAUTHINFO token stream is not long enough to contain the data it claims to.", "ERR"); + SqlClientEventSource.Log.TryTraceEvent(" FEDAUTHINFO token stream is not long enough to contain the data it claims to."); throw SQL.ParsingErrorLength(ParsingErrorState.FedAuthInfoLengthTooShortForData, tokenLen); } @@ -4232,7 +4238,7 @@ private bool TryProcessFedAuthInfo(TdsParserStateObject stateObj, int tokenLen, if (string.IsNullOrWhiteSpace(tempFedAuthInfo.stsurl) || string.IsNullOrWhiteSpace(tempFedAuthInfo.spn)) { // We should be receiving both stsurl and spn - SqlClientEventSource.Log.TryTraceEvent(" FEDAUTHINFO token stream does not contain both STSURL and SPN.", "ERR"); + SqlClientEventSource.Log.TryTraceEvent(" FEDAUTHINFO token stream does not contain both STSURL and SPN."); throw SQL.ParsingError(ParsingErrorState.FedAuthInfoDoesNotContainStsurlAndSpn); } @@ -8569,6 +8575,10 @@ internal int WriteSessionRecoveryFeatureRequest(SessionData reconnectData, bool case SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow: workflow = TdsEnums.MSALWORKFLOW_ACTIVEDIRECTORYDEVICECODEFLOW; break; + case SqlAuthenticationMethod.ActiveDirectoryManagedIdentity: + case SqlAuthenticationMethod.ActiveDirectoryMSI: + workflow = TdsEnums.MSALWORKFLOW_ACTIVEDIRECTORYMANAGEDIDENTITY; + break; default: Debug.Assert(false, "Unrecognized Authentication type for fedauth MSAL request"); break; @@ -9118,7 +9128,7 @@ internal int WriteSQLDNSCachingFeatureRequest(bool write /* if false just calcul }; if ((requestedFeatures & TdsEnums.FeatureExtension.FedAuth) != 0) { - SqlClientEventSource.Log.TryTraceEvent(" Sending federated authentication feature request", "SEC"); + SqlClientEventSource.Log.TryTraceEvent(" Sending federated authentication feature request"); Debug.Assert(fedAuthFeatureExtensionData != null, "fedAuthFeatureExtensionData should not null."); WriteFedAuthFeatureRequest(fedAuthFeatureExtensionData.Value, write: true); }; @@ -9197,7 +9207,7 @@ internal void SendFedAuthToken(SqlFedAuthToken fedAuthToken) { Debug.Assert(fedAuthToken != null, "fedAuthToken cannot be null"); Debug.Assert(fedAuthToken.accessToken != null, "fedAuthToken.accessToken cannot be null"); - SqlClientEventSource.Log.TryTraceEvent(" Sending federated authentication token", "SEC"); + SqlClientEventSource.Log.TryTraceEvent(" Sending federated authentication token"); _physicalStateObj._outputMessageType = TdsEnums.MT_FEDAUTH; @@ -9621,7 +9631,7 @@ internal void FailureCleanup(TdsParserStateObject stateObj, Exception e) _connHandler.ThreadHasParserLockForClose = originalThreadHasParserLock; } } - SqlClientEventSource.Log.TryTraceEvent(" Exception rethrown.", "ERR"); + SqlClientEventSource.Log.TryTraceEvent(" Exception rethrown."); } internal Task TdsExecuteSQLBatch(string text, int timeout, SqlNotificationRequest notificationRequest, TdsParserStateObject stateObj, bool sync, bool callerHasConnectionLock = false, byte[] enclavePackage = null) diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.Designer.cs b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.Designer.cs index 0501c22c3e..acfa8b7778 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.Designer.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.Designer.cs @@ -1833,6 +1833,60 @@ internal class Strings { } } + /// + /// Looks up a localized string similar to Access token could not be acquired.. + /// + internal static string Azure_GenericErrorMessage { + get { + return ResourceManager.GetString("Azure_GenericErrorMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unable to connect to the Managed Identity endpoint. Please check that you are running on an Azure resource that has Identity setup.. + /// + internal static string Azure_IdentityEndpointNotListening { + get { + return ResourceManager.GetString("Azure_IdentityEndpointNotListening", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Tried to get token using Managed Identity.. + /// + internal static string Azure_ManagedIdentityUsed { + get { + return ResourceManager.GetString("Azure_ManagedIdentityUsed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unable to connect to the Instance Metadata Service (IMDS). Skipping request to the Managed Identity token endpoint.. + /// + internal static string Azure_MetadataEndpointNotListening { + get { + return ResourceManager.GetString("Azure_MetadataEndpointNotListening", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Received a non-retryable error.. + /// + internal static string Azure_NonRetryableError { + get { + return ResourceManager.GetString("Azure_NonRetryableError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed after 5 retries.. + /// + internal static string Azure_RetryFailure { + get { + return ResourceManager.GetString("Azure_RetryFailure", resourceCulture); + } + } + /// /// Looks up a localized string similar to .database.chinacloudapi.cn. /// @@ -9279,6 +9333,15 @@ internal class Strings { } } + /// + /// Looks up a localized string similar to Cannot use 'Authentication=Active Directory Device Code Flow' with 'User ID', 'UID', 'Password' or 'PWD' connection string keywords.. + /// + internal static string SQL_DeviceFlowWithUsernamePassword { + get { + return ResourceManager.GetString("SQL_DeviceFlowWithUsernamePassword", resourceCulture); + } + } + /// /// Looks up a localized string similar to The duration spent while attempting to connect to this server was - [Pre-Login] initialization={0}; handshake={1}; [Login] initialization={2}; . /// @@ -9441,15 +9504,6 @@ internal class Strings { } } - /// - /// Looks up a localized string similar to Cannot use 'Authentication=Active Directory Device Code Flow' with 'User ID', 'UID', 'Password' or 'PWD' connection string keywords.. - /// - internal static string SQL_DeviceFlowWithUsernamePassword { - get { - return ResourceManager.GetString("SQL_DeviceFlowWithUsernamePassword", resourceCulture); - } - } - /// /// Looks up a localized string similar to Internal Error. /// @@ -9603,6 +9657,15 @@ internal class Strings { } } + /// + /// Looks up a localized string similar to Cannot use 'Authentication={0}' with 'Password' or 'PWD' connection string keywords.. + /// + internal static string SQL_ManagedIdentityWithPassword { + get { + return ResourceManager.GetString("SQL_ManagedIdentityWithPassword", resourceCulture); + } + } + /// /// Looks up a localized string similar to The connection does not support MultipleActiveResultSets.. /// @@ -9909,6 +9972,15 @@ internal class Strings { } } + /// + /// Looks up a localized string similar to Cannot set the Credential property if 'Authentication=Active Directory Device Code Flow' has been specified in the connection string.. + /// + internal static string SQL_SettingCredentialWithDeviceFlow { + get { + return ResourceManager.GetString("SQL_SettingCredentialWithDeviceFlow", resourceCulture); + } + } + /// /// Looks up a localized string similar to Cannot set the Credential property if 'Authentication=Active Directory Integrated' has been specified in the connection string.. /// @@ -9928,11 +10000,20 @@ internal class Strings { } /// - /// Looks up a localized string similar to Cannot set the Credential property if 'Authentication=Active Directory Device Code Flow' has been specified in the connection string.. + /// Looks up a localized string similar to Cannot set the Credential property if 'Authentication={0}' has been specified in the connection string.. /// - internal static string SQL_SettingCredentialWithDeviceFlow { + internal static string SQL_SettingCredentialWithManagedIdentity { get { - return ResourceManager.GetString("SQL_SettingCredentialWithDeviceFlow", resourceCulture); + return ResourceManager.GetString("SQL_SettingCredentialWithManagedIdentity", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cannot use 'Authentication=Active Directory Device Code Flow', if the Credential property has been set.. + /// + internal static string SQL_SettingDeviceFlowWithCredential { + get { + return ResourceManager.GetString("SQL_SettingDeviceFlowWithCredential", resourceCulture); } } @@ -9955,11 +10036,11 @@ internal class Strings { } /// - /// Looks up a localized string similar to Cannot use 'Authentication=Active Directory Device Code Flow', if the Credential property has been set.. + /// Looks up a localized string similar to Cannot use 'Authentication={0}', if the Credential property has been set.. /// - internal static string SQL_SettingDeviceFlowWithCredential { + internal static string SQL_SettingManagedIdentityWithCredential { get { - return ResourceManager.GetString("SQL_SettingDeviceFlowWithCredential", resourceCulture); + return ResourceManager.GetString("SQL_SettingManagedIdentityWithCredential", resourceCulture); } } @@ -10171,20 +10252,20 @@ internal class Strings { } /// - /// Looks up a localized string similar to Active Directory Interactive authentication timed out. The user took too long to respond to the authentication request.. + /// Looks up a localized string similar to Active Directory Device Code Flow authentication timed out. The user took too long to respond to the authentication request.. /// - internal static string SQL_Timeout_Active_Directory_Interactive_Authentication { + internal static string SQL_Timeout_Active_Directory_DeviceFlow_Authentication { get { - return ResourceManager.GetString("SQL_Timeout_Active_Directory_Interactive_Authentication", resourceCulture); + return ResourceManager.GetString("SQL_Timeout_Active_Directory_DeviceFlow_Authentication", resourceCulture); } } /// - /// Looks up a localized string similar to Active Directory Device Code Flow authentication timed out. The user took too long to respond to the authentication request.. + /// Looks up a localized string similar to Active Directory Interactive authentication timed out. The user took too long to respond to the authentication request.. /// - internal static string SQL_Timeout_Active_Directory_DeviceFlow_Authentication { + internal static string SQL_Timeout_Active_Directory_Interactive_Authentication { get { - return ResourceManager.GetString("SQL_Timeout_Active_Directory_DeviceFlow_Authentication", resourceCulture); + return ResourceManager.GetString("SQL_Timeout_Active_Directory_Interactive_Authentication", resourceCulture); } } diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.resx b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.resx index c74e4526ef..3c2d2c8e0d 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.resx +++ b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.resx @@ -2502,6 +2502,9 @@ Cannot use 'Authentication=Active Directory Device Code Flow' with 'User ID', 'UID', 'Password' or 'PWD' connection string keywords. + + Cannot use 'Authentication={0}' with 'Password' or 'PWD' connection string keywords. + Cannot use 'Authentication=Active Directory Integrated', if the Credential property has been set. @@ -4572,7 +4575,31 @@ Cannot set the Credential property if 'Authentication=Active Directory Device Code Flow' has been specified in the connection string. + + Cannot set the Credential property if 'Authentication={0}' has been specified in the connection string. + Cannot use 'Authentication=Active Directory Device Code Flow', if the Credential property has been set. - + + Cannot use 'Authentication={0}', if the Credential property has been set. + + + Access token could not be acquired. + + + Unable to connect to the Managed Identity endpoint. Please check that you are running on an Azure resource that has Identity setup. + + + Tried to get token using Managed Identity. + + + Unable to connect to the Instance Metadata Service (IMDS). Skipping request to the Managed Identity token endpoint. + + + Received a non-retryable error. + + + Failed after 5 retries. + + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ActiveDirectoryAuthenticationProvider.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ActiveDirectoryAuthenticationProvider.cs index fd3ddfe932..801b4df97a 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ActiveDirectoryAuthenticationProvider.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ActiveDirectoryAuthenticationProvider.cs @@ -97,6 +97,7 @@ public override void BeforeUnload(SqlAuthenticationMethod authentication) .Build(); result = ccApp.AcquireTokenForClient(scopes).ExecuteAsync().Result; + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token for Active Directory Service Principal auth mode. Expiry Time: {0}", result.ExpiresOn); return new SqlAuthenticationToken(result.AccessToken, result.ExpiresOn); } @@ -110,7 +111,7 @@ public override void BeforeUnload(SqlAuthenticationMethod authentication) string redirectURI = "https://login.microsoftonline.com/common/oauth2/nativeclient"; #if netcoreapp - if(parameters.AuthenticationMethod != SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow) + if (parameters.AuthenticationMethod != SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow) { redirectURI = "http://localhost"; } @@ -168,6 +169,7 @@ public override void BeforeUnload(SqlAuthenticationMethod authentication) .WithCorrelationId(parameters.ConnectionId) .ExecuteAsync().Result; } + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token for Active Directory Integrated auth mode. Expiry Time: {0}", result.ExpiresOn); } else if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryPassword) { @@ -178,6 +180,7 @@ public override void BeforeUnload(SqlAuthenticationMethod authentication) result = app.AcquireTokenByUsernamePassword(scopes, parameters.UserId, password) .WithCorrelationId(parameters.ConnectionId) .ExecuteAsync().Result; + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token for Active Directory Password auth mode. Expiry Time: {0}", result.ExpiresOn); } else if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryInteractive || parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow) @@ -198,19 +201,23 @@ public override void BeforeUnload(SqlAuthenticationMethod authentication) try { result = await app.AcquireTokenSilent(scopes, account).ExecuteAsync(); + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token (silent) for {0} auth mode. Expiry Time: {1}", parameters.AuthenticationMethod, result.ExpiresOn); } catch (MsalUiRequiredException) { result = await AcquireTokenInteractiveDeviceFlowAsync(app, scopes, parameters.ConnectionId, parameters.UserId, parameters.AuthenticationMethod); + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token (interactive) for {0} auth mode. Expiry Time: {1}", parameters.AuthenticationMethod, result.ExpiresOn); } } else { result = await AcquireTokenInteractiveDeviceFlowAsync(app, scopes, parameters.ConnectionId, parameters.UserId, parameters.AuthenticationMethod); + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token (interactive) for {0} auth mode. Expiry Time: {1}", parameters.AuthenticationMethod, result.ExpiresOn); } } else { + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | {0} authentication mode not supported by ActiveDirectoryAuthenticationProvider class.", parameters.AuthenticationMethod); throw SQL.UnsupportedAuthenticationSpecified(parameters.AuthenticationMethod); } @@ -280,6 +287,7 @@ public override void BeforeUnload(SqlAuthenticationMethod authentication) } catch (OperationCanceledException) { + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenInteractiveDeviceFlowAsync | Operation timed out while acquiring access token."); throw (authenticationMethod == SqlAuthenticationMethod.ActiveDirectoryInteractive) ? SQL.ActiveDirectoryInteractiveTimeout() : SQL.ActiveDirectoryDeviceFlowTimeout(); @@ -298,6 +306,7 @@ private Task DefaultDeviceFlowCallback(DeviceCodeResult result) // * The timeout specified by the server for the lifetime of this code (typically ~15 minutes) has been reached // * The developing application calls the Cancel() method on a CancellationToken sent into the method. // If this occurs, an OperationCanceledException will be thrown (see catch below for more details). + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenInteractiveDeviceFlowAsync | Callback triggered with Device Code Result: {0}", result.Message); Console.WriteLine(result.Message); return Task.FromResult(0); } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AzureManagedIdentityAuthenticationProvider.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AzureManagedIdentityAuthenticationProvider.cs new file mode 100644 index 0000000000..07174e96a8 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AzureManagedIdentityAuthenticationProvider.cs @@ -0,0 +1,248 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Net.Http; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Data.SqlClient +{ + internal sealed class AzureManagedIdentityAuthenticationProvider : SqlAuthenticationProvider + { + // HttpClient is intended to be instantiated once and re-used throughout the life of an application. +#if NETFRAMEWORK + private static readonly HttpClient s_httpClient = new HttpClient(); +#else + private static readonly HttpClient s_httpClient = new HttpClient(new HttpClientHandler() { CheckCertificateRevocationList = true }); +#endif + + private const string AzureSystemApiVersion = "&api-version=2019-08-01"; + private const string AzureVmImdsApiVersion = "&api-version=2018-02-01"; + private const string AccessToken = "access_token"; + private const string Expiry = "expires_on"; + private const int FileTimeLength = 10; + + private const int DefaultRetryTimeout = 0; + private const int DefaultMaxRetryCount = 5; + + // Azure Instance Metadata Service (IMDS) endpoint + private const string AzureVmImdsEndpoint = "http://169.254.169.254/metadata/identity/oauth2/token"; + + // Timeout for Azure IMDS probe request + internal const int AzureVmImdsProbeTimeoutInSeconds = 2; + internal readonly TimeSpan _azureVmImdsProbeTimeout = TimeSpan.FromSeconds(AzureVmImdsProbeTimeoutInSeconds); + + // Configurable timeout for MSI retry logic + internal readonly int _retryTimeoutInSeconds = DefaultRetryTimeout; + internal readonly int _maxRetryCount = DefaultMaxRetryCount; + + // Reference: https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-http + public override async Task AcquireTokenAsync(SqlAuthenticationParameters parameters) + { + HttpClient httpClient = s_httpClient; + + try + { + // Check if App Services MSI is available. If both these environment variables are set, then it is. + string msiEndpoint = Environment.GetEnvironmentVariable("IDENTITY_ENDPOINT"); + string msiHeader = Environment.GetEnvironmentVariable("IDENTITY_HEADER"); + + var isAppServicesMsiAvailable = !string.IsNullOrWhiteSpace(msiEndpoint) && !string.IsNullOrWhiteSpace(msiHeader); + + // if App Service MSI is not available then Azure VM IMDS may be available, test with a probe request + if (!isAppServicesMsiAvailable) + { + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | This environment is not identified as an Azure App Service environment. Proceeding to validate Azure VM IMDS endpoint availability."); + using (var internalTokenSource = new CancellationTokenSource()) + using (var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(internalTokenSource.Token, default)) + { + HttpRequestMessage imdsProbeRequest = new HttpRequestMessage(HttpMethod.Get, AzureVmImdsEndpoint); + + try + { + internalTokenSource.CancelAfter(_azureVmImdsProbeTimeout); + await httpClient.SendAsync(imdsProbeRequest, linkedTokenSource.Token).ConfigureAwait(false); + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | The Instance Metadata Service (IMDS) service endpoint is accessible. Proceeding to acquire access token."); + } + catch (OperationCanceledException) + { + // request to IMDS timed out (internal cancellation token canceled), neither Azure VM IMDS nor App Services MSI are available + if (internalTokenSource.Token.IsCancellationRequested) + { + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | The Instance Metadata Service (IMDS) service endpoint is not accessible."); + // Throw error: Tried to get token using Managed Identity. Unable to connect to the Instance Metadata Service (IMDS). Skipping request to the Managed Service Identity (MSI) token endpoint. + throw SQL.Azure_ManagedIdentityException($"{Strings.Azure_ManagedIdentityUsed} {Strings.Azure_MetadataEndpointNotListening}"); + } + + throw; + } + } + } + else + { + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | This environment is identified as an Azure App Service environment. Proceeding to acquire access token from Endpoint URL: {0}", msiEndpoint); + } + + string objectIdParameter = string.Empty; + + // If user assigned managed identity is specified, include object ID parameter in request + if (parameters.UserId != default) + { + objectIdParameter = $"&object_id={Uri.EscapeDataString(parameters.UserId)}"; + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Identity Object id received and will be used for acquiring access token {0}", parameters.UserId); + } + + // Craft request as per the MSI protocol + var requestUrl = isAppServicesMsiAvailable + ? $"{msiEndpoint}?resource={parameters.Resource}{objectIdParameter}{AzureSystemApiVersion}" + : $"{AzureVmImdsEndpoint}?resource={parameters.Resource}{objectIdParameter}{AzureVmImdsApiVersion}"; + + HttpResponseMessage response = null; + + try + { + response = await httpClient.SendAsyncWithRetry(getRequestMessage, _retryTimeoutInSeconds, _maxRetryCount, default).ConfigureAwait(false); + HttpRequestMessage getRequestMessage() + { + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUrl); + + if (isAppServicesMsiAvailable) + { + request.Headers.Add("X-IDENTITY-HEADER", msiHeader); + } + else + { + request.Headers.Add("Metadata", "true"); + } + + return request; + } + } + catch (HttpRequestException) + { + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Failed after 5 retries. Unable to connect to the Managed Service Identity (MSI) endpoint."); + // Throw error: Tried to get token using Managed Service Identity. Failed after 5 retries. Unable to connect to the Managed Service Identity (MSI) endpoint. Please check that you are running on an Azure resource that has MSI setup. + throw SQL.Azure_ManagedIdentityException($"{Strings.Azure_ManagedIdentityUsed} {Strings.Azure_RetryFailure} {Strings.Azure_IdentityEndpointNotListening}"); + } + + // If the response is successful, it should have JSON response with an access_token field + if (response.IsSuccessStatusCode) + { + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Successful response received. Status Code {0}", response.StatusCode); + string jsonResponse = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + int accessTokenStartIndex = jsonResponse.IndexOf(AccessToken) + AccessToken.Length + 3; + var imdsAccessToken = jsonResponse.Substring(accessTokenStartIndex, jsonResponse.IndexOf('"', accessTokenStartIndex) - accessTokenStartIndex); + var expiresin = jsonResponse.Substring(jsonResponse.IndexOf(Expiry) + Expiry.Length + 3, FileTimeLength); + DateTime expiryTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(long.Parse(expiresin)); + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Access Token received. Expiry Time: {0}", expiryTime); + return new SqlAuthenticationToken(imdsAccessToken, expiryTime); + } + + // RetryFailure : Failed after 5 retries. + // NonRetryableError : Received a non-retryable error. + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Request to acquire access token failed with status code {0}", response.StatusCode); + string errorStatusDetail = response.IsRetryableStatusCode() + ? Strings.Azure_RetryFailure + : Strings.Azure_NonRetryableError; + + string errorText = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Error occurred while acquiring access token: {0} Identity Response Code: {1}, Response: {2}", errorStatusDetail, response.StatusCode, errorText); + throw SQL.Azure_ManagedIdentityException($"{errorStatusDetail} Identity Response Code: {response.StatusCode}, Response: {errorText}"); + } + catch (Exception exp) + { + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Error occurred while acquiring access token: {0}", exp.Message); + if (exp is SqlException) + throw; + // Throw error: Access token could not be acquired. {exp.Message} + throw SQL.Azure_ManagedIdentityException($"{Strings.Azure_ManagedIdentityUsed} {Strings.Azure_GenericErrorMessage} {exp.Message}"); + } + } + + public override bool IsSupported(SqlAuthenticationMethod authentication) + { + return authentication == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity + || authentication == SqlAuthenticationMethod.ActiveDirectoryMSI; + } + } + + #region IMDS Retry Helper + internal static class SqlManagedIdentityRetryHelper + { + internal const int DeltaBackOffInSeconds = 2; + internal const string RetryTimeoutError = "Reached retry timeout limit set by MsiRetryTimeout parameter in connection string."; + + internal static bool IsRetryableStatusCode(this HttpResponseMessage response) + { + // 404 NotFound, 429 TooManyRequests, and 5XX server error status codes are retryable + return Regex.IsMatch(((int)response.StatusCode).ToString(), @"404|429|5\d{2}"); + } + + /// + /// Implements recommended retry guidance here: https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token#retry-guidance + /// + internal static async Task SendAsyncWithRetry(this HttpClient httpClient, Func getRequest, int retryTimeoutInSeconds, int maxRetryCount, CancellationToken cancellationToken) + { + using (var timeoutTokenSource = new CancellationTokenSource()) + using (var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutTokenSource.Token, cancellationToken)) + { + try + { + // if retry timeout is configured, configure cancellation after timeout period elapses + if (retryTimeoutInSeconds > 0) + { + timeoutTokenSource.CancelAfter(TimeSpan.FromSeconds(retryTimeoutInSeconds)); + } + + var attempts = 0; + var backoffTimeInSecs = 0; + HttpResponseMessage response; + + while (true) + { + attempts++; + + try + { + response = await httpClient.SendAsync(getRequest(), linkedTokenSource.Token).ConfigureAwait(false); + + if (response.IsSuccessStatusCode || !response.IsRetryableStatusCode() || attempts == maxRetryCount) + { + break; + } + } + catch (HttpRequestException e) + { + if (attempts == maxRetryCount) + { + throw; + } + SqlClientEventSource.Log.TryTraceEvent("SendAsyncWithRetry | Exception occurred {0} | Attempting retry: {1} of {2}", e.Message, attempts, maxRetryCount); + } + + // use recommended exponential backoff strategy, and use linked token wait handle so caller or retry timeout is still able to cancel + backoffTimeInSecs += (int)Math.Pow(DeltaBackOffInSeconds, attempts); + linkedTokenSource.Token.WaitHandle.WaitOne(TimeSpan.FromSeconds(backoffTimeInSecs)); + linkedTokenSource.Token.ThrowIfCancellationRequested(); + } + + return response; + } + catch (OperationCanceledException) + { + if (timeoutTokenSource.IsCancellationRequested) + { + throw new TimeoutException(RetryTimeoutError); + } + + throw; + } + } + } + } + #endregion +} diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AADAuthenticationTests.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AADAuthenticationTests.cs index 0e695cdd4e..18997a4d86 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AADAuthenticationTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AADAuthenticationTests.cs @@ -14,6 +14,7 @@ public class AADAuthenticationTests { private SqlConnectionStringBuilder _builder; private SqlCredential _credential = null; + [Theory] [InlineData("Test combination of Access Token and IntegratedSecurity", new object[] { "Integrated Security", true })] [InlineData("Test combination of Access Token and User Id", new object[] { "UID", "sampleUserId" })] @@ -57,6 +58,22 @@ public void CustomActiveDirectoryProviderTest() Assert.Equal(authProvider, SqlAuthenticationProvider.GetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow)); } + [Fact] + public void CustomActiveDirectoryProviderTest_AppClientId() + { + SqlAuthenticationProvider authProvider = new ActiveDirectoryAuthenticationProvider(Guid.NewGuid().ToString()); + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, authProvider); + Assert.Equal(authProvider, SqlAuthenticationProvider.GetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow)); + } + + [Fact] + public void CustomActiveDirectoryProviderTest_AppClientId_DeviceFlowCallback() + { + SqlAuthenticationProvider authProvider = new ActiveDirectoryAuthenticationProvider(CustomDeviceFlowCallback, Guid.NewGuid().ToString()); + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, authProvider); + Assert.Equal(authProvider, SqlAuthenticationProvider.GetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow)); + } + private Task CustomDeviceFlowCallback(DeviceCodeResult result) { Console.WriteLine(result.Message); diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionStringBuilderTest.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionStringBuilderTest.cs index 9f6061e40c..dc85dab6dd 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionStringBuilderTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionStringBuilderTest.cs @@ -17,6 +17,18 @@ public class SqlConnectionStringBuilderTest [InlineData("Attestation Protocol = HGS")] [InlineData("Authentication = Active Directory Password ")] [InlineData("Authentication = ActiveDirectoryPassword ")] + [InlineData("Authentication = Active Directory Integrated ")] + [InlineData("Authentication = ActiveDirectoryIntegrated ")] + [InlineData("Authentication = Active Directory Interactive ")] + [InlineData("Authentication = ActiveDirectoryInteractive ")] + [InlineData("Authentication = Active Directory Device Code Flow ")] + [InlineData("Authentication = ActiveDirectoryDeviceCodeFlow ")] + [InlineData("Authentication = Active Directory Service Principal ")] + [InlineData("Authentication = ActiveDirectoryServicePrincipal ")] + [InlineData("Authentication = Active Directory Managed Identity ")] + [InlineData("Authentication = ActiveDirectoryManagedIdentity ")] + [InlineData("Authentication = Active Directory MSI ")] + [InlineData("Authentication = ActiveDirectoryMSI ")] [InlineData("Command Timeout = 5")] [InlineData("Command Timeout = 15")] [InlineData("Command Timeout = 0")] @@ -55,7 +67,8 @@ public class SqlConnectionStringBuilderTest [InlineData("PersistSecurityInfo = true")] [InlineData("Pooling = no")] [InlineData("Pooling = false")] -#if netcoreapp // PoolBlockingPeriod is not supported in .NET Standard +#if netcoreapp + // PoolBlockingPeriod is not supported in .NET Standard [InlineData("PoolBlockingPeriod = Auto")] [InlineData("PoolBlockingperiod = NeverBlock")] #endif @@ -74,10 +87,6 @@ public void ConnectionStringTests(string connectionString) [Theory] [InlineData("Asynchronous Processing = True")] - [InlineData("Authentication = Active Directory Integrated ")] - [InlineData("Authentication = ActiveDirectoryIntegrated ")] - [InlineData("Authentication = Active Directory Interactive ")] - [InlineData("Authentication = ActiveDirectoryInteractive ")] [InlineData("Context Connection = false")] [InlineData("Network Library = dbmssocn")] [InlineData("Network = dbnmpntw")] diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/AADUtility.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/AADUtility.cs index 5e437322fb..228217c039 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/AADUtility.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/AADUtility.cs @@ -3,6 +3,9 @@ // See the LICENSE file in the project root for more information. using System; +using System.Net.Http; +using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; using Microsoft.IdentityModel.Clients.ActiveDirectory; @@ -17,10 +20,179 @@ public static async Task AzureActiveDirectoryAuthenticationCallback(stri AuthenticationResult result = await authContext.AcquireTokenAsync(resource, clientCred); if (result == null) { - throw new InvalidOperationException($"Failed to retrieve an access token for {resource}"); + throw new Exception($"Failed to retrieve an access token for {resource}"); } return result.AccessToken; } + + public static async Task GetManagedIdentityToken(string objectId = null) => + await new MockManagedIdentityTokenProvider().AcquireTokenAsync(objectId).ConfigureAwait(false); + + } + + #region Mock Managed Identity Token Provider + internal class MockManagedIdentityTokenProvider + { + // HttpClient is intended to be instantiated once and re-used throughout the life of an application. +#if NETFRAMEWORK + private static readonly HttpClient s_defaultHttpClient = new HttpClient(); +#else + private static readonly HttpClient s_defaultHttpClient = new HttpClient(new HttpClientHandler() { CheckCertificateRevocationList = true }); +#endif + + private const string AzureVmImdsApiVersion = "&api-version=2018-02-01"; + private const string AccessToken = "access_token"; + private const string Resource = "https://database.windows.net/"; + + private const int DefaultRetryTimeout = 0; + private const int DefaultMaxRetryCount = 5; + + // Azure Instance Metadata Service (IMDS) endpoint + private const string AzureVmImdsEndpoint = "http://169.254.169.254/metadata/identity/oauth2/token"; + + // Timeout for Azure IMDS probe request + internal const int AzureVmImdsProbeTimeoutInSeconds = 2; + internal readonly TimeSpan _azureVmImdsProbeTimeout = TimeSpan.FromSeconds(AzureVmImdsProbeTimeoutInSeconds); + + // Configurable timeout for MSI retry logic + internal readonly int _retryTimeoutInSeconds = DefaultRetryTimeout; + internal readonly int _maxRetryCount = DefaultMaxRetryCount; + + public async Task AcquireTokenAsync(string objectId = null) + { + // Use the httpClient specified in the constructor. If it was not specified in the constructor, use the default httpClient. + HttpClient httpClient = s_defaultHttpClient; + + try + { + // If user assigned managed identity is specified, include object ID parameter in request + string objectIdParameter = objectId != null + ? $"&object_id={objectId}" + : string.Empty; + + // Craft request as per the MSI protocol + var requestUrl = $"{AzureVmImdsEndpoint}?resource={Resource}{objectIdParameter}{AzureVmImdsApiVersion}"; + + HttpResponseMessage response = null; + + try + { + response = await httpClient.SendAsyncWithRetry(getRequestMessage, _retryTimeoutInSeconds, _maxRetryCount, default).ConfigureAwait(false); + HttpRequestMessage getRequestMessage() + { + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUrl); + request.Headers.Add("Metadata", "true"); + return request; + } + } + catch (HttpRequestException) + { + // Not throwing exception if Access Token cannot be fetched. Tests will be disabled. + return null; + } + + // If the response is successful, it should have JSON response with an access_token field + if (response.IsSuccessStatusCode) + { + string jsonResponse = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + int accessTokenStartIndex = jsonResponse.IndexOf(AccessToken) + AccessToken.Length + 3; + return jsonResponse.Substring(accessTokenStartIndex, jsonResponse.IndexOf('"', accessTokenStartIndex) - accessTokenStartIndex); + } + + // RetryFailure : Failed after 5 retries. + // NonRetryableError : Received a non-retryable error. + string errorStatusDetail = response.IsRetryableStatusCode() + ? "Failed after 5 retries" + : "Received a non-retryable error."; + + string errorText = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + + // Not throwing exception if Access Token cannot be fetched. Tests will be disabled. + return null; + } + catch (Exception) + { + // Not throwing exception if Access Token cannot be fetched. Tests will be disabled. + return null; + } + } + } + + #region IMDS Retry Helper + internal static class SqlManagedIdentityRetryHelper + { + internal const int DeltaBackOffInSeconds = 2; + internal const string RetryTimeoutError = "Reached retry timeout limit set by MsiRetryTimeout parameter in connection string."; + + internal static bool IsRetryableStatusCode(this HttpResponseMessage response) + { + // 404 NotFound, 429 TooManyRequests, and 5XX server error status codes are retryable + return Regex.IsMatch(((int)response.StatusCode).ToString(), @"404|429|5\d{2}"); + } + + /// + /// Implements recommended retry guidance here: https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token#retry-guidance + /// + internal static async Task SendAsyncWithRetry(this HttpClient httpClient, Func getRequest, int retryTimeoutInSeconds, int maxRetryCount, CancellationToken cancellationToken) + { + using (var timeoutTokenSource = new CancellationTokenSource()) + using (var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutTokenSource.Token, cancellationToken)) + { + try + { + // if retry timeout is configured, configure cancellation after timeout period elapses + if (retryTimeoutInSeconds > 0) + { + timeoutTokenSource.CancelAfter(TimeSpan.FromSeconds(retryTimeoutInSeconds)); + } + + var attempts = 0; + var backoffTimeInSecs = 0; + HttpResponseMessage response; + + while (true) + { + attempts++; + + try + { + response = await httpClient.SendAsync(getRequest(), linkedTokenSource.Token).ConfigureAwait(false); + + if (response.IsSuccessStatusCode || !response.IsRetryableStatusCode() || attempts == maxRetryCount) + { + break; + } + } + catch (HttpRequestException) + { + if (attempts == maxRetryCount) + { + throw; + } + } + + // use recommended exponential backoff strategy, and use linked token wait handle so caller or retry timeout is still able to cancel + backoffTimeInSecs += (int)Math.Pow(DeltaBackOffInSeconds, attempts); + linkedTokenSource.Token.WaitHandle.WaitOne(TimeSpan.FromSeconds(backoffTimeInSecs)); + linkedTokenSource.Token.ThrowIfCancellationRequested(); + } + + return response; + } + catch (OperationCanceledException) + { + if (timeoutTokenSource.IsCancellationRequested) + { + throw new TimeoutException(RetryTimeoutError); + } + + throw; + } + } + } } + #endregion + #endregion } + diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs index 65727fbc66..81f2689bd3 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs @@ -47,12 +47,16 @@ public static class DataTestUtility public static readonly string DNSCachingServerTR = null; // this is for the tenant ring public static readonly bool IsDNSCachingSupportedCR = false; // this is for the control ring public static readonly bool IsDNSCachingSupportedTR = false; // this is for the tenant ring + public static readonly string UserManagedIdentityObjectId = null; public static readonly string EnclaveAzureDatabaseConnString = null; - + public static bool ManagedIdentitySupported = true; public static string AADAccessToken = null; + public static string AADSystemIdentityAccessToken = null; + public static string AADUserIdentityAccessToken = null; public const string UdtTestDbName = "UdtTestDb"; public const string AKVKeyName = "TestSqlClientAzureKeyVaultProvider"; + private const string ManagedNetworkingAppContextSwitch = "Switch.Microsoft.Data.SqlClient.UseManagedNetworkingOnWindows"; private static Dictionary AvailableDatabases; @@ -83,6 +87,7 @@ static DataTestUtility() IsDNSCachingSupportedCR = c.IsDNSCachingSupportedCR; IsDNSCachingSupportedTR = c.IsDNSCachingSupportedTR; EnclaveAzureDatabaseConnString = c.EnclaveAzureDatabaseConnString; + UserManagedIdentityObjectId = c.UserManagedIdentityObjectId; System.Net.ServicePointManager.SecurityProtocol |= System.Net.SecurityProtocolType.Tls12; @@ -403,8 +408,39 @@ public static string GetAccessToken() return (null != AADAccessToken) ? new string(AADAccessToken.ToCharArray()) : null; } + public static string GetSystemIdentityAccessToken() + { + if (true == ManagedIdentitySupported && null == AADSystemIdentityAccessToken && IsAADPasswordConnStrSetup()) + { + AADSystemIdentityAccessToken = AADUtility.GetManagedIdentityToken().GetAwaiter().GetResult(); + if (AADSystemIdentityAccessToken == null) + { + ManagedIdentitySupported = false; + } + } + return (null != AADSystemIdentityAccessToken) ? new string(AADSystemIdentityAccessToken.ToCharArray()) : null; + } + + public static string GetUserIdentityAccessToken() + { + if (true == ManagedIdentitySupported && null == AADUserIdentityAccessToken && IsAADPasswordConnStrSetup()) + { + // Pass User Assigned Managed Identity Object Id here. + AADUserIdentityAccessToken = AADUtility.GetManagedIdentityToken(UserManagedIdentityObjectId).GetAwaiter().GetResult(); + if (AADUserIdentityAccessToken == null) + { + ManagedIdentitySupported = false; + } + } + return (null != AADUserIdentityAccessToken) ? new string(AADUserIdentityAccessToken.ToCharArray()) : null; + } + public static bool IsAccessTokenSetup() => !string.IsNullOrEmpty(GetAccessToken()); + public static bool IsSystemIdentityTokenSetup() => !string.IsNullOrEmpty(GetSystemIdentityAccessToken()); + + public static bool IsUserIdentityTokenSetup() => !string.IsNullOrEmpty(GetUserIdentityAccessToken()); + public static bool IsFileStreamSetup() => SupportsFileStream; private static bool CheckException(Exception ex, string exceptionMessage, bool innerExceptionMustBeNull) where TException : Exception diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AsyncTest/AsyncCancelledConnectionsTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AsyncTest/AsyncCancelledConnectionsTest.cs index c3cc2d6b07..dfcd78bc15 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AsyncTest/AsyncCancelledConnectionsTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AsyncTest/AsyncCancelledConnectionsTest.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -18,10 +17,11 @@ public class AsyncCancelledConnectionsTest public AsyncCancelledConnectionsTest(ITestOutputHelper output) { - this._output = output; + _output = output; } - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + // Disabled on Azure since this test fails on concurrent runs on same database. + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer))] public void CancelAsyncConnections() { string connectionString = DataTestUtility.TCPConnectionString; diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/AADConnectionTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/AADConnectionTest.cs index 68919c39eb..2e58b0df04 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/AADConnectionTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/AADConnectionTest.cs @@ -20,11 +20,16 @@ private static void ConnectAndDisconnect(string connectionString, SqlCredential conn.Credential = credential; } conn.Open(); + + Assert.True(conn.State == System.Data.ConnectionState.Open); } } + private static bool AreConnStringsSetup() => DataTestUtility.AreConnStringsSetup(); + private static bool IsAzure() => !DataTestUtility.IsNotAzureServer(); private static bool IsAccessTokenSetup() => DataTestUtility.IsAccessTokenSetup(); private static bool IsAADConnStringsSetup() => DataTestUtility.IsAADPasswordConnStrSetup(); + private static bool IsManagedIdentitySetup() => DataTestUtility.ManagedIdentitySupported; [ConditionalFact(nameof(IsAccessTokenSetup), nameof(IsAADConnStringsSetup))] public static void AccessTokenTest() @@ -37,6 +42,8 @@ public static void AccessTokenTest() { connection.AccessToken = DataTestUtility.GetAccessToken(); connection.Open(); + + Assert.True(connection.State == System.Data.ConnectionState.Open); } } @@ -355,6 +362,92 @@ public static void ActiveDirectoryDeviceCodeFlowWithCredentialsMustFail() Assert.Contains(expectedMessage, e.Message); } + [ConditionalFact(nameof(IsAADConnStringsSetup))] + public static void ActiveDirectoryManagedIdentityWithCredentialsMustFail() + { + // connection fails with expected error message. + string[] credKeys = { "Authentication", "User ID", "Password", "UID", "PWD" }; + string connStrWithNoCred = DataTestUtility.RemoveKeysInConnStr(DataTestUtility.AADPasswordConnectionString, credKeys) + + "Authentication=Active Directory Managed Identity;"; + + SecureString str = new SecureString(); + foreach (char c in "hello") + { + str.AppendChar(c); + } + str.MakeReadOnly(); + SqlCredential credential = new SqlCredential("someuser", str); + InvalidOperationException e = Assert.Throws(() => ConnectAndDisconnect(connStrWithNoCred, credential)); + + string expectedMessage = "Cannot set the Credential property if 'Authentication=Active Directory Managed Identity' has been specified in the connection string."; + Assert.Contains(expectedMessage, e.Message); + } + + [ConditionalFact(nameof(IsAADConnStringsSetup))] + public static void ActiveDirectoryManagedIdentityWithPasswordMustFail() + { + // connection fails with expected error message. + string[] credKeys = { "Authentication", "User ID", "Password", "UID", "PWD" }; + string connStrWithNoCred = DataTestUtility.RemoveKeysInConnStr(DataTestUtility.AADPasswordConnectionString, credKeys) + + "Authentication=Active Directory Managed Identity; Password=anything"; + + ArgumentException e = Assert.Throws(() => ConnectAndDisconnect(connStrWithNoCred)); + + string expectedMessage = "Cannot use 'Authentication=Active Directory Managed Identity' with 'Password' or 'PWD' connection string keywords."; + Assert.Contains(expectedMessage, e.Message); + } + + [InlineData("2445343 2343253")] + [InlineData("2445343$#^@@%2343253")] + [ConditionalTheory(nameof(IsAADConnStringsSetup))] + public static void ActiveDirectoryManagedIdentityWithInvalidUserIdMustFail(string userId) + { + // connection fails with expected error message. + string[] credKeys = { "Authentication", "User ID", "Password", "UID", "PWD" }; + string connStrWithNoCred = DataTestUtility.RemoveKeysInConnStr(DataTestUtility.AADPasswordConnectionString, credKeys) + + $"Authentication=Active Directory Managed Identity; User Id={userId}"; + + AggregateException e = Assert.Throws(() => ConnectAndDisconnect(connStrWithNoCred)); + + string expectedMessage = "Response: {\"error\":\"invalid_request\",\"error_description\":\"Identity not found\"}"; + Assert.Contains(expectedMessage, e.GetBaseException().Message); + } + + [ConditionalFact(nameof(IsAADConnStringsSetup))] + public static void ActiveDirectoryMSIWithCredentialsMustFail() + { + // connection fails with expected error message. + string[] credKeys = { "Authentication", "User ID", "Password", "UID", "PWD" }; + string connStrWithNoCred = DataTestUtility.RemoveKeysInConnStr(DataTestUtility.AADPasswordConnectionString, credKeys) + + "Authentication=Active Directory MSI;"; + + SecureString str = new SecureString(); + foreach (char c in "hello") + { + str.AppendChar(c); + } + str.MakeReadOnly(); + SqlCredential credential = new SqlCredential("someuser", str); + InvalidOperationException e = Assert.Throws(() => ConnectAndDisconnect(connStrWithNoCred, credential)); + + string expectedMessage = "Cannot set the Credential property if 'Authentication=Active Directory MSI' has been specified in the connection string."; + Assert.Contains(expectedMessage, e.Message); + } + + [ConditionalFact(nameof(IsAADConnStringsSetup))] + public static void ActiveDirectoryMSIWithPasswordMustFail() + { + // connection fails with expected error message. + string[] credKeys = { "Authentication", "User ID", "Password", "UID", "PWD" }; + string connStrWithNoCred = DataTestUtility.RemoveKeysInConnStr(DataTestUtility.AADPasswordConnectionString, credKeys) + + "Authentication=ActiveDirectoryMSI; Password=anything"; + + ArgumentException e = Assert.Throws(() => ConnectAndDisconnect(connStrWithNoCred)); + + string expectedMessage = "Cannot use 'Authentication=Active Directory MSI' with 'Password' or 'PWD' connection string keywords."; + Assert.Contains(expectedMessage, e.Message); + } + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsIntegratedSecuritySetup), nameof(DataTestUtility.AreConnStringsSetup))] public static void ADInteractiveUsingSSPI() { @@ -393,5 +486,112 @@ public static void ConnectionSpeed() } } } + + #region Managed Identity Authentication tests + + [ConditionalFact(nameof(IsAADConnStringsSetup), nameof(IsManagedIdentitySetup))] + public static void SystemAssigned_ManagedIdentityTest() + { + string[] removeKeys = { "Authentication", "User ID", "Password", "UID", "PWD" }; + string connStr = DataTestUtility.RemoveKeysInConnStr(DataTestUtility.AADPasswordConnectionString, removeKeys) + + $"Authentication=Active Directory Managed Identity;"; + ConnectAndDisconnect(connStr); + } + + [ConditionalFact(nameof(IsAADConnStringsSetup), nameof(IsManagedIdentitySetup))] + public static void UserAssigned_ManagedIdentityTest() + { + string[] removeKeys = { "Authentication", "User ID", "Password", "UID", "PWD" }; + string connStr = DataTestUtility.RemoveKeysInConnStr(DataTestUtility.AADPasswordConnectionString, removeKeys) + + $"Authentication=Active Directory Managed Identity; User Id={DataTestUtility.UserManagedIdentityObjectId};"; + ConnectAndDisconnect(connStr); + } + + [ConditionalFact(nameof(IsAADConnStringsSetup), nameof(IsManagedIdentitySetup))] + public static void AccessToken_SystemManagedIdentityTest() + { + string[] removeKeys = { "Authentication", "User ID", "Password", "UID", "PWD" }; + string connectionString = DataTestUtility.RemoveKeysInConnStr(DataTestUtility.AADPasswordConnectionString, removeKeys); + using (SqlConnection conn = new SqlConnection(connectionString)) + { + conn.AccessToken = DataTestUtility.GetSystemIdentityAccessToken(); + conn.Open(); + + Assert.True(conn.State == System.Data.ConnectionState.Open); + } + } + + [ConditionalFact(nameof(IsAADConnStringsSetup), nameof(IsManagedIdentitySetup))] + public static void AccessToken_UserManagedIdentityTest() + { + string[] removeKeys = { "Authentication", "User ID", "Password", "UID", "PWD" }; + string connectionString = DataTestUtility.RemoveKeysInConnStr(DataTestUtility.AADPasswordConnectionString, removeKeys); + using (SqlConnection conn = new SqlConnection(connectionString)) + { + conn.AccessToken = DataTestUtility.GetUserIdentityAccessToken(); + conn.Open(); + + Assert.True(conn.State == System.Data.ConnectionState.Open); + } + } + + [ConditionalFact(nameof(AreConnStringsSetup), nameof(IsAzure))] + public static void Azure_SystemManagedIdentityTest() + { + string[] removeKeys = { "Authentication", "User ID", "Password", "UID", "PWD", "Trusted_Connection", "Integrated Security" }; + string connectionString = DataTestUtility.RemoveKeysInConnStr(DataTestUtility.TCPConnectionString, removeKeys) + + $"Authentication=Active Directory Managed Identity;"; + + using (SqlConnection conn = new SqlConnection(connectionString)) + { + conn.Open(); + + Assert.True(conn.State == System.Data.ConnectionState.Open); + } + } + + [ConditionalFact(nameof(AreConnStringsSetup), nameof(IsAzure))] + public static void Azure_UserManagedIdentityTest() + { + string[] removeKeys = { "Authentication", "User ID", "Password", "UID", "PWD", "Trusted_Connection", "Integrated Security" }; + string connectionString = DataTestUtility.RemoveKeysInConnStr(DataTestUtility.TCPConnectionString, removeKeys) + + $"Authentication=Active Directory Managed Identity; User Id={DataTestUtility.UserManagedIdentityObjectId}"; + + using (SqlConnection conn = new SqlConnection(connectionString)) + { + conn.Open(); + + Assert.True(conn.State == System.Data.ConnectionState.Open); + } + } + + [ConditionalFact(nameof(AreConnStringsSetup), nameof(IsAzure))] + public static void Azure_AccessToken_SystemManagedIdentityTest() + { + string[] removeKeys = { "Authentication", "User ID", "Password", "UID", "PWD", "Trusted_Connection", "Integrated Security" }; + string connectionString = DataTestUtility.RemoveKeysInConnStr(DataTestUtility.TCPConnectionString, removeKeys); + using (SqlConnection conn = new SqlConnection(connectionString)) + { + conn.AccessToken = DataTestUtility.GetSystemIdentityAccessToken(); + conn.Open(); + + Assert.True(conn.State == System.Data.ConnectionState.Open); + } + } + + [ConditionalFact(nameof(AreConnStringsSetup), nameof(IsAzure))] + public static void Azure_AccessToken_UserManagedIdentityTest() + { + string[] removeKeys = { "Authentication", "User ID", "Password", "UID", "PWD", "Trusted_Connection", "Integrated Security" }; + string connectionString = DataTestUtility.RemoveKeysInConnStr(DataTestUtility.TCPConnectionString, removeKeys); + using (SqlConnection conn = new SqlConnection(connectionString)) + { + conn.AccessToken = DataTestUtility.GetUserIdentityAccessToken(); + conn.Open(); + + Assert.True(conn.State == System.Data.ConnectionState.Open); + } + } + #endregion } } diff --git a/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/Config.cs b/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/Config.cs index f0e71edadb..e5d6100f63 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/Config.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/Config.cs @@ -35,6 +35,7 @@ public class Config public bool IsDNSCachingSupportedCR = false; // this is for the control ring public bool IsDNSCachingSupportedTR = false; // this is for the tenant ring public string EnclaveAzureDatabaseConnString = null; + public string UserManagedIdentityObjectId = null; public static Config Load(string configPath = @"config.json") { diff --git a/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/config.default.json b/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/config.default.json index 2d1fa10a65..1d23281346 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/config.default.json +++ b/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/config.default.json @@ -18,5 +18,6 @@ "SupportsFileStream": false, "UseManagedSNIOnWindows": false, "IsAzureSynapse": false, - "EnclaveAzureDatabaseConnString": "" + "EnclaveAzureDatabaseConnString": "", + "UserManagedIdentityObjectId": "" }