Azure token provider with support for Managed Identities (#33807)

* Azure token provider

* Configuration for Azure token provider

* Authentication via Azure SDK for Go

* Fix typo

* ConcurrentTokenCache for Azure credentials

* Resolve AAD authority for selected Azure cloud

* Fixes

* Generic AccessToken and fixes

* Tests and wordings

* Tests for getAccessToken

* Tests for getClientSecretCredential

* Tests for token cache
This commit is contained in:
Sergey Kostrukov 2021-05-14 04:59:07 -07:00 committed by GitHub
parent 8254efc027
commit 81f6c806e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 756 additions and 66 deletions

2
go.mod
View File

@ -14,6 +14,8 @@ replace k8s.io/client-go => k8s.io/client-go v0.18.8
require (
cloud.google.com/go/storage v1.14.0
cuelang.org/go v0.3.2
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.14.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.8.0
github.com/BurntSushi/toml v0.3.1
github.com/Masterminds/semver v1.5.0
github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f

11
go.sum
View File

@ -75,7 +75,14 @@ github.com/Azure/azure-sdk-for-go v46.4.0+incompatible/go.mod h1:9XXNKU+eRnpl9mo
github.com/Azure/azure-sdk-for-go v48.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/azure-sdk-for-go v51.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/azure-sdk-for-go v51.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/azure-sdk-for-go v52.5.0+incompatible h1:/NLBWHCnIHtZyLPc1P7WIqi4Te4CC23kIQyK3Ep/7lA=
github.com/Azure/azure-sdk-for-go v52.5.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.14.0 h1:4HBTI/9UDZN7tsXyB5TYP3xCv5xVHIUTbvHHH2HFxQY=
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.14.0/go.mod h1:pElNP+u99BvCZD+0jOlhI9OC/NB2IDTOTGZOZH0Qhq8=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.8.0 h1:wb00szFWtKeIef2Q5X8gdd0mYp8oSHmJOYUh/QXD8sw=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.8.0/go.mod h1:acANgl9stsT5xflESXKjZx4rhZJSr0TGgTDYY0xJPIE=
github.com/Azure/azure-sdk-for-go/sdk/internal v0.5.0 h1:HG1ggl8L3ZkV/Ydanf7lKr5kkhhPGCpWdnr1J6v7cO4=
github.com/Azure/azure-sdk-for-go/sdk/internal v0.5.0/go.mod h1:k4KbFSunV/+0hOHL1vyFaPsiYQ1Vmvy1TBpmtvCDLZM=
github.com/Azure/azure-storage-blob-go v0.6.0/go.mod h1:oGfmITT1V6x//CswqY2gtAHND+xIP64/qL7a5QJix0Y=
github.com/Azure/azure-storage-blob-go v0.8.0/go.mod h1:lPI3aLPpuLTeUwh1sViKXFxwl2B6teiRqI0deQUvsw0=
github.com/Azure/azure-storage-queue-go v0.0.0-20181215014128-6ed74e755687/go.mod h1:K6am8mT+5iFXgingS9LUc7TmbsW6XBw3nxaRyaMyWc8=
@ -1466,6 +1473,8 @@ github.com/pierrec/lz4 v2.6.0+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi
github.com/pierrec/lz4/v4 v4.1.1/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98=
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -1947,6 +1956,7 @@ golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
@ -2064,6 +2074,7 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=

View File

@ -9,12 +9,13 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
// ApplyRoute should use the plugin route data to set auth headers and custom headers.
func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route *plugins.AppPluginRoute,
ds *models.DataSource) {
ds *models.DataSource, cfg *setting.Cfg) {
proxyPath = strings.TrimPrefix(proxyPath, route.Path)
data := templateData{
@ -53,7 +54,7 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route
logger.Error("Failed to set plugin route body content", "error", err)
}
if tokenProvider, err := getTokenProvider(ctx, ds, route, data); err != nil {
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 {
@ -66,7 +67,7 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route
logger.Info("Requesting", "url", req.URL.String())
}
func getTokenProvider(ctx context.Context, ds *models.DataSource, pluginRoute *plugins.AppPluginRoute,
func getTokenProvider(ctx context.Context, cfg *setting.Cfg, ds *models.DataSource, pluginRoute *plugins.AppPluginRoute,
data templateData) (accessTokenProvider, error) {
authType := pluginRoute.AuthType
@ -85,6 +86,13 @@ func getTokenProvider(ctx context.Context, ds *models.DataSource, pluginRoute *p
}
switch authType {
case "azure":
if tokenAuth == nil {
return nil, fmt.Errorf("'tokenAuth' not configured for authentication type '%s'", authType)
}
provider := newAzureAccessTokenProvider(ctx, cfg, ds, pluginRoute, tokenAuth)
return provider, nil
case "gce":
if jwtTokenAuth == nil {
return nil, fmt.Errorf("'jwtTokenAuth' not configured for authentication type '%s'", authType)

View File

@ -231,7 +231,7 @@ func (proxy *DataSourceProxy) director(req *http.Request) {
req.Header.Del("Referer")
if proxy.route != nil {
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, proxy.ds)
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, proxy.ds, proxy.cfg)
}
if oauthtoken.IsOAuthPassThruEnabled(proxy.ds) {

View File

@ -109,12 +109,14 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
return ctx, req
}
cfg := &setting.Cfg{}
t.Run("When matching route path", func(t *testing.T) {
ctx, req := setUp()
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "api/v4/some/method", &setting.Cfg{})
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "api/v4/some/method", cfg)
require.NoError(t, err)
proxy.route = plugin.Routes[0]
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, proxy.ds)
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, proxy.ds, cfg)
assert.Equal(t, "https://www.google.com/some/method", req.URL.String())
assert.Equal(t, "my secret 123", req.Header.Get("x-header"))
@ -122,10 +124,10 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
t.Run("When matching route path and has dynamic url", func(t *testing.T) {
ctx, req := setUp()
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "api/common/some/method", &setting.Cfg{})
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "api/common/some/method", cfg)
require.NoError(t, err)
proxy.route = plugin.Routes[3]
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, proxy.ds)
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, proxy.ds, cfg)
assert.Equal(t, "https://dynamic.grafana.com/some/method?apiKey=123", req.URL.String())
assert.Equal(t, "my secret 123", req.Header.Get("x-header"))
@ -133,20 +135,20 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
t.Run("When matching route path with no url", func(t *testing.T) {
ctx, req := setUp()
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "", &setting.Cfg{})
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "", cfg)
require.NoError(t, err)
proxy.route = plugin.Routes[4]
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, proxy.ds)
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, proxy.ds, cfg)
assert.Equal(t, "http://localhost/asd", req.URL.String())
})
t.Run("When matching route path and has dynamic body", func(t *testing.T) {
ctx, req := setUp()
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "api/body", &setting.Cfg{})
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "api/body", cfg)
require.NoError(t, err)
proxy.route = plugin.Routes[5]
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, proxy.ds)
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, proxy.ds, cfg)
content, err := ioutil.ReadAll(req.Body)
require.NoError(t, err)
@ -156,7 +158,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
t.Run("Validating request", func(t *testing.T) {
t.Run("plugin route with valid role", func(t *testing.T) {
ctx, _ := setUp()
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "api/v4/some/method", &setting.Cfg{})
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "api/v4/some/method", cfg)
require.NoError(t, err)
err = proxy.validateRequest()
require.NoError(t, err)
@ -164,7 +166,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
t.Run("plugin route with admin role and user is editor", func(t *testing.T) {
ctx, _ := setUp()
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "api/admin", &setting.Cfg{})
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "api/admin", cfg)
require.NoError(t, err)
err = proxy.validateRequest()
require.Error(t, err)
@ -173,7 +175,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
t.Run("plugin route with admin role and user is admin", func(t *testing.T) {
ctx, _ := setUp()
ctx.SignedInUser.OrgRole = models.ROLE_ADMIN
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "api/admin", &setting.Cfg{})
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "api/admin", cfg)
require.NoError(t, err)
err = proxy.validateRequest()
require.NoError(t, err)
@ -253,9 +255,11 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
client = newFakeHTTPClient(t, json)
defer func() { client = originalClient }()
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken1", &setting.Cfg{})
cfg := &setting.Cfg{}
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken1", cfg)
require.NoError(t, err)
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, plugin.Routes[0], proxy.ds)
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, plugin.Routes[0], proxy.ds, cfg)
authorizationHeaderCall1 = req.Header.Get("Authorization")
assert.Equal(t, "https://api.nr1.io/some/path", req.URL.String())
@ -268,9 +272,9 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
req, err := http.NewRequest("GET", "http://localhost/asd", nil)
require.NoError(t, err)
client = newFakeHTTPClient(t, json2)
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken2", &setting.Cfg{})
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken2", cfg)
require.NoError(t, err)
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, plugin.Routes[1], proxy.ds)
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, plugin.Routes[1], proxy.ds, cfg)
authorizationHeaderCall2 = req.Header.Get("Authorization")
@ -284,9 +288,9 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
require.NoError(t, err)
client = newFakeHTTPClient(t, []byte{})
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken1", &setting.Cfg{})
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken1", cfg)
require.NoError(t, err)
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, plugin.Routes[0], proxy.ds)
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, plugin.Routes[0], proxy.ds, cfg)
authorizationHeaderCall3 := req.Header.Get("Authorization")
assert.Equal(t, "https://api.nr1.io/some/path", req.URL.String())

View File

@ -0,0 +1,127 @@
package pluginproxy
import (
"context"
"sort"
"strings"
"sync"
"time"
)
type AccessToken struct {
Token string
ExpiresOn time.Time
}
type TokenCredential interface {
GetCacheKey() string
GetAccessToken(ctx context.Context, scopes []string) (*AccessToken, error)
}
type ConcurrentTokenCache interface {
GetAccessToken(ctx context.Context, credential TokenCredential, scopes []string) (string, error)
}
func NewConcurrentTokenCache() ConcurrentTokenCache {
return &tokenCacheImpl{}
}
type tokenCacheImpl struct {
cache sync.Map // of *credentialCacheEntry
}
type credentialCacheEntry struct {
credential TokenCredential
cache sync.Map // of *scopesCacheEntry
}
type scopesCacheEntry struct {
credential TokenCredential
scopes []string
cond *sync.Cond
refreshing bool
accessToken *AccessToken
}
func (c *tokenCacheImpl) GetAccessToken(ctx context.Context, credential TokenCredential, scopes []string) (string, error) {
var entry interface{}
var ok bool
credentialKey := credential.GetCacheKey()
scopesKey := getKeyForScopes(scopes)
if entry, ok = c.cache.Load(credentialKey); !ok {
entry, _ = c.cache.LoadOrStore(credentialKey, &credentialCacheEntry{
credential: credential,
})
}
credentialEntry := entry.(*credentialCacheEntry)
if entry, ok = credentialEntry.cache.Load(scopesKey); !ok {
entry, _ = credentialEntry.cache.LoadOrStore(scopesKey, &scopesCacheEntry{
credential: credentialEntry.credential,
scopes: scopes,
cond: sync.NewCond(&sync.Mutex{}),
})
}
scopesEntry := entry.(*scopesCacheEntry)
return scopesEntry.getAccessToken(ctx)
}
func (c *scopesCacheEntry) getAccessToken(ctx context.Context) (string, error) {
var accessToken *AccessToken
var err error
shouldRefresh := false
c.cond.L.Lock()
for {
if c.accessToken != nil && c.accessToken.ExpiresOn.After(time.Now().Add(2*time.Minute)) {
// Use the cached token since it's available and not expired yet
accessToken = c.accessToken
break
}
if !c.refreshing {
// Start refreshing the token
c.refreshing = true
shouldRefresh = true
break
}
// Wait for the token to be refreshed
c.cond.Wait()
}
c.cond.L.Unlock()
if shouldRefresh {
accessToken, err = c.credential.GetAccessToken(ctx, c.scopes)
c.cond.L.Lock()
c.refreshing = false
c.accessToken = accessToken
c.cond.Broadcast()
c.cond.L.Unlock()
if err != nil {
return "", err
}
}
return accessToken.Token, nil
}
func getKeyForScopes(scopes []string) string {
if len(scopes) > 1 {
arr := make([]string, len(scopes))
copy(arr, scopes)
sort.Strings(arr)
scopes = arr
}
return strings.Join(scopes, " ")
}

