2017-08-22 23:14:15 +08:00
package pluginproxy
import (
"bytes"
"errors"
"fmt"
2022-08-10 21:37:51 +08:00
"io"
2017-08-22 23:14:15 +08:00
"net/http"
"net/url"
2017-08-23 23:18:43 +08:00
"strconv"
2017-08-22 23:14:15 +08:00
"strings"
"time"
2022-06-28 00:23:15 +08:00
"go.opentelemetry.io/otel/attribute"
2023-10-03 20:54:20 +08:00
"go.opentelemetry.io/otel/trace"
2022-06-28 00:23:15 +08:00
2020-05-12 19:04:18 +08:00
"github.com/grafana/grafana/pkg/api/datasource"
2021-05-20 05:53:41 +08:00
"github.com/grafana/grafana/pkg/infra/httpclient"
2020-02-18 20:26:01 +08:00
glog "github.com/grafana/grafana/pkg/infra/log"
2022-01-20 18:10:12 +08:00
"github.com/grafana/grafana/pkg/infra/tracing"
2017-08-22 23:14:15 +08:00
"github.com/grafana/grafana/pkg/plugins"
2023-01-27 15:50:36 +08:00
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
2021-10-07 22:33:50 +08:00
"github.com/grafana/grafana/pkg/services/datasources"
2023-10-02 15:14:10 +08:00
"github.com/grafana/grafana/pkg/services/featuremgmt"
2020-10-24 07:34:38 +08:00
"github.com/grafana/grafana/pkg/services/oauthtoken"
2024-07-25 22:22:42 +08:00
pluginac "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
2017-08-22 23:14:15 +08:00
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
2020-03-03 18:45:16 +08:00
"github.com/grafana/grafana/pkg/util/proxyutil"
2017-08-22 23:14:15 +08:00
)
var (
2020-02-18 20:26:01 +08:00
logger = glog . New ( "data-proxy-log" )
2018-09-06 21:50:16 +08:00
client = newHTTPClient ( )
2017-08-22 23:14:15 +08:00
)
type DataSourceProxy struct {
2022-06-28 00:23:15 +08:00
ds * datasources . DataSource
2023-01-27 15:50:36 +08:00
ctx * contextmodel . ReqContext
2021-10-07 22:33:50 +08:00
targetUrl * url . URL
proxyPath string
2021-11-01 17:53:33 +08:00
matchedRoute * plugins . Route
pluginRoutes [ ] * plugins . Route
2021-10-07 22:33:50 +08:00
cfg * setting . Cfg
clientProvider httpclient . Provider
oAuthTokenService oauthtoken . OAuthTokenService
2022-02-11 22:52:14 +08:00
dataSourcesService datasources . DataSourceService
2022-01-20 18:10:12 +08:00
tracer tracing . Tracer
2023-10-02 15:14:10 +08:00
features featuremgmt . FeatureToggles
2018-06-12 23:39:38 +08:00
}
2018-06-14 19:39:46 +08:00
type httpClient interface {
2018-06-12 23:39:38 +08:00
Do ( req * http . Request ) ( * http . Response , error )
2017-08-22 23:14:15 +08:00
}
2020-02-18 20:26:01 +08:00
// NewDataSourceProxy creates a new Datasource proxy
2023-01-27 15:50:36 +08:00
func NewDataSourceProxy ( ds * datasources . DataSource , pluginRoutes [ ] * plugins . Route , ctx * contextmodel . ReqContext ,
2021-10-07 22:33:50 +08:00
proxyPath string , cfg * setting . Cfg , clientProvider httpclient . Provider ,
2022-02-11 22:52:14 +08:00
oAuthTokenService oauthtoken . OAuthTokenService , dsService datasources . DataSourceService ,
2024-11-27 18:06:39 +08:00
tracer tracing . Tracer , features featuremgmt . FeatureToggles ,
) ( * DataSourceProxy , error ) {
2023-02-03 00:22:43 +08:00
targetURL , err := datasource . ValidateURL ( ds . Type , ds . URL )
2020-04-22 16:30:06 +08:00
if err != nil {
2020-05-12 19:04:18 +08:00
return nil , err
2020-04-22 16:30:06 +08:00
}
2017-08-23 16:52:31 +08:00
2017-08-22 23:14:15 +08:00
return & DataSourceProxy {
2021-10-07 22:33:50 +08:00
ds : ds ,
2021-11-01 17:53:33 +08:00
pluginRoutes : pluginRoutes ,
2021-10-07 22:33:50 +08:00
ctx : ctx ,
proxyPath : proxyPath ,
targetUrl : targetURL ,
cfg : cfg ,
clientProvider : clientProvider ,
oAuthTokenService : oAuthTokenService ,
dataSourcesService : dsService ,
2022-01-20 18:10:12 +08:00
tracer : tracer ,
2023-10-02 15:14:10 +08:00
features : features ,
2020-04-22 16:30:06 +08:00
} , nil
2018-06-14 19:39:46 +08:00
}
func newHTTPClient ( ) httpClient {
return & http . Client {
2019-02-11 20:42:05 +08:00
Timeout : 30 * time . Second ,
2018-06-14 19:39:46 +08:00
Transport : & http . Transport { Proxy : http . ProxyFromEnvironment } ,
2017-08-22 23:14:15 +08:00
}
}
func ( proxy * DataSourceProxy ) HandleRequest ( ) {
if err := proxy . validateRequest ( ) ; err != nil {
proxy . ctx . JsonApiErr ( 403 , err . Error ( ) , nil )
return
}
2022-04-11 19:17:08 +08:00
proxyErrorLogger := logger . New (
2022-08-11 19:28:55 +08:00
"userId" , proxy . ctx . UserID ,
"orgId" , proxy . ctx . OrgID ,
2022-04-11 19:17:08 +08:00
"uname" , proxy . ctx . Login ,
"path" , proxy . ctx . Req . URL . Path ,
"remote_addr" , proxy . ctx . RemoteAddr ( ) ,
"referer" , proxy . ctx . Req . Referer ( ) ,
)
2017-08-22 23:14:15 +08:00
2022-04-26 00:57:45 +08:00
transport , err := proxy . dataSourcesService . GetHTTPTransport ( proxy . ctx . Req . Context ( ) , proxy . ds , proxy . clientProvider )
2017-08-22 23:14:15 +08:00
if err != nil {
proxy . ctx . JsonApiErr ( 400 , "Unable to load TLS certificate" , err )
return
}
2022-04-11 19:17:08 +08:00
modifyResponse := func ( resp * http . Response ) error {
if resp . StatusCode == 401 {
// The data source rejected the request as unauthorized, convert to 400 (bad request)
2022-08-10 21:37:51 +08:00
body , err := io . ReadAll ( resp . Body )
2022-04-11 19:17:08 +08:00
if err != nil {
return fmt . Errorf ( "failed to read data source response body: %w" , err )
}
_ = resp . Body . Close ( )
2022-09-21 00:32:06 +08:00
ctxLogger := proxyErrorLogger . FromContext ( resp . Request . Context ( ) )
ctxLogger . Info ( "Authentication to data source failed" , "body" , string ( body ) , "statusCode" ,
2022-04-11 19:17:08 +08:00
resp . StatusCode )
msg := "Authentication to data source failed"
* resp = http . Response {
StatusCode : 400 ,
Status : "Bad Request" ,
2022-08-10 21:37:51 +08:00
Body : io . NopCloser ( strings . NewReader ( msg ) ) ,
2022-04-11 19:17:08 +08:00
ContentLength : int64 ( len ( msg ) ) ,
Header : http . Header { } ,
2023-09-11 18:13:13 +08:00
Request : resp . Request ,
2020-11-13 20:21:43 +08:00
}
2022-04-11 19:17:08 +08:00
}
return nil
2020-01-15 20:03:12 +08:00
}
2022-04-11 19:17:08 +08:00
reverseProxy := proxyutil . NewReverseProxy (
proxyErrorLogger ,
proxy . director ,
proxyutil . WithTransport ( transport ) ,
proxyutil . WithModifyResponse ( modifyResponse ) ,
)
2017-08-22 23:14:15 +08:00
proxy . logRequest ( )
2022-01-20 18:10:12 +08:00
ctx , span := proxy . tracer . Start ( proxy . ctx . Req . Context ( ) , "datasource reverse proxy" )
defer span . End ( )
2020-11-13 20:21:43 +08:00
2021-09-01 17:18:30 +08:00
proxy . ctx . Req = proxy . ctx . Req . WithContext ( ctx )
2017-09-12 16:25:40 +08:00
2023-10-03 20:54:20 +08:00
span . SetAttributes (
attribute . String ( "datasource_name" , proxy . ds . Name ) ,
attribute . String ( "datasource_type" , proxy . ds . Type ) ,
2025-04-10 20:42:23 +08:00
attribute . String ( "user" , proxy . ctx . Login ) ,
attribute . Int64 ( "org_id" , proxy . ctx . OrgID ) ,
2023-10-03 20:54:20 +08:00
)
2017-09-12 16:25:40 +08:00
2018-03-21 05:21:24 +08:00
proxy . addTraceFromHeaderValue ( span , "X-Panel-Id" , "panel_id" )
proxy . addTraceFromHeaderValue ( span , "X-Dashboard-Id" , "dashboard_id" )
2022-01-20 18:10:12 +08:00
proxy . tracer . Inject ( ctx , proxy . ctx . Req . Header , span )
2017-09-12 17:06:37 +08:00
2021-09-01 17:18:30 +08:00
reverseProxy . ServeHTTP ( proxy . ctx . Resp , proxy . ctx . Req )
2017-08-22 23:14:15 +08:00
}
2023-10-03 20:54:20 +08:00
func ( proxy * DataSourceProxy ) addTraceFromHeaderValue ( span trace . Span , headerName string , tagName string ) {
2018-03-21 05:21:24 +08:00
panelId := proxy . ctx . Req . Header . Get ( headerName )
dashId , err := strconv . Atoi ( panelId )
if err == nil {
2023-10-03 20:54:20 +08:00
span . SetAttributes ( attribute . Int ( tagName , dashId ) )
2018-03-21 05:21:24 +08:00
}
}
2020-11-13 20:21:43 +08:00
func ( proxy * DataSourceProxy ) director ( req * http . Request ) {
req . URL . Scheme = proxy . targetUrl . Scheme
req . URL . Host = proxy . targetUrl . Host
req . Host = proxy . targetUrl . Host
reqQueryVals := req . URL . Query ( )
2022-09-21 00:32:06 +08:00
ctxLogger := logger . FromContext ( req . Context ( ) )
2020-11-13 20:21:43 +08:00
switch proxy . ds . Type {
2022-06-28 00:23:15 +08:00
case datasources . DS_INFLUXDB_08 :
2022-04-26 00:57:45 +08:00
password , err := proxy . dataSourcesService . DecryptedPassword ( req . Context ( ) , proxy . ds )
if err != nil {
2022-09-21 00:32:06 +08:00
ctxLogger . Error ( "Error interpolating proxy url" , "error" , err )
2022-04-26 00:57:45 +08:00
return
}
2021-03-17 19:17:41 +08:00
req . URL . RawPath = util . JoinURLFragments ( proxy . targetUrl . Path , "db/" + proxy . ds . Database + "/" + proxy . proxyPath )
2020-11-13 20:21:43 +08:00
reqQueryVals . Add ( "u" , proxy . ds . User )
2022-04-26 00:57:45 +08:00
reqQueryVals . Add ( "p" , password )
2020-11-13 20:21:43 +08:00
req . URL . RawQuery = reqQueryVals . Encode ( )
2022-06-28 00:23:15 +08:00
case datasources . DS_INFLUXDB :
2022-04-26 00:57:45 +08:00
password , err := proxy . dataSourcesService . DecryptedPassword ( req . Context ( ) , proxy . ds )
if err != nil {
2022-09-21 00:32:06 +08:00
ctxLogger . Error ( "Error interpolating proxy url" , "error" , err )
2022-04-26 00:57:45 +08:00
return
}
2021-03-17 19:17:41 +08:00
req . URL . RawPath = util . JoinURLFragments ( proxy . targetUrl . Path , proxy . proxyPath )
2020-11-13 20:21:43 +08:00
req . URL . RawQuery = reqQueryVals . Encode ( )
if ! proxy . ds . BasicAuth {
2021-10-07 22:33:50 +08:00
req . Header . Set (
"Authorization" ,
2022-04-26 00:57:45 +08:00
util . GetBasicAuthHeader ( proxy . ds . User , password ) ,
2021-10-07 22:33:50 +08:00
)
2017-08-22 23:14:15 +08:00
}
2020-11-13 20:21:43 +08:00
default :
2021-03-17 19:17:41 +08:00
req . URL . RawPath = util . JoinURLFragments ( proxy . targetUrl . Path , proxy . proxyPath )
2020-11-13 20:21:43 +08:00
}
2020-04-24 16:32:13 +08:00
2021-03-17 19:17:41 +08:00
unescapedPath , err := url . PathUnescape ( req . URL . RawPath )
if err != nil {
2022-09-21 00:32:06 +08:00
ctxLogger . Error ( "Failed to unescape raw path" , "rawPath" , req . URL . RawPath , "error" , err )
2021-03-17 19:17:41 +08:00
return
}
req . URL . Path = unescapedPath
2020-11-13 20:21:43 +08:00
if proxy . ds . BasicAuth {
2022-04-26 00:57:45 +08:00
password , err := proxy . dataSourcesService . DecryptedBasicAuthPassword ( req . Context ( ) , proxy . ds )
if err != nil {
2022-09-21 00:32:06 +08:00
ctxLogger . Error ( "Error interpolating proxy url" , "error" , err )
2022-04-26 00:57:45 +08:00
return
}
2020-11-13 20:21:43 +08:00
req . Header . Set ( "Authorization" , util . GetBasicAuthHeader ( proxy . ds . BasicAuthUser ,
2022-04-26 00:57:45 +08:00
password ) )
2020-11-13 20:21:43 +08:00
}
2017-08-22 23:14:15 +08:00
2020-11-13 20:21:43 +08:00
dsAuth := req . Header . Get ( "X-DS-Authorization" )
if len ( dsAuth ) > 0 {
req . Header . Del ( "X-DS-Authorization" )
req . Header . Set ( "Authorization" , dsAuth )
}
2017-08-22 23:14:15 +08:00
2022-12-15 22:28:25 +08:00
proxyutil . ApplyUserHeader ( proxy . cfg . SendUserHeader , req , proxy . ctx . SignedInUser )
2019-03-14 20:04:47 +08:00
2022-10-21 19:54:55 +08:00
proxyutil . ClearCookieHeader ( req , proxy . ds . AllowedCookies ( ) , [ ] string { proxy . cfg . LoginCookieName } )
2023-02-28 21:10:05 +08:00
req . Header . Set ( "User-Agent" , proxy . cfg . DataProxyUserAgent )
2017-08-22 23:14:15 +08:00
2023-08-30 23:46:47 +08:00
jsonData := make ( map [ string ] any )
2021-10-08 20:46:35 +08:00
if proxy . ds . JsonData != nil {
jsonData , err = proxy . ds . JsonData . Map ( )
if err != nil {
2022-09-21 00:32:06 +08:00
ctxLogger . Error ( "Failed to get json data as map" , "jsonData" , proxy . ds . JsonData , "error" , err )
2021-10-08 20:46:35 +08:00
return
}
}
2021-11-01 17:53:33 +08:00
if proxy . matchedRoute != nil {
2022-04-26 00:57:45 +08:00
decryptedValues , err := proxy . dataSourcesService . DecryptedValues ( req . Context ( ) , proxy . ds )
if err != nil {
2022-09-21 00:32:06 +08:00
ctxLogger . Error ( "Error interpolating proxy url" , "error" , err )
2022-04-26 00:57:45 +08:00
return
}
ApplyRoute ( req . Context ( ) , req , proxy . proxyPath , proxy . matchedRoute , DSInfo {
2023-02-03 00:22:43 +08:00
ID : proxy . ds . ID ,
2024-02-02 16:23:07 +08:00
URL : proxy . ds . URL ,
2021-10-08 20:46:35 +08:00
Updated : proxy . ds . Updated ,
JSONData : jsonData ,
2022-04-26 00:57:45 +08:00
DecryptedSecureJSONData : decryptedValues ,
2021-10-08 20:46:35 +08:00
} , proxy . cfg )
2020-11-13 20:21:43 +08:00
}
2019-02-02 08:40:57 +08:00
2021-07-07 14:54:17 +08:00
if proxy . oAuthTokenService . IsOAuthPassThruEnabled ( proxy . ds ) {
2025-04-09 21:08:21 +08:00
if token := proxy . oAuthTokenService . GetCurrentOAuthToken ( req . Context ( ) , proxy . ctx . SignedInUser , proxy . ctx . UserToken ) ; token != nil {
2020-11-13 20:21:43 +08:00
req . Header . Set ( "Authorization" , fmt . Sprintf ( "%s %s" , token . Type ( ) , token . AccessToken ) )
2021-11-29 22:40:05 +08:00
idToken , ok := token . Extra ( "id_token" ) . ( string )
if ok && idToken != "" {
req . Header . Set ( "X-ID-Token" , idToken )
}
2019-02-02 08:40:57 +08:00
}
2017-08-22 23:14:15 +08:00
}
2023-10-02 15:14:10 +08:00
2024-08-21 21:30:17 +08:00
proxyutil . ApplyForwardIDHeader ( req , proxy . ctx . SignedInUser )
2017-08-22 23:14:15 +08:00
}
func ( proxy * DataSourceProxy ) validateRequest ( ) error {
2024-01-23 19:36:22 +08:00
if ! proxy . checkWhiteList ( ) {
2020-11-05 20:07:06 +08:00
return errors . New ( "target URL is not a valid target" )
2017-08-22 23:14:15 +08:00
}
2022-06-28 00:23:15 +08:00
if proxy . ds . Type == datasources . DS_ES {
2021-09-01 17:18:30 +08:00
if proxy . ctx . Req . Method == "DELETE" {
2020-11-05 20:07:06 +08:00
return errors . New ( "deletes not allowed on proxied Elasticsearch datasource" )
2017-08-22 23:14:15 +08:00
}
2021-09-01 17:18:30 +08:00
if proxy . ctx . Req . Method == "PUT" {
2020-11-05 20:07:06 +08:00
return errors . New ( "puts not allowed on proxied Elasticsearch datasource" )
2017-08-22 23:14:15 +08:00
}
2021-09-01 17:18:30 +08:00
if proxy . ctx . Req . Method == "POST" && proxy . proxyPath != "_msearch" {
2020-11-05 20:07:06 +08:00
return errors . New ( "posts not allowed on proxied Elasticsearch datasource except on /_msearch" )
2017-08-22 23:14:15 +08:00
}
}
// found route if there are any
2021-11-01 17:53:33 +08:00
for _ , route := range proxy . pluginRoutes {
// method match
if route . Method != "" && route . Method != "*" && route . Method != proxy . ctx . Req . Method {
continue
}
2017-08-22 23:14:15 +08:00
2021-11-01 17:53:33 +08:00
// route match
if ! strings . HasPrefix ( proxy . proxyPath , route . Path ) {
continue
}
2021-04-15 01:06:20 +08:00
2025-02-26 01:30:58 +08:00
if ! proxy . hasAccessToRoute ( route ) {
return errors . New ( "plugin proxy route access denied" )
2021-04-15 01:06:20 +08:00
}
2021-11-01 17:53:33 +08:00
proxy . matchedRoute = route
return nil
2021-04-15 01:06:20 +08:00
}
// Trailing validation below this point for routes that were not matched
2022-06-28 00:23:15 +08:00
if proxy . ds . Type == datasources . DS_PROMETHEUS {
2021-09-01 17:18:30 +08:00
if proxy . ctx . Req . Method == "DELETE" {
2021-04-15 01:06:20 +08:00
return errors . New ( "non allow-listed DELETEs not allowed on proxied Prometheus datasource" )
}
2021-09-01 17:18:30 +08:00
if proxy . ctx . Req . Method == "PUT" {
2021-04-15 01:06:20 +08:00
return errors . New ( "non allow-listed PUTs not allowed on proxied Prometheus datasource" )
2017-08-22 23:14:15 +08:00
}
2021-09-01 17:18:30 +08:00
if proxy . ctx . Req . Method == "POST" {
2021-04-29 15:20:51 +08:00
return errors . New ( "non allow-listed POSTs not allowed on proxied Prometheus datasource" )
}
2017-08-22 23:14:15 +08:00
}
return nil
}
2024-05-21 21:05:16 +08:00
func ( proxy * DataSourceProxy ) hasAccessToRoute ( route * plugins . Route ) bool {
ctxLogger := logger . FromContext ( proxy . ctx . Req . Context ( ) )
2025-02-25 20:44:40 +08:00
if route . ReqAction != "" {
2024-07-25 22:22:42 +08:00
routeEval := pluginac . GetDataSourceRouteEvaluator ( proxy . ds . UID , route . ReqAction )
hasAccess := routeEval . Evaluate ( proxy . ctx . GetPermissions ( ) )
if ! hasAccess {
2024-05-21 21:05:16 +08:00
ctxLogger . Debug ( "plugin route is covered by RBAC, user doesn't have access" , "route" , proxy . ctx . Req . URL . Path , "action" , route . ReqAction , "path" , route . Path , "method" , route . Method )
}
2024-07-25 22:22:42 +08:00
return hasAccess
2024-05-21 21:05:16 +08:00
}
if route . ReqRole . IsValid ( ) {
if hasUserRole := proxy . ctx . HasUserRole ( route . ReqRole ) ; ! hasUserRole {
ctxLogger . Debug ( "plugin route is covered by org role, user doesn't have access" , "route" , proxy . ctx . Req . URL . Path , "role" , route . ReqRole , "path" , route . Path , "method" , route . Method )
return false
}
}
return true
}
2017-08-22 23:14:15 +08:00
func ( proxy * DataSourceProxy ) logRequest ( ) {
2021-07-15 20:30:06 +08:00
if ! proxy . cfg . DataProxyLogging {
2017-08-22 23:14:15 +08:00
return
}
var body string
2021-09-01 17:18:30 +08:00
if proxy . ctx . Req . Body != nil {
2022-08-10 21:37:51 +08:00
buffer , err := io . ReadAll ( proxy . ctx . Req . Body )
2017-08-22 23:14:15 +08:00
if err == nil {
2022-08-10 21:37:51 +08:00
proxy . ctx . Req . Body = io . NopCloser ( bytes . NewBuffer ( buffer ) )
2017-08-22 23:14:15 +08:00
body = string ( buffer )
}
}
2024-02-21 16:38:42 +08:00
panelPluginId := proxy . ctx . Req . Header . Get ( "X-Panel-Plugin-Id" )
2024-01-30 16:06:31 +08:00
2024-06-24 20:53:42 +08:00
uri , err := util . SanitizeURI ( proxy . ctx . Req . RequestURI )
2024-10-28 22:34:07 +08:00
if err != nil {
2024-06-24 20:53:42 +08:00
proxy . ctx . Logger . Error ( "Could not sanitize RequestURI" , "error" , err )
}
2022-09-21 00:32:06 +08:00
ctxLogger := logger . FromContext ( proxy . ctx . Req . Context ( ) )
ctxLogger . Info ( "Proxying incoming request" ,
2022-08-11 19:28:55 +08:00
"userid" , proxy . ctx . UserID ,
"orgid" , proxy . ctx . OrgID ,
2017-08-22 23:14:15 +08:00
"username" , proxy . ctx . Login ,
"datasource" , proxy . ds . Type ,
2024-06-24 20:53:42 +08:00
"uri" , uri ,
2021-09-01 17:18:30 +08:00
"method" , proxy . ctx . Req . Method ,
2024-02-21 16:38:42 +08:00
"panelPluginId" , panelPluginId ,
2017-08-22 23:14:15 +08:00
"body" , body )
}
2024-01-23 19:36:22 +08:00
func ( proxy * DataSourceProxy ) checkWhiteList ( ) bool {
if proxy . targetUrl . Host != "" && len ( proxy . cfg . DataProxyWhiteList ) > 0 {
if _ , exists := proxy . cfg . DataProxyWhiteList [ proxy . targetUrl . Host ] ; ! exists {
proxy . ctx . JsonApiErr ( 403 , "Data proxy hostname and ip are not included in whitelist" , nil )
2017-08-22 23:14:15 +08:00
return false
}
}
return true
}