{discovery,remote_write}/azure: Support default SDK authentication (#13099)

* discovery/azure: Offer default SDK authentication

Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
This commit is contained in:
Jan-Otto Kröpke 2024-03-16 12:06:57 +01:00 committed by GitHub
parent 5ed21c0d76
commit 302e151de8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 111 additions and 11 deletions

View file

@ -1840,7 +1840,7 @@ var expectedErrors = []struct {
}, },
{ {
filename: "azure_authentication_method.bad.yml", filename: "azure_authentication_method.bad.yml",
errMsg: "unknown authentication_type \"invalid\". Supported types are \"OAuth\" or \"ManagedIdentity\"", errMsg: "unknown authentication_type \"invalid\". Supported types are \"OAuth\", \"ManagedIdentity\" or \"SDK\"",
}, },
{ {
filename: "azure_bearertoken_basicauth.bad.yml", filename: "azure_bearertoken_basicauth.bad.yml",

View file

@ -65,6 +65,7 @@ const (
azureLabelMachineSize = azureLabel + "machine_size" azureLabelMachineSize = azureLabel + "machine_size"
authMethodOAuth = "OAuth" authMethodOAuth = "OAuth"
authMethodSDK = "SDK"
authMethodManagedIdentity = "ManagedIdentity" authMethodManagedIdentity = "ManagedIdentity"
) )
@ -164,8 +165,8 @@ func (c *SDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
} }
} }
if c.AuthenticationMethod != authMethodOAuth && c.AuthenticationMethod != authMethodManagedIdentity { if c.AuthenticationMethod != authMethodOAuth && c.AuthenticationMethod != authMethodManagedIdentity && c.AuthenticationMethod != authMethodSDK {
return fmt.Errorf("unknown authentication_type %q. Supported types are %q or %q", c.AuthenticationMethod, authMethodOAuth, authMethodManagedIdentity) return fmt.Errorf("unknown authentication_type %q. Supported types are %q, %q or %q", c.AuthenticationMethod, authMethodOAuth, authMethodManagedIdentity, authMethodSDK)
} }
return c.HTTPClientConfig.Validate() return c.HTTPClientConfig.Validate()
@ -294,6 +295,16 @@ func newCredential(cfg SDConfig, policyClientOptions policy.ClientOptions) (azco
return nil, err return nil, err
} }
credential = azcore.TokenCredential(secretCredential) credential = azcore.TokenCredential(secretCredential)
case authMethodSDK:
options := &azidentity.DefaultAzureCredentialOptions{ClientOptions: policyClientOptions}
if len(cfg.TenantID) != 0 {
options.TenantID = cfg.TenantID
}
sdkCredential, err := azidentity.NewDefaultAzureCredential(options)
if err != nil {
return nil, err
}
credential = azcore.TokenCredential(sdkCredential)
} }
return credential, nil return credential, nil
} }

View file

@ -600,8 +600,10 @@ See below for the configuration options for Azure discovery:
# The Azure environment. # The Azure environment.
[ environment: <string> | default = AzurePublicCloud ] [ environment: <string> | default = AzurePublicCloud ]
# The authentication method, either OAuth or ManagedIdentity. # The authentication method, either OAuth, ManagedIdentity or SDK.
# See https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview # See https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview
# SDK authentication method uses environment variables by default.
# See https://learn.microsoft.com/en-us/azure/developer/go/azure-sdk-authentication
[ authentication_method: <string> | default = OAuth] [ authentication_method: <string> | default = OAuth]
# The subscription ID. Always required. # The subscription ID. Always required.
subscription_id: <string> subscription_id: <string>
@ -3619,6 +3621,11 @@ azuread:
[ client_secret: <string> ] [ client_secret: <string> ]
[ tenant_id: <string> ] ] [ tenant_id: <string> ] ]
# Azure SDK auth.
# See https://learn.microsoft.com/en-us/azure/developer/go/azure-sdk-authentication
[ sdk:
[ tenant_id: <string> ] ]
# Configures the remote write request's TLS settings. # Configures the remote write request's TLS settings.
tls_config: tls_config:
[ <tls_config> ] [ <tls_config> ]

View file