View File

@ -0,0 +1,102 @@
package pluginproxy
import (
"context"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type fakeCredential struct {
key string
calledTimes int
getAccessTokenFunc func(ctx context.Context, scopes []string) (*AccessToken, error)
}
func (c *fakeCredential) GetCacheKey() string {
return c.key
}
func (c *fakeCredential) GetAccessToken(ctx context.Context, scopes []string) (*AccessToken, error) {
c.calledTimes = c.calledTimes + 1
if c.getAccessTokenFunc != nil {
return c.getAccessTokenFunc(ctx, scopes)
}
fakeAccessToken := &AccessToken{Token: fmt.Sprintf("%v-token-%v", c.key, c.calledTimes), ExpiresOn: timeNow().Add(time.Hour)}
return fakeAccessToken, nil
}
func TestConcurrentTokenCache_GetAccessToken(t *testing.T) {
ctx := context.Background()
scopes1 := []string{"Scope1"}
scopes2 := []string{"Scope2"}
t.Run("should request access token from credential", func(t *testing.T) {
cache := NewConcurrentTokenCache()
credential := &fakeCredential{key: "credential-1"}
token, err := cache.GetAccessToken(ctx, credential, scopes1)
require.NoError(t, err)
assert.Equal(t, "credential-1-token-1", token)
assert.Equal(t, 1, credential.calledTimes)
})
t.Run("should return cached token for same scopes", func(t *testing.T) {
var token1, token2 string
var err error
cache := NewConcurrentTokenCache()
credential := &fakeCredential{key: "credential-1"}
token1, err = cache.GetAccessToken(ctx, credential, scopes1)
require.NoError(t, err)
assert.Equal(t, "credential-1-token-1", token1)
token2, err = cache.GetAccessToken(ctx, credential, scopes2)
require.NoError(t, err)
assert.Equal(t, "credential-1-token-2", token2)
token1, err = cache.GetAccessToken(ctx, credential, scopes1)
require.NoError(t, err)
assert.Equal(t, "credential-1-token-1", token1)
token2, err = cache.GetAccessToken(ctx, credential, scopes2)
require.NoError(t, err)
assert.Equal(t, "credential-1-token-2", token2)
assert.Equal(t, 2, credential.calledTimes)
})
t.Run("should return cached token for same credentials", func(t *testing.T) {
var token1, token2 string
var err error
cache := NewConcurrentTokenCache()
credential1 := &fakeCredential{key: "credential-1"}
credential2 := &fakeCredential{key: "credential-2"}
token1, err = cache.GetAccessToken(ctx, credential1, scopes1)
require.NoError(t, err)
assert.Equal(t, "credential-1-token-1", token1)
token2, err = cache.GetAccessToken(ctx, credential2, scopes1)
require.NoError(t, err)
assert.Equal(t, "credential-2-token-1", token2)
token1, err = cache.GetAccessToken(ctx, credential1, scopes1)
require.NoError(t, err)
assert.Equal(t, "credential-1-token-1", token1)
token2, err = cache.GetAccessToken(ctx, credential2, scopes1)
require.NoError(t, err)
assert.Equal(t, "credential-2-token-1", token2)
assert.Equal(t, 1, credential1.calledTimes)
assert.Equal(t, 1, credential2.calledTimes)
})
}

View File

@ -0,0 +1,180 @@
package pluginproxy
import (
"context"
"crypto/sha256"
"errors"
"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 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) GetAccessToken(ctx context.Context, scopes []string) (*AccessToken, error) {
// No need to lock here because the caller is responsible for thread safety
if c.credential == nil {
var err error
c.credential, err = azidentity.NewManagedIdentityCredential(c.clientId, nil)
if err != nil {
return nil, err
}
}
// Implementation of ManagedIdentityCredential doesn't support scopes, converting to resource
if len(scopes) == 0 {
return nil, errors.New("scopes not provided")
}
resource := strings.TrimSuffix(scopes[0], "/.default")
scopes = []string{resource}
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) GetAccessToken(ctx context.Context, scopes []string) (*AccessToken, error) {
// No need to lock here because the caller is responsible for thread safety
if c.credential == nil {
var err error
c.credential, err = azidentity.NewClientSecretCredential(c.tenantId, c.clientId, c.clientSecret, nil)
if err != nil {
return nil, err
}
}
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))
}

View File

@ -0,0 +1,221 @@
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)
})
}

View File

@ -29,6 +29,7 @@ type ApplicationInsightsDatasource struct {
httpClient *http.Client
dsInfo *models.DataSource
pluginManager plugins.Manager
cfg *setting.Cfg
}
// ApplicationInsightsQuery is the model that holds the information
@ -243,7 +244,7 @@ func (e *ApplicationInsightsDatasource) createRequest(ctx context.Context, dsInf
req.Header.Set("User-Agent", fmt.Sprintf("Grafana/%s", setting.BuildVersion))
pluginproxy.ApplyRoute(ctx, req, proxyPass, appInsightsRoute, dsInfo)
pluginproxy.ApplyRoute(ctx, req, proxyPass, appInsightsRoute, dsInfo, e.cfg)
return req, nil
}

View File

@ -28,6 +28,7 @@ type AzureLogAnalyticsDatasource struct {
httpClient *http.Client
dsInfo *models.DataSource
pluginManager plugins.Manager
cfg *setting.Cfg
}
// AzureLogAnalyticsQuery is the query request that is built from the saved values for
@ -229,7 +230,7 @@ func (e *AzureLogAnalyticsDatasource) createRequest(ctx context.Context, dsInfo
if err != nil {
return nil, err
}
pluginproxy.ApplyRoute(ctx, req, proxypass, logAnalyticsRoute, dsInfo)
pluginproxy.ApplyRoute(ctx, req, proxypass, logAnalyticsRoute, dsInfo, e.cfg)
return req, nil
}

