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:
parent
9465e7182a
commit
187a1931e5
|
|
@ -167,6 +167,11 @@ to use them.
|
||||||
## A list of queries to explicitly ignore.
|
## A list of queries to explicitly ignore.
|
||||||
exclude_query = ["SQLServerAvailabilityReplicaStates", "SQLServerDatabaseReplicaStates"]
|
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 -
|
## Queries enabled by default for database_type = "SQLServer" are -
|
||||||
## SQLServerPerformanceCounters, SQLServerWaitStatsCategorized, SQLServerDatabaseIO, SQLServerProperties, SQLServerMemoryClerks,
|
## SQLServerPerformanceCounters, SQLServerWaitStatsCategorized, SQLServerDatabaseIO, SQLServerProperties, SQLServerMemoryClerks,
|
||||||
## SQLServerSchedulers, SQLServerRequests, SQLServerVolumeSpace, SQLServerCpu, SQLServerAvailabilityReplicaStates, SQLServerDatabaseReplicaStates,
|
## SQLServerSchedulers, SQLServerRequests, SQLServerVolumeSpace, SQLServerCpu, SQLServerAvailabilityReplicaStates, SQLServerDatabaseReplicaStates,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -35,6 +35,11 @@
|
||||||
## A list of queries to explicitly ignore.
|
## A list of queries to explicitly ignore.
|
||||||
exclude_query = ["SQLServerAvailabilityReplicaStates", "SQLServerDatabaseReplicaStates"]
|
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 -
|
## Queries enabled by default for database_type = "SQLServer" are -
|
||||||
## SQLServerPerformanceCounters, SQLServerWaitStatsCategorized, SQLServerDatabaseIO, SQLServerProperties, SQLServerMemoryClerks,
|
## SQLServerPerformanceCounters, SQLServerWaitStatsCategorized, SQLServerDatabaseIO, SQLServerProperties, SQLServerMemoryClerks,
|
||||||
## SQLServerSchedulers, SQLServerRequests, SQLServerVolumeSpace, SQLServerCpu, SQLServerAvailabilityReplicaStates, SQLServerDatabaseReplicaStates,
|
## SQLServerSchedulers, SQLServerRequests, SQLServerVolumeSpace, SQLServerCpu, SQLServerAvailabilityReplicaStates, SQLServerDatabaseReplicaStates,
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,9 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"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"
|
"github.com/Azure/go-autorest/autorest/adal"
|
||||||
mssql "github.com/microsoft/go-mssqldb"
|
mssql "github.com/microsoft/go-mssqldb"
|
||||||
|
|
||||||
|
|
@ -57,7 +60,16 @@ type SQLServer struct {
|
||||||
|
|
||||||
pools []*sql.DB
|
pools []*sql.DB
|
||||||
queries mapQuery
|
queries mapQuery
|
||||||
|
|
||||||
|
// Legacy token - kept for backward compatibility
|
||||||
adalToken *adal.Token
|
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
|
muCacheLock sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,7 +82,7 @@ type query struct {
|
||||||
|
|
||||||
type mapQuery map[string]query
|
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 {
|
type healthMetric struct {
|
||||||
attemptedQueries int
|
attemptedQueries int
|
||||||
successfulQueries int
|
successfulQueries int
|
||||||
|
|
@ -466,54 +478,124 @@ func (s *SQLServer) getDatabaseTypeToLog() string {
|
||||||
return logname
|
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) {
|
func (s *SQLServer) getTokenProvider() (func() (string, error), error) {
|
||||||
|
// 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 {
|
||||||
|
s.Log.Debugf("Using legacy ADAL for authentication (deprecated, will be removed in 1.40.0)")
|
||||||
|
}
|
||||||
|
|
||||||
var tokenString string
|
var tokenString string
|
||||||
|
|
||||||
// load token
|
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()
|
s.muCacheLock.RLock()
|
||||||
token, err := s.loadToken()
|
token, err := s.loadToken()
|
||||||
s.muCacheLock.RUnlock()
|
s.muCacheLock.RUnlock()
|
||||||
|
|
||||||
// if there's error while loading token or found an expired token, refresh token and save it
|
// If there's an error while loading token or found an expired token, refresh token and save it
|
||||||
if err != nil || token.IsExpired() {
|
if err != nil || token.IsExpired() {
|
||||||
// refresh token within a write-lock
|
// Refresh token within a write-lock
|
||||||
s.muCacheLock.Lock()
|
s.muCacheLock.Lock()
|
||||||
defer s.muCacheLock.Unlock()
|
defer s.muCacheLock.Unlock()
|
||||||
|
|
||||||
// load token again, in case it's been refreshed by another thread
|
// Load token again, in case it's been refreshed by another thread
|
||||||
token, err = s.loadToken()
|
token, err = s.loadToken()
|
||||||
|
|
||||||
// check loaded token's error/validity, then refresh/save token
|
// Check loaded token's error/validity, then refresh/save token
|
||||||
if err != nil || token.IsExpired() {
|
if err != nil || token.IsExpired() {
|
||||||
// get new token
|
// Get new token
|
||||||
spt, err := s.refreshToken()
|
spt, err := s.refreshToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// use the refreshed token
|
// Use the refreshed token
|
||||||
tokenString = spt.OAuthToken()
|
tokenString = spt.OAuthToken()
|
||||||
} else {
|
} else {
|
||||||
// use locally cached token
|
// Use locally cached token
|
||||||
tokenString = token.OAuthToken()
|
tokenString = token.OAuthToken()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// use locally cached token
|
// Use locally cached token
|
||||||
tokenString = token.OAuthToken()
|
tokenString = token.OAuthToken()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// return acquired token
|
// Return acquired token
|
||||||
//nolint:unparam // token provider function always returns nil error in this scenario
|
//nolint:unparam // token provider function always returns nil error in this scenario
|
||||||
return func() (string, error) {
|
return func() (string, error) {
|
||||||
return tokenString, nil
|
return tokenString, nil
|
||||||
}, 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) {
|
func (s *SQLServer) loadToken() (*adal.Token, error) {
|
||||||
// This method currently does a simplistic task of reading a from variable (in-mem cache),
|
// 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
|
// however, it's been structured here to allow extending the cache mechanism to a different approach in future
|
||||||
|
|
||||||
if s.adalToken == nil {
|
if s.adalToken == nil {
|
||||||
return nil, errors.New("token is nil or failed to load existing token")
|
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
|
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) {
|
func (s *SQLServer) refreshToken() (*adal.Token, error) {
|
||||||
// get MSI endpoint to get a token
|
// get MSI endpoint to get a token
|
||||||
msiEndpoint, err := adal.GetMSIVMEndpoint()
|
msiEndpoint, err := adal.GetMSIVMEndpoint()
|
||||||
if err != nil {
|
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
|
var spt *adal.ServicePrincipalToken
|
||||||
if s.ClientID == "" {
|
if s.ClientID == "" {
|
||||||
|
// Using system-assigned managed identity
|
||||||
|
s.Log.Debugf("Using system-assigned managed identity with ADAL")
|
||||||
spt, err = adal.NewServicePrincipalTokenFromMSI(msiEndpoint, sqlAzureResourceID)
|
spt, err = adal.NewServicePrincipalTokenFromMSI(msiEndpoint, sqlAzureResourceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("failed to create service principal token from MSI: %w", err)
|
||||||
}
|
}
|
||||||
} else {
|
} 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)
|
spt, err = adal.NewServicePrincipalTokenFromMSIWithUserAssignedID(msiEndpoint, sqlAzureResourceID, s.ClientID)
|
||||||
if err != nil {
|
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 {
|
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
|
// save token to local in-mem cache
|
||||||
|
|
@ -563,6 +653,64 @@ func (s *SQLServer) refreshToken() (*adal.Token, error) {
|
||||||
return s.adalToken, nil
|
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() {
|
func init() {
|
||||||
inputs.Add("sqlserver", func() telegraf.Input {
|
inputs.Add("sqlserver", func() telegraf.Input {
|
||||||
return &SQLServer{
|
return &SQLServer{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue