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 <marcus.efraimsson@gmail.com>

Fixes #33669
Closed #33732
This commit is contained in:
Sergey Kostrukov 2021-05-06 13:05:23 -07:00 committed by GitHub
parent bfd5d3b16a
commit 1790737cf1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 185 additions and 73 deletions

View File

@ -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) 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 { if token, err := tokenProvider.getAccessToken(); err != nil {
logger.Error("Failed to get access token", "error", err) logger.Error("Failed to get access token", "error", err)
} else { } 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, func getTokenProvider(ctx context.Context, ds *models.DataSource, pluginRoute *plugins.AppPluginRoute,
data templateData) accessTokenProvider { data templateData) (accessTokenProvider, error) {
authenticationType := ds.JsonData.Get("authenticationType").MustString() authType := pluginRoute.AuthType
switch authenticationType { // Plugin can override authentication type specified in route configuration
if authTypeOverride := ds.JsonData.Get("authenticationType").MustString(); authTypeOverride != "" {
authType = authTypeOverride
}
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": case "gce":
return newGceAccessTokenProvider(ctx, ds, pluginRoute) 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": case "jwt":
if pluginRoute.JwtTokenAuth != nil { if jwtTokenAuth == nil {
return newJwtAccessTokenProvider(ctx, ds, pluginRoute, data) return nil, fmt.Errorf("'jwtTokenAuth' not configured for authentication type '%s'", authType)
} }
default: provider := newJwtAccessTokenProvider(ctx, ds, pluginRoute, jwtTokenAuth)
// Fallback to authentication options when authentication type isn't explicitly configured return provider, nil
if pluginRoute.TokenAuth != nil {
return newGenericAccessTokenProvider(ds, pluginRoute, data) case "":
} // Fallback to authentication methods when authentication type isn't explicitly configured
if pluginRoute.JwtTokenAuth != nil { if tokenAuth != nil {
return newJwtAccessTokenProvider(ctx, ds, pluginRoute, data) provider := newGenericAccessTokenProvider(ds, pluginRoute, tokenAuth)
return provider, nil
} }
if jwtTokenAuth != nil {
provider := newJwtAccessTokenProvider(ctx, ds, pluginRoute, jwtTokenAuth)
return provider, nil
} }
return 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
} }

View File

@ -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)
})
}

View File

