From 97df4a57f483c09b61bf7859bffa8d3f10760870 Mon Sep 17 00:00:00 2001 From: Isabella Siu Date: Thu, 21 Oct 2021 16:29:56 -0400 Subject: [PATCH] GoogleCloudMonitoring: use grafana-google-sdk-go for auth (#40490) --- go.mod | 1 + go.sum | 2 + pkg/api/pluginproxy/ds_auth_provider.go | 3 +- pkg/api/pluginproxy/token_provider_gce.go | 38 +++----- pkg/api/pluginproxy/token_provider_jwt.go | 105 ++++++--------------- pkg/api/pluginproxy/token_provider_test.go | 94 ------------------ 6 files changed, 45 insertions(+), 198 deletions(-) diff --git a/go.mod b/go.mod index 2bf2fdb90f6..f198c5df6a5 100644 --- a/go.mod +++ b/go.mod @@ -186,6 +186,7 @@ require ( github.com/gopherjs/gopherjs v0.0.0-20191106031601-ce3c9ade29de // indirect github.com/gorilla/mux v1.8.0 // indirect github.com/grafana/dskit v0.0.0-20211011144203-3a88ec0b675f // indirect + github.com/grafana/grafana-google-sdk-go v0.0.0-20211019132340-3ff525a010d5 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-immutable-radix v1.3.0 // indirect diff --git a/go.sum b/go.sum index 1d9f5ec86b5..3313e034611 100644 --- a/go.sum +++ b/go.sum @@ -1204,6 +1204,8 @@ github.com/grafana/go-mssqldb v0.0.0-20210326084033-d0ce3c521036 h1:GplhUk6Xes5J github.com/grafana/go-mssqldb v0.0.0-20210326084033-d0ce3c521036/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/grafana/grafana-aws-sdk v0.7.0 h1:D+Lhxi3P/7vpyDHUK/fdX9bL2mRz8hLG04ucNf1E02o= github.com/grafana/grafana-aws-sdk v0.7.0/go.mod h1:+pPo5U+pX0zWimR7YBc7ASeSQfbRkcTyQYqMiAj7G5U= +github.com/grafana/grafana-google-sdk-go v0.0.0-20211019132340-3ff525a010d5 h1:o7w/t0nLNfkERMdj09U0h3Fl63z8ws1CxwiImeUKLIk= +github.com/grafana/grafana-google-sdk-go v0.0.0-20211019132340-3ff525a010d5/go.mod h1:Vo2TKWfDVmNTELBUM+3lkrZvFtBws0qSZdXhQxRdJrE= github.com/grafana/grafana-plugin-sdk-go v0.79.0/go.mod h1:NvxLzGkVhnoBKwzkst6CFfpMFKwAdIUZ1q8ssuLeF60= github.com/grafana/grafana-plugin-sdk-go v0.114.0 h1:9I55IXw7mOT71tZ/pdqCaWGz8vxfz31CXjaDtBV9ZBo= github.com/grafana/grafana-plugin-sdk-go v0.114.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk= diff --git a/pkg/api/pluginproxy/ds_auth_provider.go b/pkg/api/pluginproxy/ds_auth_provider.go index cd41a815c0b..ff1dfce2ade 100644 --- a/pkg/api/pluginproxy/ds_auth_provider.go +++ b/pkg/api/pluginproxy/ds_auth_provider.go @@ -105,8 +105,7 @@ func getTokenProvider(ctx context.Context, cfg *setting.Cfg, ds DSInfo, pluginRo if jwtTokenAuth == nil { return nil, fmt.Errorf("'jwtTokenAuth' not configured for authentication type '%s'", authType) } - provider := newGceAccessTokenProvider(ctx, ds, pluginRoute, jwtTokenAuth) - return provider, nil + return newGceAccessTokenProvider(ctx, ds, pluginRoute, jwtTokenAuth), nil case "jwt": if jwtTokenAuth == nil { diff --git a/pkg/api/pluginproxy/token_provider_gce.go b/pkg/api/pluginproxy/token_provider_gce.go index b1f7909c5da..2444b53634e 100644 --- a/pkg/api/pluginproxy/token_provider_gce.go +++ b/pkg/api/pluginproxy/token_provider_gce.go @@ -2,43 +2,31 @@ package pluginproxy import ( "context" - "time" + googletokenprovider "github.com/grafana/grafana-google-sdk-go/pkg/tokenprovider" "github.com/grafana/grafana/pkg/plugins" - "golang.org/x/oauth2/google" ) type gceAccessTokenProvider struct { - datasourceId int64 - datasourceUpdated time.Time - ctx context.Context - route *plugins.AppPluginRoute - authParams *plugins.JwtTokenAuth + source googletokenprovider.TokenProvider + ctx context.Context } func newGceAccessTokenProvider(ctx context.Context, ds DSInfo, pluginRoute *plugins.AppPluginRoute, authParams *plugins.JwtTokenAuth) *gceAccessTokenProvider { + cfg := googletokenprovider.Config{ + RoutePath: pluginRoute.Path, + RouteMethod: pluginRoute.Method, + DataSourceID: ds.ID, + DataSourceUpdated: ds.Updated, + Scopes: authParams.Scopes, + } return &gceAccessTokenProvider{ - datasourceId: ds.ID, - datasourceUpdated: ds.Updated, - ctx: ctx, - route: pluginRoute, - authParams: authParams, + source: googletokenprovider.NewGceAccessTokenProvider(cfg), + ctx: ctx, } } 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) - return "", err - } else { - token, err := tokenSrc.Token() - if err != nil { - logger.Error("Failed to get default access token from meta data server", "error", err) - return "", err - } else { - return token.AccessToken, nil - } - } + return provider.source.GetAccessToken(provider.ctx) } diff --git a/pkg/api/pluginproxy/token_provider_jwt.go b/pkg/api/pluginproxy/token_provider_jwt.go index 27ee7e97af0..97a7ca0b55e 100644 --- a/pkg/api/pluginproxy/token_provider_jwt.go +++ b/pkg/api/pluginproxy/token_provider_jwt.go @@ -2,95 +2,46 @@ package pluginproxy import ( "context" - "fmt" - "sync" - "time" + googletokenprovider "github.com/grafana/grafana-google-sdk-go/pkg/tokenprovider" "github.com/grafana/grafana/pkg/plugins" - "golang.org/x/oauth2" - "golang.org/x/oauth2/jwt" ) -var ( - oauthJwtTokenCache = oauthJwtTokenCacheType{ - cache: map[string]*oauth2.Token{}, - } -) - -type oauthJwtTokenCacheType struct { - cache map[string]*oauth2.Token - sync.Mutex -} - type jwtAccessTokenProvider struct { - datasourceId int64 - datasourceUpdated time.Time - ctx context.Context - route *plugins.AppPluginRoute - authParams *plugins.JwtTokenAuth + source googletokenprovider.TokenProvider + ctx context.Context } func newJwtAccessTokenProvider(ctx context.Context, ds DSInfo, pluginRoute *plugins.AppPluginRoute, authParams *plugins.JwtTokenAuth) *jwtAccessTokenProvider { + jwtConf := &googletokenprovider.JwtTokenConfig{} + if val, ok := authParams.Params["client_email"]; ok { + jwtConf.Email = val + } + + if val, ok := authParams.Params["private_key"]; ok { + jwtConf.PrivateKey = []byte(val) + } + + if val, ok := authParams.Params["token_uri"]; ok { + jwtConf.URI = val + } + + cfg := googletokenprovider.Config{ + RoutePath: pluginRoute.Path, + RouteMethod: pluginRoute.Method, + DataSourceID: ds.ID, + DataSourceUpdated: ds.Updated, + Scopes: authParams.Scopes, + JwtTokenConfig: jwtConf, + } + return &jwtAccessTokenProvider{ - datasourceId: ds.ID, - datasourceUpdated: ds.Updated, - ctx: ctx, - route: pluginRoute, - authParams: authParams, + source: googletokenprovider.NewJwtAccessTokenProvider(cfg), + ctx: ctx, } } func (provider *jwtAccessTokenProvider) GetAccessToken() (string, error) { - oauthJwtTokenCache.Lock() - defer oauthJwtTokenCache.Unlock() - if cachedToken, found := oauthJwtTokenCache.cache[provider.getAccessTokenCacheKey()]; found { - if cachedToken.Expiry.After(timeNow().Add(time.Second * 10)) { - logger.Debug("Using token from cache") - return cachedToken.AccessToken, nil - } - } - - conf := &jwt.Config{} - - if val, ok := provider.authParams.Params["client_email"]; ok { - conf.Email = val - } - - if val, ok := provider.authParams.Params["private_key"]; ok { - conf.PrivateKey = []byte(val) - } - - if val, ok := provider.authParams.Params["token_uri"]; ok { - conf.TokenURL = val - } - - conf.Scopes = provider.authParams.Scopes - - token, err := getTokenSource(conf, provider.ctx) - if err != nil { - return "", err - } - - oauthJwtTokenCache.cache[provider.getAccessTokenCacheKey()] = token - - logger.Info("Got new access token", "ExpiresOn", token.Expiry) - - return token.AccessToken, nil -} - -// getTokenSource gets a token source. -// Stubbable by tests. -var getTokenSource = func(conf *jwt.Config, ctx context.Context) (*oauth2.Token, error) { - tokenSrc := conf.TokenSource(ctx) - token, err := tokenSrc.Token() - if err != nil { - return nil, err - } - - return token, nil -} - -func (provider *jwtAccessTokenProvider) getAccessTokenCacheKey() string { - return fmt.Sprintf("%v_%v_%v_%v", provider.datasourceId, provider.datasourceUpdated.Unix(), provider.route.Path, provider.route.Method) + return provider.source.GetAccessToken(provider.ctx) } diff --git a/pkg/api/pluginproxy/token_provider_test.go b/pkg/api/pluginproxy/token_provider_test.go index d2a89a89ace..baf3425bfe4 100644 --- a/pkg/api/pluginproxy/token_provider_test.go +++ b/pkg/api/pluginproxy/token_provider_test.go @@ -1,7 +1,6 @@ package pluginproxy import ( - "context" "encoding/json" "net/http" "net/http/httptest" @@ -13,105 +12,12 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/plugins" - "golang.org/x/oauth2" - "golang.org/x/oauth2/jwt" ) var ( token map[string]interface{} ) -func TestAccessToken_pluginWithJWTTokenAuthRoute(t *testing.T) { - pluginRoute := &plugins.AppPluginRoute{ - Path: "pathwithjwttoken1", - URL: "https://api.jwt.io/some/path", - Method: "GET", - JwtTokenAuth: &plugins.JwtTokenAuth{ - Url: "https://login.server.com/{{.JsonData.tenantId}}/oauth2/token", - Scopes: []string{ - "https://www.testapi.com/auth/monitoring.read", - "https://www.testapi.com/auth/cloudplatformprojects.readonly", - }, - Params: map[string]string{ - "token_uri": "{{.JsonData.tokenUri}}", - "client_email": "{{.JsonData.clientEmail}}", - "private_key": "{{.SecureJsonData.privateKey}}", - }, - }, - } - - authParams := &plugins.JwtTokenAuth{ - Url: "https://login.server.com/{{.JsonData.tenantId}}/oauth2/token", - Scopes: []string{ - "https://www.testapi.com/auth/monitoring.read", - "https://www.testapi.com/auth/cloudplatformprojects.readonly", - }, - Params: map[string]string{ - "token_uri": "login.url.com/token", - "client_email": "test@test.com", - "private_key": "testkey", - }, - } - - setUp := func(t *testing.T, fn func(*jwt.Config, context.Context) (*oauth2.Token, error)) { - origFn := getTokenSource - t.Cleanup(func() { - getTokenSource = origFn - }) - - getTokenSource = fn - } - - ds := DSInfo{ID: 1, Updated: time.Now()} - - t.Run("should fetch token using JWT private key", func(t *testing.T) { - setUp(t, func(conf *jwt.Config, ctx context.Context) (*oauth2.Token, error) { - return &oauth2.Token{AccessToken: "abc"}, nil - }) - provider := newJwtAccessTokenProvider(context.Background(), ds, pluginRoute, authParams) - token, err := provider.GetAccessToken() - require.NoError(t, err) - - assert.Equal(t, "abc", token) - }) - - t.Run("should set JWT config values", func(t *testing.T) { - setUp(t, func(conf *jwt.Config, ctx context.Context) (*oauth2.Token, error) { - assert.Equal(t, "test@test.com", conf.Email) - assert.Equal(t, []byte("testkey"), conf.PrivateKey) - assert.Equal(t, 2, len(conf.Scopes)) - assert.Equal(t, "https://www.testapi.com/auth/monitoring.read", conf.Scopes[0]) - assert.Equal(t, "https://www.testapi.com/auth/cloudplatformprojects.readonly", conf.Scopes[1]) - assert.Equal(t, "login.url.com/token", conf.TokenURL) - - return &oauth2.Token{AccessToken: "abc"}, nil - }) - - provider := newJwtAccessTokenProvider(context.Background(), ds, pluginRoute, authParams) - _, err := provider.GetAccessToken() - require.NoError(t, err) - }) - - t.Run("should use cached token on second call", func(t *testing.T) { - setUp(t, func(conf *jwt.Config, ctx context.Context) (*oauth2.Token, error) { - return &oauth2.Token{ - AccessToken: "abc", - Expiry: time.Now().Add(1 * time.Minute)}, nil - }) - provider := newJwtAccessTokenProvider(context.Background(), ds, pluginRoute, authParams) - 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() - require.NoError(t, err) - assert.Equal(t, "abc", token2) - }) -} - func TestAccessToken_pluginWithTokenAuthRoute(t *testing.T) { apiHandler := http.NewServeMux() server := httptest.NewServer(apiHandler)