Skip to content
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

Add | Add Workload Identity Support #2159

Merged
merged 8 commits into from Dec 7, 2023

Conversation

wsugarman
Copy link
Contributor

@wsugarman wsugarman commented Sep 16, 2023

Resolves #2158

In this pull request, I make two changes:

  1. Use the User Id as the Client Id for workload identity if specified when using Authentication = Active Directory Default
  2. Add a new value ActiveDirectoryWorkloadIdentity for the enum SqlAuthenticationMethod that operates very similarly to ActiveDirectoryManagedIdentity except that it uses WorkloadIdentityCredential instead.
    • Like with managed identities, it will use the value of the User Id parameter in the connection string for its Client Id if specified. But unlike managed identity, WorkloadIdentityCredentialOptions defaults its value from environment variables: AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_FEDERATED_TOKEN_FILE. As of writing this description, only the Client Id may be overridden by the connection string.

@@ -453,6 +453,27 @@ public static void ActiveDirectoryManagedIdentityWithCredentialsMustFail()
Assert.Contains(expectedMessage, e.Message);
}

[ConditionalFact(nameof(IsAADConnStringsSetup))]
public static void ActiveDirectoryWorkloadIdentityWithCredentialsMustFail()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still need to write test cases for auth when the test environment supports workload identity, but I need to do more research in how to write that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, kind of new here, but do you have an update on this? It's 2 months old.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our automated test environment is unlikely to ever support workload identity unless there is a way to emulate it without setting up an expensive Kubernetes environment.

@wsugarman - Do you think that (emulation) could ever happen?

Either way, I'm okay with manual testing of this feature for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unaware of any existing emulation of workload identity, but perhaps it may make its way into other AKS-based offerings that could be simpler to set-up, like Azure Container Apps? I think in the meantime, without AKS, it will need to be manually tested

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please let me know if you have any additional questions or asks. My team has actually been using our own SQL authentication provider in AKS using similar logic to that included in the PR.

if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryServicePrincipal)
{
AccessToken accessToken = await new ClientSecretCredential(audience, parameters.UserId, parameters.Password, tokenCredentialOptions).GetTokenAsync(tokenRequestContext, cts.Token).ConfigureAwait(false);
SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token for Active Directory Service Principal auth mode. Expiry Time: {0}", accessToken.ExpiresOn);
return new SqlAuthenticationToken(accessToken.Token, accessToken.ExpiresOn);
}

