зеркало из https://github.com/mono/corefx.git
Merge pull request #30342 from AfsanehR/AccessTokenAAD
Azure Active Directory Authentication using Access Token
This commit is contained in:
Коммит
a74bf42009
|
@ -495,6 +495,7 @@ namespace System.Data.SqlClient
|
|||
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 string AccessToken { get { throw null; } set { } }
|
||||
|
||||
}
|
||||
public sealed partial class SqlConnectionStringBuilder : System.Data.Common.DbConnectionStringBuilder
|
||||
|
|
|
@ -1296,4 +1296,25 @@
|
|||
<data name="SQL_ChangePasswordUseOfUnallowedKey" xml:space="preserve">
|
||||
<value>The keyword '{0}' must not be specified in the connectionString argument to ChangePassword.</value>
|
||||
</data>
|
||||
<data name="SQL_ParsingErrorWithState" xml:space="preserve">
|
||||
<value>Internal connection fatal error. Error state: {0}.</value>
|
||||
</data>
|
||||
<data name="SQL_ParsingErrorValue" xml:space="preserve">
|
||||
<value>Internal connection fatal error. Error state: {0}, Value: {1}.</value>
|
||||
</data>
|
||||
<data name="ADP_InvalidMixedUsageOfAccessTokenAndIntegratedSecurity" xml:space="preserve">
|
||||
<value>Cannot set the AccessToken property if the 'Integrated Security' connection string keyword has been set to 'true' or 'SSPI'.</value>
|
||||
</data>
|
||||
<data name="ADP_InvalidMixedUsageOfAccessTokenAndUserIDPassword" xml:space="preserve">
|
||||
<value>Cannot set the AccessToken property if 'UserID', 'UID', 'Password', or 'PWD' has been specified in connection string.</value>
|
||||
</data>
|
||||
<data name="ADP_InvalidMixedUsageOfCredentialAndAccessToken" xml:space="preserve">
|
||||
<value>Cannot set the Credential property if the AccessToken property is already set.</value>
|
||||
</data>
|
||||
<data name="SQL_ParsingErrorFeatureId" xml:space="preserve">
|
||||
<value>Internal connection fatal error. Error state: {0}, Feature Id: {1}.</value>
|
||||
</data>
|
||||
<data name="SQL_ParsingErrorAuthLibraryType" xml:space="preserve">
|
||||
<value>Internal connection fatal error. Error state: {0}, Authentication Library Type: {1}.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
|
@ -910,5 +910,17 @@ namespace System.Data.Common
|
|||
{
|
||||
return Argument(SR.GetString(SR.ADP_InvalidMixedUsageOfSecureCredentialAndIntegratedSecurity));
|
||||
}
|
||||
internal static InvalidOperationException InvalidMixedUsageOfAccessTokenAndIntegratedSecurity()
|
||||
{
|
||||
return ADP.InvalidOperation(SR.GetString(SR.ADP_InvalidMixedUsageOfAccessTokenAndIntegratedSecurity));
|
||||
}
|
||||
static internal InvalidOperationException InvalidMixedUsageOfAccessTokenAndUserIDPassword()
|
||||
{
|
||||
return ADP.InvalidOperation(SR.GetString(SR.ADP_InvalidMixedUsageOfAccessTokenAndUserIDPassword));
|
||||
}
|
||||
static internal Exception InvalidMixedUsageOfCredentialAndAccessToken()
|
||||
{
|
||||
return ADP.InvalidOperation(SR.GetString(SR.ADP_InvalidMixedUsageOfCredentialAndAccessToken));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ namespace System.Data.SqlClient
|
|||
private SqlCredential _credential;
|
||||
private string _connectionString;
|
||||
private int _connectRetryCount;
|
||||
private string _accessToken; // Access Token to be used for token based authententication
|
||||
|
||||
// connection resiliency
|
||||
private object _reconnectLock = new object();
|
||||
|
@ -97,6 +98,7 @@ namespace System.Data.SqlClient
|
|||
_credential = new SqlCredential(connection._credential.UserId, password);
|
||||
}
|
||||
|
||||
_accessToken = connection._accessToken;
|
||||
CacheConnectionStringProperties();
|
||||
}
|
||||
|
||||
|
@ -222,12 +224,19 @@ namespace System.Data.SqlClient
|
|||
}
|
||||
set
|
||||
{
|
||||
if (_credential != null)
|
||||
if (_credential != null || _accessToken != null)
|
||||
{
|
||||
SqlConnectionString connectionOptions = new SqlConnectionString(value);
|
||||
CheckAndThrowOnInvalidCombinationOfConnectionStringAndSqlCredential(connectionOptions);
|
||||
if (_credential != null)
|
||||
{
|
||||
CheckAndThrowOnInvalidCombinationOfConnectionStringAndSqlCredential(connectionOptions);
|
||||
}
|
||||
else
|
||||
{
|
||||
CheckAndThrowOnInvalidCombinationOfConnectionOptionAndAccessToken(connectionOptions);
|
||||
}
|
||||
}
|
||||
ConnectionString_Set(new SqlConnectionPoolKey(value, _credential));
|
||||
ConnectionString_Set(new SqlConnectionPoolKey(value, _credential, _accessToken));
|
||||
_connectionString = value; // Change _connectionString value only after value is validated
|
||||
CacheConnectionStringProperties();
|
||||
}
|
||||
|
@ -242,6 +251,37 @@ namespace System.Data.SqlClient
|
|||
}
|
||||
}
|
||||
|
||||
// AccessToken: To be used for token based authentication
|
||||
public string AccessToken
|
||||
{
|
||||
get
|
||||
{
|
||||
string result = _accessToken;
|
||||
// When a connection is connecting or is ever opened, make AccessToken available only if "Persist Security Info" is set to true
|
||||
// otherwise, return null
|
||||
SqlConnectionString connectionOptions = (SqlConnectionString)UserConnectionOptions;
|
||||
return InnerConnection.ShouldHidePassword && connectionOptions != null && !connectionOptions.PersistSecurityInfo ? null : _accessToken;
|
||||
}
|
||||
set
|
||||
{
|
||||
// If a connection is connecting or is ever opened, AccessToken cannot be set
|
||||
if (!InnerConnection.AllowSetConnectionString)
|
||||
{
|
||||
throw ADP.OpenConnectionPropertySet("AccessToken", InnerConnection.State);
|
||||
}
|
||||
|
||||
if (value != null)
|
||||
{
|
||||
// Check if the usage of AccessToken has any conflict with the keys used in connection string and credential
|
||||
CheckAndThrowOnInvalidCombinationOfConnectionOptionAndAccessToken((SqlConnectionString)ConnectionOptions);
|
||||
}
|
||||
|
||||
// Need to call ConnectionString_Set to do proper pool group check
|
||||
ConnectionString_Set(new SqlConnectionPoolKey(_connectionString, credential: _credential, accessToken: value));
|
||||
_accessToken = value;
|
||||
}
|
||||
}
|
||||
|
||||
public override string Database
|
||||
{
|
||||
// if the connection is open, we need to ask the inner connection what it's
|
||||
|
@ -396,12 +436,16 @@ namespace System.Data.SqlClient
|
|||
if (value != null)
|
||||
{
|
||||
CheckAndThrowOnInvalidCombinationOfConnectionStringAndSqlCredential((SqlConnectionString)ConnectionOptions);
|
||||
if (_accessToken != null)
|
||||
{
|
||||
throw ADP.InvalidMixedUsageOfCredentialAndAccessToken();
|
||||
}
|
||||
}
|
||||
|
||||
_credential = value;
|
||||
|
||||
// Need to call ConnectionString_Set to do proper pool group check
|
||||
ConnectionString_Set(new SqlConnectionPoolKey(_connectionString, _credential));
|
||||
ConnectionString_Set(new SqlConnectionPoolKey(_connectionString, _credential, accessToken: _accessToken));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -422,6 +466,29 @@ namespace System.Data.SqlClient
|
|||
}
|
||||
}
|
||||
|
||||
// CheckAndThrowOnInvalidCombinationOfConnectionOptionAndAccessToken: check if the usage of AccessToken has any conflict
|
||||
// with the keys used in connection string and credential
|
||||
// If there is any conflict, it throws InvalidOperationException
|
||||
// This is to be used setter of ConnectionString and AccessToken properties
|
||||
private void CheckAndThrowOnInvalidCombinationOfConnectionOptionAndAccessToken(SqlConnectionString connectionOptions)
|
||||
{
|
||||
if (UsesClearUserIdOrPassword(connectionOptions))
|
||||
{
|
||||
throw ADP.InvalidMixedUsageOfAccessTokenAndUserIDPassword();
|
||||
}
|
||||
|
||||
if (UsesIntegratedSecurity(connectionOptions))
|
||||
{
|
||||
throw ADP.InvalidMixedUsageOfAccessTokenAndIntegratedSecurity();
|
||||
}
|
||||
|
||||
// Check if the usage of AccessToken has the conflict with credential
|
||||
if (_credential != null)
|
||||
{
|
||||
throw ADP.InvalidMixedUsageOfCredentialAndAccessToken();
|
||||
}
|
||||
}
|
||||
|
||||
protected override DbProviderFactory DbProviderFactory
|
||||
{
|
||||
get => SqlClientFactory.Instance;
|
||||
|
@ -654,6 +721,7 @@ namespace System.Data.SqlClient
|
|||
private void DisposeMe(bool disposing)
|
||||
{
|
||||
_credential = null;
|
||||
_accessToken = null;
|
||||
|
||||
if (!disposing)
|
||||
{
|
||||
|
@ -1360,7 +1428,7 @@ namespace System.Data.SqlClient
|
|||
throw ADP.InvalidArgumentLength(nameof(newPassword), TdsEnums.MAXLEN_NEWPASSWORD);
|
||||
}
|
||||
|
||||
SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential: null);
|
||||
SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential: null, accessToken: null);
|
||||
|
||||
SqlConnectionString connectionOptions = SqlConnectionFactory.FindSqlConnectionOptions(key);
|
||||
if (connectionOptions.IntegratedSecurity)
|
||||
|
@ -1403,7 +1471,7 @@ namespace System.Data.SqlClient
|
|||
throw ADP.InvalidArgumentLength(nameof(newSecurePassword), TdsEnums.MAXLEN_NEWPASSWORD);
|
||||
}
|
||||
|
||||
SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential);
|
||||
SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential: null, accessToken: null);
|
||||
|
||||
SqlConnectionString connectionOptions = SqlConnectionFactory.FindSqlConnectionOptions(key);
|
||||
|
||||
|
@ -1441,7 +1509,7 @@ namespace System.Data.SqlClient
|
|||
if (con != null)
|
||||
con.Dispose();
|
||||
}
|
||||
SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential);
|
||||
SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential: null, accessToken: null);
|
||||
|
||||
SqlConnectionFactory.SingletonInstance.ClearPool(key);
|
||||
}
|
||||
|
|
|
@ -132,7 +132,7 @@ namespace System.Data.SqlClient
|
|||
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, key.Credential, poolGroupProviderInfo, "", null, redirectedUserInstance, userOpt, recoverySessionData, applyTransientFaultHandling: applyTransientFaultHandling);
|
||||
result = new SqlInternalConnectionTds(identity, opt, key.Credential, poolGroupProviderInfo, "", null, redirectedUserInstance, userOpt, recoverySessionData, applyTransientFaultHandling: applyTransientFaultHandling, key.AccessToken);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
//------------------------------------------------------------------------------
|
||||
|
||||
using System.Data.Common;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace System.Data.SqlClient
|
||||
{
|
||||
|
@ -16,16 +17,20 @@ namespace System.Data.SqlClient
|
|||
{
|
||||
private int _hashValue;
|
||||
private SqlCredential _credential;
|
||||
private readonly string _accessToken;
|
||||
|
||||
internal SqlConnectionPoolKey(string connectionString, SqlCredential credential) : base(connectionString)
|
||||
internal SqlConnectionPoolKey(string connectionString, SqlCredential credential, string accessToken) : base(connectionString)
|
||||
{
|
||||
Debug.Assert(_credential == null || _accessToken == null, "Credential and AccessToken can't have the value at the same time.");
|
||||
_credential = credential;
|
||||
_accessToken = accessToken;
|
||||
CalculateHashCode();
|
||||
}
|
||||
|
||||
private SqlConnectionPoolKey(SqlConnectionPoolKey key) : base(key)
|
||||
{
|
||||
_credential = key.Credential;
|
||||
_accessToken = key.AccessToken;
|
||||
CalculateHashCode();
|
||||
}
|
||||
|
||||
|
@ -50,12 +55,18 @@ namespace System.Data.SqlClient
|
|||
|
||||
internal SqlCredential Credential => _credential;
|
||||
|
||||
internal string AccessToken
|
||||
{
|
||||
get
|
||||
{
|
||||
return _accessToken;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
SqlConnectionPoolKey key = obj as SqlConnectionPoolKey;
|
||||
return (key != null &&
|
||||
ConnectionString == key.ConnectionString &&
|
||||
Credential == key.Credential);
|
||||
return (key != null && _credential == key._credential && ConnectionString == key.ConnectionString && Object.ReferenceEquals(_accessToken, key._accessToken));
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
|
@ -74,6 +85,13 @@ namespace System.Data.SqlClient
|
|||
_hashValue = _hashValue * 17 + _credential.GetHashCode();
|
||||
}
|
||||
}
|
||||
else if (_accessToken != null)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
_hashValue = _hashValue * 17 + _accessToken.GetHashCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -106,6 +106,7 @@ namespace System.Data.SqlClient
|
|||
private TdsParser _parser;
|
||||
private SqlLoginAck _loginAck;
|
||||
private SqlCredential _credential;
|
||||
private FederatedAuthenticationFeatureExtensionData? _fedAuthFeatureExtensionData;
|
||||
|
||||
// Connection Resiliency
|
||||
private bool _sessionRecoveryRequested;
|
||||
|
@ -113,6 +114,13 @@ namespace System.Data.SqlClient
|
|||
internal SessionData _currentSessionData; // internal for use from TdsParser only, other should use CurrentSessionData property that will fix database and language
|
||||
private SessionData _recoverySessionData;
|
||||
|
||||
// Federated Authentication
|
||||
// Response obtained from the server for FEDAUTHREQUIRED prelogin option.
|
||||
internal bool _fedAuthRequired;
|
||||
internal bool _federatedAuthenticationRequested;
|
||||
internal bool _federatedAuthenticationAcknowledged;
|
||||
internal byte[] _accessTokenInBytes;
|
||||
|
||||
// The errors in the transient error set are contained in
|
||||
// https://azure.microsoft.com/en-us/documentation/articles/sql-database-develop-error-messages/#transient-faults-connection-loss-and-other-temporary-errors
|
||||
private static readonly HashSet<int> s_transientErrors = new HashSet<int>
|
||||
|
@ -295,7 +303,7 @@ namespace System.Data.SqlClient
|
|||
private RoutingInfo _routingInfo = null;
|
||||
private Guid _originalClientConnectionId = Guid.Empty;
|
||||
private string _routingDestination = null;
|
||||
|
||||
private readonly TimeoutTimer _timeout;
|
||||
|
||||
// although the new password is generally not used it must be passed to the ctor
|
||||
// the new Login7 packet will always write out the new password (or a length of zero and no bytes if not present)
|
||||
|
@ -310,7 +318,9 @@ namespace System.Data.SqlClient
|
|||
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,
|
||||
bool applyTransientFaultHandling = false) : base(connectionOptions)
|
||||
bool applyTransientFaultHandling = false,
|
||||
string accessToken = null) : base(connectionOptions)
|
||||
|
||||
{
|
||||
#if DEBUG
|
||||
if (reconnectSessionData != null)
|
||||
|
@ -335,6 +345,10 @@ namespace System.Data.SqlClient
|
|||
}
|
||||
}
|
||||
|
||||
if (accessToken != null)
|
||||
{
|
||||
_accessTokenInBytes = System.Text.Encoding.Unicode.GetBytes(accessToken);
|
||||
}
|
||||
|
||||
_identity = identity;
|
||||
Debug.Assert(newSecurePassword != null || newPassword != null, "cannot have both new secure change password and string based change password to be null");
|
||||
|
@ -355,9 +369,10 @@ namespace System.Data.SqlClient
|
|||
|
||||
_parserLock.Wait(canReleaseFromAnyThread: false);
|
||||
ThreadHasParserLockForClose = true; // In case of error, let ourselves know that we already own the parser lock
|
||||
|
||||
try
|
||||
{
|
||||
var timeout = TimeoutTimer.StartSecondsTimeout(connectionOptions.ConnectTimeout);
|
||||
_timeout = TimeoutTimer.StartSecondsTimeout(connectionOptions.ConnectTimeout);
|
||||
|
||||
// If transient fault handling is enabled then we can retry the login up to the ConnectRetryCount.
|
||||
int connectionEstablishCount = applyTransientFaultHandling ? connectionOptions.ConnectRetryCount + 1 : 1;
|
||||
|
@ -366,7 +381,7 @@ namespace System.Data.SqlClient
|
|||
{
|
||||
try
|
||||
{
|
||||
OpenLoginEnlist(timeout, connectionOptions, credential, newPassword, newSecurePassword, redirectedUserInstance);
|
||||
OpenLoginEnlist(_timeout, connectionOptions, credential, newPassword, newSecurePassword, redirectedUserInstance);
|
||||
|
||||
break;
|
||||
}
|
||||
|
@ -374,8 +389,8 @@ namespace System.Data.SqlClient
|
|||
{
|
||||
if (i + 1 == connectionEstablishCount
|
||||
|| !applyTransientFaultHandling
|
||||
|| timeout.IsExpired
|
||||
|| timeout.MillisecondsRemaining < transientRetryIntervalInMilliSeconds
|
||||
|| _timeout.IsExpired
|
||||
|| _timeout.MillisecondsRemaining < transientRetryIntervalInMilliSeconds
|
||||
|| !IsTransientError(sqlex))
|
||||
{
|
||||
throw sqlex;
|
||||
|
@ -1017,6 +1032,10 @@ namespace System.Data.SqlClient
|
|||
|
||||
if (_routingInfo == null)
|
||||
{ // ROR should not affect state of connection recovery
|
||||
if (_federatedAuthenticationRequested && !_federatedAuthenticationAcknowledged)
|
||||
{
|
||||
throw SQL.ParsingError(ParsingErrorState.FedAuthNotAcknowledged);
|
||||
}
|
||||
if (!_sessionRecoveryAcknowledged)
|
||||
{
|
||||
_currentSessionData = null;
|
||||
|
@ -1125,9 +1144,22 @@ namespace System.Data.SqlClient
|
|||
_sessionRecoveryRequested = true;
|
||||
}
|
||||
|
||||
if (_accessTokenInBytes != null)
|
||||
{
|
||||
requestedFeatures |= TdsEnums.FeatureExtension.FedAuth;
|
||||
_fedAuthFeatureExtensionData = new FederatedAuthenticationFeatureExtensionData
|
||||
{
|
||||
libraryType = TdsEnums.FedAuthLibrary.SecurityToken,
|
||||
fedAuthRequiredPreLoginResponse = _fedAuthRequired,
|
||||
accessToken = _accessTokenInBytes
|
||||
};
|
||||
// No need any further info from the server for token based authentication. So set _federatedAuthenticationRequested to true
|
||||
_federatedAuthenticationRequested = true;
|
||||
}
|
||||
|
||||
// The GLOBALTRANSACTIONS feature is implicitly requested
|
||||
requestedFeatures |= TdsEnums.FeatureExtension.GlobalTransactions;
|
||||
_parser.TdsLogin(login, requestedFeatures, _recoverySessionData);
|
||||
_parser.TdsLogin(login, requestedFeatures, _recoverySessionData, _fedAuthFeatureExtensionData);
|
||||
}
|
||||
|
||||
private void LoginFailure()
|
||||
|
@ -1900,6 +1932,32 @@ namespace System.Data.SqlClient
|
|||
}
|
||||
break;
|
||||
}
|
||||
case TdsEnums.FEATUREEXT_FEDAUTH:
|
||||
{
|
||||
if (!_federatedAuthenticationRequested)
|
||||
{
|
||||
throw SQL.ParsingErrorFeatureId(ParsingErrorState.UnrequestedFeatureAckReceived, featureId);
|
||||
}
|
||||
|
||||
Debug.Assert(_fedAuthFeatureExtensionData != null, "_fedAuthFeatureExtensionData must not be null when _federatedAuthenticatonRequested == true");
|
||||
|
||||
switch (_fedAuthFeatureExtensionData.Value.libraryType)
|
||||
{
|
||||
case TdsEnums.FedAuthLibrary.SecurityToken:
|
||||
// The server shouldn't have sent any additional data with the ack (like a nonce)
|
||||
if (data.Length != 0)
|
||||
{
|
||||
throw SQL.ParsingError(ParsingErrorState.FedAuthFeatureAckContainsExtraData);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
Debug.Assert(false, "Unknown _fedAuthLibrary type");
|
||||
throw SQL.ParsingErrorLibraryType(ParsingErrorState.FedAuthFeatureAckUnknownLibraryType, (int)_fedAuthFeatureExtensionData.Value.libraryType);
|
||||
}
|
||||
_federatedAuthenticationAcknowledged = true;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
|
|
|
@ -188,6 +188,10 @@ namespace System.Data.SqlClient
|
|||
{
|
||||
return ADP.Argument(SR.GetString(SR.SQL_UserInstanceFailoverNotCompatible));
|
||||
}
|
||||
internal static Exception ParsingErrorLibraryType(ParsingErrorState state, int libraryType)
|
||||
{
|
||||
return ADP.InvalidOperation(SR.GetString(SR.SQL_ParsingErrorAuthLibraryType, ((int)state).ToString(CultureInfo.InvariantCulture), libraryType));
|
||||
}
|
||||
internal static Exception InvalidSQLServerVersionUnknown()
|
||||
{
|
||||
return ADP.DataAdapter(SR.GetString(SR.SQL_InvalidSQLServerVersionUnknown));
|
||||
|
@ -407,6 +411,18 @@ namespace System.Data.SqlClient
|
|||
{
|
||||
return ADP.InvalidOperation(SR.GetString(SR.SQL_ParsingError));
|
||||
}
|
||||
static internal Exception ParsingError(ParsingErrorState state)
|
||||
{
|
||||
return ADP.InvalidOperation(SR.GetString(SR.SQL_ParsingErrorWithState, ((int)state).ToString(CultureInfo.InvariantCulture)));
|
||||
}
|
||||
static internal Exception ParsingErrorValue(ParsingErrorState state, int value)
|
||||
{
|
||||
return ADP.InvalidOperation(SR.GetString(SR.SQL_ParsingErrorValue, ((int)state).ToString(CultureInfo.InvariantCulture), value));
|
||||
}
|
||||
static internal Exception ParsingErrorFeatureId(ParsingErrorState state, int featureId)
|
||||
{
|
||||
return ADP.InvalidOperation(SR.GetString(SR.SQL_ParsingErrorFeatureId, ((int)state).ToString(CultureInfo.InvariantCulture), featureId));
|
||||
}
|
||||
internal static Exception MoneyOverflow(string moneyValue)
|
||||
{
|
||||
return ADP.Overflow(SR.GetString(SR.SQL_MoneyOverflow, moneyValue));
|
||||
|
|
|
@ -202,15 +202,29 @@ namespace System.Data.SqlClient
|
|||
public const byte FEATUREEXT_TERMINATOR = 0xFF;
|
||||
public const byte FEATUREEXT_SRECOVERY = 0x01;
|
||||
public const byte FEATUREEXT_GLOBALTRANSACTIONS = 0x05;
|
||||
public const byte FEATUREEXT_FEDAUTH = 0x02;
|
||||
|
||||
[Flags]
|
||||
public enum FeatureExtension : uint
|
||||
{
|
||||
None = 0,
|
||||
SessionRecovery = 1,
|
||||
FedAuth = 2,
|
||||
GlobalTransactions = 8,
|
||||
}
|
||||
|
||||
public const byte FEDAUTHLIB_LIVEID = 0X00;
|
||||
public const byte FEDAUTHLIB_SECURITYTOKEN = 0x01;
|
||||
public const byte FEDAUTHLIB_ADAL = 0x02;
|
||||
public const byte FEDAUTHLIB_RESERVED = 0X7F;
|
||||
|
||||
public enum FedAuthLibrary : byte
|
||||
{
|
||||
LiveId = FEDAUTHLIB_LIVEID,
|
||||
SecurityToken = FEDAUTHLIB_SECURITYTOKEN,
|
||||
ADAL = FEDAUTHLIB_ADAL, // For later support
|
||||
Default = FEDAUTHLIB_RESERVED
|
||||
}
|
||||
|
||||
// Loginrec defines
|
||||
public const byte MAX_LOG_NAME = 30; // TDS 4.2 login rec max name length
|
||||
|
@ -927,5 +941,30 @@ namespace System.Data.SqlClient
|
|||
Snix_Close,
|
||||
Snix_SendRows,
|
||||
}
|
||||
|
||||
internal enum ParsingErrorState
|
||||
{
|
||||
Undefined = 0,
|
||||
FedAuthInfoLengthTooShortForCountOfInfoIds = 1,
|
||||
FedAuthInfoLengthTooShortForData = 2,
|
||||
FedAuthInfoFailedToReadCountOfInfoIds = 3,
|
||||
FedAuthInfoFailedToReadTokenStream = 4,
|
||||
FedAuthInfoInvalidOffset = 5,
|
||||
FedAuthInfoFailedToReadData = 6,
|
||||
FedAuthInfoDataNotUnicode = 7,
|
||||
FedAuthInfoDoesNotContainStsurlAndSpn = 8,
|
||||
FedAuthInfoNotReceived = 9,
|
||||
FedAuthNotAcknowledged = 10,
|
||||
FedAuthFeatureAckContainsExtraData = 11,
|
||||
FedAuthFeatureAckUnknownLibraryType = 12,
|
||||
UnrequestedFeatureAckReceived = 13,
|
||||
UnknownFeatureAck = 14,
|
||||
InvalidTdsTokenReceived = 15,
|
||||
SessionStateLengthTooShort = 16,
|
||||
SessionStateInvalidStatus = 17,
|
||||
CorruptedTdsStream = 18,
|
||||
ProcessSniPacketFailed = 19,
|
||||
FedAuthRequiredPreLoginResponseInvalidValue = 20,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -366,7 +366,7 @@ namespace System.Data.SqlClient
|
|||
|
||||
_physicalStateObj.SniContext = SniContext.Snix_PreLogin;
|
||||
|
||||
PreLoginHandshakeStatus status = ConsumePreLoginHandshake(encrypt, trustServerCert, integratedSecurity, out marsCapable);
|
||||
PreLoginHandshakeStatus status = ConsumePreLoginHandshake(encrypt, trustServerCert, integratedSecurity, out marsCapable, out _connHandler._fedAuthRequired);
|
||||
|
||||
if (status == PreLoginHandshakeStatus.InstanceFailure)
|
||||
{
|
||||
|
@ -388,7 +388,7 @@ namespace System.Data.SqlClient
|
|||
Debug.Assert(retCode == TdsEnums.SNI_SUCCESS, "Unexpected failure state upon calling SniGetConnectionId");
|
||||
|
||||
SendPreLoginHandshake(instanceName, encrypt);
|
||||
status = ConsumePreLoginHandshake(encrypt, trustServerCert, integratedSecurity, out marsCapable);
|
||||
status = ConsumePreLoginHandshake(encrypt, trustServerCert, integratedSecurity, out marsCapable, out _connHandler._fedAuthRequired);
|
||||
|
||||
// Don't need to check for Sphinx failure, since we've already consumed
|
||||
// one pre-login packet and know we are connecting to Shiloh.
|
||||
|
@ -632,6 +632,12 @@ namespace System.Data.SqlClient
|
|||
optionDataSize += actIdSize;
|
||||
break;
|
||||
|
||||
case (int)PreLoginOptions.FEDAUTHREQUIRED:
|
||||
payload[payloadLength++] = 0x01;
|
||||
offset += 1;
|
||||
optionDataSize += 1;
|
||||
break;
|
||||
|
||||
default:
|
||||
Debug.Assert(false, "UNKNOWN option in SendPreLoginHandshake");
|
||||
break;
|
||||
|
@ -652,9 +658,10 @@ namespace System.Data.SqlClient
|
|||
_physicalStateObj.WritePacket(TdsEnums.HARDFLUSH);
|
||||
}
|
||||
|
||||
private PreLoginHandshakeStatus ConsumePreLoginHandshake(bool encrypt, bool trustServerCert, bool integratedSecurity, out bool marsCapable)
|
||||
private PreLoginHandshakeStatus ConsumePreLoginHandshake(bool encrypt, bool trustServerCert, bool integratedSecurity, out bool marsCapable, out bool fedAuthRequired )
|
||||
{
|
||||
marsCapable = _fMARS; // Assign default value
|
||||
fedAuthRequired = false;
|
||||
bool isYukonOrLater = false;
|
||||
Debug.Assert(_physicalStateObj._syncOverAsync, "Should not attempt pends in a synchronous call");
|
||||
bool result = _physicalStateObj.TryReadNetworkPacket();
|
||||
|
@ -772,7 +779,10 @@ namespace System.Data.SqlClient
|
|||
_encryptionOption == EncryptionOptions.LOGIN)
|
||||
{
|
||||
uint error = 0;
|
||||
uint info = ((encrypt && !trustServerCert) ? TdsEnums.SNI_SSL_VALIDATE_CERTIFICATE : 0)
|
||||
// If we're using legacy server certificate validation behavior (not using access token), then validate if Encrypt=true and Trust Sever Certificate = false.
|
||||
// If using access token, validate if Trust Server Certificate=false.
|
||||
bool shouldValidateServerCert = (encrypt && !trustServerCert) || (_connHandler._accessTokenInBytes != null && !trustServerCert);
|
||||
uint info = (shouldValidateServerCert ? TdsEnums.SNI_SSL_VALIDATE_CERTIFICATE : 0)
|
||||
| (isYukonOrLater ? TdsEnums.SNI_SSL_USE_SCHANNEL_CACHE : 0);
|
||||
|
||||
if (encrypt && !integratedSecurity)
|
||||
|
@ -835,6 +845,23 @@ namespace System.Data.SqlClient
|
|||
offset += 4;
|
||||
break;
|
||||
|
||||
case (int)PreLoginOptions.FEDAUTHREQUIRED:
|
||||
payloadOffset = payload[offset++] << 8 | payload[offset++];
|
||||
payloadLength = payload[offset++] << 8 | payload[offset++];
|
||||
|
||||
// Only 0x00 and 0x01 are accepted values from the server.
|
||||
if (payload[payloadOffset] != 0x00 && payload[payloadOffset] != 0x01)
|
||||
{
|
||||
throw SQL.ParsingErrorValue(ParsingErrorState.FedAuthRequiredPreLoginResponseInvalidValue, (int)payload[payloadOffset]);
|
||||
}
|
||||
|
||||
// We must NOT use the response for the FEDAUTHREQUIRED PreLogin option, if AccessToken is not null, meaning token based authentication is used.
|
||||
if (_connHandler.ConnectionOptions != null || _connHandler._accessTokenInBytes != null)
|
||||
{
|
||||
fedAuthRequired = payload[payloadOffset] == 0x01 ? true : false;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
Debug.Assert(false, "UNKNOWN option in ConsumePreLoginHandshake, option:" + option);
|
||||
|
||||
|
@ -5963,6 +5990,70 @@ namespace System.Data.SqlClient
|
|||
return len;
|
||||
}
|
||||
|
||||
internal int WriteFedAuthFeatureRequest(FederatedAuthenticationFeatureExtensionData fedAuthFeatureData,
|
||||
bool write /* if false just calculates the length */)
|
||||
{
|
||||
Debug.Assert(fedAuthFeatureData.libraryType == TdsEnums.FedAuthLibrary.SecurityToken,
|
||||
"only Security Token are supported in writing feature request");
|
||||
|
||||
int dataLen = 0;
|
||||
int totalLen = 0;
|
||||
|
||||
// set dataLen and totalLen
|
||||
switch (fedAuthFeatureData.libraryType)
|
||||
{
|
||||
case TdsEnums.FedAuthLibrary.SecurityToken:
|
||||
Debug.Assert(fedAuthFeatureData.accessToken != null, "AccessToken should not be null.");
|
||||
dataLen = 1 + sizeof(int) + fedAuthFeatureData.accessToken.Length; // length of feature data = 1 byte for library and echo, security token length and sizeof(int) for token lengh itself
|
||||
break;
|
||||
default:
|
||||
Debug.Assert(false, "Unrecognized library type for fedauth feature extension request");
|
||||
break;
|
||||
}
|
||||
|
||||
totalLen = dataLen + 5; // length of feature id (1 byte), data length field (4 bytes), and feature data (dataLen)
|
||||
|
||||
// write feature id
|
||||
if (write)
|
||||
{
|
||||
_physicalStateObj.WriteByte(TdsEnums.FEATUREEXT_FEDAUTH);
|
||||
|
||||
// set options
|
||||
byte options = 0x00;
|
||||
|
||||
// set upper 7 bits of options to indicate fed auth library type
|
||||
switch (fedAuthFeatureData.libraryType)
|
||||
{
|
||||
case TdsEnums.FedAuthLibrary.SecurityToken:
|
||||
Debug.Assert(_connHandler._federatedAuthenticationRequested == true, "_federatedAuthenticationRequested field should be true");
|
||||
options |= TdsEnums.FEDAUTHLIB_SECURITYTOKEN << 1;
|
||||
break;
|
||||
default:
|
||||
Debug.Assert(false, "Unrecognized FedAuthLibrary type for feature extension request");
|
||||
break;
|
||||
}
|
||||
|
||||
options |= (byte)(fedAuthFeatureData.fedAuthRequiredPreLoginResponse == true ? 0x01 : 0x00);
|
||||
|
||||
// write dataLen and options
|
||||
WriteInt(dataLen, _physicalStateObj);
|
||||
_physicalStateObj.WriteByte(options);
|
||||
|
||||
// write accessToken for FedAuthLibrary.SecurityToken
|
||||
switch (fedAuthFeatureData.libraryType)
|
||||
{
|
||||
case TdsEnums.FedAuthLibrary.SecurityToken:
|
||||
WriteInt(fedAuthFeatureData.accessToken.Length, _physicalStateObj);
|
||||
_physicalStateObj.WriteByteArray(fedAuthFeatureData.accessToken, fedAuthFeatureData.accessToken.Length, 0);
|
||||
break;
|
||||
default:
|
||||
Debug.Fail("Unrecognized FedAuthLibrary type for feature extension request");
|
||||
break;
|
||||
}
|
||||
}
|
||||
return totalLen;
|
||||
}
|
||||
|
||||
internal int WriteGlobalTransactionsFeatureRequest(bool write /* if false just calculates the length */)
|
||||
{
|
||||
int len = 5; // 1byte = featureID, 4bytes = featureData length
|
||||
|
@ -5977,13 +6068,17 @@ namespace System.Data.SqlClient
|
|||
return len;
|
||||
}
|
||||
|
||||
internal void TdsLogin(SqlLogin rec, TdsEnums.FeatureExtension requestedFeatures, SessionData recoverySessionData)
|
||||
internal void TdsLogin(SqlLogin rec, TdsEnums.FeatureExtension requestedFeatures, SessionData recoverySessionData, FederatedAuthenticationFeatureExtensionData? fedAuthFeatureExtensionData)
|
||||
{
|
||||
_physicalStateObj.SetTimeoutSeconds(rec.timeout);
|
||||
|
||||
Debug.Assert(recoverySessionData == null || (requestedFeatures & TdsEnums.FeatureExtension.SessionRecovery) != 0, "Recovery session data without session recovery feature request");
|
||||
Debug.Assert(TdsEnums.MAXLEN_HOSTNAME >= rec.hostName.Length, "_workstationId.Length exceeds the max length for this value");
|
||||
|
||||
Debug.Assert(!rec.useSSPI || (requestedFeatures & TdsEnums.FeatureExtension.FedAuth) == 0, "Cannot use both SSPI and FedAuth");
|
||||
Debug.Assert(fedAuthFeatureExtensionData == null || (requestedFeatures & TdsEnums.FeatureExtension.FedAuth) != 0, "fedAuthFeatureExtensionData provided without fed auth feature request");
|
||||
Debug.Assert(fedAuthFeatureExtensionData != null || (requestedFeatures & TdsEnums.FeatureExtension.FedAuth) == 0, "Fed Auth feature requested without specifying fedAuthFeatureExtensionData.");
|
||||
|
||||
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");
|
||||
|
||||
|
@ -6060,12 +6155,12 @@ namespace System.Data.SqlClient
|
|||
byte[] outSSPIBuff = null;
|
||||
uint outSSPILength = 0;
|
||||
|
||||
// only add lengths of password and username if not using SSPI
|
||||
if (!rec.useSSPI)
|
||||
// only add lengths of password and username if not using SSPI or requesting federated authentication info
|
||||
if (!rec.useSSPI && !_connHandler._federatedAuthenticationRequested)
|
||||
{
|
||||
checked
|
||||
{
|
||||
length += (userName.Length * 2) + encryptedPasswordLengthInBytes
|
||||
length += (userName.Length * 2) + encryptedPasswordLengthInBytes
|
||||
+ encryptedChangePasswordLengthInBytes;
|
||||
}
|
||||
}
|
||||
|
@ -6110,6 +6205,11 @@ namespace System.Data.SqlClient
|
|||
{
|
||||
length += WriteGlobalTransactionsFeatureRequest(false);
|
||||
}
|
||||
if ((requestedFeatures & TdsEnums.FeatureExtension.FedAuth) != 0)
|
||||
{
|
||||
Debug.Assert(fedAuthFeatureExtensionData != null, "fedAuthFeatureExtensionData should not null.");
|
||||
length += WriteFedAuthFeatureRequest(fedAuthFeatureExtensionData.Value, write: false);
|
||||
}
|
||||
length++; // for terminator
|
||||
}
|
||||
|
||||
|
@ -6325,7 +6425,6 @@ namespace System.Data.SqlClient
|
|||
_physicalStateObj.WriteByteArray(outSSPIBuff, (int)outSSPILength, 0);
|
||||
|
||||
WriteString(rec.attachDBFilename, _physicalStateObj);
|
||||
|
||||
if (!rec.useSSPI)
|
||||
{
|
||||
if (rec.newSecurePassword != null)
|
||||
|
@ -6348,6 +6447,11 @@ namespace System.Data.SqlClient
|
|||
{
|
||||
WriteGlobalTransactionsFeatureRequest(true);
|
||||
}
|
||||
if ((requestedFeatures & TdsEnums.FeatureExtension.FedAuth) != 0)
|
||||
{
|
||||
Debug.Assert(fedAuthFeatureExtensionData != null, "fedAuthFeatureExtensionData should not null.");
|
||||
WriteFedAuthFeatureRequest(fedAuthFeatureExtensionData.Value, write: true);
|
||||
};
|
||||
_physicalStateObj.WriteByte(0xFF); // terminator
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@ namespace System.Data.SqlClient
|
|||
THREADID,
|
||||
MARS,
|
||||
TRACEID,
|
||||
FEDAUTHREQUIRED,
|
||||
NUMOPT,
|
||||
LASTOPT = 255
|
||||
}
|
||||
|
@ -66,6 +67,16 @@ namespace System.Data.SqlClient
|
|||
Broken,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Struct encapsulating the data to be sent to the server as part of Federated Authentication Feature Extension.
|
||||
/// </summary>
|
||||
internal struct FederatedAuthenticationFeatureExtensionData
|
||||
{
|
||||
internal TdsEnums.FedAuthLibrary libraryType;
|
||||
internal bool fedAuthRequiredPreLoginResponse;
|
||||
internal byte[] accessToken;
|
||||
}
|
||||
|
||||
sealed internal class SqlCollation
|
||||
{
|
||||
// First 20 bits of info field represent the lcid, bits 21-25 are compare options
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
// 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 Xunit;
|
||||
|
||||
namespace System.Data.SqlClient.Tests
|
||||
{
|
||||
public class AADAccessTokenTest
|
||||
{
|
||||
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" })]
|
||||
[InlineData("Test combination of Access Token and Password", new object[] { "PWD", "samplePassword" })]
|
||||
[InlineData("Test combination of Access Token and Credentials", new object[] { "sampleUserId" })]
|
||||
public void InvalidCombinationOfAccessToken(string description, object[] Params)
|
||||
{
|
||||
_builder = new SqlConnectionStringBuilder
|
||||
{
|
||||
["Data Source"] = "sample.database.windows.net"
|
||||
};
|
||||
|
||||
if (Params.Length == 1)
|
||||
{
|
||||
Security.SecureString password = new Security.SecureString();
|
||||
password.MakeReadOnly();
|
||||
_credential = new SqlCredential(Params[0] as string, password);
|
||||
}
|
||||
else
|
||||
{
|
||||
_builder[Params[0] as string] = Params[1];
|
||||
}
|
||||
InvalidCombinationCheck(_credential);
|
||||
}
|
||||
|
||||
private void InvalidCombinationCheck(SqlCredential credential)
|
||||
{
|
||||
using (SqlConnection connection = new SqlConnection(_builder.ConnectionString, credential))
|
||||
{
|
||||
Assert.Throws<InvalidOperationException>(() => connection.AccessToken = "SampleAccessToken");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@
|
|||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'netstandard-Windows_NT-Debug|AnyCPU'" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'netstandard-Windows_NT-Release|AnyCPU'" />
|
||||
<ItemGroup>
|
||||
<Compile Include="AADAccessTokenTest.cs" />
|
||||
<Compile Include="CloneTests.cs" />
|
||||
<Compile Include="BaseProviderAsyncTest\BaseProviderAsyncTest.cs" />
|
||||
<Compile Include="BaseProviderAsyncTest\MockCommand.cs" />
|
||||
|
|
|
@ -17,6 +17,10 @@ namespace System.Data.SqlClient.ManualTesting.Tests
|
|||
private static readonly Type s_tdsParserStateObjectFactory = s_systemDotData?.GetType("System.Data.SqlClient.TdsParserStateObjectFactory");
|
||||
private static readonly PropertyInfo s_useManagedSNI = s_tdsParserStateObjectFactory?.GetProperty("UseManagedSNI", BindingFlags.Static | BindingFlags.Public);
|
||||
|
||||
private static readonly string[] s_azureSqlServerEndpoints = {".database.windows.net",
|
||||
".database.cloudapi.de",
|
||||
".database.usgovcloudapi.net",
|
||||
".database.chinacloudapi.cn"};
|
||||
static DataTestUtility()
|
||||
{
|
||||
NpConnStr = Environment.GetEnvironmentVariable("TEST_NP_CONN_STR");
|
||||
|
@ -64,6 +68,42 @@ namespace System.Data.SqlClient.ManualTesting.Tests
|
|||
|
||||
public static bool IsIntegratedSecuritySetup() => int.TryParse(Environment.GetEnvironmentVariable("TEST_INTEGRATEDSECURITY_SETUP"), out int result) ? result == 1 : false;
|
||||
|
||||
public static string getAccessToken()
|
||||
{
|
||||
return Environment.GetEnvironmentVariable("TEST_ACCESSTOKEN_SETUP");
|
||||
}
|
||||
|
||||
public static bool IsAccessTokenSetup() => string.IsNullOrEmpty(getAccessToken()) ? false : true;
|
||||
|
||||
// This method assumes dataSource parameter is in TCP connection string format.
|
||||
public static bool IsAzureSqlServer(string dataSource)
|
||||
{
|
||||
int i = dataSource.LastIndexOf(',');
|
||||
if (i >= 0)
|
||||
{
|
||||
dataSource = dataSource.Substring(0, i);
|
||||
}
|
||||
|
||||
i = dataSource.LastIndexOf('\\');
|
||||
if (i >= 0)
|
||||
{
|
||||
dataSource = dataSource.Substring(0, i);
|
||||
}
|
||||
|
||||
// trim redundant whitespace
|
||||
dataSource = dataSource.Trim();
|
||||
|
||||
// check if servername end with any azure endpoints
|
||||
for (i = 0; i < s_azureSqlServerEndpoints.Length; i++)
|
||||
{
|
||||
if (dataSource.EndsWith(s_azureSqlServerEndpoints[i], StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool CheckException<TException>(Exception ex, string exceptionMessage, bool innerExceptionMustBeNull) where TException : Exception
|
||||
{
|
||||
return ((ex != null) && (ex is TException) &&
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
// 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.Text.RegularExpressions;
|
||||
using Xunit;
|
||||
|
||||
namespace System.Data.SqlClient.ManualTesting.Tests
|
||||
{
|
||||
public class AADAccessTokenTest
|
||||
{
|
||||
private static bool IsAccessTokenSetup() => DataTestUtility.IsAccessTokenSetup();
|
||||
private static bool IsAzureServer() => DataTestUtility.IsAzureSqlServer(GetDataSource());
|
||||
|
||||
[ConditionalFact(nameof(IsAccessTokenSetup), nameof(IsAzureServer))]
|
||||
public static void AccessTokenTest()
|
||||
{
|
||||
using (SqlConnection connection = new SqlConnection(DataTestUtility.TcpConnStr))
|
||||
{
|
||||
connection.AccessToken = DataTestUtility.getAccessToken();
|
||||
connection.Open();
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetDataSource()
|
||||
{
|
||||
// Obtain Data source from connection string
|
||||
string tcpConnStr = DataTestUtility.TcpConnStr.Replace(" ", string.Empty);
|
||||
Regex regex = new Regex("DataSource=(.*?);");
|
||||
Match match = regex.Match(tcpConnStr);
|
||||
return match.Groups[1].ToString();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -29,6 +29,7 @@
|
|||
<Compile Include="DataCommon\DataTestUtility.cs" />
|
||||
<Compile Include="DataCommon\ProxyServer.cs" />
|
||||
<Compile Include="DataCommon\SystemDataResourceManager.cs" />
|
||||
<Compile Include="SQL\ConnectivityTests\AADAccessTokenTest.cs" />
|
||||
<Compile Include="SQL\ParameterTest\DateTimeVariantTest.cs" />
|
||||
<Compile Include="SQL\ParameterTest\OutputParameter.cs" />
|
||||
<Compile Include="SQL\ParameterTest\ParametersTest.cs" />
|
||||
|
|
Загрузка…
Ссылка в новой задаче