mirror of https://github.com/grafana/grafana.git
				
				
				
			
		
			
				
	
	
		
			198 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			198 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			Go
		
	
	
	
| package pluginproxy
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"encoding/json"
 | |
| 	"io"
 | |
| 	"net/http"
 | |
| 	"net/url"
 | |
| 
 | |
| 	"github.com/grafana/grafana/pkg/infra/tracing"
 | |
| 	"github.com/grafana/grafana/pkg/plugins"
 | |
| 	contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
 | |
| 	"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
 | |
| 	"github.com/grafana/grafana/pkg/services/secrets"
 | |
| 	"github.com/grafana/grafana/pkg/setting"
 | |
| 	"github.com/grafana/grafana/pkg/util"
 | |
| 	"github.com/grafana/grafana/pkg/util/proxyutil"
 | |
| 	"github.com/grafana/grafana/pkg/web"
 | |
| 	"go.opentelemetry.io/otel/attribute"
 | |
| )
 | |
| 
 | |
| type PluginProxy struct {
 | |
| 	ps             *pluginsettings.DTO
 | |
| 	pluginRoutes   []*plugins.Route
 | |
| 	ctx            *contextmodel.ReqContext
 | |
| 	proxyPath      string
 | |
| 	matchedRoute   *plugins.Route
 | |
| 	cfg            *setting.Cfg
 | |
| 	secretsService secrets.Service
 | |
| 	tracer         tracing.Tracer
 | |
| 	transport      *http.Transport
 | |
| }
 | |
| 
 | |
| // NewPluginProxy creates a plugin proxy.
 | |
| func NewPluginProxy(ps *pluginsettings.DTO, routes []*plugins.Route, ctx *contextmodel.ReqContext,
 | |
| 	proxyPath string, cfg *setting.Cfg, secretsService secrets.Service, tracer tracing.Tracer,
 | |
| 	transport *http.Transport) (*PluginProxy, error) {
 | |
| 	return &PluginProxy{
 | |
| 		ps:             ps,
 | |
| 		pluginRoutes:   routes,
 | |
| 		ctx:            ctx,
 | |
| 		proxyPath:      proxyPath,
 | |
| 		cfg:            cfg,
 | |
| 		secretsService: secretsService,
 | |
| 		tracer:         tracer,
 | |
| 		transport:      transport,
 | |
| 	}, nil
 | |
| }
 | |
| 
 | |