@ -61,6 +61,12 @@ type OAuthConfig struct {
TenantID string `yaml:"tenant_id,omitempty"` TenantID string `yaml:"tenant_id,omitempty"`
} }
// SDKConfig is used to store azure SDK config values.
type SDKConfig struct {
// TenantID is the tenantId of the azure active directory application that is being used to authenticate.
TenantID string `yaml:"tenant_id,omitempty"`
}
// AzureADConfig is used to store the config values. // AzureADConfig is used to store the config values.
type AzureADConfig struct { //nolint:revive // exported. type AzureADConfig struct { //nolint:revive // exported.
// ManagedIdentity is the managed identity that is being used to authenticate. // ManagedIdentity is the managed identity that is being used to authenticate.
@ -69,6 +75,9 @@ type AzureADConfig struct { //nolint:revive // exported.
// OAuth is the oauth config that is being used to authenticate. // OAuth is the oauth config that is being used to authenticate.
OAuth *OAuthConfig `yaml:"oauth,omitempty"` OAuth *OAuthConfig `yaml:"oauth,omitempty"`
// OAuth is the oauth config that is being used to authenticate.
SDK *SDKConfig `yaml:"sdk,omitempty"`
// Cloud is the Azure cloud in which the service is running. Example: AzurePublic/AzureGovernment/AzureChina. // Cloud is the Azure cloud in which the service is running. Example: AzurePublic/AzureGovernment/AzureChina.
Cloud string `yaml:"cloud,omitempty"` Cloud string `yaml:"cloud,omitempty"`
} }
@ -102,14 +111,22 @@ func (c *AzureADConfig) Validate() error {
return fmt.Errorf("must provide a cloud in the Azure AD config") return fmt.Errorf("must provide a cloud in the Azure AD config")
} }
if c.ManagedIdentity == nil && c.OAuth == nil { if c.ManagedIdentity == nil && c.OAuth == nil && c.SDK == nil {
return fmt.Errorf("must provide an Azure Managed Identity or Azure OAuth in the Azure AD config") return fmt.Errorf("must provide an Azure Managed Identity, Azure OAuth or Azure SDK in the Azure AD config")
} }
if c.ManagedIdentity != nil && c.OAuth != nil { if c.ManagedIdentity != nil && c.OAuth != nil {
return fmt.Errorf("cannot provide both Azure Managed Identity and Azure OAuth in the Azure AD config") return fmt.Errorf("cannot provide both Azure Managed Identity and Azure OAuth in the Azure AD config")
} }
if c.ManagedIdentity != nil && c.SDK != nil {
return fmt.Errorf("cannot provide both Azure Managed Identity and Azure SDK in the Azure AD config")
}
if c.OAuth != nil && c.SDK != nil {
return fmt.Errorf("cannot provide both Azure OAuth and Azure SDK in the Azure AD config")
}
if c.ManagedIdentity != nil { if c.ManagedIdentity != nil {
if c.ManagedIdentity.ClientID == "" { if c.ManagedIdentity.ClientID == "" {
return fmt.Errorf("must provide an Azure Managed Identity client_id in the Azure AD config") return fmt.Errorf("must provide an Azure Managed Identity client_id in the Azure AD config")
@ -143,6 +160,17 @@ func (c *AzureADConfig) Validate() error {
} }
} }
if c.SDK != nil {
var err error
if c.SDK.TenantID != "" {
_, err = regexp.MatchString("^[0-9a-zA-Z-.]+$", c.SDK.TenantID)
if err != nil {
return fmt.Errorf("the provided Azure OAuth tenant_id is invalid")
}
}
}
return nil return nil
} }
@ -225,6 +253,16 @@ func newTokenCredential(cfg *AzureADConfig) (azcore.TokenCredential, error) {
} }
} }
if cfg.SDK != nil {
sdkConfig := &SDKConfig{
TenantID: cfg.SDK.TenantID,
}
cred, err = newSDKTokenCredential(clientOpts, sdkConfig)
if err != nil {
return nil, err
}
}
return cred, nil return cred, nil
} }
@ -241,6 +279,12 @@ func newOAuthTokenCredential(clientOpts *azcore.ClientOptions, oAuthConfig *OAut
return azidentity.NewClientSecretCredential(oAuthConfig.TenantID, oAuthConfig.ClientID, oAuthConfig.ClientSecret, opts) return azidentity.NewClientSecretCredential(oAuthConfig.TenantID, oAuthConfig.ClientID, oAuthConfig.ClientSecret, opts)
} }
// newSDKTokenCredential returns new SDK token credential.
func newSDKTokenCredential(clientOpts *azcore.ClientOptions, sdkConfig *SDKConfig) (azcore.TokenCredential, error) {
opts := &azidentity.DefaultAzureCredentialOptions{ClientOptions: *clientOpts, TenantID: sdkConfig.TenantID}
return azidentity.NewDefaultAzureCredential(opts)
}
// newTokenProvider helps to fetch accessToken for different types of credential. This also takes care of // newTokenProvider helps to fetch accessToken for different types of credential. This also takes care of
// refreshing the accessToken before expiry. This accessToken is attached to the Authorization header while making requests. // refreshing the accessToken before expiry. This accessToken is attached to the Authorization header while making requests.
func newTokenProvider(cfg *AzureADConfig, cred azcore.TokenCredential) (*tokenProvider, error) { func newTokenProvider(cfg *AzureADConfig, cred azcore.TokenCredential) (*tokenProvider, error) {

View file

@ -39,7 +39,7 @@ const (
testTokenString = "testTokenString" testTokenString = "testTokenString"
) )
var testTokenExpiry = time.Now().Add(5 * time.Second) func testTokenExpiry() time.Time { return time.Now().Add(5 * time.Second) }
type AzureAdTestSuite struct { type AzureAdTestSuite struct {
suite.Suite suite.Suite
@ -94,7 +94,7 @@ func (ad *AzureAdTestSuite) TestAzureAdRoundTripper() {
testToken := &azcore.AccessToken{ testToken := &azcore.AccessToken{
Token: testTokenString, Token: testTokenString,
ExpiresOn: testTokenExpiry, ExpiresOn: testTokenExpiry(),
} }
ad.mockCredential.On("GetToken", mock.Anything, mock.Anything).Return(*testToken, nil) ad.mockCredential.On("GetToken", mock.Anything, mock.Anything).Return(*testToken, nil)
@ -145,7 +145,7 @@ func TestAzureAdConfig(t *testing.T) {
// Missing managedidentiy or oauth field. // Missing managedidentiy or oauth field.
{ {
filename: "testdata/azuread_bad_configmissing.yaml", filename: "testdata/azuread_bad_configmissing.yaml",
err: "must provide an Azure Managed Identity or Azure OAuth in the Azure AD config", err: "must provide an Azure Managed Identity, Azure OAuth or Azure SDK in the Azure AD config",
}, },
// Invalid managedidentity client id. // Invalid managedidentity client id.
{ {
@ -162,6 +162,11 @@ func TestAzureAdConfig(t *testing.T) {
filename: "testdata/azuread_bad_twoconfig.yaml", filename: "testdata/azuread_bad_twoconfig.yaml",
err: "cannot provide both Azure Managed Identity and Azure OAuth in the Azure AD config", err: "cannot provide both Azure Managed Identity and Azure OAuth in the Azure AD config",
}, },
// Invalid config when both sdk and oauth is provided.
{
filename: "testdata/azuread_bad_oauthsdkconfig.yaml",
err: "cannot provide both Azure OAuth and Azure SDK in the Azure AD config",
},
// Valid config with missing optionally cloud field. // Valid config with missing optionally cloud field.
{ {
filename: "testdata/azuread_good_cloudmissing.yaml", filename: "testdata/azuread_good_cloudmissing.yaml",
@ -174,6 +179,10 @@ func TestAzureAdConfig(t *testing.T) {
{ {
filename: "testdata/azuread_good_oauth.yaml", filename: "testdata/azuread_good_oauth.yaml",
}, },
// Valid SDK config.
{
filename: "testdata/azuread_good_sdk.yaml",
},
} }
for _, c := range cases { for _, c := range cases {
_, err := loadAzureAdConfig(c.filename) _, err := loadAzureAdConfig(c.filename)
@ -232,6 +241,16 @@ func (s *TokenProviderTestSuite) TestNewTokenProvider() {
}, },
err: "Cloud is not specified or is incorrect: ", err: "Cloud is not specified or is incorrect: ",
}, },
// Invalid tokenProvider for SDK.
{
cfg: &AzureADConfig{
Cloud: "PublicAzure",
SDK: &SDKConfig{
TenantID: dummyTenantID,
},
},
err: "Cloud is not specified or is incorrect: ",
},
// Valid tokenProvider for managedidentity. // Valid tokenProvider for managedidentity.
{ {
cfg: &AzureADConfig{ cfg: &AzureADConfig{
@ -252,6 +271,15 @@ func (s *TokenProviderTestSuite) TestNewTokenProvider() {
}, },
}, },
}, },
// Valid tokenProvider for SDK.
{
cfg: &AzureADConfig{
Cloud: "AzurePublic",
SDK: &SDKConfig{
TenantID: dummyTenantID,
},
},
},
} }
mockGetTokenCallCounter := 1 mockGetTokenCallCounter := 1
for _, c := range cases { for _, c := range cases {
@ -264,11 +292,11 @@ func (s *TokenProviderTestSuite) TestNewTokenProvider() {
} else { } else {
testToken := &azcore.AccessToken{ testToken := &azcore.AccessToken{
Token: testTokenString, Token: testTokenString,
ExpiresOn: testTokenExpiry, ExpiresOn: testTokenExpiry(),
} }
s.mockCredential.On("GetToken", mock.Anything, mock.Anything).Return(*testToken, nil).Once(). s.mockCredential.On("GetToken", mock.Anything, mock.Anything).Return(*testToken, nil).Once().
On("GetToken", mock.Anything, mock.Anything).Return(getToken(), nil) On("GetToken", mock.Anything, mock.Anything).Return(getToken(), nil).Once()
actualTokenProvider, actualErr := newTokenProvider(c.cfg, s.mockCredential) actualTokenProvider, actualErr := newTokenProvider(c.cfg, s.mockCredential)

View file

@ -0,0 +1,7 @@
cloud: AzurePublic
oauth:
client_id: 00000000-0000-0000-0000-000000000000
client_secret: Cl1ent$ecret!
tenant_id: 00000000-a12b-3cd4-e56f-000000000000
sdk:
tenant_id: 00000000-a12b-3cd4-e56f-000000000000

View file

@ -0,0 +1,3 @@
cloud: AzurePublic
sdk:
tenant_id: 00000000-a12b-3cd4-e56f-000000000000