Shared Azure middleware between Azure Monitor and Prometheus datasources (#46002)

* Scopes in Azure middleware

* Enable Azure middleware without feature flag

* Use common Azure middleware in Azure Monitor

* Apply feature flag to JsonData configuration of Azure auth

* Enforce feature flag in Prometheus datasource

* Prometheus provider tests

* Datasource service tests

* Fix http client provider tests

* Pass sdkhttpclient.Options by reference

* Add middleware to httpclient.Options

* Remove dependency on Grafana settings

* Unit-tests updated

* Fix ds_proxy_test

* Fix service_test
This commit is contained in:
Sergey Kostrukov
2022-04-01 04:26:49 -07:00
committed by GitHub
parent 16db1ad46d
commit 656ade9884
39 changed files with 729 additions and 434 deletions

View File

@@ -5,6 +5,8 @@ import (
"crypto/tls"
"fmt"
"net/http"
"net/url"
"path"
"strconv"
"strings"
"sync"
@@ -23,12 +25,14 @@ import (
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/azcredentials"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/azhttpclient"
)
type Service struct {
Bus bus.Bus
SQLStore *sqlstore.SQLStore
SecretsService secrets.Service
cfg *setting.Cfg
features featuremgmt.FeatureToggles
permissionsService accesscontrol.PermissionsService
@@ -57,7 +61,7 @@ type cachedDecryptedJSON struct {
}
func ProvideService(
bus bus.Bus, store *sqlstore.SQLStore, secretsService secrets.Service, features featuremgmt.FeatureToggles,
bus bus.Bus, store *sqlstore.SQLStore, secretsService secrets.Service, cfg *setting.Cfg, features featuremgmt.FeatureToggles,
ac accesscontrol.AccessControl, permissionsServices accesscontrol.PermissionsServices,
) *Service {
s := &Service{
@@ -70,6 +74,7 @@ func ProvideService(
dsDecryptionCache: secureJSONDecryptionCache{
cache: make(map[int64]cachedDecryptedJSON),
},
cfg: cfg,
features: features,
permissionsService: permissionsServices.GetDataSourceService(),
}
@@ -233,7 +238,7 @@ func (s *Service) GetHTTPTransport(ds *models.DataSource, provider httpclient.Pr
return nil, err
}
opts.Middlewares = customMiddlewares
opts.Middlewares = append(opts.Middlewares, customMiddlewares...)
rt, err := provider.GetTransport(*opts)
if err != nil {
@@ -337,7 +342,8 @@ func (s *Service) httpClientOptions(ds *models.DataSource) (*sdkhttpclient.Optio
}
}
if ds.JsonData != nil {
// Azure authentication
if ds.JsonData != nil && s.features.IsEnabled(featuremgmt.FlagHttpclientproviderAzureAuth) {
credentials, err := azcredentials.FromDatasourceData(ds.JsonData.MustMap(), s.DecryptedValues(ds))
if err != nil {
err = fmt.Errorf("invalid Azure credentials: %s", err)
@@ -345,7 +351,22 @@ func (s *Service) httpClientOptions(ds *models.DataSource) (*sdkhttpclient.Optio
}
if credentials != nil {
opts.CustomOptions["_azureCredentials"] = credentials
resourceIdStr := ds.JsonData.Get("azureEndpointResourceId").MustString()
if resourceIdStr == "" {
err := fmt.Errorf("endpoint resource ID (audience) not provided")
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
}
resourceId.Path = path.Join(resourceId.Path, ".default")
scopes := []string{resourceId.String()}
azhttpclient.AddAzureAuthentication(opts, s.cfg.Azure, credentials, scopes)
}
}

View File

@@ -22,12 +22,13 @@ import (
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/azcredentials"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/azsettings"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestService(t *testing.T) {
cfg := &setting.Cfg{}
sqlStore := sqlstore.InitTestDB(t)
origSecret := setting.SecretKey
@@ -37,7 +38,7 @@ func TestService(t *testing.T) {
})
secretsService := secretsManager.SetupTestService(t, database.ProvideSecretsStore(sqlStore))
s := ProvideService(bus.New(), sqlStore, secretsService, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
s := ProvideService(bus.New(), sqlStore, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
var ds *models.DataSource
@@ -220,6 +221,8 @@ func TestService_IDScopeResolver(t *testing.T) {
//nolint:goconst
func TestService_GetHttpTransport(t *testing.T) {
cfg := &setting.Cfg{}
t.Run("Should use cached proxy", func(t *testing.T) {
var configuredTransport *http.Transport
provider := httpclient.NewProvider(sdkhttpclient.ProviderOptions{
@@ -235,7 +238,7 @@ func TestService_GetHttpTransport(t *testing.T) {
}
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
rt1, err := dsService.GetHTTPTransport(&ds, provider)
require.NoError(t, err)
@@ -268,7 +271,7 @@ func TestService_GetHttpTransport(t *testing.T) {
json.Set("tlsAuthWithCACert", true)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
tlsCaCert, err := secretsService.Encrypt(context.Background(), []byte(caCert), secrets.WithoutScope())
require.NoError(t, err)
@@ -318,7 +321,7 @@ func TestService_GetHttpTransport(t *testing.T) {
json.Set("tlsAuth", true)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
tlsClientCert, err := secretsService.Encrypt(context.Background(), []byte(clientCert), secrets.WithoutScope())
require.NoError(t, err)
@@ -361,7 +364,7 @@ func TestService_GetHttpTransport(t *testing.T) {
json.Set("serverName", "server-name")
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
tlsCaCert, err := secretsService.Encrypt(context.Background(), []byte(caCert), secrets.WithoutScope())
require.NoError(t, err)
@@ -398,7 +401,7 @@ func TestService_GetHttpTransport(t *testing.T) {
json.Set("tlsSkipVerify", true)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
ds := models.DataSource{
Id: 1,
@@ -429,7 +432,7 @@ func TestService_GetHttpTransport(t *testing.T) {
})
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
encryptedData, err := secretsService.Encrypt(context.Background(), []byte(`Bearer xf5yhfkpsnmgo`), secrets.WithoutScope())
require.NoError(t, err)
@@ -488,7 +491,7 @@ func TestService_GetHttpTransport(t *testing.T) {
})
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
ds := models.DataSource{
Id: 1,
@@ -521,7 +524,7 @@ func TestService_GetHttpTransport(t *testing.T) {
require.NoError(t, err)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
ds := models.DataSource{
Type: models.DS_ES,
@@ -537,6 +540,7 @@ func TestService_GetHttpTransport(t *testing.T) {
}
func TestService_getTimeout(t *testing.T) {
cfg := &setting.Cfg{}
originalTimeout := sdkhttpclient.DefaultTimeoutOptions.Timeout
sdkhttpclient.DefaultTimeoutOptions.Timeout = 60 * time.Second
t.Cleanup(func() {
@@ -555,7 +559,7 @@ func TestService_getTimeout(t *testing.T) {
}
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
for _, tc := range testCases {
ds := &models.DataSource{
@@ -566,9 +570,11 @@ func TestService_getTimeout(t *testing.T) {
}
func TestService_DecryptedValue(t *testing.T) {
cfg := &setting.Cfg{}
t.Run("When datasource hasn't been updated, encrypted JSON should be fetched from cache", func(t *testing.T) {
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
encryptedJsonData, err := secretsService.EncryptJsonData(
context.Background(),
@@ -622,7 +628,7 @@ func TestService_DecryptedValue(t *testing.T) {
SecureJsonData: encryptedJsonData,
}
dsService := ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
// Populate cache
password, ok := dsService.DecryptedValue(&ds, "password")
@@ -644,6 +650,10 @@ func TestService_DecryptedValue(t *testing.T) {
}
func TestService_HTTPClientOptions(t *testing.T) {
cfg := &setting.Cfg{
Azure: &azsettings.AzureSettings{},
}
emptyJsonData := simplejson.New()
emptySecureJsonData := map[string][]byte{}
@@ -654,70 +664,144 @@ func TestService_HTTPClientOptions(t *testing.T) {
}
t.Run("Azure authentication", func(t *testing.T) {
t.Run("should be disabled if no Azure credentials configured", func(t *testing.T) {
t.Cleanup(func() { ds.JsonData = emptyJsonData; ds.SecureJsonData = emptySecureJsonData })
t.Run("given feature flag enabled", func(t *testing.T) {
features := featuremgmt.WithFeatures(featuremgmt.FlagHttpclientproviderAzureAuth)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
t.Run("should set Azure middleware when JsonData contains valid credentials", func(t *testing.T) {
t.Cleanup(func() { ds.JsonData = emptyJsonData; ds.SecureJsonData = emptySecureJsonData })
opts, err := dsService.httpClientOptions(&ds)
require.NoError(t, err)
ds.JsonData = simplejson.NewFromAny(map[string]interface{}{
"httpMethod": "POST",
"azureCredentials": map[string]interface{}{
"authType": "msi",
},
"azureEndpointResourceId": "https://api.example.com/abd5c4ce-ca73-41e9-9cb2-bed39aa2adb5",
})
assert.NotContains(t, opts.CustomOptions, "_azureCredentials")
})
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService, cfg, features, acmock.New(), acmock.NewPermissionsServicesMock())
t.Run("should be enabled if Azure credentials configured", func(t *testing.T) {
t.Cleanup(func() { ds.JsonData = emptyJsonData; ds.SecureJsonData = emptySecureJsonData })
opts, err := dsService.httpClientOptions(&ds)
require.NoError(t, err)
ds.JsonData = simplejson.NewFromAny(map[string]interface{}{
"azureCredentials": map[string]interface{}{
"authType": "msi",
},
require.NotNil(t, opts.Middlewares)
assert.Len(t, opts.Middlewares, 1)
})
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
t.Run("should not set Azure middleware when JsonData doesn't contain valid credentials", func(t *testing.T) {
t.Cleanup(func() { ds.JsonData = emptyJsonData; ds.SecureJsonData = emptySecureJsonData })
opts, err := dsService.httpClientOptions(&ds)
require.NoError(t, err)
ds.JsonData = simplejson.NewFromAny(map[string]interface{}{
"httpMethod": "POST",
})
require.Contains(t, opts.CustomOptions, "_azureCredentials")
credentials := opts.CustomOptions["_azureCredentials"]
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService, cfg, features, acmock.New(), acmock.NewPermissionsServicesMock())
assert.IsType(t, &azcredentials.AzureManagedIdentityCredentials{}, credentials)
})
opts, err := dsService.httpClientOptions(&ds)
require.NoError(t, err)
t.Run("should fail if credentials are invalid", func(t *testing.T) {
t.Cleanup(func() { ds.JsonData = emptyJsonData; ds.SecureJsonData = emptySecureJsonData })
ds.JsonData = simplejson.NewFromAny(map[string]interface{}{
"azureCredentials": "invalid",
if opts.Middlewares != nil {
assert.Len(t, opts.Middlewares, 0)
}
})
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
t.Run("should return error when JsonData contains invalid credentials", func(t *testing.T) {
t.Cleanup(func() { ds.JsonData = emptyJsonData; ds.SecureJsonData = emptySecureJsonData })
_, err := dsService.httpClientOptions(&ds)
assert.Error(t, err)
})
ds.JsonData = simplejson.NewFromAny(map[string]interface{}{
"httpMethod": "POST",
"azureCredentials": "invalid",
})
t.Run("should pass resourceId from JsonData", func(t *testing.T) {
t.Cleanup(func() { ds.JsonData = emptyJsonData; ds.SecureJsonData = emptySecureJsonData })
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService, cfg, features, acmock.New(), acmock.NewPermissionsServicesMock())
ds.JsonData = simplejson.NewFromAny(map[string]interface{}{
"azureEndpointResourceId": "https://api.example.com/abd5c4ce-ca73-41e9-9cb2-bed39aa2adb5",
_, err := dsService.httpClientOptions(&ds)
assert.Error(t, err)
})
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
t.Run("should set Azure middleware when JsonData contains credentials and valid audience", func(t *testing.T) {
t.Cleanup(func() { ds.JsonData = emptyJsonData; ds.SecureJsonData = emptySecureJsonData })
opts, err := dsService.httpClientOptions(&ds)
require.NoError(t, err)
ds.JsonData = simplejson.NewFromAny(map[string]interface{}{
"httpMethod": "POST",
"azureCredentials": map[string]interface{}{
"authType": "msi",
},
"azureEndpointResourceId": "https://api.example.com/abd5c4ce-ca73-41e9-9cb2-bed39aa2adb5",
})
require.Contains(t, opts.CustomOptions, "azureEndpointResourceId")
azureEndpointResourceId := opts.CustomOptions["azureEndpointResourceId"]
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService, cfg, features, acmock.New(), acmock.NewPermissionsServicesMock())
assert.Equal(t, "https://api.example.com/abd5c4ce-ca73-41e9-9cb2-bed39aa2adb5", azureEndpointResourceId)
opts, err := dsService.httpClientOptions(&ds)
require.NoError(t, err)
require.NotNil(t, opts.Middlewares)
assert.Len(t, opts.Middlewares, 1)
})
t.Run("should not set Azure middleware when JsonData doesn't contain credentials", func(t *testing.T) {
t.Cleanup(func() { ds.JsonData = emptyJsonData; ds.SecureJsonData = emptySecureJsonData })
ds.JsonData = simplejson.NewFromAny(map[string]interface{}{
"httpMethod": "POST",
"azureEndpointResourceId": "https://api.example.com/abd5c4ce-ca73-41e9-9cb2-bed39aa2adb5",
})
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService, cfg, features, acmock.New(), acmock.NewPermissionsServicesMock())
opts, err := dsService.httpClientOptions(&ds)
require.NoError(t, err)
if opts.Middlewares != nil {
assert.Len(t, opts.Middlewares, 0)
}
})
t.Run("should return error when JsonData contains invalid audience", func(t *testing.T) {
t.Cleanup(func() { ds.JsonData = emptyJsonData; ds.SecureJsonData = emptySecureJsonData })
ds.JsonData = simplejson.NewFromAny(map[string]interface{}{
"httpMethod": "POST",
"azureCredentials": map[string]interface{}{
"authType": "msi",
},
"azureEndpointResourceId": "invalid",
})
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService, cfg, features, acmock.New(), acmock.NewPermissionsServicesMock())
_, err := dsService.httpClientOptions(&ds)
assert.Error(t, err)
})
})
t.Run("given feature flag not enabled", func(t *testing.T) {
t.Run("should not set Azure middleware even when JsonData contains credentials", func(t *testing.T) {
t.Cleanup(func() { ds.JsonData = emptyJsonData; ds.SecureJsonData = emptySecureJsonData })
ds.JsonData = simplejson.NewFromAny(map[string]interface{}{
"httpMethod": "POST",
"azureCredentials": map[string]interface{}{
"authType": "msi",
},
"azureEndpointResourceId": "https://api.example.com/abd5c4ce-ca73-41e9-9cb2-bed39aa2adb5",
})
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
opts, err := dsService.httpClientOptions(&ds)
require.NoError(t, err)
if opts.Middlewares != nil {
assert.Len(t, opts.Middlewares, 0)
}
})
})
})
}