if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity || parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryMSI)
{
WorkloadIdentityCredentialOptions options = new()
Copy link
Contributor Author

@wsugarman wsugarman Sep 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is documented well, it should work I suppose. I'll let @David-Engel take final call to review if this feature fits well in the AAD stack potentially in sync with other drivers as well.

@codecov
Copy link

codecov bot commented Sep 16, 2023

Codecov Report

Attention: 18 lines in your changes are missing coverage. Please review.

Comparison is base (4203237) 72.58% compared to head (139bbd6) 72.40%.

Files Patch % Lines
...SqlClient/ActiveDirectoryAuthenticationProvider.cs 41.66% 7 Missing ⚠️
...nt/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs 0.00% 4 Missing ⚠️
.../netcore/src/Microsoft/Data/SqlClient/TdsParser.cs 0.00% 2 Missing ⚠️
...ent/SqlAuthenticationProviderManager.NetCoreApp.cs 0.00% 1 Missing ⚠️
...core/src/Microsoft/Data/SqlClient/SqlConnection.cs 85.71% 1 Missing ⚠️
...Data/SqlClient/SqlAuthenticationProviderManager.cs 50.00% 1 Missing ⚠️
...etfx/src/Microsoft/Data/SqlClient/SqlConnection.cs 85.71% 1 Missing ⚠️
...rc/Microsoft/Data/SqlClient/SqlConnectionString.cs 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2159      +/-   ##
==========================================
- Coverage   72.58%   72.40%   -0.19%     
==========================================
  Files         309      309              
  Lines       61959    62003      +44     
==========================================
- Hits        44975    44894      -81     
- Misses      16984    17109     +125     
Flag Coverage Δ
addons 92.88% <ø> (ø)
netcore 76.51% <62.50%> (-0.31%) ⬇️
netfx 69.92% <61.11%> (-0.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@JRahnama
Copy link
Member

@wsugarman,I have not investigated the details yet, but couldn’t’ve achieved this by setting a custom provider? I will dig deeper into this next week, but do we have to add every possible scenario to the driver?

@wsugarman
Copy link
Contributor Author

@wsugarman,I have not investigated the details yet, but couldn’t’ve achieved this by setting a custom provider? I will dig deeper into this next week, but do we have to add every possible scenario to the driver?

I had no idea this API existed 😅 This would be a great workaround!
I do think the library will benefit from including it in the driver as it replaces more and more managed identity scenarios.

@David-Engel
Copy link
Contributor

In addition to creating a custom provider that overrides existing authentication options, there is a new, simpler AccessTokenCallback property added in 5.2 preview 3 that makes implementing new token authentication methods much easier. (Not quite as easy as a connection string adjustment, but a-few-lines-of-code easy.)

using Azure.Core;
using Azure.Identity;
using Microsoft.Data.SqlClient;
...

            string s_defaultScopeSuffix = "/.default";
            DefaultAzureCredentialOptions options = new()
            {
                //set your options here
                ExcludeAzureCliCredential = true,
                ExcludeVisualStudioCredential = true
            };
            var cred = new DefaultAzureCredential(options);
            Func<SqlAuthenticationParameters, CancellationToken, Task<SqlAuthenticationToken>> myTokenCallback = async (ctx, cancellationToken) =>
            {
                string scope = ctx.Resource.EndsWith(s_defaultScopeSuffix) ? ctx.Resource : ctx.Resource + s_defaultScopeSuffix;
                AccessToken token = await cred.GetTokenAsync(new TokenRequestContext(new[] { scope }), cancellationToken);
                return new SqlAuthenticationToken(token.Token, token.ExpiresOn);
            };

            using (SqlConnection sqlConnection = new SqlConnection("Server=name.database.windows.net;Database=db;"))
            {
                sqlConnection.AccessTokenCallback = myTokenCallback;
                sqlConnection.Open();
                Console.WriteLine("Connected successfully!");
            }

@wsugarman
Copy link
Contributor Author

In addition to creating a custom provider that overrides existing authentication options, there is a new, simpler AccessTokenCallback property added in 5.2 preview 3 that makes implementing new token authentication methods much easier. (Not quite as easy as a connection string adjustment, but a-few-lines-of-code easy.)

using Azure.Core;
using Azure.Identity;
using Microsoft.Data.SqlClient;
...

            string s_defaultScopeSuffix = "/.default";
            DefaultAzureCredentialOptions options = new()
            {
                //set your options here
                ExcludeAzureCliCredential = true,
                ExcludeVisualStudioCredential = true
            };
            var cred = new DefaultAzureCredential(options);
            Func<SqlAuthenticationParameters, CancellationToken, Task<SqlAuthenticationToken>> myTokenCallback = async (ctx, cancellationToken) =>
            {
                string scope = ctx.Resource.EndsWith(s_defaultScopeSuffix) ? ctx.Resource : ctx.Resource + s_defaultScopeSuffix;
                AccessToken token = await cred.GetTokenAsync(new TokenRequestContext(new[] { scope }), cancellationToken);
                return new SqlAuthenticationToken(token.Token, token.ExpiresOn);
            };

            using (SqlConnection sqlConnection = new SqlConnection("Server=name.database.windows.net;Database=db;"))
            {
                sqlConnection.AccessTokenCallback = myTokenCallback;
                sqlConnection.Open();
                Console.WriteLine("Connected successfully!");
            }

Oh! That will be great! So, it will join the existing AccessToken property, but this will be a more generic callback complete with the auth parameters from the connection string?

That will be super helpful, but I still think a common scenario like workload identity will warrant inclusion in the driver. Otherwise, users will have to create the same workload identity SqlAuthenticationProvider or likely create some abstraction around SqlConnection that automatically assigns the AccessTokenCallback if there a lot of places in the codebase where a SqlConnection is created and opened.

@David-Engel
Copy link
Contributor

Oh! That will be great! So, it will join the existing AccessToken property, but this will be a more generic callback complete with the auth parameters from the connection string?

Yes. The AccessToken property suffers from some major drawbacks that affect connection pooling:

  • SqlClient doesn't know when the token will expire, resulting in errors when a connection with an expired token is obtained from the connection pool.
  • Since it's part of the pool key, if you set min pool size, those min connections will never be automatically cleaned up unless ClearPool() or ClearAllPools() is called.

The callback gives SqlClient the expiration date and allows it to get a new token when a new physical connection is required, just like the built-in methods.

That will be super helpful, but I still think a common scenario like workload identity will warrant inclusion in the driver. Otherwise, users will have to create the same workload identity SqlAuthenticationProvider or likely create some abstraction around SqlConnection that automatically assigns the AccessTokenCallback if there a lot of places in the codebase where a SqlConnection is created and opened.

Oh definitely. It's just difficult to keep up with all the new Azure AD authentication methods they keep coming up with. So a generic method at least takes the pressure off implementing every new one ASAP. You can write a wrapper around SqlConnection to authenticate however you want and still have connection pooling work correctly.

@DavoudEshtehari DavoudEshtehari added this to the 5.2.0-preview4 milestone Oct 10, 2023
@DavoudEshtehari DavoudEshtehari added this to In progress in SqlClient v5.2 via automation Oct 31, 2023
@DavoudEshtehari DavoudEshtehari added the 💡 Enhancement New feature request label Oct 31, 2023
@@ -453,6 +453,27 @@ public static void ActiveDirectoryManagedIdentityWithCredentialsMustFail()
Assert.Contains(expectedMessage, e.Message);
}

[ConditionalFact(nameof(IsAADConnStringsSetup))]
public static void ActiveDirectoryWorkloadIdentityWithCredentialsMustFail()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, kind of new here, but do you have an update on this? It's 2 months old.

SqlClient v5.2 automation moved this from In progress to Reviewer approved Dec 7, 2023
@JRahnama JRahnama changed the title Add Workload Identity Support Add | Add Workload Identity Support Dec 7, 2023
@JRahnama JRahnama merged commit 17590d3 into dotnet:main Dec 7, 2023
146 of 148 checks passed
SqlClient v5.2 automation moved this from Reviewer approved to Done Dec 7, 2023
@wsugarman
Copy link
Contributor Author

@JRahnama - Thanks for merging it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
💡 Enhancement New feature request
Projects
No open projects
Development

Successfully merging this pull request may close these issues.

Workload Identity Support
6 participants