From 1790737cf1e96dc2cde7c7d590c93855886aa348 Mon Sep 17 00:00:00 2001 From: Sergey Kostrukov Date: Thu, 6 May 2021 13:05:23 -0700 Subject: [PATCH] Plugins: AuthType in route configuration and params interpolation (#33674) * AuthType in route configuration * Pass interpolated auth parameters to token provider * Unit tests * Update after review Co-authored-by: Marcus Efraimsson Fixes #33669 Closed #33732 --- pkg/api/pluginproxy/ds_auth_provider.go | 95 +++++++++++++++---- pkg/api/pluginproxy/ds_auth_provider_test.go | 62 ++++++++++++ pkg/api/pluginproxy/token_provider_gce.go | 7 +- pkg/api/pluginproxy/token_provider_generic.go | 21 ++-- pkg/api/pluginproxy/token_provider_jwt.go | 32 ++----- pkg/api/pluginproxy/token_provider_test.go | 40 ++++---- pkg/plugins/app_plugin.go | 1 + 7 files changed, 185 insertions(+), 73 deletions(-) create mode 100644 pkg/api/pluginproxy/ds_auth_provider_test.go diff --git a/pkg/api/pluginproxy/ds_auth_provider.go b/pkg/api/pluginproxy/ds_auth_provider.go index 45ee9f3baaa..03119262b4b 100644 --- a/pkg/api/pluginproxy/ds_auth_provider.go +++ b/pkg/api/pluginproxy/ds_auth_provider.go @@ -53,7 +53,9 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route logger.Error("Failed to set plugin route body content", "error", err) } - if tokenProvider := getTokenProvider(ctx, ds, route, data); tokenProvider != nil { + if tokenProvider, err := getTokenProvider(ctx, 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 { logger.Error("Failed to get access token", "error", err) } else { @@ -65,25 +67,80 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route } func getTokenProvider(ctx context.Context, ds *models.DataSource, pluginRoute *plugins.AppPluginRoute, - data templateData) accessTokenProvider { - authenticationType := ds.JsonData.Get("authenticationType").MustString() + data templateData) (accessTokenProvider, error) { + authType := pluginRoute.AuthType - switch authenticationType { - case "gce": - return newGceAccessTokenProvider(ctx, ds, pluginRoute) - case "jwt": - if pluginRoute.JwtTokenAuth != nil { - return newJwtAccessTokenProvider(ctx, ds, pluginRoute, data) - } - default: - // Fallback to authentication options when authentication type isn't explicitly configured - if pluginRoute.TokenAuth != nil { - return newGenericAccessTokenProvider(ds, pluginRoute, data) - } - if pluginRoute.JwtTokenAuth != nil { - return newJwtAccessTokenProvider(ctx, ds, pluginRoute, data) - } + // Plugin can override authentication type specified in route configuration + if authTypeOverride := ds.JsonData.Get("authenticationType").MustString(); authTypeOverride != "" { + authType = authTypeOverride } - return nil + tokenAuth, err := interpolateAuthParams(pluginRoute.TokenAuth, data) + if err != nil { + return nil, err + } + jwtTokenAuth, err := interpolateAuthParams(pluginRoute.JwtTokenAuth, data) + if err != nil { + return nil, err + } + + switch authType { + case "gce": + if jwtTokenAuth == nil { + return nil, fmt.Errorf("'jwtTokenAuth' not configured for authentication type '%s'", authType) + } + provider := newGceAccessTokenProvider(ctx, ds, pluginRoute, jwtTokenAuth) + return provider, nil + + case "jwt": + if jwtTokenAuth == nil { + return nil, fmt.Errorf("'jwtTokenAuth' not configured for authentication type '%s'", authType) + } + provider := newJwtAccessTokenProvider(ctx, ds, pluginRoute, jwtTokenAuth) + return provider, nil + + case "": + // Fallback to authentication methods when authentication type isn't explicitly configured + if tokenAuth != nil { + provider := newGenericAccessTokenProvider(ds, pluginRoute, tokenAuth) + return provider, nil + } + if jwtTokenAuth != nil { + provider := newJwtAccessTokenProvider(ctx, ds, pluginRoute, jwtTokenAuth) + return provider, nil + } + + // No authentication + return nil, nil + + default: + return nil, fmt.Errorf("authentication type '%s' not supported", authType) + } +} + +func interpolateAuthParams(tokenAuth *plugins.JwtTokenAuth, data templateData) (*plugins.JwtTokenAuth, error) { + if tokenAuth == nil { + // Nothing to interpolate + return nil, nil + } + + interpolatedUrl, err := interpolateString(tokenAuth.Url, data) + if err != nil { + return nil, err + } + + interpolatedParams := make(map[string]string) + for key, value := range tokenAuth.Params { + interpolatedParam, err := interpolateString(value, data) + if err != nil { + return nil, err + } + interpolatedParams[key] = interpolatedParam + } + + return &plugins.JwtTokenAuth{ + Url: interpolatedUrl, + Scopes: tokenAuth.Scopes, + Params: interpolatedParams, + }, nil } diff --git a/pkg/api/pluginproxy/ds_auth_provider_test.go b/pkg/api/pluginproxy/ds_auth_provider_test.go new file mode 100644 index 00000000000..e0f2b0f08cd --- /dev/null +++ b/pkg/api/pluginproxy/ds_auth_provider_test.go @@ -0,0 +1,62 @@ +package pluginproxy + +import ( + "testing" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestApplyRoute_interpolateAuthParams(t *testing.T) { + pluginRoute := &plugins.AppPluginRoute{ + Path: "pathwithjwttoken1", + URL: "https://api.jwt.io/some/path", + Method: "GET", + TokenAuth: &plugins.JwtTokenAuth{ + Url: "https://login.server.com/{{.JsonData.tenantId}}/oauth2/token", + Scopes: []string{ + "https://www.testapi.com/auth/Read.All", + "https://www.testapi.com/auth/Write.All", + }, + Params: map[string]string{ + "token_uri": "{{.JsonData.tokenUri}}", + "client_email": "{{.JsonData.clientEmail}}", + "private_key": "{{.SecureJsonData.privateKey}}", + }, + }, + } + + templateData := templateData{ + JsonData: map[string]interface{}{ + "clientEmail": "test@test.com", + "tokenUri": "login.url.com/token", + "tenantId": "f09c86ac", + }, + SecureJsonData: map[string]string{ + "privateKey": "testkey", + }, + } + + t.Run("should interpolate JwtTokenAuth struct using given JsonData", func(t *testing.T) { + interpolated, err := interpolateAuthParams(pluginRoute.TokenAuth, templateData) + require.NoError(t, err) + require.NotNil(t, interpolated) + + assert.Equal(t, "https://login.server.com/f09c86ac/oauth2/token", interpolated.Url) + + assert.Equal(t, 2, len(interpolated.Scopes)) + assert.Equal(t, "https://www.testapi.com/auth/Read.All", interpolated.Scopes[0]) + assert.Equal(t, "https://www.testapi.com/auth/Write.All", interpolated.Scopes[1]) + + assert.Equal(t, "login.url.com/token", interpolated.Params["token_uri"]) + assert.Equal(t, "test@test.com", interpolated.Params["client_email"]) + assert.Equal(t, "testkey", interpolated.Params["private_key"]) + }) + + t.Run("should return Nil if given JwtTokenAuth is Nil", func(t *testing.T) { + interpolated, err := interpolateAuthParams(pluginRoute.JwtTokenAuth, templateData) + require.NoError(t, err) + require.Nil(t, interpolated) + }) +} diff --git a/pkg/api/pluginproxy/token_provider_gce.go b/pkg/api/pluginproxy/token_provider_gce.go index 7f8b01e8c06..41b122b76d9 100644 --- a/pkg/api/pluginproxy/token_provider_gce.go +++ b/pkg/api/pluginproxy/token_provider_gce.go @@ -13,19 +13,22 @@ type gceAccessTokenProvider struct { datasourceVersion int ctx context.Context route *plugins.AppPluginRoute + authParams *plugins.JwtTokenAuth } -func newGceAccessTokenProvider(ctx context.Context, ds *models.DataSource, pluginRoute *plugins.AppPluginRoute) *gceAccessTokenProvider { +func newGceAccessTokenProvider(ctx context.Context, ds *models.DataSource, pluginRoute *plugins.AppPluginRoute, + authParams *plugins.JwtTokenAuth) *gceAccessTokenProvider { return &gceAccessTokenProvider{ datasourceId: ds.Id, datasourceVersion: ds.Version, ctx: ctx, route: pluginRoute, + authParams: authParams, } } func (provider *gceAccessTokenProvider) getAccessToken() (string, error) { - tokenSrc, err := google.DefaultTokenSource(provider.ctx, provider.route.JwtTokenAuth.Scopes...) + 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 diff --git a/pkg/api/pluginproxy/token_provider_generic.go b/pkg/api/pluginproxy/token_provider_generic.go index ef234de844b..7e08260556d 100644 --- a/pkg/api/pluginproxy/token_provider_generic.go +++ b/pkg/api/pluginproxy/token_provider_generic.go @@ -29,7 +29,7 @@ type genericAccessTokenProvider struct { datasourceId int64 datasourceVersion int route *plugins.AppPluginRoute - data templateData + authParams *plugins.JwtTokenAuth } type jwtToken struct { @@ -69,12 +69,12 @@ func (token *jwtToken) UnmarshalJSON(b []byte) error { } func newGenericAccessTokenProvider(ds *models.DataSource, pluginRoute *plugins.AppPluginRoute, - data templateData) *genericAccessTokenProvider { + authParams *plugins.JwtTokenAuth) *genericAccessTokenProvider { return &genericAccessTokenProvider{ datasourceId: ds.Id, datasourceVersion: ds.Version, route: pluginRoute, - data: data, + authParams: authParams, } } @@ -88,21 +88,14 @@ func (provider *genericAccessTokenProvider) getAccessToken() (string, error) { } } - urlInterpolated, err := interpolateString(provider.route.TokenAuth.Url, provider.data) - if err != nil { - return "", err - } + tokenUrl := provider.authParams.Url params := make(url.Values) - for key, value := range provider.route.TokenAuth.Params { - interpolatedParam, err := interpolateString(value, provider.data) - if err != nil { - return "", err - } - params.Add(key, interpolatedParam) + for key, value := range provider.authParams.Params { + params.Add(key, value) } - getTokenReq, err := http.NewRequest("POST", urlInterpolated, bytes.NewBufferString(params.Encode())) + getTokenReq, err := http.NewRequest("POST", tokenUrl, bytes.NewBufferString(params.Encode())) if err != nil { return "", err } diff --git a/pkg/api/pluginproxy/token_provider_jwt.go b/pkg/api/pluginproxy/token_provider_jwt.go index 69c07154e44..33f240f51bd 100644 --- a/pkg/api/pluginproxy/token_provider_jwt.go +++ b/pkg/api/pluginproxy/token_provider_jwt.go @@ -28,17 +28,17 @@ type jwtAccessTokenProvider struct { datasourceVersion int ctx context.Context route *plugins.AppPluginRoute - data templateData + authParams *plugins.JwtTokenAuth } func newJwtAccessTokenProvider(ctx context.Context, ds *models.DataSource, pluginRoute *plugins.AppPluginRoute, - data templateData) *jwtAccessTokenProvider { + authParams *plugins.JwtTokenAuth) *jwtAccessTokenProvider { return &jwtAccessTokenProvider{ datasourceId: ds.Id, datasourceVersion: ds.Version, ctx: ctx, route: pluginRoute, - data: data, + authParams: authParams, } } @@ -54,31 +54,19 @@ func (provider *jwtAccessTokenProvider) getAccessToken() (string, error) { conf := &jwt.Config{} - if val, ok := provider.route.JwtTokenAuth.Params["client_email"]; ok { - interpolatedVal, err := interpolateString(val, provider.data) - if err != nil { - return "", err - } - conf.Email = interpolatedVal + if val, ok := provider.authParams.Params["client_email"]; ok { + conf.Email = val } - if val, ok := provider.route.JwtTokenAuth.Params["private_key"]; ok { - interpolatedVal, err := interpolateString(val, provider.data) - if err != nil { - return "", err - } - conf.PrivateKey = []byte(interpolatedVal) + if val, ok := provider.authParams.Params["private_key"]; ok { + conf.PrivateKey = []byte(val) } - if val, ok := provider.route.JwtTokenAuth.Params["token_uri"]; ok { - interpolatedVal, err := interpolateString(val, provider.data) - if err != nil { - return "", err - } - conf.TokenURL = interpolatedVal + if val, ok := provider.authParams.Params["token_uri"]; ok { + conf.TokenURL = val } - conf.Scopes = provider.route.JwtTokenAuth.Scopes + conf.Scopes = provider.authParams.Scopes token, err := getTokenSource(conf, provider.ctx) if err != nil { diff --git a/pkg/api/pluginproxy/token_provider_test.go b/pkg/api/pluginproxy/token_provider_test.go index 06a01808ded..acbfa7d85a6 100644 --- a/pkg/api/pluginproxy/token_provider_test.go +++ b/pkg/api/pluginproxy/token_provider_test.go @@ -41,13 +41,16 @@ func TestAccessToken_pluginWithJWTTokenAuthRoute(t *testing.T) { }, } - templateData := templateData{ - JsonData: map[string]interface{}{ - "clientEmail": "test@test.com", - "tokenUri": "login.url.com/token", + 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", }, - SecureJsonData: map[string]string{ - "privateKey": "testkey", + Params: map[string]string{ + "token_uri": "login.url.com/token", + "client_email": "test@test.com", + "private_key": "testkey", }, } @@ -66,7 +69,7 @@ func TestAccessToken_pluginWithJWTTokenAuthRoute(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, templateData) + provider := newJwtAccessTokenProvider(context.Background(), ds, pluginRoute, authParams) token, err := provider.getAccessToken() require.NoError(t, err) @@ -85,7 +88,7 @@ func TestAccessToken_pluginWithJWTTokenAuthRoute(t *testing.T) { return &oauth2.Token{AccessToken: "abc"}, nil }) - provider := newJwtAccessTokenProvider(context.Background(), ds, pluginRoute, templateData) + provider := newJwtAccessTokenProvider(context.Background(), ds, pluginRoute, authParams) _, err := provider.getAccessToken() require.NoError(t, err) }) @@ -96,7 +99,7 @@ func TestAccessToken_pluginWithJWTTokenAuthRoute(t *testing.T) { AccessToken: "abc", Expiry: time.Now().Add(1 * time.Minute)}, nil }) - provider := newJwtAccessTokenProvider(context.Background(), ds, pluginRoute, templateData) + provider := newJwtAccessTokenProvider(context.Background(), ds, pluginRoute, authParams) token1, err := provider.getAccessToken() require.NoError(t, err) assert.Equal(t, "abc", token1) @@ -135,13 +138,18 @@ func TestAccessToken_pluginWithTokenAuthRoute(t *testing.T) { }, } - templateData := templateData{ - JsonData: map[string]interface{}{ - "client_id": "my_client_id", - "audience": "www.example.com", + authParams := &plugins.JwtTokenAuth{ + Url: server.URL + "/oauth/token", + Scopes: []string{ + "https://www.testapi.com/auth/monitoring.read", + "https://www.testapi.com/auth/cloudplatformprojects.readonly", }, - SecureJsonData: map[string]string{ + Params: map[string]string{ + "grant_type": "client_credentials", + "client_id": "my_client_id", "client_secret": "my_secret", + "audience": "www.example.com", + "client_name": "datasource_plugin", }, } @@ -162,7 +170,7 @@ func TestAccessToken_pluginWithTokenAuthRoute(t *testing.T) { mockTimeNow(time.Now()) defer resetTimeNow() - provider := newGenericAccessTokenProvider(&models.DataSource{}, pluginRoute, templateData) + provider := newGenericAccessTokenProvider(&models.DataSource{}, pluginRoute, authParams) testCases := []tokenTestDescription{ { @@ -244,7 +252,7 @@ func TestAccessToken_pluginWithTokenAuthRoute(t *testing.T) { mockTimeNow(time.Now()) defer resetTimeNow() - provider := newGenericAccessTokenProvider(&models.DataSource{}, pluginRoute, templateData) + provider := newGenericAccessTokenProvider(&models.DataSource{}, pluginRoute, authParams) token = map[string]interface{}{ "access_token": "2YotnFZFEjr1zCsicMWpAA", diff --git a/pkg/plugins/app_plugin.go b/pkg/plugins/app_plugin.go index 53f76b7fc5c..243f801d294 100644 --- a/pkg/plugins/app_plugin.go +++ b/pkg/plugins/app_plugin.go @@ -33,6 +33,7 @@ type AppPluginRoute struct { URL string `json:"url"` URLParams []AppPluginRouteURLParam `json:"urlParams"` Headers []AppPluginRouteHeader `json:"headers"` + AuthType string `json:"authType"` TokenAuth *JwtTokenAuth `json:"tokenAuth"` JwtTokenAuth *JwtTokenAuth `json:"jwtTokenAuth"` Body json.RawMessage `json:"body"`