diff --git a/pkg/azuredx/client/endpoints.go b/pkg/azuredx/client/endpoints.go index c9e767e9..1bb2fd3c 100644 --- a/pkg/azuredx/client/endpoints.go +++ b/pkg/azuredx/client/endpoints.go @@ -2,6 +2,7 @@ package client import ( "fmt" + "strings" "github.com/grafana/grafana-azure-sdk-go/azsettings" ) @@ -53,10 +54,15 @@ var azureDataExplorerEndpoints = map[string][]string{ }, } -func getAdxEndpoints(azureCloud string) ([]string, error) { +func getAdxEndpoints(azureCloud string, trustedClustersURLs string) ([]string, error) { if endpoints, ok := azureDataExplorerEndpoints[azureCloud]; !ok { return nil, fmt.Errorf("the Azure cloud '%s' not supported by Azure Data Explorer datasource", azureCloud) } else { + // Append the trusted URLs for all clouds + var trustedUrls = strings.Split(trustedClustersURLs, ",") + if len(trustedUrls) > 1 || trustedUrls[0] != "" { + endpoints = append(endpoints, trustedUrls...) + } return endpoints, nil } } diff --git a/pkg/azuredx/client/endpoints_test.go b/pkg/azuredx/client/endpoints_test.go new file mode 100644 index 00000000..d96c9fca --- /dev/null +++ b/pkg/azuredx/client/endpoints_test.go @@ -0,0 +1,151 @@ +package client + +import ( + "testing" + + "github.com/grafana/grafana-azure-sdk-go/azsettings" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetAdxEndpoints_EmptyTrustedClusters(t *testing.T) { + tests := []struct { + description string + cloud string + trustedClustersUrls string + expectedLastUrl string + }{ + { + description: "test public cloud", + cloud: azsettings.AzurePublic, + trustedClustersUrls: "", + expectedLastUrl: "https://*.kusto.fabric.microsoft.com", + }, + { + description: "test US Government cloud - Texas", + cloud: azsettings.AzureUSGovernment, + trustedClustersUrls: "", + expectedLastUrl: "https://*.kustomfa.usgovcloudapi.net", + }, + { + description: "test US Government cloud - Virginia", + cloud: azsettings.AzureUSGovernment, + trustedClustersUrls: "", + expectedLastUrl: "https://*.kustomfa.usgovcloudapi.net", + }, + { + description: "test China cloud", + cloud: azsettings.AzureChina, + trustedClustersUrls: "", + expectedLastUrl: "https://*.playfab.cn", + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + endpoints, err := getAdxEndpoints(tt.cloud, tt.trustedClustersUrls) + require.NoError(t, err) + + assert.Len(t, endpoints, 22) + assert.Equal(t, tt.expectedLastUrl, endpoints[len(endpoints)-1]) + }) + } +} + +func TestGetAdxEndpoints_OneTrustedClusters(t *testing.T) { + tests := []struct { + description string + cloud string + trustedClustersUrls string + expectedLastUrl string + }{ + { + description: "test public cloud", + cloud: azsettings.AzurePublic, + trustedClustersUrls: "https://*.kusto.proxy.com", + expectedLastUrl: "https://*.kusto.proxy.com", + }, + { + description: "test US Government cloud - Texas", + cloud: azsettings.AzureUSGovernment, + trustedClustersUrls: "https://*.kusto.proxy.com", + expectedLastUrl: "https://*.kusto.proxy.com", + }, + { + description: "test US Government cloud - Virginia", + cloud: azsettings.AzureUSGovernment, + trustedClustersUrls: "https://*.kusto.proxy.com", + expectedLastUrl: "https://*.kusto.proxy.com", + }, + { + description: "test China cloud", + cloud: azsettings.AzureChina, + trustedClustersUrls: "https://*.kusto.proxy.com", + expectedLastUrl: "https://*.kusto.proxy.com", + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + endpoints, err := getAdxEndpoints(tt.cloud, tt.trustedClustersUrls) + require.NoError(t, err) + + assert.Len(t, endpoints, 23) + assert.Equal(t, tt.expectedLastUrl, endpoints[len(endpoints)-1]) + }) + } +} + +func TestGetAdxEndpoints_MoreThanOneTrustedClusters(t *testing.T) { + tests := []struct { + description string + cloud string + trustedClustersUrls string + expectedLastUrl string + }{ + { + description: "test public cloud", + cloud: azsettings.AzurePublic, + trustedClustersUrls: "https://*.kusto.proxy.com,https://*.kusto.proxy-2.com", + expectedLastUrl: "https://*.kusto.proxy-2.com", + }, + { + description: "test US Government cloud - Texas", + cloud: azsettings.AzureUSGovernment, + trustedClustersUrls: "https://*.kusto.proxy.com,https://*.kusto.proxy-2.com", + expectedLastUrl: "https://*.kusto.proxy-2.com", + }, + { + description: "test US Government cloud - Virginia", + cloud: azsettings.AzureUSGovernment, + trustedClustersUrls: "https://*.kusto.proxy.com,https://*.kusto.proxy-2.com", + expectedLastUrl: "https://*.kusto.proxy-2.com", + }, + { + description: "test China cloud", + cloud: azsettings.AzureChina, + trustedClustersUrls: "https://*.kusto.proxy.com,https://*.kusto.proxy-2.com", + expectedLastUrl: "https://*.kusto.proxy-2.com", + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + endpoints, err := getAdxEndpoints(tt.cloud, tt.trustedClustersUrls) + require.NoError(t, err) + + assert.Len(t, endpoints, 24) + assert.Equal(t, tt.expectedLastUrl, endpoints[len(endpoints)-1]) + }) + } +} + +func TestGetAdxEndpoints_UnknownClouds(t *testing.T) { + t.Run("should fail when cloud is unknown", func(t *testing.T) { + trustedClustersUrls := "https://abc.northeurope.unknown.net" + + _, err := getAdxEndpoints("Unknown", trustedClustersUrls) + assert.Error(t, err) + }) +} diff --git a/pkg/azuredx/client/httpclient.go b/pkg/azuredx/client/httpclient.go index 16fee66c..a91da90d 100644 --- a/pkg/azuredx/client/httpclient.go +++ b/pkg/azuredx/client/httpclient.go @@ -77,7 +77,7 @@ func getAuthOpts(azureSettings *azsettings.AzureSettings, dsSettings *models.Dat // Enforce only trusted Azure Data Explorer endpoints if enabled if userProvidedEndpoint && dsSettings.EnforceTrustedEndpoints { - endpoints, err := getAdxEndpoints(azureCloud) + endpoints, err := getAdxEndpoints(azureCloud, dsSettings.TrustedClustersURLs) if err != nil { return nil, err } diff --git a/pkg/azuredx/models/settings.go b/pkg/azuredx/models/settings.go index 37eba439..f6742e65 100644 --- a/pkg/azuredx/models/settings.go +++ b/pkg/azuredx/models/settings.go @@ -13,12 +13,13 @@ import ( // DatasourceSettings holds the datasource configuration information for Azure Data Explorer's API // that is needed to execute a request against Azure's Data Explorer API. type DatasourceSettings struct { - ClusterURL string `json:"clusterUrl"` - DefaultDatabase string `json:"defaultDatabase"` - DataConsistency string `json:"dataConsistency"` - CacheMaxAge string `json:"cacheMaxAge"` - DynamicCaching bool `json:"dynamicCaching"` - EnableUserTracking bool `json:"enableUserTracking"` + ClusterURL string `json:"clusterUrl"` + TrustedClustersURLs string `json:"trustedClustersURLs"` + DefaultDatabase string `json:"defaultDatabase"` + DataConsistency string `json:"dataConsistency"` + CacheMaxAge string `json:"cacheMaxAge"` + DynamicCaching bool `json:"dynamicCaching"` + EnableUserTracking bool `json:"enableUserTracking"` // QueryTimeoutRaw is a duration string set in the datasource settings and corresponds // to the server execution timeout. diff --git a/src/components/ConfigEditor/ConnectionConfig.tsx b/src/components/ConfigEditor/ConnectionConfig.tsx index 3c67787d..58469ec7 100644 --- a/src/components/ConfigEditor/ConnectionConfig.tsx +++ b/src/components/ConfigEditor/ConnectionConfig.tsx @@ -13,17 +13,31 @@ const ConnectionConfig: React.FC = ({ options, updateJson const { jsonData } = options; return ( - - ) => updateJsonData('clusterUrl', ev.target.value)} - /> - + <> + + ) => updateJsonData('clusterUrl', ev.target.value)} + /> + + + + ) => updateJsonData('trustedClustersURLs', ev.target.value)} + /> + + ); }; diff --git a/src/components/__fixtures__/ConfigEditor.fixtures.ts b/src/components/__fixtures__/ConfigEditor.fixtures.ts index 9b216654..0f6397ed 100644 --- a/src/components/__fixtures__/ConfigEditor.fixtures.ts +++ b/src/components/__fixtures__/ConfigEditor.fixtures.ts @@ -29,6 +29,7 @@ export const mockConfigEditorProps = (optionsOverrides?: Partial