From 2b83cf46186f745e356a24e4c78a1fbc24b3051e Mon Sep 17 00:00:00 2001 From: Sergey Kostrukov Date: Mon, 30 May 2022 08:43:32 -0700 Subject: [PATCH] Prometheus: Predefined scopes for Azure authentication (#49557) * Predefined scopes for Azure Prometheus * Allow override of audience --- .../src/types/featureToggles.gen.ts | 2 +- .../datasources/service/datasource_service.go | 23 +++-- .../service/datasource_service_test.go | 3 +- pkg/services/featuremgmt/registry.go | 10 +-- pkg/services/featuremgmt/toggles_gen.go | 8 +- .../buffered/promclient/provider_azure.go | 89 ++++++++++++++++--- .../promclient/provider_azure_test.go | 7 +- .../configuration/AzureAuthSettings.tsx | 71 ++++++++++----- .../prometheus/configuration/ConfigEditor.tsx | 2 +- 9 files changed, 153 insertions(+), 62 deletions(-) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 7fdfab135f2..2d990c58dda 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -18,7 +18,6 @@ export interface FeatureToggles { trimDefaults?: boolean; disableEnvelopeEncryption?: boolean; - httpclientprovider_azure_auth?: boolean; serviceAccounts?: boolean; database_metrics?: boolean; dashboardPreviews?: boolean; @@ -33,6 +32,7 @@ export interface FeatureToggles { tempoServiceGraph?: boolean; lokiBackendMode?: boolean; prometheus_azure_auth?: boolean; + prometheusAzureOverrideAudience?: boolean; influxdbBackendMigration?: boolean; newNavigation?: boolean; showFeatureFlagsInUI?: boolean; diff --git a/pkg/services/datasources/service/datasource_service.go b/pkg/services/datasources/service/datasource_service.go index 8cbab36d30a..c95c6294fdf 100644 --- a/pkg/services/datasources/service/datasource_service.go +++ b/pkg/services/datasources/service/datasource_service.go @@ -6,8 +6,6 @@ import ( "encoding/json" "fmt" "net/http" - "net/url" - "path" "strconv" "strings" "sync" @@ -27,6 +25,7 @@ import ( "github.com/grafana/grafana/pkg/services/secrets/kvstore" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tsdb/prometheus/buffered/promclient" ) type Service struct { @@ -402,7 +401,8 @@ func (s *Service) httpClientOptions(ctx context.Context, ds *models.DataSource) } } - if ds.JsonData != nil && s.features.IsEnabled(featuremgmt.FlagHttpclientproviderAzureAuth) { + // TODO: #35857 Required for templating queries in Prometheus datasource when Azure authentication enabled + if ds.JsonData != nil && s.features.IsEnabled(featuremgmt.FlagPrometheusAzureAuth) { credentials, err := azcredentials.FromDatasourceData(ds.JsonData.MustMap(), decryptedValues) if err != nil { err = fmt.Errorf("invalid Azure credentials: %s", err) @@ -410,21 +410,18 @@ func (s *Service) httpClientOptions(ctx context.Context, ds *models.DataSource) } if credentials != nil { - resourceIdStr := ds.JsonData.Get("azureEndpointResourceId").MustString() - if resourceIdStr == "" { - err := fmt.Errorf("endpoint resource ID (audience) not provided") + var scopes []string + + if scopes, err = promclient.GetOverriddenScopes(ds.JsonData.MustMap()); err != nil { return nil, err } - resourceId, err := url.Parse(resourceIdStr) - if err != nil || resourceId.Scheme == "" || resourceId.Host == "" { - err := fmt.Errorf("endpoint resource ID (audience) '%s' invalid", resourceIdStr) - return nil, err + if scopes == nil { + if scopes, err = promclient.GetPrometheusScopes(s.cfg.Azure, credentials); err != nil { + return nil, err + } } - resourceId.Path = path.Join(resourceId.Path, ".default") - scopes := []string{resourceId.String()} - azhttpclient.AddAzureAuthentication(opts, s.cfg.Azure, credentials, scopes) } } diff --git a/pkg/services/datasources/service/datasource_service_test.go b/pkg/services/datasources/service/datasource_service_test.go index 6cb930bd714..5a1a3487ad0 100644 --- a/pkg/services/datasources/service/datasource_service_test.go +++ b/pkg/services/datasources/service/datasource_service_test.go @@ -561,7 +561,7 @@ func TestService_HTTPClientOptions(t *testing.T) { t.Run("Azure authentication", func(t *testing.T) { t.Run("given feature flag enabled", func(t *testing.T) { - features := featuremgmt.WithFeatures(featuremgmt.FlagHttpclientproviderAzureAuth) + features := featuremgmt.WithFeatures(featuremgmt.FlagPrometheusAzureAuth) t.Run("should set Azure middleware when JsonData contains valid credentials", func(t *testing.T) { t.Cleanup(func() { ds.JsonData = emptyJsonData; ds.SecureJsonData = emptySecureJsonData }) @@ -571,7 +571,6 @@ func TestService_HTTPClientOptions(t *testing.T) { "azureCredentials": map[string]interface{}{ "authType": "msi", }, - "azureEndpointResourceId": "https://api.example.com/abd5c4ce-ca73-41e9-9cb2-bed39aa2adb5", }) secretsStore := kvstore.SetupTestService(t) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index e3040ac1270..69d4781256f 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -18,11 +18,6 @@ var ( Description: "Disable envelope encryption (emergency only)", State: FeatureStateStable, }, - { - Name: "httpclientprovider_azure_auth", - Description: "Experimental. Allow datasources to configure Azure authentication directly via JsonData", - State: FeatureStateBeta, - }, { Name: "serviceAccounts", Description: "support service accounts", @@ -100,6 +95,11 @@ var ( Description: "Experimental. Azure authentication for Prometheus datasource", State: FeatureStateBeta, }, + { + Name: "prometheusAzureOverrideAudience", + Description: "Experimental. Allow override default AAD audience for Azure Prometheus endpoint", + State: FeatureStateBeta, + }, { Name: "influxdbBackendMigration", Description: "Query InfluxDB InfluxQL without the proxy", diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index c9b410c2b39..f41fbacd7cb 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -15,10 +15,6 @@ const ( // Disable envelope encryption (emergency only) FlagDisableEnvelopeEncryption = "disableEnvelopeEncryption" - // FlagHttpclientproviderAzureAuth - // Experimental. Allow datasources to configure Azure authentication directly via JsonData - FlagHttpclientproviderAzureAuth = "httpclientprovider_azure_auth" - // FlagServiceAccounts // support service accounts FlagServiceAccounts = "serviceAccounts" @@ -75,6 +71,10 @@ const ( // Experimental. Azure authentication for Prometheus datasource FlagPrometheusAzureAuth = "prometheus_azure_auth" + // FlagPrometheusAzureOverrideAudience + // Experimental. Allow override default AAD audience for Azure Prometheus endpoint + FlagPrometheusAzureOverrideAudience = "prometheusAzureOverrideAudience" + // FlagInfluxdbBackendMigration // Query InfluxDB InfluxQL without the proxy FlagInfluxdbBackendMigration = "influxdbBackendMigration" diff --git a/pkg/tsdb/prometheus/buffered/promclient/provider_azure.go b/pkg/tsdb/prometheus/buffered/promclient/provider_azure.go index 318867ef681..ba53aa3652d 100644 --- a/pkg/tsdb/prometheus/buffered/promclient/provider_azure.go +++ b/pkg/tsdb/prometheus/buffered/promclient/provider_azure.go @@ -7,12 +7,21 @@ import ( "github.com/grafana/grafana-azure-sdk-go/azcredentials" "github.com/grafana/grafana-azure-sdk-go/azhttpclient" + "github.com/grafana/grafana-azure-sdk-go/azsettings" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/util/maputil" ) +var ( + azurePrometheusScopes = map[string][]string{ + azsettings.AzurePublic: {"https://prometheus.monitor.azure.com/.default"}, + azsettings.AzureChina: {"https://prometheus.monitor.chinacloudapp.cn/.default"}, + azsettings.AzureUSGovernment: {"https://prometheus.monitor.usgovcloudapi.net/.default"}, + } +) + func (p *Provider) configureAzureAuthentication(opts *sdkhttpclient.Options) error { // Azure authentication is experimental (#35857) if !p.features.IsEnabled(featuremgmt.FlagPrometheusAzureAuth) { @@ -21,30 +30,84 @@ func (p *Provider) configureAzureAuthentication(opts *sdkhttpclient.Options) err credentials, err := azcredentials.FromDatasourceData(p.jsonData, p.settings.DecryptedSecureJSONData) if err != nil { - err = fmt.Errorf("invalid Azure credentials: %s", err) + err = fmt.Errorf("invalid Azure credentials: %w", err) return err } if credentials != nil { - resourceIdStr, err := maputil.GetStringOptional(p.jsonData, "azureEndpointResourceId") - if err != nil { - return err - } else if resourceIdStr == "" { - err := fmt.Errorf("endpoint resource ID (audience) not provided") + var scopes []string + + if scopes, err = GetOverriddenScopes(p.jsonData); err != nil { return err } - resourceId, err := url.Parse(resourceIdStr) - if err != nil || resourceId.Scheme == "" || resourceId.Host == "" { - err := fmt.Errorf("endpoint resource ID (audience) '%s' invalid", resourceIdStr) - return err + if scopes == nil { + if scopes, err = GetPrometheusScopes(p.cfg.Azure, credentials); err != nil { + return err + } } - resourceId.Path = path.Join(resourceId.Path, ".default") - scopes := []string{resourceId.String()} - azhttpclient.AddAzureAuthentication(opts, p.cfg.Azure, credentials, scopes) } return nil } + +func GetOverriddenScopes(jsonData map[string]interface{}) ([]string, error) { + resourceIdStr, err := maputil.GetStringOptional(jsonData, "azureEndpointResourceId") + if err != nil { + err = fmt.Errorf("overridden resource ID (audience) invalid") + return nil, err + } else if resourceIdStr == "" { + return nil, nil + } + + resourceId, err := url.Parse(resourceIdStr) + if err != nil || resourceId.Scheme == "" || resourceId.Host == "" { + err = fmt.Errorf("overridden endpoint resource ID (audience) '%s' invalid", resourceIdStr) + return nil, err + } + + resourceId.Path = path.Join(resourceId.Path, ".default") + scopes := []string{resourceId.String()} + return scopes, nil +} + +func GetPrometheusScopes(settings *azsettings.AzureSettings, credentials azcredentials.AzureCredentials) ([]string, error) { + // Extract cloud from credentials + azureCloud, err := getAzureCloudFromCredentials(settings, credentials) + if err != nil { + return nil, err + } + + // Get scopes for the given cloud + if scopes, ok := azurePrometheusScopes[azureCloud]; !ok { + err := fmt.Errorf("the Azure cloud '%s' not supported by Prometheus datasource", azureCloud) + return nil, err + } else { + return scopes, nil + } +} + +// To be part of grafana-azure-sdk-go +func getAzureCloudFromCredentials(settings *azsettings.AzureSettings, credentials azcredentials.AzureCredentials) (string, error) { + switch c := credentials.(type) { + case *azcredentials.AzureManagedIdentityCredentials: + // In case of managed identity, the cloud is always same as where Grafana is hosted + return getDefaultAzureCloud(settings), nil + case *azcredentials.AzureClientSecretCredentials: + return c.AzureCloud, nil + default: + err := fmt.Errorf("the Azure credentials of type '%s' not supported by Prometheus datasource", c.AzureAuthType()) + return "", err + } +} + +// To be part of grafana-azure-sdk-go +func getDefaultAzureCloud(settings *azsettings.AzureSettings) string { + cloudName := settings.Cloud + if cloudName == "" { + return azsettings.AzurePublic + } + return cloudName +} diff --git a/pkg/tsdb/prometheus/buffered/promclient/provider_azure_test.go b/pkg/tsdb/prometheus/buffered/promclient/provider_azure_test.go index ee4c4536664..74c60e94ecf 100644 --- a/pkg/tsdb/prometheus/buffered/promclient/provider_azure_test.go +++ b/pkg/tsdb/prometheus/buffered/promclient/provider_azure_test.go @@ -3,8 +3,10 @@ package promclient import ( "testing" + "github.com/grafana/grafana-azure-sdk-go/azsettings" "github.com/grafana/grafana-plugin-sdk-go/backend" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/assert" @@ -12,7 +14,9 @@ import ( ) func TestConfigureAzureAuthentication(t *testing.T) { - cfg := &setting.Cfg{} + cfg := &setting.Cfg{ + Azure: &azsettings.AzureSettings{}, + } settings := backend.DataSourceInstanceSettings{} t.Run("given feature flag enabled", func(t *testing.T) { @@ -24,7 +28,6 @@ func TestConfigureAzureAuthentication(t *testing.T) { "azureCredentials": map[string]interface{}{ "authType": "msi", }, - "azureEndpointResourceId": "https://api.example.com/abd5c4ce-ca73-41e9-9cb2-bed39aa2adb5", } var p = NewProvider(settings, jsonData, nil, cfg, features, nil) diff --git a/public/app/plugins/datasource/prometheus/configuration/AzureAuthSettings.tsx b/public/app/plugins/datasource/prometheus/configuration/AzureAuthSettings.tsx index 1108dff3a9c..412e63f9389 100644 --- a/public/app/plugins/datasource/prometheus/configuration/AzureAuthSettings.tsx +++ b/public/app/plugins/datasource/prometheus/configuration/AzureAuthSettings.tsx @@ -1,7 +1,7 @@ -import React, { FunctionComponent, useMemo } from 'react'; +import React, { FunctionComponent, FormEvent, useMemo, useState } from 'react'; import { config } from '@grafana/runtime'; -import { InlineFormLabel, Input } from '@grafana/ui'; +import { InlineField, InlineFieldRow, InlineSwitch, Input } from '@grafana/ui'; import { HttpSettingsBaseProps } from '@grafana/ui/src/components/DataSourceSettings/types'; import { KnownAzureClouds, AzureCredentials } from './AzureCredentials'; @@ -11,12 +11,38 @@ import { AzureCredentialsForm } from './AzureCredentialsForm'; export const AzureAuthSettings: FunctionComponent = (props: HttpSettingsBaseProps) => { const { dataSourceConfig, onChange } = props; + const [overrideAudienceAllowed] = useState( + config.featureToggles.prometheusAzureOverrideAudience || !!dataSourceConfig.jsonData.azureEndpointResourceId + ); + const [overrideAudienceChecked, setOverrideAudienceChecked] = useState( + !!dataSourceConfig.jsonData.azureEndpointResourceId + ); + const credentials = useMemo(() => getCredentials(dataSourceConfig), [dataSourceConfig]); const onCredentialsChange = (credentials: AzureCredentials): void => { onChange(updateCredentials(dataSourceConfig, credentials)); }; + const onOverrideAudienceChange = (ev: FormEvent): void => { + setOverrideAudienceChecked(ev.currentTarget.checked); + if (!ev.currentTarget.checked) { + onChange({ + ...dataSourceConfig, + jsonData: { ...dataSourceConfig.jsonData, azureEndpointResourceId: undefined }, + }); + } + }; + + const onResourceIdChange = (ev: FormEvent): void => { + if (overrideAudienceChecked) { + onChange({ + ...dataSourceConfig, + jsonData: { ...dataSourceConfig.jsonData, azureEndpointResourceId: ev.currentTarget.value }, + }); + } + }; + return ( <>
Azure Authentication
@@ -26,26 +52,29 @@ export const AzureAuthSettings: FunctionComponent = (prop azureCloudOptions={KnownAzureClouds} onCredentialsChange={onCredentialsChange} /> -
Azure Configuration
-
-
-
- AAD resource ID -
- - onChange({ - ...dataSourceConfig, - jsonData: { ...dataSourceConfig.jsonData, azureEndpointResourceId: event.currentTarget.value }, - }) - } - /> -
+ {overrideAudienceAllowed && ( + <> +
Azure Configuration
+
+ + + + + + {overrideAudienceChecked && ( + + + + + + )}
-
-
+ + )} ); }; diff --git a/public/app/plugins/datasource/prometheus/configuration/ConfigEditor.tsx b/public/app/plugins/datasource/prometheus/configuration/ConfigEditor.tsx index 692e7cfe24d..82d80b12a65 100644 --- a/public/app/plugins/datasource/prometheus/configuration/ConfigEditor.tsx +++ b/public/app/plugins/datasource/prometheus/configuration/ConfigEditor.tsx @@ -18,7 +18,7 @@ export const ConfigEditor = (props: Props) => { const alertmanagers = getAllAlertmanagerDataSources(); const azureAuthSettings = { - azureAuthSupported: config.featureToggles['prometheus_azure_auth'] ?? false, + azureAuthSupported: !!config.featureToggles.prometheus_azure_auth, getAzureAuthEnabled: (config: DataSourceSettings): boolean => hasCredentials(config), setAzureAuthEnabled: (config: DataSourceSettings, enabled: boolean) => enabled ? setDefaultCredentials(config) : resetCredentials(config),