View File

@ -28,6 +28,7 @@ type AzureMonitorDatasource struct {
httpClient *http.Client
dsInfo *models.DataSource
pluginManager plugins.Manager
cfg *setting.Cfg
}
var (
@ -259,7 +260,7 @@ func (e *AzureMonitorDatasource) createRequest(ctx context.Context, dsInfo *mode
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", fmt.Sprintf("Grafana/%s", setting.BuildVersion))
pluginproxy.ApplyRoute(ctx, req, proxyPass, azureMonitorRoute, dsInfo)
pluginproxy.ApplyRoute(ctx, req, proxyPass, azureMonitorRoute, dsInfo, e.cfg)
return req, nil
}

View File

@ -10,6 +10,7 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/setting"
)
var (
@ -27,6 +28,7 @@ func init() {
type Service struct {
PluginManager plugins.Manager `inject:""`
Cfg *setting.Cfg `inject:""`
}
func (s *Service) Init() error {
@ -38,6 +40,7 @@ type AzureMonitorExecutor struct {
httpClient *http.Client
dsInfo *models.DataSource
pluginManager plugins.Manager
cfg *setting.Cfg
}
// NewAzureMonitorExecutor initializes a http client
@ -52,6 +55,7 @@ func (s *Service) NewExecutor(dsInfo *models.DataSource) (plugins.DataPlugin, er
httpClient: httpClient,
dsInfo: dsInfo,
pluginManager: s.PluginManager,
cfg: s.Cfg,
}, nil
}
@ -90,24 +94,28 @@ func (e *AzureMonitorExecutor) DataQuery(ctx context.Context, dsInfo *models.Dat
httpClient: e.httpClient,
dsInfo: e.dsInfo,
pluginManager: e.pluginManager,
cfg: e.cfg,
}
aiDatasource := &ApplicationInsightsDatasource{
httpClient: e.httpClient,
dsInfo: e.dsInfo,
pluginManager: e.pluginManager,
cfg: e.cfg,
}
alaDatasource := &AzureLogAnalyticsDatasource{
httpClient: e.httpClient,
dsInfo: e.dsInfo,
pluginManager: e.pluginManager,
cfg: e.cfg,
}
iaDatasource := &InsightsAnalyticsDatasource{
httpClient: e.httpClient,
dsInfo: e.dsInfo,
pluginManager: e.pluginManager,
cfg: e.cfg,
}
azResult, err := azDatasource.executeTimeSeriesQuery(ctx, azureMonitorQueries, *tsdbQuery.TimeRange)

View File

@ -25,6 +25,7 @@ type InsightsAnalyticsDatasource struct {
httpClient *http.Client
dsInfo *models.DataSource
pluginManager plugins.Manager
cfg *setting.Cfg
}
type InsightsAnalyticsQuery struct {
@ -217,7 +218,7 @@ func (e *InsightsAnalyticsDatasource) createRequest(ctx context.Context, dsInfo
req.Header.Set("User-Agent", fmt.Sprintf("Grafana/%s", setting.BuildVersion))
pluginproxy.ApplyRoute(ctx, req, proxyPass, appInsightsRoute, dsInfo)
pluginproxy.ApplyRoute(ctx, req, proxyPass, appInsightsRoute, dsInfo, e.cfg)
return req, nil
}

View File

@ -74,6 +74,7 @@ func init() {
type Service struct {
PluginManager plugins.Manager `inject:""`
Cfg *setting.Cfg `inject:""`
}
func (s *Service) Init() error {
@ -85,6 +86,7 @@ type Executor struct {
httpClient *http.Client
dsInfo *models.DataSource
pluginManager plugins.Manager
cfg *setting.Cfg
}
// NewExecutor returns an Executor.
@ -99,6 +101,7 @@ func (s *Service) NewExecutor(dsInfo *models.DataSource) (plugins.DataPlugin, er
httpClient: httpClient,
dsInfo: dsInfo,
pluginManager: s.PluginManager,
cfg: s.Cfg,
}, nil
}
@ -522,7 +525,7 @@ func (e *Executor) createRequest(ctx context.Context, dsInfo *models.DataSource,
}
}
pluginproxy.ApplyRoute(ctx, req, proxyPass, cloudMonitoringRoute, dsInfo)
pluginproxy.ApplyRoute(ctx, req, proxyPass, cloudMonitoringRoute, dsInfo, e.cfg)
return req, nil
}

View File

@ -33,13 +33,15 @@
"path": "azuremonitor",
"method": "GET",
"url": "https://management.azure.com",
"authType": "azure",
"tokenAuth": {
"url": "https://login.microsoftonline.com/{{.JsonData.tenantId}}/oauth2/token",
"scopes": ["https://management.azure.com/.default"],
"params": {
"grant_type": "client_credentials",
"azure_auth_type": "{{.JsonData.azureAuthType}}",
"azure_cloud": "AzureCloud",
"tenant_id": "{{.JsonData.tenantId}}",
"client_id": "{{.JsonData.clientId}}",
"client_secret": "{{.SecureJsonData.clientSecret}}",
"resource": "https://management.azure.com/"
"client_secret": "{{.SecureJsonData.clientSecret}}"
}
},
"headers": [{ "name": "x-ms-app", "content": "Grafana" }]
@ -48,13 +50,15 @@
"path": "govazuremonitor",
"method": "GET",
"url": "https://management.usgovcloudapi.net",
"authType": "azure",
"tokenAuth": {
"url": "https://login.microsoftonline.us/{{.JsonData.tenantId}}/oauth2/token",
"scopes": ["https://management.usgovcloudapi.net/.default"],
"params": {
"grant_type": "client_credentials",
"azure_auth_type": "{{.JsonData.azureAuthType}}",
"azure_cloud": "AzureUSGovernment",
"tenant_id": "{{.JsonData.tenantId}}",
"client_id": "{{.JsonData.clientId}}",
"client_secret": "{{.SecureJsonData.clientSecret}}",
"resource": "https://management.usgovcloudapi.net/"
"client_secret": "{{.SecureJsonData.clientSecret}}"
}
},
"headers": [{ "name": "x-ms-app", "content": "Grafana" }]
@ -63,13 +67,15 @@
"path": "germanyazuremonitor",
"method": "GET",
"url": "https://management.microsoftazure.de",
"authType": "azure",
"tokenAuth": {
"url": "https://login.microsoftonline.de/{{.JsonData.tenantId}}/oauth2/token",
"scopes": ["https://management.microsoftazure.de/.default"],
"params": {
"grant_type": "client_credentials",
"azure_auth_type": "{{.JsonData.azureAuthType}}",
"azure_cloud": "AzureGermanCloud",
"tenant_id": "{{.JsonData.tenantId}}",
"client_id": "{{.JsonData.clientId}}",
"client_secret": "{{.SecureJsonData.clientSecret}}",
"resource": "https://management.microsoftazure.de/"
"client_secret": "{{.SecureJsonData.clientSecret}}"
}
},
"headers": [{ "name": "x-ms-app", "content": "Grafana" }]
@ -78,13 +84,15 @@
"path": "chinaazuremonitor",
"method": "GET",
"url": "https://management.chinacloudapi.cn",
"authType": "azure",
"tokenAuth": {
"url": "https://login.chinacloudapi.cn/{{.JsonData.tenantId}}/oauth2/token",
"scopes": ["https://management.chinacloudapi.cn/.default"],
"params": {
"grant_type": "client_credentials",
"azure_auth_type": "{{.JsonData.azureAuthType}}",
"azure_cloud": "AzureChinaCloud",
"tenant_id": "{{.JsonData.tenantId}}",
"client_id": "{{.JsonData.clientId}}",
"client_secret": "{{.SecureJsonData.clientSecret}}",
"resource": "https://management.chinacloudapi.cn/"
"client_secret": "{{.SecureJsonData.clientSecret}}"
}
},
"headers": [{ "name": "x-ms-app", "content": "Grafana" }]
@ -111,13 +119,15 @@
"path": "workspacesloganalytics",
"method": "GET",
"url": "https://management.azure.com",
"authType": "azure",
"tokenAuth": {
"url": "https://login.microsoftonline.com/{{.JsonData.logAnalyticsTenantId}}/oauth2/token",
"scopes": ["https://management.azure.com/.default"],
"params": {
"grant_type": "client_credentials",
"azure_auth_type": "{{.JsonData.azureAuthType}}",
"azure_cloud": "AzureCloud",
"tenant_id": "{{.JsonData.logAnalyticsTenantId}}",
"client_id": "{{.JsonData.logAnalyticsClientId}}",
"client_secret": "{{.SecureJsonData.logAnalyticsClientSecret}}",
"resource": "https://management.azure.com/"
"client_secret": "{{.SecureJsonData.logAnalyticsClientSecret}}"
}
},
"headers": [{ "name": "x-ms-app", "content": "Grafana" }]
@ -126,13 +136,15 @@
"path": "chinaworkspacesloganalytics",
"method": "GET",
"url": "https://management.chinacloudapi.cn",
"authType": "azure",
"tokenAuth": {
"url": "https://login.chinacloudapi.cn/{{.JsonData.logAnalyticsTenantId}}/oauth2/token",
"scopes": ["https://management.chinacloudapi.cn/.default"],
"params": {
"grant_type": "client_credentials",
"azure_auth_type": "{{.JsonData.azureAuthType}}",
"azure_cloud": "AzureChinaCloud",
"tenant_id": "{{.JsonData.logAnalyticsTenantId}}",
"client_id": "{{.JsonData.logAnalyticsClientId}}",
"client_secret": "{{.SecureJsonData.logAnalyticsClientSecret}}",
"resource": "https://management.chinacloudapi.cn/"
"client_secret": "{{.SecureJsonData.logAnalyticsClientSecret}}"
}
},
"headers": [{ "name": "x-ms-app", "content": "Grafana" }]
@ -141,13 +153,15 @@
"path": "govworkspacesloganalytics",
"method": "GET",
"url": "https://management.usgovcloudapi.net",
"authType": "azure",
"tokenAuth": {
"url": "https://login.microsoftonline.us/{{.JsonData.logAnalyticsTenantId}}/oauth2/token",
"scopes": ["https://management.usgovcloudapi.net/.default"],
"params": {
"grant_type": "client_credentials",
"azure_auth_type": "{{.JsonData.azureAuthType}}",
"azure_cloud": "AzureUSGovernment",
"tenant_id": "{{.JsonData.logAnalyticsTenantId}}",
"client_id": "{{.JsonData.logAnalyticsClientId}}",
"client_secret": "{{.SecureJsonData.logAnalyticsClientSecret}}",
"resource": "https://management.usgovcloudapi.net/"
"client_secret": "{{.SecureJsonData.logAnalyticsClientSecret}}"
}
},
"headers": [{ "name": "x-ms-app", "content": "Grafana" }]
@ -156,13 +170,15 @@
"path": "loganalyticsazure",
"method": "GET",
"url": "https://api.loganalytics.io/v1/workspaces",
"authType": "azure",
"tokenAuth": {
"url": "https://login.microsoftonline.com/{{.JsonData.logAnalyticsTenantId}}/oauth2/token",
"scopes": ["https://api.loganalytics.io/.default"],
"params": {
"grant_type": "client_credentials",
"azure_auth_type": "{{.JsonData.azureAuthType}}",
"azure_cloud": "AzureCloud",
"tenant_id": "{{.JsonData.logAnalyticsTenantId}}",
"client_id": "{{.JsonData.logAnalyticsClientId}}",
"client_secret": "{{.SecureJsonData.logAnalyticsClientSecret}}",
"resource": "https://api.loganalytics.io"
"client_secret": "{{.SecureJsonData.logAnalyticsClientSecret}}"
}
},
"headers": [
@ -174,13 +190,15 @@
"path": "chinaloganalyticsazure",
"method": "GET",
"url": "https://api.loganalytics.azure.cn/v1/workspaces",
"authType": "azure",
"tokenAuth": {
"url": "https://login.chinacloudapi.cn/{{.JsonData.logAnalyticsTenantId}}/oauth2/token",
"scopes": ["https://api.loganalytics.azure.cn/.default"],
"params": {
"grant_type": "client_credentials",
"azure_auth_type": "{{.JsonData.azureAuthType}}",
"azure_cloud": "AzureChinaCloud",
"tenant_id": "{{.JsonData.logAnalyticsTenantId}}",
"client_id": "{{.JsonData.logAnalyticsClientId}}",
"client_secret": "{{.SecureJsonData.logAnalyticsClientSecret}}",
"resource": "https://api.loganalytics.azure.cn"
"client_secret": "{{.SecureJsonData.logAnalyticsClientSecret}}"
}
},
"headers": [
@ -192,13 +210,15 @@
"path": "govloganalyticsazure",
"method": "GET",
"url": "https://api.loganalytics.us/v1/workspaces",
"authType": "azure",
"tokenAuth": {
"url": "https://login.microsoftonline.us/{{.JsonData.logAnalyticsTenantId}}/oauth2/token",
"scopes": ["https://api.loganalytics.us/.default"],
"params": {
"grant_type": "client_credentials",
"azure_auth_type": "{{.JsonData.azureAuthType}}",
"azure_cloud": "AzureUSGovernment",
"tenant_id": "{{.JsonData.logAnalyticsTenantId}}",
"client_id": "{{.JsonData.logAnalyticsClientId}}",
"client_secret": "{{.SecureJsonData.logAnalyticsClientSecret}}",
"resource": "https://api.loganalytics.us"
"client_secret": "{{.SecureJsonData.logAnalyticsClientSecret}}"
}
},
"headers": [