提交 9786b737 编写于 作者: J Joel Braun 提交者: Saurabh Singh

Add SqlCredential dependent APIs (dotnet/corefx#27645)

* Initial port of SqlCredential apis

* Remove unused var

* Update logic and tests based on feedback

* Update ChangePassword method signature

* Additional style and functional changes

* Additonal IsYukonOrNewer removals

* Remove another unnecessary service reference


Commit migrated from https://github.com/dotnet/corefx/commit/86f5fb24b56b2333a9542bb6d80db0128a0b119e
上级 0d8f438b
......@@ -459,6 +459,7 @@ public sealed partial class SqlConnection : System.Data.Common.DbConnection, Sys
{
public SqlConnection() { }
public SqlConnection(string connectionString) { }
public SqlConnection(string connectionString, System.Data.SqlClient.SqlCredential credential) { }
public System.Guid ClientConnectionId { get { throw null; } }
object ICloneable.Clone() { throw null; }
public override string ConnectionString { get { throw null; } set { } }
......@@ -471,6 +472,7 @@ public sealed partial class SqlConnection : System.Data.Common.DbConnection, Sys
public override System.Data.ConnectionState State { get { throw null; } }
public bool StatisticsEnabled { get { throw null; } set { } }
public string WorkstationId { get { throw null; } }
public System.Data.SqlClient.SqlCredential Credential { get { throw null; } set { } }
public event System.Data.SqlClient.SqlInfoMessageEventHandler InfoMessage { add { } remove { } }
protected override System.Data.Common.DbTransaction BeginDbTransaction(System.Data.IsolationLevel isolationLevel) { throw null; }
public new System.Data.SqlClient.SqlTransaction BeginTransaction() { throw null; }
......@@ -490,6 +492,9 @@ public sealed partial class SqlConnection : System.Data.Common.DbConnection, Sys
public override System.Threading.Tasks.Task OpenAsync(System.Threading.CancellationToken cancellationToken) { throw null; }
public void ResetStatistics() { }
public System.Collections.IDictionary RetrieveStatistics() { throw null; }
public static void ChangePassword(string connectionString, string newPassword) { throw null; }
public static void ChangePassword(string connectionString, System.Data.SqlClient.SqlCredential credential, System.Security.SecureString newPassword) { throw null; }
}
public sealed partial class SqlConnectionStringBuilder : System.Data.Common.DbConnectionStringBuilder
{
......
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
......@@ -1091,7 +1150,7 @@
<value>TCP Provider</value>
</data>
<data name="SNI_PN8" xml:space="preserve">
<value></value>
<value />
</data>
<data name="SNI_PN9" xml:space="preserve">
<value>SQL Network Interfaces</value>
......@@ -1219,4 +1278,22 @@
<data name="ADP_MustBeReadOnly" xml:space="preserve">
<value>{0} must be marked as read only.</value>
</data>
</root>
<data name="ADP_InvalidMixedUsageOfSecureAndClearCredential" xml:space="preserve">
<value>Cannot use Credential with UserID, UID, Password, or PWD connection string keywords.</value>
</data>
<data name="ADP_InvalidMixedUsageOfSecureCredentialAndIntegratedSecurity" xml:space="preserve">
<value>Cannot use Credential with Integrated Security connection string keyword.</value>
</data>
<data name="SQL_ChangePasswordArgumentMissing" xml:space="preserve">
<value>The '{0}' argument must not be null or empty.</value>
</data>
<data name="SQL_ChangePasswordConflictsWithSSPI" xml:space="preserve">
<value>ChangePassword can only be used with SQL authentication, not with integrated security.</value>
</data>
<data name="SQL_ChangePasswordRequiresYukon" xml:space="preserve">
<value>ChangePassword requires SQL Server 9.0 or later.</value>
</data>
<data name="SQL_ChangePasswordUseOfUnallowedKey" xml:space="preserve">
<value>The keyword '{0}' must not be specified in the connectionString argument to ChangePassword.</value>
</data>
</root>
\ No newline at end of file
......@@ -891,5 +891,24 @@ internal static ArgumentException MustBeReadOnly(string argumentName)
return Argument(SR.GetString(SR.ADP_MustBeReadOnly, argumentName));
}
internal static InvalidOperationException InvalidMixedUsageOfSecureAndClearCredential()
{
return InvalidOperation(SR.GetString(SR.ADP_InvalidMixedUsageOfSecureAndClearCredential));
}
internal static ArgumentException InvalidMixedArgumentOfSecureAndClearCredential()
{
return Argument(SR.GetString(SR.ADP_InvalidMixedUsageOfSecureAndClearCredential));
}
internal static InvalidOperationException InvalidMixedUsageOfSecureCredentialAndIntegratedSecurity()
{
return InvalidOperation(SR.GetString(SR.ADP_InvalidMixedUsageOfSecureCredentialAndIntegratedSecurity));
}
internal static ArgumentException InvalidMixedArgumentOfSecureCredentialAndIntegratedSecurity()
{
return Argument(SR.GetString(SR.ADP_InvalidMixedUsageOfSecureCredentialAndIntegratedSecurity));
}
}
}
......@@ -14,6 +14,7 @@
using System.Reflection;
using System.IO;
using System.Globalization;
using System.Security;
namespace System.Data.SqlClient
{
......@@ -30,6 +31,7 @@ public sealed partial class SqlConnection : DbConnection, ICloneable
// root task associated with current async invocation
private Tuple<TaskCompletionSource<DbConnectionInternal>, Task> _currentCompletion;
private SqlCredential _credential;
private string _connectionString;
private int _connectRetryCount;
......@@ -57,11 +59,43 @@ public SqlConnection(string connectionString) : this()
CacheConnectionStringProperties();
}
public SqlConnection(string connectionString, SqlCredential credential) : this()
{
ConnectionString = connectionString;
if (credential != null)
{
// The following checks are necessary as setting Credential property will call CheckAndThrowOnInvalidCombinationOfConnectionStringAndSqlCredential
// CheckAndThrowOnInvalidCombinationOfConnectionStringAndSqlCredential it will throw InvalidOperationException rather than Arguemtn exception
// Need to call setter on Credential property rather than setting _credential directly as pool groups need to be checked
SqlConnectionString connectionOptions = (SqlConnectionString)ConnectionOptions;
if (UsesClearUserIdOrPassword(connectionOptions))
{
throw ADP.InvalidMixedArgumentOfSecureAndClearCredential();
}
if (UsesIntegratedSecurity(connectionOptions))
{
throw ADP.InvalidMixedArgumentOfSecureCredentialAndIntegratedSecurity();
}
Credential = credential;
}
// else
// credential == null: we should not set "Credential" as this will do additional validation check and
// checking pool groups which is not necessary. All necessary operation is already done by calling "ConnectionString = connectionString"
CacheConnectionStringProperties();
}
private SqlConnection(SqlConnection connection)
{
GC.SuppressFinalize(this);
CopyFrom(connection);
_connectionString = connection._connectionString;
if (connection._credential != null)
{
SecureString password = connection._credential.Password.Copy();
password.MakeReadOnly();
_credential = new SqlCredential(connection._credential.UserId, password);
}
CacheConnectionStringProperties();
}
......@@ -143,6 +177,23 @@ internal bool AsyncCommandInProgress
set => _AsyncCommandInProgress = value;
}
// Does this connection use Integrated Security?
private bool UsesIntegratedSecurity(SqlConnectionString opt)
{
return opt != null ? opt.IntegratedSecurity : false;
}
// Does this connection use old style of clear userID or Password in connection string?
private bool UsesClearUserIdOrPassword(SqlConnectionString opt)
{
bool result = false;
if (null != opt)
{
result = (!string.IsNullOrEmpty(opt.UserID) || !string.IsNullOrEmpty(opt.Password));
}
return result;
}
internal SqlConnectionString.TransactionBindingEnum TransactionBinding
{
get => ((SqlConnectionString)ConnectionOptions).TransactionBinding;
......@@ -171,7 +222,12 @@ public override string ConnectionString
}
set
{
ConnectionString_Set(new SqlConnectionPoolKey(value));
if (_credential != null)
{
SqlConnectionString connectionOptions = new SqlConnectionString(value);
CheckAndThrowOnInvalidCombinationOfConnectionStringAndSqlCredential(connectionOptions);
}
ConnectionString_Set(new SqlConnectionPoolKey(value, _credential));
_connectionString = value; // Change _connectionString value only after value is validated
CacheConnectionStringProperties();
}
......@@ -311,6 +367,61 @@ public string WorkstationId
}
}
public SqlCredential Credential
{
get
{
SqlCredential result = _credential;
// When a connection is connecting or is ever opened, make credential available only if "Persist Security Info" is set to true
// otherwise, return null
SqlConnectionString connectionOptions = (SqlConnectionString)UserConnectionOptions;
if (InnerConnection.ShouldHidePassword && connectionOptions != null && !connectionOptions.PersistSecurityInfo)
{
result = null;
}
return result;
}
set
{
// If a connection is connecting or is ever opened, user id/password cannot be set
if (!InnerConnection.AllowSetConnectionString)
{
throw ADP.OpenConnectionPropertySet(nameof(Credential), InnerConnection.State);
}
// check if the usage of credential has any conflict with the keys used in connection string
if (value != null)
{
CheckAndThrowOnInvalidCombinationOfConnectionStringAndSqlCredential((SqlConnectionString)ConnectionOptions);
}
_credential = value;
// Need to call ConnectionString_Set to do proper pool group check
ConnectionString_Set(new SqlConnectionPoolKey(_connectionString, _credential));
}
}
// CheckAndThrowOnInvalidCombinationOfConnectionStringAndSqlCredential: check if the usage of credential has any conflict
// with the keys used in connection string
// If there is any conflict, it throws InvalidOperationException
// This is used in the setter of ConnectionString and Credential properties.
private void CheckAndThrowOnInvalidCombinationOfConnectionStringAndSqlCredential(SqlConnectionString connectionOptions)
{
if (UsesClearUserIdOrPassword(connectionOptions))
{
throw ADP.InvalidMixedUsageOfSecureAndClearCredential();
}
if (UsesIntegratedSecurity(connectionOptions))
{
throw ADP.InvalidMixedUsageOfSecureCredentialAndIntegratedSecurity();
}
}
protected override DbProviderFactory DbProviderFactory
{
get => SqlClientFactory.Instance;
......@@ -542,6 +653,8 @@ public override void Close()
private void DisposeMe(bool disposing)
{
_credential = null;
if (!disposing)
{
// For non-pooled connections we need to make sure that if the SqlConnection was not closed,
......@@ -1231,6 +1344,108 @@ internal void OnInfoMessage(SqlInfoMessageEventArgs imevent, out bool notified)
}
}
public static void ChangePassword(string connectionString, string newPassword)
{
if (string.IsNullOrEmpty(connectionString))
{
throw SQL.ChangePasswordArgumentMissing(nameof(newPassword));
}
if (string.IsNullOrEmpty(newPassword))
{
throw SQL.ChangePasswordArgumentMissing(nameof(newPassword));
}
if (TdsEnums.MAXLEN_NEWPASSWORD < newPassword.Length)
{
throw ADP.InvalidArgumentLength(nameof(newPassword), TdsEnums.MAXLEN_NEWPASSWORD);
}
SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential: null);
SqlConnectionString connectionOptions = SqlConnectionFactory.FindSqlConnectionOptions(key);
if (connectionOptions.IntegratedSecurity)
{
throw SQL.ChangePasswordConflictsWithSSPI();
}
if (!string.IsNullOrEmpty(connectionOptions.AttachDBFilename))
{
throw SQL.ChangePasswordUseOfUnallowedKey(SqlConnectionString.KEY.AttachDBFilename);
}
ChangePassword(connectionString, connectionOptions, null, newPassword, null);
}
public static void ChangePassword(string connectionString, SqlCredential credential, SecureString newSecurePassword)
{
if (string.IsNullOrEmpty(connectionString))
{
throw SQL.ChangePasswordArgumentMissing(nameof(connectionString));
}
// check credential; not necessary to check the length of password in credential as the check is done by SqlCredential class
if (credential == null)
{
throw SQL.ChangePasswordArgumentMissing(nameof(credential));
}
if (newSecurePassword == null || newSecurePassword.Length == 0)
{
throw SQL.ChangePasswordArgumentMissing(nameof(newSecurePassword));
}
if (!newSecurePassword.IsReadOnly())
{
throw ADP.MustBeReadOnly(nameof(newSecurePassword));
}
if (TdsEnums.MAXLEN_NEWPASSWORD < newSecurePassword.Length)
{
throw ADP.InvalidArgumentLength(nameof(newSecurePassword), TdsEnums.MAXLEN_NEWPASSWORD);
}
SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential);
SqlConnectionString connectionOptions = SqlConnectionFactory.FindSqlConnectionOptions(key);
// Check for connection string values incompatible with SqlCredential
if (!string.IsNullOrEmpty(connectionOptions.UserID) || !string.IsNullOrEmpty(connectionOptions.Password))
{
throw ADP.InvalidMixedArgumentOfSecureAndClearCredential();
}
if (connectionOptions.IntegratedSecurity)
{
throw SQL.ChangePasswordConflictsWithSSPI();
}
if (!string.IsNullOrEmpty(connectionOptions.AttachDBFilename))
{
throw SQL.ChangePasswordUseOfUnallowedKey(SqlConnectionString.KEY.AttachDBFilename);
}
ChangePassword(connectionString, connectionOptions, credential, null, newSecurePassword);
}
private static void ChangePassword(string connectionString, SqlConnectionString connectionOptions, SqlCredential credential, string newPassword, SecureString newSecurePassword)
{
// note: This is the only case where we directly construct the internal connection, passing in the new password.
// Normally we would simply create a regular connection and open it, but there is no other way to pass the
// new password down to the constructor. This would have an unwanted impact on the connection pool.
SqlInternalConnectionTds con = null;
try
{
con = new SqlInternalConnectionTds(null, connectionOptions, credential, null, newPassword, newSecurePassword, false);
}
finally
{
if (con != null)
con.Dispose();
}
SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential);
SqlConnectionFactory.SingletonInstance.ClearPool(key);
}
//
// SQL DEBUGGING SUPPORT
//
......
......@@ -95,7 +95,7 @@ override protected DbConnectionInternal CreateConnection(DbConnectionOptions opt
// This first connection is established to SqlExpress to get the instance name
// of the UserInstance.
SqlConnectionString sseopt = new SqlConnectionString(opt, opt.DataSource, userInstance: true, setEnlistValue: false);
sseConnection = new SqlInternalConnectionTds(identity, sseopt, null, false, applyTransientFaultHandling: applyTransientFaultHandling);
sseConnection = new SqlInternalConnectionTds(identity, sseopt, key.Credential, null, "", null, false, applyTransientFaultHandling: applyTransientFaultHandling);
// NOTE: Retrieve <UserInstanceName> here. This user instance name will be used below to connect to the Sql Express User Instance.
instanceName = sseConnection.InstanceName;
......@@ -132,7 +132,7 @@ override protected DbConnectionInternal CreateConnection(DbConnectionOptions opt
opt = new SqlConnectionString(opt, instanceName, userInstance: false, setEnlistValue: null);
poolGroupProviderInfo = null; // null so we do not pass to constructor below...
}
result = new SqlInternalConnectionTds(identity, opt, poolGroupProviderInfo, redirectedUserInstance, userOpt, recoverySessionData, applyTransientFaultHandling: applyTransientFaultHandling);
result = new SqlInternalConnectionTds(identity, opt, key.Credential, poolGroupProviderInfo, "", null, redirectedUserInstance, userOpt, recoverySessionData, applyTransientFaultHandling: applyTransientFaultHandling);
return result;
}
......
......@@ -15,14 +15,17 @@ namespace System.Data.SqlClient
internal class SqlConnectionPoolKey : DbConnectionPoolKey
{
private int _hashValue;
private SqlCredential _credential;
internal SqlConnectionPoolKey(string connectionString) : base(connectionString)
internal SqlConnectionPoolKey(string connectionString, SqlCredential credential) : base(connectionString)
{
_credential = credential;
CalculateHashCode();
}
private SqlConnectionPoolKey(SqlConnectionPoolKey key) : base(key)
{
_credential = key.Credential;
CalculateHashCode();
}
......@@ -45,13 +48,14 @@ internal override string ConnectionString
}
}
internal SqlCredential Credential => _credential;
public override bool Equals(object obj)
{
SqlConnectionPoolKey key = obj as SqlConnectionPoolKey;
return (key != null &&
ConnectionString == key.ConnectionString);
ConnectionString == key.ConnectionString &&
Credential == key.Credential);
}
public override int GetHashCode()
......@@ -62,6 +66,14 @@ public override int GetHashCode()
private void CalculateHashCode()
{
_hashValue = base.GetHashCode();
if (_credential != null)
{
unchecked
{
_hashValue = _hashValue * 17 + _credential.GetHashCode();
}
}
}
}
}
......@@ -15,6 +15,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using System.Transactions;
using System.Security;
namespace System.Data.SqlClient
{
......@@ -104,6 +105,7 @@ sealed internal class SqlInternalConnectionTds : SqlInternalConnection, IDisposa
private readonly SqlConnectionPoolGroupProviderInfo _poolGroupProviderInfo; // will only be null when called for ChangePassword, or creating SSE User Instance
private TdsParser _parser;
private SqlLoginAck _loginAck;
private SqlCredential _credential;
// Connection Resiliency
private bool _sessionRecoveryRequested;
......@@ -301,7 +303,10 @@ internal SqlConnectionTimeoutErrorInternal TimeoutErrorInternal
internal SqlInternalConnectionTds(
DbConnectionPoolIdentity identity,
SqlConnectionString connectionOptions,
SqlCredential credential,
object providerInfo,
string newPassword,
SecureString newSecurePassword,
bool redirectedUserInstance,
SqlConnectionString userConnectionOptions = null, // NOTE: userConnectionOptions may be different to connectionOptions if the connection string has been expanded (see SqlConnectionString.Expand)
SessionData reconnectSessionData = null,
......@@ -332,6 +337,10 @@ internal SqlConnectionTimeoutErrorInternal TimeoutErrorInternal
_identity = identity;
Debug.Assert(newSecurePassword != null || newPassword != null, "cannot have both new secure change password and string based change password to be null");
Debug.Assert(credential == null || (string.IsNullOrEmpty(connectionOptions.UserID) && string.IsNullOrEmpty(connectionOptions.Password)), "cannot mix the new secure password system and the connection string based password");
Debug.Assert(credential == null || !connectionOptions.IntegratedSecurity, "Cannot use SqlCredential and Integrated Security");
_poolGroupProviderInfo = (SqlConnectionPoolGroupProviderInfo)providerInfo;
_fResetConnection = connectionOptions.ConnectionReset;
......@@ -342,6 +351,7 @@ internal SqlConnectionTimeoutErrorInternal TimeoutErrorInternal
}
_timeoutErrorInternal = new SqlConnectionTimeoutErrorInternal();
_credential = credential;
_parserLock.Wait(canReleaseFromAnyThread: false);
ThreadHasParserLockForClose = true; // In case of error, let ourselves know that we already own the parser lock
......@@ -356,7 +366,8 @@ internal SqlConnectionTimeoutErrorInternal TimeoutErrorInternal
{
try
{
OpenLoginEnlist(timeout, connectionOptions, redirectedUserInstance);
OpenLoginEnlist(timeout, connectionOptions, credential, newPassword, newSecurePassword, redirectedUserInstance);
break;
}
catch (SqlException sqlex)
......@@ -1053,7 +1064,7 @@ private void CompleteLogin(bool enlistOK)
_parser._physicalStateObj.SniContext = SniContext.Snix_Login;
}
private void Login(ServerInfo server, TimeoutTimer timeout)
private void Login(ServerInfo server, TimeoutTimer timeout, string newPassword, SecureString newSecurePassword)
{
// create a new login record
SqlLogin login = new SqlLogin();
......@@ -1099,7 +1110,13 @@ private void Login(ServerInfo server, TimeoutTimer timeout)
login.useReplication = ConnectionOptions.Replication;
login.useSSPI = ConnectionOptions.IntegratedSecurity;
login.packetSize = _currentPacketSize;
login.newPassword = newPassword;
login.readOnlyIntent = ConnectionOptions.ApplicationIntent == ApplicationIntent.ReadOnly;
login.credential = _credential;
if (newSecurePassword != null)
{
login.newSecurePassword = newSecurePassword;
}
TdsEnums.FeatureExtension requestedFeatures = TdsEnums.FeatureExtension.None;
if (ConnectionOptions.ConnectRetryCount > 0)
......@@ -1127,6 +1144,9 @@ private void LoginFailure()
private void OpenLoginEnlist(TimeoutTimer timeout,
SqlConnectionString connectionOptions,
SqlCredential credential,
string newPassword,
SecureString newSecurePassword,
bool redirectedUserInstance)
{
bool useFailoverPartner; // should we use primary or secondary first
......@@ -1160,8 +1180,11 @@ private void LoginFailure()
useFailoverPartner,
dataSource,
failoverPartner,
newPassword,
newSecurePassword,
redirectedUserInstance,
connectionOptions,
credential,
timeout);
}
else
......@@ -1169,8 +1192,11 @@ private void LoginFailure()
_timeoutErrorInternal.SetFailoverScenario(false); // not a failover scenario
LoginNoFailover(
dataSource,
newPassword,
newSecurePassword,
redirectedUserInstance,
connectionOptions,
credential,
timeout);
}
_timeoutErrorInternal.EndPhase(SqlConnectionTimeoutErrorPhase.PostLogin);
......@@ -1210,9 +1236,12 @@ private bool IsDoNotRetryConnectError(SqlException exc)
// Changes to either one should be examined to see if they need to be reflected in the other
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
private void LoginNoFailover(ServerInfo serverInfo,
bool redirectedUserInstance,
SqlConnectionString connectionOptions,
TimeoutTimer timeout)
string newPassword,
SecureString newSecurePassword,
bool redirectedUserInstance,
SqlConnectionString connectionOptions,
SqlCredential credential,
TimeoutTimer timeout)
{
Debug.Assert(object.ReferenceEquals(connectionOptions, this.ConnectionOptions), "ConnectionOptions argument and property must be the same"); // consider removing the argument
int routingAttempts = 0;
......@@ -1273,8 +1302,10 @@ private bool IsDoNotRetryConnectError(SqlException exc)
try
{
AttemptOneLogin(serverInfo,
newPassword,
newSecurePassword,
!connectionOptions.MultiSubnetFailover, // ignore timeout for SniOpen call unless MSF
connectionOptions.MultiSubnetFailover ? intervalTimer : timeout);
connectionOptions.MultiSubnetFailover ? intervalTimer : timeout);
if (connectionOptions.MultiSubnetFailover && null != ServerProvidedFailOverPartner)
{
......@@ -1353,9 +1384,12 @@ private bool IsDoNotRetryConnectError(SqlException exc)
true, // start by using failover partner, since we already failed to connect to the primary
serverInfo,
ServerProvidedFailOverPartner,
redirectedUserInstance,
newPassword,
newSecurePassword,
redirectedUserInstance,
connectionOptions,
timeout);
credential,
timeout);
return; // LoginWithFailover successfully connected and handled entire connection setup
}
......@@ -1395,9 +1429,12 @@ private bool IsDoNotRetryConnectError(SqlException exc)
bool useFailoverHost,
ServerInfo primaryServerInfo,
string failoverHost,
bool redirectedUserInstance,
string newPassword,
SecureString newSecurePassword,
bool redirectedUserInstance,
SqlConnectionString connectionOptions,
TimeoutTimer timeout
SqlCredential credential,
TimeoutTimer timeout
)
{
Debug.Assert(!connectionOptions.MultiSubnetFailover, "MultiSubnetFailover should not be set if failover partner is used");
......@@ -1475,7 +1512,9 @@ TimeoutTimer timeout
// Attempt login. Use timerInterval for attempt timeout unless infinite timeout was requested.
AttemptOneLogin(
currentServerInfo,
false, // Use timeout in SniOpen
newPassword,
newSecurePassword,
false, // Use timeout in SniOpen
intervalTimer,
withFailover: true
);
......@@ -1561,10 +1600,13 @@ private void ResolveExtendedServerName(ServerInfo serverInfo, bool aliasLookup,
}
// Common code path for making one attempt to establish a connection and log in to server.
private void AttemptOneLogin(ServerInfo serverInfo,
private void AttemptOneLogin(
ServerInfo serverInfo,
string newPassword,
SecureString newSecurePassword,
bool ignoreSniOpenTimeout,
TimeoutTimer timeout,
bool withFailover = false)
TimeoutTimer timeout,
bool withFailover = false)
{
_routingInfo = null; // forget routing information
......@@ -1583,7 +1625,7 @@ private void ResolveExtendedServerName(ServerInfo serverInfo, bool aliasLookup,
_timeoutErrorInternal.SetAndBeginPhase(SqlConnectionTimeoutErrorPhase.LoginBegin);
_parser._physicalStateObj.SniContext = SniContext.Snix_Login;
this.Login(serverInfo, timeout);
this.Login(serverInfo, timeout, newPassword, newSecurePassword);
_timeoutErrorInternal.EndPhase(SqlConnectionTimeoutErrorPhase.ProcessConnectionAuth);
_timeoutErrorInternal.SetAndBeginPhase(SqlConnectionTimeoutErrorPhase.PostLogin);
......
......@@ -204,6 +204,22 @@ internal static Exception InstanceFailure()
{
return ADP.InvalidOperation(SR.GetString(SR.SQL_InstanceFailure));
}
internal static Exception ChangePasswordArgumentMissing(string argumentName)
{
return ADP.ArgumentNull(SR.GetString(SR.SQL_ChangePasswordArgumentMissing, argumentName));
}
internal static Exception ChangePasswordConflictsWithSSPI()
{
return ADP.Argument(SR.GetString(SR.SQL_ChangePasswordConflictsWithSSPI));
}
internal static Exception ChangePasswordRequiresYukon()
{
return ADP.InvalidOperation(SR.GetString(SR.SQL_ChangePasswordRequiresYukon));
}
static internal Exception ChangePasswordUseOfUnallowedKey(string key)
{
return ADP.InvalidOperation(SR.GetString(SR.SQL_ChangePasswordUseOfUnallowedKey, key));
}
//
// Global Transactions.
......
......@@ -193,7 +193,6 @@ internal EncryptionOptions EncryptionOptions
}
}
internal bool IsKatmaiOrNewer
{
get
......@@ -5986,8 +5985,13 @@ internal void TdsLogin(SqlLogin rec, TdsEnums.FeatureExtension requestedFeatures
Debug.Assert(TdsEnums.MAXLEN_HOSTNAME >= rec.hostName.Length, "_workstationId.Length exceeds the max length for this value");
Debug.Assert(rec.userName == null || (rec.userName != null && TdsEnums.MAXLEN_USERNAME >= rec.userName.Length), "_userID.Length exceeds the max length for this value");
Debug.Assert(rec.credential == null || (rec.credential != null && TdsEnums.MAXLEN_USERNAME >= rec.credential.UserId.Length), "_credential.UserId.Length exceeds the max length for this value");
Debug.Assert(rec.password == null || (rec.password != null && TdsEnums.MAXLEN_PASSWORD >= rec.password.Length), "_password.Length exceeds the max length for this value");
Debug.Assert(rec.credential == null || (rec.credential != null && TdsEnums.MAXLEN_PASSWORD >= rec.credential.Password.Length), "_credential.Password.Length exceeds the max length for this value");
Debug.Assert(rec.credential != null || rec.userName != null || rec.password != null, "cannot mix the new secure password system and the connection string based password");
Debug.Assert(rec.newSecurePassword != null || rec.newPassword != null, "cannot have both new secure change password and string based change password");
Debug.Assert(TdsEnums.MAXLEN_APPNAME >= rec.applicationName.Length, "_applicationName.Length exceeds the max length for this value");
Debug.Assert(TdsEnums.MAXLEN_SERVERNAME >= rec.serverName.Length, "_dataSource.Length exceeds the max length for this value");
Debug.Assert(TdsEnums.MAXLEN_LANGUAGE >= rec.language.Length, "_currentLanguage .Length exceeds the max length for this value");
......@@ -6000,15 +6004,34 @@ internal void TdsLogin(SqlLogin rec, TdsEnums.FeatureExtension requestedFeatures
// get the password up front to use in sspi logic below
byte[] encryptedPassword = null;
byte[] encryptedChangePassword = null;
int encryptedPasswordLengthInBytes;
int encryptedChangePasswordLengthInBytes;
bool useFeatureExt = (requestedFeatures != TdsEnums.FeatureExtension.None);
string userName;
userName = rec.userName;
encryptedPassword = TdsParserStaticMethods.ObfuscatePassword(rec.password);
encryptedPasswordLengthInBytes = encryptedPassword.Length; // password in clear text is already encrypted and its length is in byte
if (rec.credential != null)
{
userName = rec.credential.UserId;
encryptedPasswordLengthInBytes = rec.credential.Password.Length * 2;
}
else
{
userName = rec.userName;
encryptedPassword = TdsParserStaticMethods.ObfuscatePassword(rec.password);
encryptedPasswordLengthInBytes = encryptedPassword.Length; // password in clear text is already encrypted and its length is in byte
}
if (rec.newSecurePassword != null)
{
encryptedChangePasswordLengthInBytes = rec.newSecurePassword.Length * 2;
}
else
{
encryptedChangePassword = TdsParserStaticMethods.ObfuscatePassword(rec.newPassword);
encryptedChangePasswordLengthInBytes = encryptedChangePassword.Length;
}
// set the message type
_physicalStateObj._outputMessageType = TdsEnums.MT_LOGIN7;
......@@ -6042,7 +6065,8 @@ internal void TdsLogin(SqlLogin rec, TdsEnums.FeatureExtension requestedFeatures
{
checked
{
length += (userName.Length * 2) + encryptedPasswordLengthInBytes;
length += (userName.Length * 2) + encryptedPasswordLengthInBytes
+ encryptedChangePasswordLengthInBytes;
}
}
else
......@@ -6158,6 +6182,10 @@ internal void TdsLogin(SqlLogin rec, TdsEnums.FeatureExtension requestedFeatures
}
// 4th one
if (!string.IsNullOrEmpty(rec.newPassword) || (rec.newSecurePassword != null && rec.newSecurePassword.Length != 0))
{
log7Flags |= 1 << 24;
}
if (rec.userInstance)
{
log7Flags |= 1 << 26;
......@@ -6256,7 +6284,7 @@ internal void TdsLogin(SqlLogin rec, TdsEnums.FeatureExtension requestedFeatures
offset += rec.attachDBFilename.Length * 2;
WriteShort(offset, _physicalStateObj); // reset password offset
WriteShort(0, _physicalStateObj);
WriteShort(encryptedChangePasswordLengthInBytes / 2, _physicalStateObj);
WriteInt(0, _physicalStateObj); // reserved for chSSPI
......@@ -6269,6 +6297,11 @@ internal void TdsLogin(SqlLogin rec, TdsEnums.FeatureExtension requestedFeatures
{
WriteString(userName, _physicalStateObj);
if (rec.credential != null)
{
_physicalStateObj.WriteSecureString(rec.credential.Password);
}
else
{
_physicalStateObj.WriteByteArray(encryptedPassword, encryptedPasswordLengthInBytes, 0);
}
......@@ -6292,6 +6325,19 @@ internal void TdsLogin(SqlLogin rec, TdsEnums.FeatureExtension requestedFeatures
_physicalStateObj.WriteByteArray(outSSPIBuff, (int)outSSPILength, 0);
WriteString(rec.attachDBFilename, _physicalStateObj);
if (!rec.useSSPI)
{
if (rec.newSecurePassword != null)
{
_physicalStateObj.WriteSecureString(rec.newSecurePassword);
}
else
{
_physicalStateObj.WriteByteArray(encryptedChangePassword, encryptedChangePasswordLengthInBytes, 0);
}
}
if (useFeatureExt)
{
if ((requestedFeatures & TdsEnums.FeatureExtension.SessionRecovery) != 0)
......@@ -6318,6 +6364,7 @@ internal void TdsLogin(SqlLogin rec, TdsEnums.FeatureExtension requestedFeatures
}
_physicalStateObj.WritePacket(TdsEnums.HARDFLUSH);
_physicalStateObj.ResetSecurePasswordsInformation();
_physicalStateObj._pendingData = true;
_physicalStateObj._messageStatus = 0;
}// tdsLogin
......
......@@ -11,6 +11,7 @@
using System.Data.Common;
using System.Data.SqlTypes;
using System.Diagnostics;
using System.Security;
using System.Text;
using Microsoft.SqlServer.Server;
......@@ -250,9 +251,12 @@ sealed internal class SqlLogin
internal string database = ""; // initial database
internal string attachDBFilename = ""; // DB filename to be attached
internal bool useReplication = false; // user login for replication
internal string newPassword = ""; // new password for reset password
internal bool useSSPI = false; // use integrated security
internal int packetSize = SqlConnectionString.DEFAULT.Packet_Size; // packet size
internal bool readOnlyIntent = false; // read-only intent
internal SqlCredential credential; // user id and password in SecureString
internal SecureString newSecurePassword;
}
sealed internal class SqlLoginAck
......
......@@ -10,6 +10,8 @@
using System.Threading;
using System.Threading.Tasks;
using System.Text;
using System.Security;
using System.Runtime.InteropServices;
namespace System.Data.SqlClient
{
......@@ -90,6 +92,10 @@ internal abstract class TdsParserStateObject
private readonly LastIOTimer _lastSuccessfulIOTimer;
// secure password information to be stored
// At maximum number of secure string that need to be stored is two; one for login password and the other for new change password
private SecureString[] _securePasswords = new SecureString[2] { null, null };
private int[] _securePasswordOffsetsInBuffer = new int[2];
// This variable is used to track whether another thread has requested a cancel. The
// synchronization points are
......@@ -2651,6 +2657,32 @@ private void ChangeNetworkPacketTimeout(int dueTime, int period)
}
}
private void SetBufferSecureStrings()
{
if (_securePasswords != null)
{
for (int i = 0; i < _securePasswords.Length; i++)
{
if (_securePasswords[i] != null)
{
IntPtr str = IntPtr.Zero;
try
{
str = Marshal.SecureStringToBSTR(_securePasswords[i]);
byte[] data = new byte[_securePasswords[i].Length * 2];
Marshal.Copy(str, data, 0, _securePasswords[i].Length * 2);
TdsParserStaticMethods.ObfuscatePassword(data);
data.CopyTo(_outBuff, _securePasswordOffsetsInBuffer[i]);
}
finally
{
Marshal.ZeroFreeBSTR(str);
}
}
}
}
}
public void ReadAsyncCallback<T>(T packet, UInt32 error)
{
ReadAsyncCallback(IntPtr.Zero, packet, error);
......@@ -2876,7 +2908,34 @@ public void WriteAsyncCallback<T>(IntPtr key, T packet, UInt32 sniError)
// Network/Packet Writing & Processing //
/////////////////////////////////////////
internal void WriteSecureString(SecureString secureString)
{
Debug.Assert(_securePasswords[0] == null || _securePasswords[1] == null, "There are more than two secure passwords");
int index = _securePasswords[0] != null ? 1 : 0;
_securePasswords[index] = secureString;
_securePasswordOffsetsInBuffer[index] = _outBytesUsed;
// loop through and write the entire array
int lengthInBytes = secureString.Length * 2;
// It is guaranteed both secure password and secure change password should fit into the first packet
// Given current TDS format and implementation it is not possible that one of secure string is the last item and exactly fill up the output buffer
// if this ever happens and it is correct situation, the packet needs to be written out after _outBytesUsed is update
Debug.Assert((_outBytesUsed + lengthInBytes) < _outBuff.Length, "Passwords cannot be splited into two different packet or the last item which fully fill up _outBuff!!!");
_outBytesUsed += lengthInBytes;
}
internal void ResetSecurePasswordsInformation()
{
for (int i = 0; i < _securePasswords.Length; ++i)
{
_securePasswords[i] = null;
_securePasswordOffsetsInBuffer[i] = 0;
}
}
internal Task WaitForAccumulatedWrites()
{
......@@ -3348,6 +3407,7 @@ private Task WriteSni(bool canAccumulate)
// Prepare packet, and write to packet.
object packet = GetResetWritePacket();
SetBufferSecureStrings();
SetPacketData(packet, _outBuff, _outBytesUsed);
uint sniError;
......
......@@ -41,6 +41,20 @@ internal static Byte[] ObfuscatePassword(string password)
return bObfuscated;
}
internal static byte[] ObfuscatePassword(byte[] password)
{
byte bLo;
byte bHi;
for (int i = 0; i < password.Length; i++)
{
bLo = (byte)(password[i] & 0x0f);
bHi = (byte)(password[i] & 0xf0);
password[i] = (Byte)(((bHi >> 4) | (bLo << 4)) ^ 0xa5);
}
return password;
}
private const int NoProcessId = -1;
private static int s_currentProcessId = NoProcessId;
internal static int GetCurrentProcessIdForTdsLoginOnly()
......
......@@ -6,6 +6,7 @@
using System.Data.Common;
using System.Diagnostics;
using System.Reflection;
using System.Security;
using System.Threading;
using Xunit;
......@@ -150,6 +151,51 @@ public void ExceptionsWithMinPoolSizeCanBeHandled()
}
}
[Fact]
public void ConnectionTestInvalidCredentialCombination()
{
var cleartextCredsConnStr = "User=test;Password=test;";
var sspiConnStr = "Integrated Security=true;";
var testPassword = new SecureString();
testPassword.MakeReadOnly();
var sqlCredential = new SqlCredential(string.Empty, testPassword);
// Verify that SSPI and cleartext username/password are not in the connection string.
Assert.Throws<ArgumentException>(() => { new SqlConnection(cleartextCredsConnStr, sqlCredential); });
Assert.Throws<ArgumentException>(() => { new SqlConnection(sspiConnStr, sqlCredential); });
// Verify that credential may not be set with cleartext username/password or SSPI.
using (var conn = new SqlConnection(cleartextCredsConnStr))
{
Assert.Throws<InvalidOperationException>(() => { conn.Credential = sqlCredential; });
}
using (var conn = new SqlConnection(sspiConnStr))
{
Assert.Throws<InvalidOperationException>(() => { conn.Credential = sqlCredential; });
}
// Verify that connection string with username/password or SSPI may not be set with credential present.
using (var conn = new SqlConnection(string.Empty, sqlCredential))
{
Assert.Throws<InvalidOperationException>(() => { conn.ConnectionString = cleartextCredsConnStr; });
Assert.Throws<InvalidOperationException>(() => { conn.ConnectionString = sspiConnStr; });
}
}
[Fact]
public void ConnectionTestValidCredentialCombination()
{
var testPassword = new SecureString();
testPassword.MakeReadOnly();
var sqlCredential = new SqlCredential(string.Empty, testPassword);
var conn = new SqlConnection(string.Empty, sqlCredential);
Assert.Equal(sqlCredential, conn.Credential);
}
public class ConnectionWorker
{
private static ManualResetEventSlim startEvent = new ManualResetEventSlim(false);
......
// 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.Collections.Generic;
using System.Data.SqlClient;
using System.Data.SqlClient.ManualTesting.Tests;
using System.Linq;
using System.Security;
using System.Text;
using System.Threading.Tasks;
using Xunit;
namespace System.Data.SqlClient.ManualTesting.Tests
{
public static class SqlCredentialTest
{
[CheckConnStrSetupFact]
public static void CreateSqlConnectionWithCredential()
{
var user = "u" + Guid.NewGuid().ToString().Replace("-", "");
var passStr = "Pax561O$T5K#jD";
try
{
createTestUser(user, passStr);
var csb = new SqlConnectionStringBuilder(DataTestUtility.TcpConnStr);
csb.Remove("User ID");
csb.Remove("Password");
csb.IntegratedSecurity = false;
var password = new SecureString();
passStr.ToCharArray().ToList().ForEach(x => password.AppendChar(x));
password.MakeReadOnly();
using (var conn = new SqlConnection(csb.ConnectionString, new SqlCredential(user, password)))
using (var cmd = new SqlCommand("SELECT 1;", conn))
{
conn.Open();
Assert.Equal(1, cmd.ExecuteScalar());
}
}
finally
{
dropTestUser(user);
}
}
[CheckConnStrSetupFact]
public static void SqlConnectionChangePasswordPlaintext()
{
var user = "u" + Guid.NewGuid().ToString().Replace("-", "");
var pass = "!21Ja3Ims7LI&n";
var newPass = "fmVCNf@24Dg*8j";
try
{
createTestUser(user, pass);
var csb = new SqlConnectionStringBuilder(DataTestUtility.TcpConnStr);
csb.UserID = user;
csb.Password = pass;
csb.IntegratedSecurity = false;
// Change password and try opening connection.
SqlConnection.ChangePassword(csb.ConnectionString, newPass);
csb.Password = newPass;
using (var conn = new SqlConnection(csb.ConnectionString))
using (var cmd = new SqlCommand("SELECT 1;", conn))
{
conn.Open();
Assert.Equal(1, cmd.ExecuteScalar());
}
}
finally
{
dropTestUser(user);
}
}
[CheckConnStrSetupFact]
public static void SqlConnectionChangePasswordSecureString()
{
var user = "u" + Guid.NewGuid().ToString().Replace("-", "");
var passStr = "tcM0qB^izt%3u7";
var newPassStr = "JSG2e(Vp0WCXE&";
try
{
createTestUser(user, passStr);
var csb = new SqlConnectionStringBuilder(DataTestUtility.TcpConnStr);
csb.Remove("User ID");
csb.Remove("Password");
csb.IntegratedSecurity = false;
var password = new SecureString();
passStr.ToCharArray().ToList().ForEach(x => password.AppendChar(x));
password.MakeReadOnly();
var newPassword = new SecureString();
newPassStr.ToCharArray().ToList().ForEach(x => newPassword.AppendChar(x));
newPassword.MakeReadOnly();
// Change password and try opening connection.
SqlConnection.ChangePassword(csb.ConnectionString, new SqlCredential(user, password), newPassword);
using (var conn = new SqlConnection(csb.ConnectionString, new SqlCredential(user, newPassword)))
using (var cmd = new SqlCommand("SELECT 1;", conn))
{
conn.Open();
Assert.Equal(1, cmd.ExecuteScalar());
}
}
finally
{
dropTestUser(user);
}
}
private static void createTestUser(string username, string password)
{
// Creates a test user with read permissions.
string createUserCmd = $"CREATE LOGIN {username} WITH PASSWORD = '{password}';"
+ $"EXEC sp_adduser '{username}', '{username}', 'db_datareader';";
using (var conn = new SqlConnection(DataTestUtility.TcpConnStr))
using (var cmd = new SqlCommand(createUserCmd, conn))
{
conn.Open();
cmd.ExecuteNonQuery();
}
}
private static void dropTestUser(string username)
{
// Removes a created test user.
string dropUserCmd = $"DROP SCHEMA IF EXISTS {username};"
+ $"DROP USER IF EXISTS {username};"
+ $"DROP LOGIN {username}";
// Pool must be cleared to prevent DROP LOGIN failure.
SqlConnection.ClearAllPools();
using (var conn = new SqlConnection(DataTestUtility.TcpConnStr))
using (var cmd = new SqlCommand(dropUserCmd, conn))
{
conn.Open();
cmd.ExecuteNonQuery();
}
}
}
}
......@@ -60,6 +60,7 @@
<Compile Include="SQL\MARSTest\MARSTest.cs" />
<Compile Include="SQL\MirroringTest\ConnectionOnMirroringTest.cs" />
<Compile Include="SQL\ParallelTransactionsTest\ParallelTransactionsTest.cs" />
<Compile Include="SQL\SqlCredentialTest\SqlCredentialTest.cs" />
<Compile Include="SQL\SqlSchemaInfoTest\SqlSchemaInfoTest.cs" />
<Compile Include="SQL\SqlBulkCopyTest\AdjustPrecScaleForBulkCopy.cs" />
<Compile Include="SQL\SqlBulkCopyTest\ErrorOnRowsMarkedAsDeleted.cs" />
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册