chore(inputs.sqlserver): Migrate Azure AD Authentication from ADAL to MSAL (#16730)

Co-authored-by: Sven Rebhan <36194019+srebhan@users.noreply.github.com>
This commit is contained in:
skartikey 2025-05-08 11:58:07 +01:00 committed by GitHub
parent 9465e7182a
commit 187a1931e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 223 additions and 48 deletions

View File

@ -167,6 +167,11 @@ to use them.
## A list of queries to explicitly ignore.
exclude_query = ["SQLServerAvailabilityReplicaStates", "SQLServerDatabaseReplicaStates"]
## Force using the deprecated ADAL authentication method instead of the recommended
## MSAL method. Setting this option is not recommended and only exists for backward
## compatibility.
# use_deprecated_adal_authentication = false
## Queries enabled by default for database_type = "SQLServer" are -
## SQLServerPerformanceCounters, SQLServerWaitStatsCategorized, SQLServerDatabaseIO, SQLServerProperties, SQLServerMemoryClerks,
## SQLServerSchedulers, SQLServerRequests, SQLServerVolumeSpace, SQLServerCpu, SQLServerAvailabilityReplicaStates, SQLServerDatabaseReplicaStates,

View File

@ -0,0 +1,17 @@
package sqlserver
import "time"
// New token structure for Azure Identity SDK
type azureToken struct {
token string
expiresOn time.Time
}
// IsExpired helper method for Azure token expiry
func (t *azureToken) IsExpired() bool {
if t == nil {
return true
}
return time.Now().After(t.expiresOn)
}

View File

@ -35,6 +35,11 @@
## A list of queries to explicitly ignore.
exclude_query = ["SQLServerAvailabilityReplicaStates", "SQLServerDatabaseReplicaStates"]
## Force using the deprecated ADAL authentication method instead of the recommended
## MSAL method. Setting this option is not recommended and only exists for backward
## compatibility.
# use_deprecated_adal_authentication = false
## Queries enabled by default for database_type = "SQLServer" are -
## SQLServerPerformanceCounters, SQLServerWaitStatsCategorized, SQLServerDatabaseIO, SQLServerProperties, SQLServerMemoryClerks,
## SQLServerSchedulers, SQLServerRequests, SQLServerVolumeSpace, SQLServerCpu, SQLServerAvailabilityReplicaStates, SQLServerDatabaseReplicaStates,

View File

@ -11,6 +11,9 @@ import (
"sync"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
// Legacy ADAL package - kept for backward compatibility
"github.com/Azure/go-autorest/autorest/adal"
mssql "github.com/microsoft/go-mssqldb"
@ -55,9 +58,18 @@ type SQLServer struct {
HealthMetric bool `toml:"health_metric"`
Log telegraf.Logger `toml:"-"`
pools []*sql.DB
queries mapQuery
adalToken *adal.Token
pools []*sql.DB
queries mapQuery
// Legacy token - kept for backward compatibility
adalToken *adal.Token
// New token using Azure Identity SDK
azToken *azureToken
// Config option to use legacy ADAL authentication instead of the newer Azure Identity SDK
// When true, the deprecated ADAL library will be used
// When false (default), the new Azure Identity SDK will be used
UseAdalToken bool `toml:"use_deprecated_adal_authentication" deprecated:"1.40.0;migrate to MSAL authentication"`
muCacheLock sync.RWMutex
}
@ -70,7 +82,7 @@ type query struct {
type mapQuery map[string]query
// healthMetric struct tracking the number of attempted vs successful connections for each connection string
// healthMetric struct tracking the number of attempted vs. successful connections for each connection string
type healthMetric struct {
attemptedQueries int
successfulQueries int
@ -466,54 +478,124 @@ func (s *SQLServer) getDatabaseTypeToLog() string {
return logname
}
// Get Token Provider by loading cached token or refreshed token
// ------------------------------------------------------------------------------
// Token Provider Implementation
// ------------------------------------------------------------------------------
// getTokenProvider returns a function that provides authentication tokens for SQL Server.
//
// DEPRECATION NOTICE:
// The ADAL authentication library is deprecated and will be removed in a future version.
// It is strongly recommended to migrate to the Azure Identity SDK.
// See the migration documentation at: https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-migration
//
// This implementation supports both authentication methods:
// 1. Azure Identity SDK (default, recommended)
// 2. Legacy ADAL library (deprecated, maintained for backward compatibility)
//
// To control which authentication library is used, set the use_deprecated_adal_authentication config option:
// - use_deprecated_adal_authentication = true : Use legacy ADAL authentication (deprecated)
// - use_deprecated_adal_authentication = false : Use Azure Identity SDK (recommended)
// - Not set : Use Azure Identity SDK (recommended)
func (s *SQLServer) getTokenProvider() (func() (string, error), error) {
var tokenString string
// load token
s.muCacheLock.RLock()
token, err := s.loadToken()
s.muCacheLock.RUnlock()
// if there's error while loading token or found an expired token, refresh token and save it
if err != nil || token.IsExpired() {
// refresh token within a write-lock
s.muCacheLock.Lock()
defer s.muCacheLock.Unlock()
// load token again, in case it's been refreshed by another thread
token, err = s.loadToken()
// check loaded token's error/validity, then refresh/save token
if err != nil || token.IsExpired() {
// get new token
spt, err := s.refreshToken()
if err != nil {
return nil, err
}
// use the refreshed token
tokenString = spt.OAuthToken()
} else {
// use locally cached token
tokenString = token.OAuthToken()
}
// Check if use_deprecated_adal_authentication config option is set to determine which auth method to use
// Default to using Azure Identity SDK if the config is not set
useAzureIdentity := !s.UseAdalToken
if useAzureIdentity {
s.Log.Debugf("Using Azure Identity SDK for authentication (recommended)")
} else {
// use locally cached token
tokenString = token.OAuthToken()
s.Log.Debugf("Using legacy ADAL for authentication (deprecated, will be removed in 1.40.0)")
}
// return acquired token
var tokenString string
if useAzureIdentity {
// Use Azure Identity SDK
s.muCacheLock.RLock()
token, err := s.loadAzureToken()
s.muCacheLock.RUnlock()
// If the token is nil, expired, or there was an error loading it, refresh the token
if err != nil || token == nil || token.IsExpired() {
// Refresh token within a write-lock
s.muCacheLock.Lock()
defer s.muCacheLock.Unlock()
// Load token again, in case it's been refreshed by another thread
token, err = s.loadAzureToken()
// Check loaded token's error/validity, then refresh/save token
if err != nil || token == nil || token.IsExpired() {
// Get new token
newToken, err := s.refreshAzureToken()
if err != nil {
return nil, err
}
// Use the refreshed token
tokenString = newToken.token
} else {
// Use locally cached token
tokenString = token.token
}
} else {
// Use locally cached token
tokenString = token.token
}
} else {
// Use legacy ADAL approach for backward compatibility
s.muCacheLock.RLock()
token, err := s.loadToken()
s.muCacheLock.RUnlock()
// If there's an error while loading token or found an expired token, refresh token and save it
if err != nil || token.IsExpired() {
// Refresh token within a write-lock
s.muCacheLock.Lock()
defer s.muCacheLock.Unlock()
// Load token again, in case it's been refreshed by another thread
token, err = s.loadToken()
// Check loaded token's error/validity, then refresh/save token
if err != nil || token.IsExpired() {
// Get new token
spt, err := s.refreshToken()
if err != nil {
return nil, err
}
// Use the refreshed token
tokenString = spt.OAuthToken()
} else {
// Use locally cached token
tokenString = token.OAuthToken()
}
} else {
// Use locally cached token
tokenString = token.OAuthToken()
}
}
// Return acquired token
//nolint:unparam // token provider function always returns nil error in this scenario
return func() (string, error) {
return tokenString, nil
}, nil
}
// Load token from in-mem cache
// ------------------------------------------------------------------------------
// Legacy ADAL Token Methods - Kept for backward compatibility
// ------------------------------------------------------------------------------
// loadToken loads a token from in-memory cache using the legacy ADAL method.
//
// Deprecated: This method uses the deprecated ADAL library and will be removed in a future version.
// Use the Azure Identity SDK instead of setting use_deprecated_adal_authentication = false or omitting it.
// See migration documentation: https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-migration
func (s *SQLServer) loadToken() (*adal.Token, error) {
// This method currently does a simplistic task of reading a from variable (in-mem cache),
// however it's been structured here to allow extending the cache mechanism to a different approach in future
// This method currently does a simplistic task of reading from a variable (in-mem cache);
// however, it's been structured here to allow extending the cache mechanism to a different approach in future
if s.adalToken == nil {
return nil, errors.New("token is nil or failed to load existing token")
@ -522,31 +604,39 @@ func (s *SQLServer) loadToken() (*adal.Token, error) {
return s.adalToken, nil
}
// Refresh token for the resource, and save to in-mem cache
// refreshToken refreshes the token using the legacy ADAL method.
//
// Deprecated: This method uses the deprecated ADAL library and will be removed in a future version.
// Use the Azure Identity SDK instead of setting use_deprecated_adal_authentication = false or omitting it.
// See migration documentation: https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-migration
func (s *SQLServer) refreshToken() (*adal.Token, error) {
// get MSI endpoint to get a token
msiEndpoint, err := adal.GetMSIVMEndpoint()
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to get MSI endpoint: %w", err)
}
// get new token for the resource id
// get a new token for the resource id
var spt *adal.ServicePrincipalToken
if s.ClientID == "" {
// Using system-assigned managed identity
s.Log.Debugf("Using system-assigned managed identity with ADAL")
spt, err = adal.NewServicePrincipalTokenFromMSI(msiEndpoint, sqlAzureResourceID)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to create service principal token from MSI: %w", err)
}
} else {
// Using user-assigned managed identity
s.Log.Debugf("Using user-assigned managed identity with ClientID: %s with ADAL", s.ClientID)
spt, err = adal.NewServicePrincipalTokenFromMSIWithUserAssignedID(msiEndpoint, sqlAzureResourceID, s.ClientID)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to create service principal token from MSI with user-assigned ID: %w", err)
}
}
// ensure token is fresh
// ensure the token is fresh
if err := spt.EnsureFresh(); err != nil {
return nil, err
return nil, fmt.Errorf("failed to ensure token freshness: %w", err)
}
// save token to local in-mem cache
@ -563,6 +653,64 @@ func (s *SQLServer) refreshToken() (*adal.Token, error) {
return s.adalToken, nil
}
// ------------------------------------------------------------------------------
// New Azure Identity SDK Token Methods
// ------------------------------------------------------------------------------
// loadAzureToken loads a token from in-memory cache using the Azure Identity SDK.
//
// This is the recommended authentication method for Azure SQL resources.
func (s *SQLServer) loadAzureToken() (*azureToken, error) {
// This method reads from variable (in-mem cache) but can be extended
// for different cache mechanisms in the future
if s.azToken == nil {
return nil, errors.New("token is nil or failed to load existing token")
}
return s.azToken, nil
}
// refreshAzureToken refreshes the token using the Azure Identity SDK.
//
// This is the recommended authentication method for Azure SQL resources.
func (s *SQLServer) refreshAzureToken() (*azureToken, error) {
var options *azidentity.ManagedIdentityCredentialOptions
if s.ClientID != "" {
options = &azidentity.ManagedIdentityCredentialOptions{
ID: azidentity.ResourceID(s.ClientID),
}
}
cred, err := azidentity.NewManagedIdentityCredential(options)
if err != nil {
return nil, fmt.Errorf("failed to create managed identity credential: %w", err)
}
// Get token from Azure AD
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
accessToken, err := cred.GetToken(ctx, policy.TokenRequestOptions{
Scopes: []string{sqlAzureResourceID + "/.default"},
})
if err != nil {
credType := "system-assigned"
if s.ClientID != "" {
credType = fmt.Sprintf("user-assigned (ClientID: %s)", s.ClientID)
}
return nil, fmt.Errorf("failed to get token using %s managed identity: %w", credType, err)
}
// Save token to cache
s.azToken = &azureToken{
token: accessToken.Token,
expiresOn: accessToken.ExpiresOn,
}
return s.azToken, nil
}
func init() {
inputs.Add("sqlserver", func() telegraf.Input {
return &SQLServer{