mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
bfd5d3b16a
commit
1790737cf1
@ -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
|
||||
}
|
||||
|
62
pkg/api/pluginproxy/ds_auth_provider_test.go
Normal file
62
pkg/api/pluginproxy/ds_auth_provider_test.go
Normal 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)
|
||||
})
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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",
|
||||
|
@ -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"`
|
||||
|
Loading…
Reference in New Issue
Block a user