From 302e151de8937c4c2c77567e33e7c9ae208a2e39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Sat, 16 Mar 2024 12:06:57 +0100 Subject: [PATCH] {discovery,remote_write}/azure: Support default SDK authentication (#13099) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * discovery/azure: Offer default SDK authentication Signed-off-by: Jan-Otto Kröpke --- config/config_test.go | 2 +- discovery/azure/azure.go | 15 +++++- docs/configuration/configuration.md | 9 +++- storage/remote/azuread/azuread.go | 48 ++++++++++++++++++- storage/remote/azuread/azuread_test.go | 38 +++++++++++++-- .../testdata/azuread_bad_oauthsdkconfig.yaml | 7 +++ .../azuread/testdata/azuread_good_sdk.yaml | 3 ++ 7 files changed, 111 insertions(+), 11 deletions(-) create mode 100644 storage/remote/azuread/testdata/azuread_bad_oauthsdkconfig.yaml create mode 100644 storage/remote/azuread/testdata/azuread_good_sdk.yaml diff --git a/config/config_test.go b/config/config_test.go index 36b125f79..14981d25f 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1840,7 +1840,7 @@ var expectedErrors = []struct { }, { 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", diff --git a/discovery/azure/azure.go b/discovery/azure/azure.go index 16628c7bf..746496a69 100644 --- a/discovery/azure/azure.go +++ b/discovery/azure/azure.go @@ -65,6 +65,7 @@ const ( azureLabelMachineSize = azureLabel + "machine_size" authMethodOAuth = "OAuth" + authMethodSDK = "SDK" authMethodManagedIdentity = "ManagedIdentity" ) @@ -164,8 +165,8 @@ func (c *SDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { } } - if c.AuthenticationMethod != authMethodOAuth && c.AuthenticationMethod != authMethodManagedIdentity { - return fmt.Errorf("unknown authentication_type %q. Supported types are %q or %q", c.AuthenticationMethod, authMethodOAuth, authMethodManagedIdentity) + if c.AuthenticationMethod != authMethodOAuth && c.AuthenticationMethod != authMethodManagedIdentity && c.AuthenticationMethod != authMethodSDK { + return fmt.Errorf("unknown authentication_type %q. Supported types are %q, %q or %q", c.AuthenticationMethod, authMethodOAuth, authMethodManagedIdentity, authMethodSDK) } return c.HTTPClientConfig.Validate() @@ -294,6 +295,16 @@ func newCredential(cfg SDConfig, policyClientOptions policy.ClientOptions) (azco return nil, err } 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 } diff --git a/docs/configuration/configuration.md b/docs/configuration/configuration.md index e134ae02b..d751a4084 100644 --- a/docs/configuration/configuration.md +++ b/docs/configuration/configuration.md @@ -600,8 +600,10 @@ See below for the configuration options for Azure discovery: # The Azure environment. [ environment: | 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 +# SDK authentication method uses environment variables by default. +# See https://learn.microsoft.com/en-us/azure/developer/go/azure-sdk-authentication [ authentication_method: | default = OAuth] # The subscription ID. Always required. subscription_id: @@ -3619,6 +3621,11 @@ azuread: [ client_secret: ] [ tenant_id: ] ] + # Azure SDK auth. + # See https://learn.microsoft.com/en-us/azure/developer/go/azure-sdk-authentication + [ sdk: + [ tenant_id: ] ] + # Configures the remote write request's TLS settings. tls_config: [ ] diff --git a/storage/remote/azuread/azuread.go b/storage/remote/azuread/azuread.go index 20d48d008..e2058fb54 100644 --- a/storage/remote/azuread/azuread.go +++ b/storage/remote/azuread/azuread.go @@ -61,6 +61,12 @@ type OAuthConfig struct { 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. type AzureADConfig struct { //nolint:revive // exported. // 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 *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 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") } - if c.ManagedIdentity == nil && c.OAuth == nil { - return fmt.Errorf("must provide an Azure Managed Identity or Azure OAuth in the Azure AD config") + if c.ManagedIdentity == nil && c.OAuth == nil && c.SDK == nil { + 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 { 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.ClientID == "" { 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 } @@ -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 } @@ -241,6 +279,12 @@ func newOAuthTokenCredential(clientOpts *azcore.ClientOptions, oAuthConfig *OAut 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 // 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) { diff --git a/storage/remote/azuread/azuread_test.go b/storage/remote/azuread/azuread_test.go index 5eed2c0b1..7c9713812 100644 --- a/storage/remote/azuread/azuread_test.go +++ b/storage/remote/azuread/azuread_test.go @@ -39,7 +39,7 @@ const ( testTokenString = "testTokenString" ) -var testTokenExpiry = time.Now().Add(5 * time.Second) +func testTokenExpiry() time.Time { return time.Now().Add(5 * time.Second) } type AzureAdTestSuite struct { suite.Suite @@ -94,7 +94,7 @@ func (ad *AzureAdTestSuite) TestAzureAdRoundTripper() { testToken := &azcore.AccessToken{ Token: testTokenString, - ExpiresOn: testTokenExpiry, + ExpiresOn: testTokenExpiry(), } 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. { 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. { @@ -162,6 +162,11 @@ func TestAzureAdConfig(t *testing.T) { filename: "testdata/azuread_bad_twoconfig.yaml", 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. { filename: "testdata/azuread_good_cloudmissing.yaml", @@ -174,6 +179,10 @@ func TestAzureAdConfig(t *testing.T) { { filename: "testdata/azuread_good_oauth.yaml", }, + // Valid SDK config. + { + filename: "testdata/azuread_good_sdk.yaml", + }, } for _, c := range cases { _, err := loadAzureAdConfig(c.filename) @@ -232,6 +241,16 @@ func (s *TokenProviderTestSuite) TestNewTokenProvider() { }, 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. { cfg: &AzureADConfig{ @@ -252,6 +271,15 @@ func (s *TokenProviderTestSuite) TestNewTokenProvider() { }, }, }, + // Valid tokenProvider for SDK. + { + cfg: &AzureADConfig{ + Cloud: "AzurePublic", + SDK: &SDKConfig{ + TenantID: dummyTenantID, + }, + }, + }, } mockGetTokenCallCounter := 1 for _, c := range cases { @@ -264,11 +292,11 @@ func (s *TokenProviderTestSuite) TestNewTokenProvider() { } else { testToken := &azcore.AccessToken{ Token: testTokenString, - ExpiresOn: testTokenExpiry, + ExpiresOn: testTokenExpiry(), } 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) diff --git a/storage/remote/azuread/testdata/azuread_bad_oauthsdkconfig.yaml b/storage/remote/azuread/testdata/azuread_bad_oauthsdkconfig.yaml new file mode 100644 index 000000000..825759d31 --- /dev/null +++ b/storage/remote/azuread/testdata/azuread_bad_oauthsdkconfig.yaml @@ -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 diff --git a/storage/remote/azuread/testdata/azuread_good_sdk.yaml b/storage/remote/azuread/testdata/azuread_good_sdk.yaml new file mode 100644 index 000000000..53de8897d --- /dev/null +++ b/storage/remote/azuread/testdata/azuread_good_sdk.yaml @@ -0,0 +1,3 @@ +cloud: AzurePublic +sdk: + tenant_id: 00000000-a12b-3cd4-e56f-000000000000