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

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
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

View File

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

View File

@ -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 {

View File

@ -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",

View File

@ -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"`