AzureMonitor: Use auth middleware for QueryData requests (#35343)

This commit is contained in:
Andres Martinez Gotor
2021-06-11 17:02:24 +02:00
committed by GitHub
parent 36c997a625
commit 7109285ac9
26 changed files with 1274 additions and 647 deletions
+3 -2
View File
@@ -10,6 +10,7 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/tokenprovider"
"github.com/grafana/grafana/pkg/util"
)
@@ -57,7 +58,7 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route
if tokenProvider, err := getTokenProvider(ctx, cfg, ds, route, data); err != nil {
logger.Error("Failed to resolve auth token provider", "error", err)
} else if tokenProvider != nil {
if token, err := tokenProvider.getAccessToken(); err != nil {
if token, err := tokenProvider.GetAccessToken(); err != nil {
logger.Error("Failed to get access token", "error", err)
} else {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
@@ -90,7 +91,7 @@ func getTokenProvider(ctx context.Context, cfg *setting.Cfg, ds *models.DataSour
if tokenAuth == nil {
return nil, fmt.Errorf("'tokenAuth' not configured for authentication type '%s'", authType)
}
provider := newAzureAccessTokenProvider(ctx, cfg, ds, pluginRoute, tokenAuth)
provider := tokenprovider.NewAzureAccessTokenProvider(ctx, cfg, tokenAuth)
return provider, nil
case "gce":
+1 -1
View File
@@ -3,7 +3,7 @@ package pluginproxy
import "time"
type accessTokenProvider interface {
getAccessToken() (string, error)
GetAccessToken() (string, error)
}
var (
-173
View File
@@ -1,173 +0,0 @@
package pluginproxy
import (
"context"
"crypto/sha256"
"fmt"
"strings"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/setting"
)
var (
azureTokenCache = NewConcurrentTokenCache()
)
type azureAccessTokenProvider struct {
datasourceId int64
datasourceVersion int
ctx context.Context
cfg *setting.Cfg
route *plugins.AppPluginRoute
authParams *plugins.JwtTokenAuth
}
func newAzureAccessTokenProvider(ctx context.Context, cfg *setting.Cfg, ds *models.DataSource, pluginRoute *plugins.AppPluginRoute,
authParams *plugins.JwtTokenAuth) *azureAccessTokenProvider {
return &azureAccessTokenProvider{
datasourceId: ds.Id,
datasourceVersion: ds.Version,
ctx: ctx,
cfg: cfg,
route: pluginRoute,
authParams: authParams,
}
}
func (provider *azureAccessTokenProvider) getAccessToken() (string, error) {
var credential TokenCredential
if provider.isManagedIdentityCredential() {
if !provider.cfg.Azure.ManagedIdentityEnabled {
err := fmt.Errorf("managed identity authentication is not enabled in Grafana config")
return "", err
} else {
credential = provider.getManagedIdentityCredential()
}
} else {
credential = provider.getClientSecretCredential()
}
accessToken, err := azureTokenCache.GetAccessToken(provider.ctx, credential, provider.authParams.Scopes)
if err != nil {
return "", err
}
return accessToken, nil
}
func (provider *azureAccessTokenProvider) isManagedIdentityCredential() bool {
authType := strings.ToLower(provider.authParams.Params["azure_auth_type"])
clientId := provider.authParams.Params["client_id"]
// Type of authentication being determined by the following logic:
// * If authType is set to 'msi' then user explicitly selected the managed identity authentication
// * If authType isn't set but other fields are configured then it's a datasource which was configured
// before managed identities where introduced, therefore use client secret authentication
// * If authType and other fields aren't set then it means the datasource never been configured
// and managed identity is the default authentication choice as long as managed identities are enabled
return authType == "msi" || (authType == "" && clientId == "" && provider.cfg.Azure.ManagedIdentityEnabled)
}
func (provider *azureAccessTokenProvider) getManagedIdentityCredential() TokenCredential {
clientId := provider.cfg.Azure.ManagedIdentityClientId
return &managedIdentityCredential{clientId: clientId}
}
func (provider *azureAccessTokenProvider) getClientSecretCredential() TokenCredential {
authority := provider.resolveAuthorityHost(provider.authParams.Params["azure_cloud"])
tenantId := provider.authParams.Params["tenant_id"]
clientId := provider.authParams.Params["client_id"]
clientSecret := provider.authParams.Params["client_secret"]
return &clientSecretCredential{authority: authority, tenantId: tenantId, clientId: clientId, clientSecret: clientSecret}
}
func (provider *azureAccessTokenProvider) resolveAuthorityHost(cloudName string) string {
// Known Azure clouds
switch cloudName {
case setting.AzurePublic:
return azidentity.AzurePublicCloud
case setting.AzureChina:
return azidentity.AzureChina
case setting.AzureUSGovernment:
return azidentity.AzureGovernment
case setting.AzureGermany:
return azidentity.AzureGermany
}
// Fallback to direct URL
return provider.authParams.Url
}
type managedIdentityCredential struct {
clientId string
credential azcore.TokenCredential
}
func (c *managedIdentityCredential) GetCacheKey() string {
clientId := c.clientId
if clientId == "" {
clientId = "system"
}
return fmt.Sprintf("azure|msi|%s", clientId)
}
func (c *managedIdentityCredential) Init() error {
if credential, err := azidentity.NewManagedIdentityCredential(c.clientId, nil); err != nil {
return err
} else {
c.credential = credential
return nil
}
}
func (c *managedIdentityCredential) GetAccessToken(ctx context.Context, scopes []string) (*AccessToken, error) {
accessToken, err := c.credential.GetToken(ctx, azcore.TokenRequestOptions{Scopes: scopes})
if err != nil {
return nil, err
}
return &AccessToken{Token: accessToken.Token, ExpiresOn: accessToken.ExpiresOn}, nil
}
type clientSecretCredential struct {
authority string
tenantId string
clientId string
clientSecret string
credential azcore.TokenCredential
}
func (c *clientSecretCredential) GetCacheKey() string {
return fmt.Sprintf("azure|clientsecret|%s|%s|%s|%s", c.authority, c.tenantId, c.clientId, hashSecret(c.clientSecret))
}
func (c *clientSecretCredential) Init() error {
options := &azidentity.ClientSecretCredentialOptions{AuthorityHost: c.authority}
if credential, err := azidentity.NewClientSecretCredential(c.tenantId, c.clientId, c.clientSecret, options); err != nil {
return err
} else {
c.credential = credential
return nil
}
}
func (c *clientSecretCredential) GetAccessToken(ctx context.Context, scopes []string) (*AccessToken, error) {
accessToken, err := c.credential.GetToken(ctx, azcore.TokenRequestOptions{Scopes: scopes})
if err != nil {
return nil, err
}
return &AccessToken{Token: accessToken.Token, ExpiresOn: accessToken.ExpiresOn}, nil
}
func hashSecret(secret string) string {
hash := sha256.New()
_, _ = hash.Write([]byte(secret))
return fmt.Sprintf("%x", hash.Sum(nil))
}
@@ -1,221 +0,0 @@
package pluginproxy
import (
"context"
"testing"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var getAccessTokenFunc func(credential TokenCredential, scopes []string)
type tokenCacheFake struct{}
func (c *tokenCacheFake) GetAccessToken(_ context.Context, credential TokenCredential, scopes []string) (string, error) {
getAccessTokenFunc(credential, scopes)
return "4cb83b87-0ffb-4abd-82f6-48a8c08afc53", nil
}
func TestAzureTokenProvider_isManagedIdentityCredential(t *testing.T) {
ctx := context.Background()
cfg := &setting.Cfg{}
ds := &models.DataSource{Id: 1, Version: 2}
route := &plugins.AppPluginRoute{}
authParams := &plugins.JwtTokenAuth{
Scopes: []string{
"https://management.azure.com/.default",
},
Params: map[string]string{
"azure_auth_type": "",
"azure_cloud": "AzureCloud",
"tenant_id": "",
"client_id": "",
"client_secret": "",
},
}
provider := newAzureAccessTokenProvider(ctx, cfg, ds, route, authParams)
t.Run("when managed identities enabled", func(t *testing.T) {
cfg.Azure.ManagedIdentityEnabled = true
t.Run("should be managed identity if auth type is managed identity", func(t *testing.T) {
authParams.Params = map[string]string{
"azure_auth_type": "msi",
}
assert.True(t, provider.isManagedIdentityCredential())
})
t.Run("should be client secret if auth type is client secret", func(t *testing.T) {
authParams.Params = map[string]string{
"azure_auth_type": "clientsecret",
}
assert.False(t, provider.isManagedIdentityCredential())
})
t.Run("should be managed identity if datasource not configured", func(t *testing.T) {
authParams.Params = map[string]string{
"azure_auth_type": "",
"tenant_id": "",
"client_id": "",
"client_secret": "",
}
assert.True(t, provider.isManagedIdentityCredential())
})
t.Run("should be client secret if auth type not specified but credentials configured", func(t *testing.T) {
authParams.Params = map[string]string{
"azure_auth_type": "",
"tenant_id": "06da9207-bdd9-4558-aee4-377450893cb4",
"client_id": "b8c58fe8-1fca-4e30-a0a8-b44d0e5f70d6",
"client_secret": "9bcd4434-824f-4887-a8a8-94c287bf0a7b",
}
assert.False(t, provider.isManagedIdentityCredential())
})
})
t.Run("when managed identities disabled", func(t *testing.T) {
cfg.Azure.ManagedIdentityEnabled = false
t.Run("should be managed identity if auth type is managed identity", func(t *testing.T) {
authParams.Params = map[string]string{
"azure_auth_type": "msi",
}
assert.True(t, provider.isManagedIdentityCredential())
})
t.Run("should be client secret if datasource not configured", func(t *testing.T) {
authParams.Params = map[string]string{
"azure_auth_type": "",
"tenant_id": "",
"client_id": "",
"client_secret": "",
}
assert.False(t, provider.isManagedIdentityCredential())
})
})
}
func TestAzureTokenProvider_getAccessToken(t *testing.T) {
ctx := context.Background()
cfg := &setting.Cfg{}
ds := &models.DataSource{Id: 1, Version: 2}
route := &plugins.AppPluginRoute{}
authParams := &plugins.JwtTokenAuth{
Scopes: []string{
"https://management.azure.com/.default",
},
Params: map[string]string{
"azure_auth_type": "",
"azure_cloud": "AzureCloud",
"tenant_id": "",
"client_id": "",
"client_secret": "",
},
}
provider := newAzureAccessTokenProvider(ctx, cfg, ds, route, authParams)
original := azureTokenCache
azureTokenCache = &tokenCacheFake{}
t.Cleanup(func() { azureTokenCache = original })
t.Run("when managed identities enabled", func(t *testing.T) {
cfg.Azure.ManagedIdentityEnabled = true
t.Run("should resolve managed identity credential if auth type is managed identity", func(t *testing.T) {
authParams.Params = map[string]string{
"azure_auth_type": "msi",
}
getAccessTokenFunc = func(credential TokenCredential, scopes []string) {
assert.IsType(t, &managedIdentityCredential{}, credential)
}
_, err := provider.getAccessToken()
require.NoError(t, err)
})
t.Run("should resolve client secret credential if auth type is client secret", func(t *testing.T) {
authParams.Params = map[string]string{
"azure_auth_type": "clientsecret",
}
getAccessTokenFunc = func(credential TokenCredential, scopes []string) {
assert.IsType(t, &clientSecretCredential{}, credential)
}
_, err := provider.getAccessToken()
require.NoError(t, err)
})
})
t.Run("when managed identities disabled", func(t *testing.T) {
cfg.Azure.ManagedIdentityEnabled = false
t.Run("should return error if auth type is managed identity", func(t *testing.T) {
authParams.Params = map[string]string{
"azure_auth_type": "msi",
}
getAccessTokenFunc = func(credential TokenCredential, scopes []string) {
assert.Fail(t, "token cache not expected to be called")
}
_, err := provider.getAccessToken()
require.Error(t, err)
})
})
}
func TestAzureTokenProvider_getClientSecretCredential(t *testing.T) {
ctx := context.Background()
cfg := &setting.Cfg{}
ds := &models.DataSource{Id: 1, Version: 2}
route := &plugins.AppPluginRoute{}
authParams := &plugins.JwtTokenAuth{
Scopes: []string{
"https://management.azure.com/.default",
},
Params: map[string]string{
"azure_auth_type": "",
"azure_cloud": "AzureCloud",
"tenant_id": "7dcf1d1a-4ec0-41f2-ac29-c1538a698bc4",
"client_id": "1af7c188-e5b6-4f96-81b8-911761bdd459",
"client_secret": "0416d95e-8af8-472c-aaa3-15c93c46080a",
},
}
provider := newAzureAccessTokenProvider(ctx, cfg, ds, route, authParams)
t.Run("should return clientSecretCredential with values", func(t *testing.T) {
result := provider.getClientSecretCredential()
assert.IsType(t, &clientSecretCredential{}, result)
credential := (result).(*clientSecretCredential)
assert.Equal(t, "https://login.microsoftonline.com/", credential.authority)
assert.Equal(t, "7dcf1d1a-4ec0-41f2-ac29-c1538a698bc4", credential.tenantId)
assert.Equal(t, "1af7c188-e5b6-4f96-81b8-911761bdd459", credential.clientId)
assert.Equal(t, "0416d95e-8af8-472c-aaa3-15c93c46080a", credential.clientSecret)
})
}
+1 -1
View File
@@ -27,7 +27,7 @@ func newGceAccessTokenProvider(ctx context.Context, ds *models.DataSource, plugi
}
}
func (provider *gceAccessTokenProvider) getAccessToken() (string, error) {
func (provider *gceAccessTokenProvider) GetAccessToken() (string, error) {
tokenSrc, err := google.DefaultTokenSource(provider.ctx, provider.authParams.Scopes...)
if err != nil {
logger.Error("Failed to get default token from meta data server", "error", err)
@@ -78,7 +78,7 @@ func newGenericAccessTokenProvider(ds *models.DataSource, pluginRoute *plugins.A
}
}
func (provider *genericAccessTokenProvider) getAccessToken() (string, error) {
func (provider *genericAccessTokenProvider) GetAccessToken() (string, error) {
tokenCache.Lock()
defer tokenCache.Unlock()
if cachedToken, found := tokenCache.cache[provider.getAccessTokenCacheKey()]; found {
+1 -1
View File
@@ -42,7 +42,7 @@ func newJwtAccessTokenProvider(ctx context.Context, ds *models.DataSource, plugi
}
}
func (provider *jwtAccessTokenProvider) getAccessToken() (string, error) {
func (provider *jwtAccessTokenProvider) GetAccessToken() (string, error) {
oauthJwtTokenCache.Lock()
defer oauthJwtTokenCache.Unlock()
if cachedToken, found := oauthJwtTokenCache.cache[provider.getAccessTokenCacheKey()]; found {
+9 -9
View File
@@ -70,7 +70,7 @@ func TestAccessToken_pluginWithJWTTokenAuthRoute(t *testing.T) {
return &oauth2.Token{AccessToken: "abc"}, nil
})
provider := newJwtAccessTokenProvider(context.Background(), ds, pluginRoute, authParams)
token, err := provider.getAccessToken()
token, err := provider.GetAccessToken()
require.NoError(t, err)
assert.Equal(t, "abc", token)
@@ -89,7 +89,7 @@ func TestAccessToken_pluginWithJWTTokenAuthRoute(t *testing.T) {
})
provider := newJwtAccessTokenProvider(context.Background(), ds, pluginRoute, authParams)
_, err := provider.getAccessToken()
_, err := provider.GetAccessToken()
require.NoError(t, err)
})
@@ -100,14 +100,14 @@ func TestAccessToken_pluginWithJWTTokenAuthRoute(t *testing.T) {
Expiry: time.Now().Add(1 * time.Minute)}, nil
})
provider := newJwtAccessTokenProvider(context.Background(), ds, pluginRoute, authParams)
token1, err := provider.getAccessToken()
token1, err := provider.GetAccessToken()
require.NoError(t, err)
assert.Equal(t, "abc", token1)
getTokenSource = func(conf *jwt.Config, ctx context.Context) (*oauth2.Token, error) {
return &oauth2.Token{AccessToken: "error: cache not used"}, nil
}
token2, err := provider.getAccessToken()
token2, err := provider.GetAccessToken()
require.NoError(t, err)
assert.Equal(t, "abc", token2)
})
@@ -224,12 +224,12 @@ func TestAccessToken_pluginWithTokenAuthRoute(t *testing.T) {
token["expires_on"] = testCase.expiresOn
}
accessToken, err := provider.getAccessToken()
accessToken, err := provider.GetAccessToken()
require.NoError(t, err)
assert.Equal(t, token["access_token"], accessToken)
// getAccessToken should use internal cache
accessToken, err = provider.getAccessToken()
// GetAccessToken should use internal cache
accessToken, err = provider.GetAccessToken()
require.NoError(t, err)
assert.Equal(t, token["access_token"], accessToken)
assert.Equal(t, 1, authCalls)
@@ -259,13 +259,13 @@ func TestAccessToken_pluginWithTokenAuthRoute(t *testing.T) {
"token_type": "3600",
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
}
accessToken, err := provider.getAccessToken()
accessToken, err := provider.GetAccessToken()
require.NoError(t, err)
assert.Equal(t, token["access_token"], accessToken)
mockTimeNow(timeNow().Add(3601 * time.Second))
accessToken, err = provider.getAccessToken()
accessToken, err = provider.GetAccessToken()
require.NoError(t, err)
assert.Equal(t, token["access_token"], accessToken)
assert.Equal(t, 2, authCalls)