| func (proxy *PluginProxy) HandleRequest() {
 | |
| 	// found route if there are any
 | |
| 	for _, route := range proxy.pluginRoutes {
 | |
| 		// method match
 | |
| 		if route.Method != "" && route.Method != "*" && route.Method != proxy.ctx.Req.Method {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		t := web.NewTree()
 | |
| 		t.Add(route.Path, nil)
 | |
| 		_, params, isMatch := t.Match(proxy.proxyPath)
 | |
| 
 | |
| 		if !isMatch {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		if route.ReqRole.IsValid() {
 | |
| 			if !proxy.ctx.HasUserRole(route.ReqRole) {
 | |
| 				proxy.ctx.JsonApiErr(http.StatusForbidden, "plugin proxy route access denied", nil)
 | |
| 				return
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if path, exists := params["*"]; exists {
 | |
| 			proxy.proxyPath = path
 | |
| 		} else {
 | |
| 			proxy.proxyPath = ""
 | |
| 		}
 | |
| 
 | |
| 		proxy.matchedRoute = route
 | |
| 		break
 | |
| 	}
 | |
| 
 | |
| 	if proxy.matchedRoute == nil {
 | |
| 		proxy.ctx.JsonApiErr(http.StatusNotFound, "plugin route match not found", nil)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	proxyErrorLogger := logger.New(
 | |
| 		"userId", proxy.ctx.UserID,
 | |
| 		"orgId", proxy.ctx.OrgID,
 | |
| 		"uname", proxy.ctx.Login,
 | |
| 		"path", proxy.ctx.Req.URL.Path,
 | |
| 		"remote_addr", proxy.ctx.RemoteAddr(),
 | |
| 		"referer", proxy.ctx.Req.Referer(),
 | |
| 	)
 | |
| 
 | |
| 	reverseProxy := proxyutil.NewReverseProxy(
 | |
| 		proxyErrorLogger,
 | |
| 		proxy.director,
 | |
| 		proxyutil.WithTransport(proxy.transport),
 | |
| 	)
 | |
| 
 | |
| 	proxy.logRequest()
 | |
| 	ctx, span := proxy.tracer.Start(proxy.ctx.Req.Context(), "plugin reverse proxy")
 | |
| 	defer span.End()
 | |
| 
 | |
| 	proxy.ctx.Req = proxy.ctx.Req.WithContext(ctx)
 | |
| 
 | |
| 	span.SetAttributes("user", proxy.ctx.SignedInUser.Login, attribute.Key("user").String(proxy.ctx.SignedInUser.Login))
 | |
| 	span.SetAttributes("org_id", proxy.ctx.SignedInUser.OrgID, attribute.Key("org_id").Int64(proxy.ctx.SignedInUser.OrgID))
 | |
| 
 | |
| 	proxy.tracer.Inject(ctx, proxy.ctx.Req.Header, span)
 | |
| 
 | |
| 	reverseProxy.ServeHTTP(proxy.ctx.Resp, proxy.ctx.Req)
 | |
| }
 | |
| 
 | |
| func (proxy PluginProxy) director(req *http.Request) {
 | |
| 	secureJsonData, err := proxy.secretsService.DecryptJsonData(proxy.ctx.Req.Context(), proxy.ps.SecureJSONData)
 | |
| 	if err != nil {
 | |
| 		proxy.ctx.JsonApiErr(500, "Failed to decrypt plugin settings", err)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	data := templateData{
 | |
| 		JsonData:       proxy.ps.JSONData,
 | |
| 		SecureJsonData: secureJsonData,
 | |
| 	}
 | |
| 
 | |
| 	interpolatedURL, err := interpolateString(proxy.matchedRoute.URL, data)
 | |
| 	if err != nil {
 | |
| 		proxy.ctx.JsonApiErr(500, "Could not interpolate plugin route url", err)
 | |
| 		return
 | |
| 	}
 | |
| 	targetURL, err := url.Parse(interpolatedURL)
 | |
| 	if err != nil {
 | |
| 		proxy.ctx.JsonApiErr(500, "Could not parse url", err)
 | |
| 		return
 | |
| 	}
 | |
| 	req.URL.Scheme = targetURL.Scheme
 | |
| 	req.URL.Host = targetURL.Host
 | |
| 	req.Host = targetURL.Host
 | |
| 	req.URL.Path = util.JoinURLFragments(targetURL.Path, proxy.proxyPath)
 | |
| 
 | |
| 	// clear cookie headers
 | |
| 	req.Header.Del("Cookie")
 | |
| 	req.Header.Del("Set-Cookie")
 | |
| 
 | |
| 	// Create a HTTP header with the context in it.
 | |
| 	ctxJSON, err := json.Marshal(proxy.ctx.SignedInUser)
 | |
| 	if err != nil {
 | |
| 		proxy.ctx.JsonApiErr(500, "failed to marshal context to json.", err)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	req.Header.Set("X-Grafana-Context", string(ctxJSON))
 | |
| 
 | |
| 	proxyutil.ApplyUserHeader(proxy.cfg.SendUserHeader, req, proxy.ctx.SignedInUser)
 | |
| 
 | |
| 	if err := addHeaders(&req.Header, proxy.matchedRoute, data); err != nil {
 | |
| 		proxy.ctx.JsonApiErr(500, "Failed to render plugin headers", err)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if err := setBodyContent(req, proxy.matchedRoute, data); err != nil {
 | |
| 		logger.FromContext(req.Context()).Error("Failed to set plugin route body content", "error", err)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (proxy PluginProxy) logRequest() {
 | |
| 	if !proxy.cfg.DataProxyLogging {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	var body string
 | |
| 	if proxy.ctx.Req.Body != nil {
 | |
| 		buffer, err := io.ReadAll(proxy.ctx.Req.Body)
 | |
| 		if err == nil {
 | |
| 			proxy.ctx.Req.Body = io.NopCloser(bytes.NewBuffer(buffer))
 | |
| 			body = string(buffer)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	ctxLogger := logger.FromContext(proxy.ctx.Req.Context())
 | |
| 	ctxLogger.Info("Proxying incoming request",
 | |
| 		"userid", proxy.ctx.UserID,
 | |
| 		"orgid", proxy.ctx.OrgID,
 | |
| 		"username", proxy.ctx.Login,
 | |
| 		"app", proxy.ps.PluginID,
 | |
| 		"uri", proxy.ctx.Req.RequestURI,
 | |
| 		"method", proxy.ctx.Req.Method,
 | |
| 		"body", body)
 | |
| }
 | |
| 
 | |
| type templateData struct {
 | |
| 	JsonData       map[string]any
 | |
| 	SecureJsonData map[string]string
 | |
| }
 |