From 40152922d39e13edcf5513aaab66ac12ea77d313 Mon Sep 17 00:00:00 2001 From: Sergey Kostrukov Date: Wed, 15 Nov 2023 06:59:23 -0800 Subject: [PATCH] Azure Monitor: support AzureCredentials in common format on backend (#77424) * Use GetDefaultCloud from SDK * Use GetAzureCloud from SDK * Credentials parser moved to azmoncredentials * Refactor legacy credentials * Tests * Fix test description Co-authored-by: Andreas Christou --------- Co-authored-by: Andreas Christou --- .../azuremonitor/azmoncredentials/builder.go | 119 ++++++++ .../azmoncredentials/builder_test.go | 215 ++++++++++++++ .../azuremonitor/azmoncredentials/default.go | 16 ++ pkg/tsdb/azuremonitor/azuremonitor.go | 28 +- pkg/tsdb/azuremonitor/azuremonitor_test.go | 10 +- pkg/tsdb/azuremonitor/credentials.go | 136 --------- pkg/tsdb/azuremonitor/credentials_test.go | 266 ------------------ pkg/tsdb/azuremonitor/types/types.go | 12 - 8 files changed, 371 insertions(+), 431 deletions(-) create mode 100644 pkg/tsdb/azuremonitor/azmoncredentials/builder.go create mode 100644 pkg/tsdb/azuremonitor/azmoncredentials/builder_test.go create mode 100644 pkg/tsdb/azuremonitor/azmoncredentials/default.go delete mode 100644 pkg/tsdb/azuremonitor/credentials.go delete mode 100644 pkg/tsdb/azuremonitor/credentials_test.go diff --git a/pkg/tsdb/azuremonitor/azmoncredentials/builder.go b/pkg/tsdb/azuremonitor/azmoncredentials/builder.go new file mode 100644 index 00000000000..24bd0b70a6a --- /dev/null +++ b/pkg/tsdb/azuremonitor/azmoncredentials/builder.go @@ -0,0 +1,119 @@ +package azmoncredentials + +import ( + "fmt" + + "github.com/grafana/grafana-azure-sdk-go/azcredentials" + "github.com/grafana/grafana-azure-sdk-go/azsettings" + "github.com/grafana/grafana-azure-sdk-go/util/maputil" +) + +func FromDatasourceData(data map[string]interface{}, secureData map[string]string) (azcredentials.AzureCredentials, error) { + var credentials azcredentials.AzureCredentials + var err error + + credentials, err = azcredentials.FromDatasourceData(data, secureData) + if err != nil { + return nil, err + } + + // Fallback to legacy credentials format + if credentials == nil { + credentials, err = getFromLegacy(data, secureData) + if err != nil { + return nil, err + } + } + + return credentials, err +} + +func getFromLegacy(data map[string]interface{}, secureData map[string]string) (azcredentials.AzureCredentials, error) { + authType, err := maputil.GetStringOptional(data, "azureAuthType") + if err != nil { + return nil, err + } + tenantId, err := maputil.GetStringOptional(data, "tenantId") + if err != nil { + return nil, err + } + clientId, err := maputil.GetStringOptional(data, "clientId") + if err != nil { + return nil, err + } + + if authType == "" { + // Some very old legacy datasources may not have explicit auth type specified, + // but they imply App Registration authentication + if tenantId != "" && clientId != "" { + authType = azcredentials.AzureAuthClientSecret + } else { + // No configuration present + return nil, nil + } + } + + switch authType { + case azcredentials.AzureAuthManagedIdentity: + credentials := &azcredentials.AzureManagedIdentityCredentials{} + return credentials, nil + + case azcredentials.AzureAuthWorkloadIdentity: + credentials := &azcredentials.AzureWorkloadIdentityCredentials{} + return credentials, nil + + case azcredentials.AzureAuthClientSecret: + legacyCloud, err := maputil.GetStringOptional(data, "cloudName") + if err != nil { + return nil, err + } + cloud, err := resolveLegacyCloudName(legacyCloud) + if err != nil { + return nil, err + } + clientSecret := secureData["clientSecret"] + + if secureData["clientSecret"] == "" { + return nil, fmt.Errorf("unable to instantiate credentials, clientSecret must be set") + } + + credentials := &azcredentials.AzureClientSecretCredentials{ + AzureCloud: cloud, + TenantId: tenantId, + ClientId: clientId, + ClientSecret: clientSecret, + } + + return credentials, nil + + default: + err := fmt.Errorf("the authentication type '%s' not supported", authType) + return nil, err + } +} + +// Legacy Azure cloud names used by the Azure Monitor datasource +const ( + azureMonitorPublic = "azuremonitor" + azureMonitorChina = "chinaazuremonitor" + azureMonitorUSGovernment = "govazuremonitor" + azureMonitorCustomized = "customizedazuremonitor" +) + +func resolveLegacyCloudName(cloudName string) (string, error) { + switch cloudName { + case azureMonitorPublic: + return azsettings.AzurePublic, nil + case azureMonitorChina: + return azsettings.AzureChina, nil + case azureMonitorUSGovernment: + return azsettings.AzureUSGovernment, nil + case azureMonitorCustomized: + return azsettings.AzureCustomized, nil + case "": + return azsettings.AzurePublic, nil + default: + err := fmt.Errorf("the Azure cloud '%s' not supported by Azure Monitor datasource", cloudName) + return "", err + } +} diff --git a/pkg/tsdb/azuremonitor/azmoncredentials/builder_test.go b/pkg/tsdb/azuremonitor/azmoncredentials/builder_test.go new file mode 100644 index 00000000000..701f91cb73f --- /dev/null +++ b/pkg/tsdb/azuremonitor/azmoncredentials/builder_test.go @@ -0,0 +1,215 @@ +package azmoncredentials + +import ( + "testing" + + "github.com/grafana/grafana-azure-sdk-go/azcredentials" + "github.com/grafana/grafana-azure-sdk-go/azsettings" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFromDatasourceData(t *testing.T) { + t.Run("should return nil when no credentials configured", func(t *testing.T) { + var data = map[string]interface{}{} + var secureData = map[string]string{} + + result, err := FromDatasourceData(data, secureData) + require.NoError(t, err) + + assert.Nil(t, result) + }) + + t.Run("should return managed identity credentials when auth type is managed identity", func(t *testing.T) { + data := map[string]interface{}{ + "azureAuthType": "msi", + "cloudName": "chinaazuremonitor", + "tenantId": "LEGACY-TENANT-ID", + "clientId": "LEGACY-CLIENT-ID", + } + var secureData = map[string]string{ + "clientSecret": "FAKE-LEGACY-SECRET", + } + + credentials, err := FromDatasourceData(data, secureData) + require.NoError(t, err) + require.IsType(t, &azcredentials.AzureManagedIdentityCredentials{}, credentials) + msiCredentials := credentials.(*azcredentials.AzureManagedIdentityCredentials) + + // Azure Monitor datasource doesn't support user-assigned managed identities (ClientId is always empty) + assert.Equal(t, "", msiCredentials.ClientId) + }) + + t.Run("should return workload identity credentials when auth type is workload identity", func(t *testing.T) { + data := map[string]interface{}{ + "azureAuthType": azcredentials.AzureAuthWorkloadIdentity, + } + var secureData = map[string]string{} + + credentials, err := FromDatasourceData(data, secureData) + require.NoError(t, err) + require.IsType(t, &azcredentials.AzureWorkloadIdentityCredentials{}, credentials) + }) + + t.Run("when legacy client secret configuration present", func(t *testing.T) { + t.Run("should return client secret credentials when auth type is client secret", func(t *testing.T) { + var data = map[string]interface{}{ + "azureAuthType": "clientsecret", + "cloudName": "chinaazuremonitor", + "tenantId": "LEGACY-TENANT-ID", + "clientId": "LEGACY-CLIENT-ID", + } + var secureData = map[string]string{ + "clientSecret": "FAKE-LEGACY-SECRET", + } + + result, err := FromDatasourceData(data, secureData) + require.NoError(t, err) + + require.NotNil(t, result) + assert.IsType(t, &azcredentials.AzureClientSecretCredentials{}, result) + credential := (result).(*azcredentials.AzureClientSecretCredentials) + + assert.Equal(t, azsettings.AzureChina, credential.AzureCloud) + assert.Equal(t, "LEGACY-TENANT-ID", credential.TenantId) + assert.Equal(t, "LEGACY-CLIENT-ID", credential.ClientId) + assert.Equal(t, "FAKE-LEGACY-SECRET", credential.ClientSecret) + }) + + t.Run("should return client secret credentials when auth type is not specified but configuration present", func(t *testing.T) { + var data = map[string]interface{}{ + "cloudName": "chinaazuremonitor", + "tenantId": "LEGACY-TENANT-ID", + "clientId": "LEGACY-CLIENT-ID", + } + var secureData = map[string]string{ + "clientSecret": "FAKE-LEGACY-SECRET", + } + + result, err := FromDatasourceData(data, secureData) + require.NoError(t, err) + + require.NotNil(t, result) + assert.IsType(t, &azcredentials.AzureClientSecretCredentials{}, result) + credential := (result).(*azcredentials.AzureClientSecretCredentials) + + assert.Equal(t, azsettings.AzureChina, credential.AzureCloud) + assert.Equal(t, "LEGACY-TENANT-ID", credential.TenantId) + assert.Equal(t, "LEGACY-CLIENT-ID", credential.ClientId) + assert.Equal(t, "FAKE-LEGACY-SECRET", credential.ClientSecret) + }) + + t.Run("should error if no client secret is set", func(t *testing.T) { + var data = map[string]interface{}{ + "azureAuthType": "clientsecret", + "cloudName": "chinaazuremonitor", + "tenantId": "LEGACY-TENANT-ID", + "clientId": "LEGACY-CLIENT-ID", + } + var secureData = map[string]string{} + + _, err := FromDatasourceData(data, secureData) + require.Error(t, err) + + assert.ErrorContains(t, err, "clientSecret must be set") + }) + }) + + t.Run("should return client secret credentials when client secret auth configured even if legacy configuration present", func(t *testing.T) { + var data = map[string]interface{}{ + "azureCredentials": map[string]interface{}{ + "authType": "clientsecret", + "azureCloud": "AzureChinaCloud", + "tenantId": "TENANT-ID", + "clientId": "CLIENT-TD", + }, + "azureAuthType": "clientsecret", + "cloudName": "azuremonitor", + "tenantId": "LEGACY-TENANT-ID", + "clientId": "LEGACY-CLIENT-ID", + } + var secureData = map[string]string{ + "azureClientSecret": "FAKE-SECRET", + "clientSecret": "FAKE-LEGACY-SECRET", + } + + result, err := FromDatasourceData(data, secureData) + require.NoError(t, err) + + require.NotNil(t, result) + assert.IsType(t, &azcredentials.AzureClientSecretCredentials{}, result) + credential := (result).(*azcredentials.AzureClientSecretCredentials) + + assert.Equal(t, credential.AzureCloud, azsettings.AzureChina) + assert.Equal(t, credential.TenantId, "TENANT-ID") + assert.Equal(t, credential.ClientId, "CLIENT-TD") + assert.Equal(t, credential.ClientSecret, "FAKE-SECRET") + }) + + t.Run("should return error when credentials not supported even if legacy configuration present", func(t *testing.T) { + var data = map[string]interface{}{ + "azureCredentials": map[string]interface{}{ + "authType": "invalid", + "azureCloud": "AzureChinaCloud", + "tenantId": "TENANT-ID", + "clientId": "CLIENT-TD", + }, + "cloudName": "azuremonitor", + "tenantId": "LEGACY-TENANT-ID", + "clientId": "LEGACY-CLIENT-ID", + "onBehalfOf": true, + "oauthPassThru": true, + } + var secureData = map[string]string{ + "azureClientSecret": "FAKE-SECRET", + "clientSecret": "FAKE-LEGACY-SECRET", + } + + _, err := FromDatasourceData(data, secureData) + assert.Error(t, err) + }) +} + +func TestNormalizedCloudName(t *testing.T) { + t.Run("should return normalized cloud name", func(t *testing.T) { + tests := []struct { + description string + legacyCloud string + normalizedCloud string + }{ + { + legacyCloud: azureMonitorPublic, + normalizedCloud: azsettings.AzurePublic, + }, + { + legacyCloud: azureMonitorChina, + normalizedCloud: azsettings.AzureChina, + }, + { + legacyCloud: azureMonitorUSGovernment, + normalizedCloud: azsettings.AzureUSGovernment, + }, + { + legacyCloud: "", + normalizedCloud: azsettings.AzurePublic, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + actualCloud, err := resolveLegacyCloudName(tt.legacyCloud) + require.NoError(t, err) + + assert.Equal(t, tt.normalizedCloud, actualCloud) + }) + } + }) + + t.Run("should fail when cloud is unknown", func(t *testing.T) { + legacyCloud := "unknown" + + _, err := resolveLegacyCloudName(legacyCloud) + assert.Error(t, err) + }) +} diff --git a/pkg/tsdb/azuremonitor/azmoncredentials/default.go b/pkg/tsdb/azuremonitor/azmoncredentials/default.go new file mode 100644 index 00000000000..27b1c01347e --- /dev/null +++ b/pkg/tsdb/azuremonitor/azmoncredentials/default.go @@ -0,0 +1,16 @@ +package azmoncredentials + +import ( + "github.com/grafana/grafana-azure-sdk-go/azcredentials" + "github.com/grafana/grafana-azure-sdk-go/azsettings" +) + +func GetDefaultCredentials(settings *azsettings.AzureSettings) azcredentials.AzureCredentials { + if settings.ManagedIdentityEnabled { + return &azcredentials.AzureManagedIdentityCredentials{} + } else if settings.WorkloadIdentityEnabled { + return &azcredentials.AzureWorkloadIdentityCredentials{} + } else { + return &azcredentials.AzureClientSecretCredentials{AzureCloud: settings.GetDefaultCloud()} + } +} diff --git a/pkg/tsdb/azuremonitor/azuremonitor.go b/pkg/tsdb/azuremonitor/azuremonitor.go index 9cf434c2f73..03fdc7c0945 100644 --- a/pkg/tsdb/azuremonitor/azuremonitor.go +++ b/pkg/tsdb/azuremonitor/azuremonitor.go @@ -10,6 +10,7 @@ import ( "net/http" "strconv" + "github.com/grafana/grafana-azure-sdk-go/azcredentials" "github.com/grafana/grafana-azure-sdk-go/azsettings" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" @@ -19,6 +20,7 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/azmoncredentials" "github.com/grafana/grafana/pkg/tsdb/azuremonitor/loganalytics" "github.com/grafana/grafana/pkg/tsdb/azuremonitor/metrics" "github.com/grafana/grafana/pkg/tsdb/azuremonitor/resourcegraph" @@ -77,19 +79,26 @@ func getDatasourceService(ctx context.Context, settings *backend.DataSourceInsta func NewInstanceSettings(cfg *setting.Cfg, clientProvider *httpclient.Provider, executors map[string]azDatasourceExecutor) datasource.InstanceFactoryFunc { return func(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { - jsonDataObj := map[string]any{} - err := json.Unmarshal(settings.JSONData, &jsonDataObj) + jsonData := map[string]any{} + err := json.Unmarshal(settings.JSONData, &jsonData) if err != nil { return nil, fmt.Errorf("error reading settings: %w", err) } - azSettings := types.AzureSettings{} - err = json.Unmarshal(settings.JSONData, &azSettings) + azMonitorSettings := types.AzureMonitorSettings{} + err = json.Unmarshal(settings.JSONData, &azMonitorSettings) if err != nil { return nil, fmt.Errorf("error reading settings: %w", err) } - cloud, err := getAzureCloud(cfg, &azSettings.AzureClientSettings) + credentials, err := azmoncredentials.FromDatasourceData(jsonData, settings.DecryptedSecureJSONData) + if err != nil { + return nil, fmt.Errorf("error getting credentials: %w", err) + } else if credentials == nil { + credentials = azmoncredentials.GetDefaultCredentials(cfg.Azure) + } + + cloud, err := azcredentials.GetAzureCloud(cfg.Azure, credentials) if err != nil { return nil, fmt.Errorf("error getting credentials: %w", err) } @@ -99,16 +108,11 @@ func NewInstanceSettings(cfg *setting.Cfg, clientProvider *httpclient.Provider, return nil, err } - credentials, err := getAzureCredentials(cfg, &azSettings.AzureClientSettings, settings.DecryptedSecureJSONData) - if err != nil { - return nil, fmt.Errorf("error getting credentials: %w", err) - } - model := types.DatasourceInfo{ Cloud: cloud, Credentials: credentials, - Settings: azSettings.AzureMonitorSettings, - JSONData: jsonDataObj, + Settings: azMonitorSettings, + JSONData: jsonData, DecryptedSecureJSONData: settings.DecryptedSecureJSONData, DatasourceID: settings.ID, Routes: routesForModel, diff --git a/pkg/tsdb/azuremonitor/azuremonitor_test.go b/pkg/tsdb/azuremonitor/azuremonitor_test.go index 15aa4d787de..6ccc527228d 100644 --- a/pkg/tsdb/azuremonitor/azuremonitor_test.go +++ b/pkg/tsdb/azuremonitor/azuremonitor_test.go @@ -155,19 +155,19 @@ func Test_newMux(t *testing.T) { { name: "creates an Azure Monitor executor", queryType: azureMonitor, - expectedURL: routes[azureMonitorPublic][azureMonitor].URL, + expectedURL: routes[azsettings.AzurePublic][azureMonitor].URL, Err: require.NoError, }, { name: "creates an Azure Log Analytics executor", queryType: azureLogAnalytics, - expectedURL: routes[azureMonitorPublic][azureLogAnalytics].URL, + expectedURL: routes[azsettings.AzurePublic][azureLogAnalytics].URL, Err: require.NoError, }, { name: "creates an Azure Traces executor", queryType: azureTraces, - expectedURL: routes[azureMonitorPublic][azureLogAnalytics].URL, + expectedURL: routes[azsettings.AzurePublic][azureLogAnalytics].URL, Err: require.NoError, }, } @@ -176,10 +176,10 @@ func Test_newMux(t *testing.T) { t.Run(tt.name, func(t *testing.T) { s := &Service{ im: &fakeInstance{ - routes: routes[azureMonitorPublic], + routes: routes[azsettings.AzurePublic], services: map[string]types.DatasourceService{ tt.queryType: { - URL: routes[azureMonitorPublic][tt.queryType].URL, + URL: routes[azsettings.AzurePublic][tt.queryType].URL, HTTPClient: &http.Client{}, }, }, diff --git a/pkg/tsdb/azuremonitor/credentials.go b/pkg/tsdb/azuremonitor/credentials.go deleted file mode 100644 index 6e19f743f59..00000000000 --- a/pkg/tsdb/azuremonitor/credentials.go +++ /dev/null @@ -1,136 +0,0 @@ -package azuremonitor - -import ( - "fmt" - - "github.com/grafana/grafana-azure-sdk-go/azcredentials" - "github.com/grafana/grafana-azure-sdk-go/azsettings" - - "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/tsdb/azuremonitor/types" -) - -// Azure cloud names specific to Azure Monitor -const ( - azureMonitorPublic = "azuremonitor" - azureMonitorChina = "chinaazuremonitor" - azureMonitorUSGovernment = "govazuremonitor" - azureMonitorCustomized = "customizedazuremonitor" -) - -func getAuthType(cfg *setting.Cfg, jsonData *types.AzureClientSettings) string { - if azureAuthType := jsonData.AzureAuthType; azureAuthType != "" { - return azureAuthType - } else { - tenantId := jsonData.TenantId - clientId := jsonData.ClientId - - // If authentication type isn't explicitly specified and datasource has client credentials, - // then this is existing datasource which is configured for app registration (client secret) - if tenantId != "" && clientId != "" { - return azcredentials.AzureAuthClientSecret - } - - // For newly created datasource with no configuration the order is as follows: - // Managed identity is the default if enabled - // Workload identity is the next option if enabled - // Client secret is the final fallback - if cfg.Azure.ManagedIdentityEnabled { - return azcredentials.AzureAuthManagedIdentity - } else if cfg.Azure.WorkloadIdentityEnabled { - return azcredentials.AzureAuthWorkloadIdentity - } else { - return azcredentials.AzureAuthClientSecret - } - } -} - -func getDefaultAzureCloud(cfg *setting.Cfg) (string, error) { - // Allow only known cloud names - cloudName := "" - if cfg != nil && cfg.Azure != nil { - cloudName = cfg.Azure.Cloud - } - switch cloudName { - case azsettings.AzurePublic: - return azsettings.AzurePublic, nil - case azsettings.AzureChina: - return azsettings.AzureChina, nil - case azsettings.AzureUSGovernment: - return azsettings.AzureUSGovernment, nil - case azsettings.AzureCustomized: - return azsettings.AzureCustomized, nil - case "": - // Not set cloud defaults to public - return azsettings.AzurePublic, nil - default: - err := fmt.Errorf("the cloud '%s' not supported", cloudName) - return "", err - } -} - -func normalizeAzureCloud(cloudName string) (string, error) { - switch cloudName { - case azureMonitorPublic: - return azsettings.AzurePublic, nil - case azureMonitorChina: - return azsettings.AzureChina, nil - case azureMonitorUSGovernment: - return azsettings.AzureUSGovernment, nil - case azureMonitorCustomized: - return azsettings.AzureCustomized, nil - default: - err := fmt.Errorf("the cloud '%s' not supported", cloudName) - return "", err - } -} - -func getAzureCloud(cfg *setting.Cfg, jsonData *types.AzureClientSettings) (string, error) { - authType := getAuthType(cfg, jsonData) - switch authType { - case azcredentials.AzureAuthManagedIdentity, azcredentials.AzureAuthWorkloadIdentity: - // In case of managed identity and workload identity, the cloud is always same as where Grafana is hosted - return getDefaultAzureCloud(cfg) - case azcredentials.AzureAuthClientSecret: - if cloud := jsonData.CloudName; cloud != "" { - return normalizeAzureCloud(cloud) - } else { - return getDefaultAzureCloud(cfg) - } - default: - err := fmt.Errorf("the authentication type '%s' not supported", authType) - return "", err - } -} - -func getAzureCredentials(cfg *setting.Cfg, jsonData *types.AzureClientSettings, secureJsonData map[string]string) (azcredentials.AzureCredentials, error) { - authType := getAuthType(cfg, jsonData) - - switch authType { - case azcredentials.AzureAuthManagedIdentity: - credentials := &azcredentials.AzureManagedIdentityCredentials{} - return credentials, nil - case azcredentials.AzureAuthWorkloadIdentity: - credentials := &azcredentials.AzureWorkloadIdentityCredentials{} - return credentials, nil - case azcredentials.AzureAuthClientSecret: - cloud, err := getAzureCloud(cfg, jsonData) - if err != nil { - return nil, err - } - if secureJsonData["clientSecret"] == "" { - return nil, fmt.Errorf("unable to instantiate credentials, clientSecret must be set") - } - credentials := &azcredentials.AzureClientSecretCredentials{ - AzureCloud: cloud, - TenantId: jsonData.TenantId, - ClientId: jsonData.ClientId, - ClientSecret: secureJsonData["clientSecret"], - } - return credentials, nil - - default: - err := fmt.Errorf("the authentication type '%s' not supported", authType) - return nil, err - } -} diff --git a/pkg/tsdb/azuremonitor/credentials_test.go b/pkg/tsdb/azuremonitor/credentials_test.go deleted file mode 100644 index 58ea12660cd..00000000000 --- a/pkg/tsdb/azuremonitor/credentials_test.go +++ /dev/null @@ -1,266 +0,0 @@ -package azuremonitor - -import ( - "testing" - - "github.com/grafana/grafana/pkg/tsdb/azuremonitor/types" - - "github.com/grafana/grafana-azure-sdk-go/azcredentials" - "github.com/grafana/grafana-azure-sdk-go/azsettings" - - "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestCredentials_getAuthType(t *testing.T) { - cfg := &setting.Cfg{ - Azure: &azsettings.AzureSettings{}, - } - - t.Run("when managed identities enabled", func(t *testing.T) { - cfg.Azure.ManagedIdentityEnabled = true - - t.Run("should be client secret if auth type is set to client secret", func(t *testing.T) { - jsonData := &types.AzureClientSettings{ - AzureAuthType: azcredentials.AzureAuthClientSecret, - } - - authType := getAuthType(cfg, jsonData) - - assert.Equal(t, azcredentials.AzureAuthClientSecret, authType) - }) - - t.Run("should be managed identity if datasource not configured", func(t *testing.T) { - jsonData := &types.AzureClientSettings{ - AzureAuthType: "", - } - - authType := getAuthType(cfg, jsonData) - - assert.Equal(t, azcredentials.AzureAuthManagedIdentity, authType) - }) - - t.Run("should be client secret if auth type not specified but credentials configured", func(t *testing.T) { - jsonData := &types.AzureClientSettings{ - AzureAuthType: "", - TenantId: "9b9d90ee-a5cc-49c2-b97e-0d1b0f086b5c", - ClientId: "849ccbb0-92eb-4226-b228-ef391abd8fe6", - } - - authType := getAuthType(cfg, jsonData) - - assert.Equal(t, azcredentials.AzureAuthClientSecret, authType) - }) - }) - - t.Run("when managed identities disabled", func(t *testing.T) { - cfg.Azure.ManagedIdentityEnabled = false - - t.Run("should be managed identity if auth type is set to managed identity", func(t *testing.T) { - jsonData := &types.AzureClientSettings{ - AzureAuthType: azcredentials.AzureAuthManagedIdentity, - } - - authType := getAuthType(cfg, jsonData) - - assert.Equal(t, azcredentials.AzureAuthManagedIdentity, authType) - }) - - t.Run("should be client secret if datasource not configured", func(t *testing.T) { - jsonData := &types.AzureClientSettings{ - AzureAuthType: "", - } - - authType := getAuthType(cfg, jsonData) - - assert.Equal(t, azcredentials.AzureAuthClientSecret, authType) - }) - }) - - t.Run("when workload identities enabled", func(t *testing.T) { - cfg.Azure.WorkloadIdentityEnabled = true - - t.Run("should be client secret if auth type is set to client secret", func(t *testing.T) { - jsonData := &types.AzureClientSettings{ - AzureAuthType: azcredentials.AzureAuthClientSecret, - } - - authType := getAuthType(cfg, jsonData) - - assert.Equal(t, azcredentials.AzureAuthClientSecret, authType) - }) - - t.Run("should be workload identity if datasource not configured and managed identity is disabled", func(t *testing.T) { - jsonData := &types.AzureClientSettings{ - AzureAuthType: "", - } - - authType := getAuthType(cfg, jsonData) - - assert.Equal(t, azcredentials.AzureAuthWorkloadIdentity, authType) - }) - - t.Run("should be client secret if auth type not specified but credentials configured", func(t *testing.T) { - jsonData := &types.AzureClientSettings{ - AzureAuthType: "", - TenantId: "9b9d90ee-a5cc-49c2-b97e-0d1b0f086b5c", - ClientId: "849ccbb0-92eb-4226-b228-ef391abd8fe6", - } - - authType := getAuthType(cfg, jsonData) - - assert.Equal(t, azcredentials.AzureAuthClientSecret, authType) - }) - }) - - t.Run("when workload identities disabled", func(t *testing.T) { - cfg.Azure.WorkloadIdentityEnabled = false - - t.Run("should be workload identity if auth type is set to workload identity", func(t *testing.T) { - jsonData := &types.AzureClientSettings{ - AzureAuthType: azcredentials.AzureAuthWorkloadIdentity, - } - - authType := getAuthType(cfg, jsonData) - - assert.Equal(t, azcredentials.AzureAuthWorkloadIdentity, authType) - }) - - t.Run("should be client secret if datasource not configured", func(t *testing.T) { - jsonData := &types.AzureClientSettings{ - AzureAuthType: "", - } - - authType := getAuthType(cfg, jsonData) - - assert.Equal(t, azcredentials.AzureAuthClientSecret, authType) - }) - }) -} - -func TestCredentials_getAzureCloud(t *testing.T) { - cfg := &setting.Cfg{ - Azure: &azsettings.AzureSettings{ - Cloud: azsettings.AzureChina, - }, - } - - t.Run("when auth type is managed identity", func(t *testing.T) { - jsonData := &types.AzureClientSettings{ - AzureAuthType: azcredentials.AzureAuthManagedIdentity, - CloudName: azureMonitorUSGovernment, - } - - t.Run("should be from server configuration regardless of datasource value", func(t *testing.T) { - cloud, err := getAzureCloud(cfg, jsonData) - require.NoError(t, err) - - assert.Equal(t, azsettings.AzureChina, cloud) - }) - - t.Run("should be public if not set in server configuration", func(t *testing.T) { - cfg := &setting.Cfg{ - Azure: &azsettings.AzureSettings{ - Cloud: "", - }, - } - - cloud, err := getAzureCloud(cfg, jsonData) - require.NoError(t, err) - - assert.Equal(t, azsettings.AzurePublic, cloud) - }) - }) - - t.Run("when auth type is client secret", func(t *testing.T) { - t.Run("should be from datasource value normalized to known cloud name", func(t *testing.T) { - jsonData := &types.AzureClientSettings{ - AzureAuthType: azcredentials.AzureAuthClientSecret, - CloudName: azureMonitorUSGovernment, - } - - cloud, err := getAzureCloud(cfg, jsonData) - require.NoError(t, err) - - assert.Equal(t, azsettings.AzureUSGovernment, cloud) - }) - - t.Run("should be from server configuration if not set in datasource", func(t *testing.T) { - jsonData := &types.AzureClientSettings{ - AzureAuthType: azcredentials.AzureAuthClientSecret, - CloudName: "", - } - - cloud, err := getAzureCloud(cfg, jsonData) - require.NoError(t, err) - - assert.Equal(t, azsettings.AzureChina, cloud) - }) - }) -} - -func TestCredentials_getAzureCredentials(t *testing.T) { - cfg := &setting.Cfg{ - Azure: &azsettings.AzureSettings{ - Cloud: azsettings.AzureChina, - }, - } - - secureJsonData := map[string]string{ - "clientSecret": "59e3498f-eb12-4943-b8f0-a5aa42640058", - } - - t.Run("when auth type is managed identity", func(t *testing.T) { - jsonData := &types.AzureClientSettings{ - AzureAuthType: azcredentials.AzureAuthManagedIdentity, - CloudName: azureMonitorUSGovernment, - TenantId: "9b9d90ee-a5cc-49c2-b97e-0d1b0f086b5c", - ClientId: "849ccbb0-92eb-4226-b228-ef391abd8fe6", - } - - t.Run("should return managed identity credentials", func(t *testing.T) { - credentials, err := getAzureCredentials(cfg, jsonData, secureJsonData) - require.NoError(t, err) - require.IsType(t, &azcredentials.AzureManagedIdentityCredentials{}, credentials) - msiCredentials := credentials.(*azcredentials.AzureManagedIdentityCredentials) - - // Azure Monitor datasource doesn't support user-assigned managed identities (ClientId is always empty) - assert.Equal(t, "", msiCredentials.ClientId) - }) - }) - - t.Run("when auth type is client secret", func(t *testing.T) { - jsonData := &types.AzureClientSettings{ - AzureAuthType: azcredentials.AzureAuthClientSecret, - CloudName: azureMonitorUSGovernment, - TenantId: "9b9d90ee-a5cc-49c2-b97e-0d1b0f086b5c", - ClientId: "849ccbb0-92eb-4226-b228-ef391abd8fe6", - } - - t.Run("should return client secret credentials", func(t *testing.T) { - cfg := &setting.Cfg{} - - credentials, err := getAzureCredentials(cfg, jsonData, secureJsonData) - require.NoError(t, err) - require.IsType(t, &azcredentials.AzureClientSecretCredentials{}, credentials) - clientSecretCredentials := credentials.(*azcredentials.AzureClientSecretCredentials) - - assert.Equal(t, azsettings.AzureUSGovernment, clientSecretCredentials.AzureCloud) - assert.Equal(t, "9b9d90ee-a5cc-49c2-b97e-0d1b0f086b5c", clientSecretCredentials.TenantId) - assert.Equal(t, "849ccbb0-92eb-4226-b228-ef391abd8fe6", clientSecretCredentials.ClientId) - assert.Equal(t, "59e3498f-eb12-4943-b8f0-a5aa42640058", clientSecretCredentials.ClientSecret) - - // Azure Monitor datasource doesn't support custom IdP authorities (Authority is always empty) - assert.Equal(t, "", clientSecretCredentials.Authority) - }) - - t.Run("should error if no client secret is set", func(t *testing.T) { - cfg := &setting.Cfg{} - _, err := getAzureCredentials(cfg, jsonData, map[string]string{ - "clientSecret": "", - }) - require.ErrorContains(t, err, "clientSecret must be set") - }) - }) -} diff --git a/pkg/tsdb/azuremonitor/types/types.go b/pkg/tsdb/azuremonitor/types/types.go index b8a0d709c8d..36dfa6b70ce 100644 --- a/pkg/tsdb/azuremonitor/types/types.go +++ b/pkg/tsdb/azuremonitor/types/types.go @@ -30,24 +30,12 @@ type AzRoute struct { Headers map[string]string } -type AzureSettings struct { - AzureMonitorSettings - AzureClientSettings -} - type AzureMonitorSettings struct { SubscriptionId string `json:"subscriptionId"` LogAnalyticsDefaultWorkspace string `json:"logAnalyticsDefaultWorkspace"` AppInsightsAppId string `json:"appInsightsAppId"` } -type AzureClientSettings struct { - AzureAuthType string - CloudName string - TenantId string - ClientId string -} - // AzureMonitorCustomizedCloudSettings is the extended Azure Monitor settings for customized cloud type AzureMonitorCustomizedCloudSettings struct { CustomizedRoutes map[string]AzRoute `json:"customizedRoutes"`