@ -13,19 +13,22 @@ type gceAccessTokenProvider struct {
datasourceVersion int datasourceVersion int
ctx context.Context ctx context.Context
route *plugins.AppPluginRoute 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{ return &gceAccessTokenProvider{
datasourceId: ds.Id, datasourceId: ds.Id,
datasourceVersion: ds.Version, datasourceVersion: ds.Version,
ctx: ctx, ctx: ctx,
route: pluginRoute, route: pluginRoute,
authParams: authParams,
} }
} }
func (provider *gceAccessTokenProvider) getAccessToken() (string, error) { 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 { if err != nil {
logger.Error("Failed to get default token from meta data server", "error", err) logger.Error("Failed to get default token from meta data server", "error", err)
return "", err return "", err

View File

@ -29,7 +29,7 @@ type genericAccessTokenProvider struct {
datasourceId int64 datasourceId int64
datasourceVersion int datasourceVersion int
route *plugins.AppPluginRoute route *plugins.AppPluginRoute
data templateData authParams *plugins.JwtTokenAuth
} }
type jwtToken struct { type jwtToken struct {
@ -69,12 +69,12 @@ func (token *jwtToken) UnmarshalJSON(b []byte) error {
} }
func newGenericAccessTokenProvider(ds *models.DataSource, pluginRoute *plugins.AppPluginRoute, func newGenericAccessTokenProvider(ds *models.DataSource, pluginRoute *plugins.AppPluginRoute,
data templateData) *genericAccessTokenProvider { authParams *plugins.JwtTokenAuth) *genericAccessTokenProvider {
return &genericAccessTokenProvider{ return &genericAccessTokenProvider{
datasourceId: ds.Id, datasourceId: ds.Id,
datasourceVersion: ds.Version, datasourceVersion: ds.Version,
route: pluginRoute, 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) tokenUrl := provider.authParams.Url
if err != nil {
return "", err
}
params := make(url.Values) params := make(url.Values)
for key, value := range provider.route.TokenAuth.Params { for key, value := range provider.authParams.Params {
interpolatedParam, err := interpolateString(value, provider.data) params.Add(key, value)
if err != nil {
return "", err
}
params.Add(key, interpolatedParam)
} }
getTokenReq, err := http.NewRequest("POST", urlInterpolated, bytes.NewBufferString(params.Encode())) getTokenReq, err := http.NewRequest("POST", tokenUrl, bytes.NewBufferString(params.Encode()))
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@ -28,17 +28,17 @@ type jwtAccessTokenProvider struct {
datasourceVersion int datasourceVersion int
ctx context.Context ctx context.Context
route *plugins.AppPluginRoute route *plugins.AppPluginRoute
data templateData authParams *plugins.JwtTokenAuth
} }
func newJwtAccessTokenProvider(ctx context.Context, ds *models.DataSource, pluginRoute *plugins.AppPluginRoute, func newJwtAccessTokenProvider(ctx context.Context, ds *models.DataSource, pluginRoute *plugins.AppPluginRoute,
data templateData) *jwtAccessTokenProvider { authParams *plugins.JwtTokenAuth) *jwtAccessTokenProvider {
return &jwtAccessTokenProvider{ return &jwtAccessTokenProvider{
datasourceId: ds.Id, datasourceId: ds.Id,
datasourceVersion: ds.Version, datasourceVersion: ds.Version,
ctx: ctx, ctx: ctx,
route: pluginRoute, route: pluginRoute,
data: data, authParams: authParams,
} }
} }
@ -54,31 +54,19 @@ func (provider *jwtAccessTokenProvider) getAccessToken() (string, error) {
conf := &jwt.Config{} conf := &jwt.Config{}
if val, ok := provider.route.JwtTokenAuth.Params["client_email"]; ok { if val, ok := provider.authParams.Params["client_email"]; ok {
interpolatedVal, err := interpolateString(val, provider.data) conf.Email = val
if err != nil {
return "", err
}
conf.Email = interpolatedVal
} }
if val, ok := provider.route.JwtTokenAuth.Params["private_key"]; ok { if val, ok := provider.authParams.Params["private_key"]; ok {
interpolatedVal, err := interpolateString(val, provider.data) conf.PrivateKey = []byte(val)
if err != nil {
return "", err
}
conf.PrivateKey = []byte(interpolatedVal)
} }
if val, ok := provider.route.JwtTokenAuth.Params["token_uri"]; ok { if val, ok := provider.authParams.Params["token_uri"]; ok {
interpolatedVal, err := interpolateString(val, provider.data) conf.TokenURL = val
if err != nil {
return "", err
}
conf.TokenURL = interpolatedVal
} }
conf.Scopes = provider.route.JwtTokenAuth.Scopes conf.Scopes = provider.authParams.Scopes
token, err := getTokenSource(conf, provider.ctx) token, err := getTokenSource(conf, provider.ctx)
if err != nil { if err != nil {

View File

@ -41,13 +41,16 @@ func TestAccessToken_pluginWithJWTTokenAuthRoute(t *testing.T) {
}, },
} }
templateData := templateData{ authParams := &plugins.JwtTokenAuth{
JsonData: map[string]interface{}{ Url: "https://login.server.com/{{.JsonData.tenantId}}/oauth2/token",
"clientEmail": "test@test.com", Scopes: []string{
"tokenUri": "login.url.com/token", "https://www.testapi.com/auth/monitoring.read",
"https://www.testapi.com/auth/cloudplatformprojects.readonly",
}, },
SecureJsonData: map[string]string{ Params: map[string]string{
"privateKey": "testkey", "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) { setUp(t, func(conf *jwt.Config, ctx context.Context) (*oauth2.Token, error) {
return &oauth2.Token{AccessToken: "abc"}, nil return &oauth2.Token{AccessToken: "abc"}, nil
}) })
provider := newJwtAccessTokenProvider(context.Background(), ds, pluginRoute, templateData) provider := newJwtAccessTokenProvider(context.Background(), ds, pluginRoute, authParams)
token, err := provider.getAccessToken() token, err := provider.getAccessToken()
require.NoError(t, err) require.NoError(t, err)
@ -85,7 +88,7 @@ func TestAccessToken_pluginWithJWTTokenAuthRoute(t *testing.T) {
return &oauth2.Token{AccessToken: "abc"}, nil return &oauth2.Token{AccessToken: "abc"}, nil
}) })
provider := newJwtAccessTokenProvider(context.Background(), ds, pluginRoute, templateData) provider := newJwtAccessTokenProvider(context.Background(), ds, pluginRoute, authParams)
_, err := provider.getAccessToken() _, err := provider.getAccessToken()
require.NoError(t, err) require.NoError(t, err)
}) })
@ -96,7 +99,7 @@ func TestAccessToken_pluginWithJWTTokenAuthRoute(t *testing.T) {
AccessToken: "abc", AccessToken: "abc",
Expiry: time.Now().Add(1 * time.Minute)}, nil 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() token1, err := provider.getAccessToken()
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "abc", token1) assert.Equal(t, "abc", token1)
@ -135,13 +138,18 @@ func TestAccessToken_pluginWithTokenAuthRoute(t *testing.T) {
}, },
} }
templateData := templateData{ authParams := &plugins.JwtTokenAuth{
JsonData: map[string]interface{}{ Url: server.URL + "/oauth/token",
"client_id": "my_client_id", Scopes: []string{
"audience": "www.example.com", "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", "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()) mockTimeNow(time.Now())
defer resetTimeNow() defer resetTimeNow()
provider := newGenericAccessTokenProvider(&models.DataSource{}, pluginRoute, templateData) provider := newGenericAccessTokenProvider(&models.DataSource{}, pluginRoute, authParams)
testCases := []tokenTestDescription{ testCases := []tokenTestDescription{
{ {
@ -244,7 +252,7 @@ func TestAccessToken_pluginWithTokenAuthRoute(t *testing.T) {
mockTimeNow(time.Now()) mockTimeNow(time.Now())
defer resetTimeNow() defer resetTimeNow()
provider := newGenericAccessTokenProvider(&models.DataSource{}, pluginRoute, templateData) provider := newGenericAccessTokenProvider(&models.DataSource{}, pluginRoute, authParams)
token = map[string]interface{}{ token = map[string]interface{}{
"access_token": "2YotnFZFEjr1zCsicMWpAA", "access_token": "2YotnFZFEjr1zCsicMWpAA",

View File

@ -33,6 +33,7 @@ type AppPluginRoute struct {
URL string `json:"url"` URL string `json:"url"`
URLParams []AppPluginRouteURLParam `json:"urlParams"` URLParams []AppPluginRouteURLParam `json:"urlParams"`
Headers []AppPluginRouteHeader `json:"headers"` Headers []AppPluginRouteHeader `json:"headers"`
AuthType string `json:"authType"`
TokenAuth *JwtTokenAuth `json:"tokenAuth"` TokenAuth *JwtTokenAuth `json:"tokenAuth"`
JwtTokenAuth *JwtTokenAuth `json:"jwtTokenAuth"` JwtTokenAuth *JwtTokenAuth `json:"jwtTokenAuth"`
Body json.RawMessage `json:"body"` Body json.RawMessage `json:"body"`