-
Notifications
You must be signed in to change notification settings - Fork 331
Fix AccessTokenCallback TNIR behavior and make SspiContextProvider mutually exclusive with token auth #4520
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
48085ae
dbc53bf
964a428
9c92fda
feb5e85
126ce1e
bd56892
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -262,6 +262,13 @@ private SqlConnection(SqlConnection connection) | |
|
|
||
| _accessToken = connection._accessToken; | ||
| _accessTokenCallback = connection._accessTokenCallback; | ||
|
|
||
| // CopyFrom retains the source PoolGroup, and therefore the source ConnectionPoolKey. | ||
| // The provider must be copied along with it, otherwise the clone would authenticate | ||
| // with a provider that its public property does not report, and would let the caller | ||
| // set AccessToken/AccessTokenCallback without tripping the mutual-exclusivity checks. | ||
| _sspiContextProvider = connection._sspiContextProvider; | ||
|
|
||
| CacheConnectionStringProperties(); | ||
| } | ||
|
|
||
|
|
@@ -763,8 +770,16 @@ public string AccessToken | |
| CheckAndThrowOnInvalidCombinationOfConnectionOptionAndAccessToken(ConnectionOptions); | ||
| } | ||
|
|
||
| // Need to call ConnectionString_Set to do proper pool group check | ||
| ConnectionString_Set(new ConnectionPoolKey(_connectionString, credential: _credential, accessToken: value, accessTokenCallback: null, sspiContextProvider: null)); | ||
| // Need to call ConnectionString_Set to do proper pool group check. | ||
| // AccessTokenCallback and SspiContextProvider are mutually exclusive with AccessToken | ||
| // (validated above), so they are always null here when a token is supplied. Passing the | ||
| // current values through matters only when the token is being cleared. | ||
| ConnectionString_Set(new ConnectionPoolKey( | ||
| _connectionString, | ||
| credential: _credential, | ||
| accessToken: value, | ||
| accessTokenCallback: _accessTokenCallback, | ||
| sspiContextProvider: _sspiContextProvider)); | ||
| _accessToken = value; | ||
| } | ||
| } | ||
|
|
@@ -787,7 +802,12 @@ public Func<SqlAuthenticationParameters, CancellationToken, Task<SqlAuthenticati | |
| CheckAndThrowOnInvalidCombinationOfConnectionOptionAndAccessTokenCallback(ConnectionOptions); | ||
| } | ||
|
|
||
| ConnectionString_Set(new ConnectionPoolKey(_connectionString, credential: _credential, accessToken: null, accessTokenCallback: value, sspiContextProvider: null)); | ||
| ConnectionString_Set(new ConnectionPoolKey( | ||
| _connectionString, | ||
| credential: _credential, | ||
| accessToken: _accessToken, | ||
| accessTokenCallback: value, | ||
| sspiContextProvider: _sspiContextProvider)); | ||
| _accessTokenCallback = value; | ||
| } | ||
| } | ||
|
|
@@ -804,7 +824,21 @@ public SspiContextProvider SspiContextProvider | |
| throw ADP.OpenConnectionPropertySet(nameof(SspiContextProvider), InnerConnection.State); | ||
| } | ||
|
|
||
| ConnectionString_Set(new ConnectionPoolKey(_connectionString, credential: _credential, accessToken: null, accessTokenCallback: null, sspiContextProvider: value)); | ||
| if (value != null) | ||
| { | ||
| // SSPI is an alternative to token-based authentication, so the two are mutually exclusive. | ||
| CheckAndThrowOnInvalidCombinationOfConnectionOptionAndSspiContextProvider(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we copy
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch, and the bypass you describe is real. Fixed in bd56892. The copy constructor now copies
|
||
| } | ||
|
|
||
| // Because SSPI and token authentication are mutually exclusive (validated above), the token | ||
| // state is always null when a provider is supplied. Passing the current values through | ||
| // matters only when the provider is being cleared, where any token state must survive. | ||
| ConnectionString_Set(new ConnectionPoolKey( | ||
| _connectionString, | ||
| credential: _credential, | ||
| accessToken: _accessToken, | ||
| accessTokenCallback: _accessTokenCallback, | ||
|
cheenamalhotra marked this conversation as resolved.
|
||
| sspiContextProvider: value)); | ||
| _sspiContextProvider = value; | ||
| } | ||
| } | ||
|
|
@@ -1101,7 +1135,12 @@ public SqlCredential Credential | |
| _credential = value; | ||
|
|
||
| // Need to call ConnectionString_Set to do proper pool group check | ||
| ConnectionString_Set(new ConnectionPoolKey(_connectionString, _credential, accessToken: _accessToken, accessTokenCallback: _accessTokenCallback, sspiContextProvider: null)); | ||
| ConnectionString_Set(new ConnectionPoolKey( | ||
| _connectionString, | ||
| _credential, | ||
| accessToken: _accessToken, | ||
| accessTokenCallback: _accessTokenCallback, | ||
| sspiContextProvider: _sspiContextProvider)); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -1157,6 +1196,11 @@ private void CheckAndThrowOnInvalidCombinationOfConnectionOptionAndAccessToken(S | |
| { | ||
| throw ADP.InvalidMixedUsageOfAccessTokenAndTokenCallback(); | ||
| } | ||
|
|
||
| if (_sspiContextProvider != null) | ||
| { | ||
| throw ADP.InvalidMixedUsageOfAccessTokenAndSspiContextProvider(); | ||
| } | ||
|
cheenamalhotra marked this conversation as resolved.
|
||
| } | ||
|
|
||
| // CheckAndThrowOnInvalidCombinationOfConnectionOptionAndAccessTokenCallback: check if the usage of AccessTokenCallback has any conflict | ||
|
|
@@ -1179,6 +1223,23 @@ private void CheckAndThrowOnInvalidCombinationOfConnectionOptionAndAccessTokenCa | |
| { | ||
| throw ADP.InvalidMixedUsageOfAccessTokenAndTokenCallback(); | ||
| } | ||
|
|
||
| if (_sspiContextProvider != null) | ||
| { | ||
| throw ADP.InvalidMixedUsageOfAccessTokenAndSspiContextProvider(); | ||
| } | ||
| } | ||
|
|
||
| // CheckAndThrowOnInvalidCombinationOfConnectionOptionAndSspiContextProvider: SSPI is an alternative to | ||
| // token-based authentication, so a context provider cannot be combined with AccessToken or | ||
| // AccessTokenCallback. If there is any conflict, it throws InvalidOperationException. | ||
| // This is to be used by the setter of the SspiContextProvider property. | ||
| private void CheckAndThrowOnInvalidCombinationOfConnectionOptionAndSspiContextProvider() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we make the
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed — that was the last setter still hard-coding a sibling to null. Fixed in bd56892ac: the I went with preserve rather than validate. The two are not conceptually exclusive the way SSPI and token auth are, and adding a throw here would be a behaviour change for anyone who sets both, which this PR does not need to take on. Preserving simply removes the divergence.
|
||
| { | ||
| if (_accessToken != null || _accessTokenCallback != null) | ||
| { | ||
| throw ADP.InvalidMixedUsageOfSspiContextProviderAndAccessToken(); | ||
| } | ||
| } | ||
|
|
||
| /// <include file='../../../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml' path='docs/members[@name="SqlConnection"]/DbProviderFactory/*' /> | ||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,9 @@ | |
| using System; | ||
| using Microsoft.Data.SqlClient.Tests.Common; | ||
| using Xunit; | ||
| #if NETFRAMEWORK | ||
| using SqlConnectionInternal = global::Microsoft.Data.SqlClient.Connection.SqlConnectionInternal; | ||
| #endif | ||
|
|
||
| namespace Microsoft.Data.SqlClient.UnitTests.Microsoft.Data.SqlClient | ||
| { | ||
|
|
@@ -63,6 +66,48 @@ public void TestDefaultTnir(string dataSource, bool? tnirEnabledInConnString, bo | |
| // Assert | ||
| Assert.Equal(expectedValue, connectionString.TransparentNetworkIPResolution); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// TNIR is disabled by default whenever federated authentication is in play, including when | ||
| /// the token is supplied directly through <c>AccessToken</c> or <c>AccessTokenCallback</c>, | ||
| /// unless the user explicitly specified the TNIR keyword. | ||
| /// </summary> | ||
| [Theory] | ||
| // Non-Azure endpoint, no explicit TNIR keyword, no token: TNIR stays enabled. | ||
| [InlineData("my.test.server", false, null, false)] | ||
| // Non-Azure endpoint, no explicit TNIR keyword, token supplied: TNIR is disabled. | ||
| [InlineData("my.test.server", true, null, true)] | ||
| // Azure endpoint always disables TNIR when the keyword is absent. | ||
| [InlineData("test.database.windows.net", false, null, true)] | ||
| [InlineData("test.database.windows.net", true, null, true)] | ||
| // An explicit TNIR keyword always wins, regardless of access token or endpoint. Note that | ||
| // this method only decides whether the default is overridden; an explicit false is honoured | ||
| // by the caller through SqlConnectionOptions.TransparentNetworkIPResolution itself. | ||
| [InlineData("my.test.server", true, true, false)] | ||
| [InlineData("test.database.windows.net", true, true, false)] | ||
| [InlineData("test.database.windows.net", false, true, false)] | ||
| [InlineData("my.test.server", true, false, false)] | ||
| [InlineData("my.test.server", false, false, false)] | ||
| [InlineData("test.database.windows.net", true, false, false)] | ||
| [InlineData("test.database.windows.net", false, false, false)] | ||
| public void TestShouldDisableTnirWithCallerSuppliedToken( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we add regression coverage through both
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're right on both counts, and the "reverting the property leaves these tests green" check is the useful bar. Addressed in bd56892. Explicit Coverage through
One caveat worth stating plainly: I'm on macOS, so I could only run the net8.0 leg locally. The netfx arm compiles under the same theory but its assertions will first execute in CI. If you'd rather not carry a test-only member in A note on what I could not cover: I also tried asserting the certificate-validation site ( |
||
| string dataSource, | ||
| bool isAccessTokenProvided, | ||
| bool? tnirInConnString, | ||
| bool expectedValue) | ||
| { | ||
| SqlConnectionStringBuilder builder = new() { DataSource = dataSource }; | ||
| if (tnirInConnString.HasValue) | ||
| { | ||
| builder.TransparentNetworkIPResolution = tnirInConnString.Value; | ||
| } | ||
|
|
||
| SqlConnectionOptions connectionOptions = new(builder.ConnectionString); | ||
|
|
||
| Assert.Equal( | ||
| expectedValue, | ||
| SqlConnectionInternal.ShouldDisableTnir(connectionOptions, isAccessTokenProvided)); | ||
| } | ||
| #endif | ||
| /// <summary> | ||
| /// Test MSF values when set through connection string and through app context switch. | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.