mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
[main] Plugin fixes (#57399)
* Plugins: Remove support for V1 manifests
* Plugins: Make proxy endpoints not leak sensitive HTTP headers
* Security: Fix do not forward login cookie in outgoing requests
(cherry picked from commit 4539c33fce
)
Co-authored-by: Will Browne <wbrowne@users.noreply.github.com>
This commit is contained in:
parent
af17123b5f
commit
6f8fcae01b
@ -827,7 +827,7 @@ func (hs *HTTPServer) checkDatasourceHealth(c *models.ReqContext, ds *datasource
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
proxyutil.ClearCookieHeader(c.Req, ds.AllowedCookies())
|
proxyutil.ClearCookieHeader(c.Req, ds.AllowedCookies(), []string{hs.Cfg.LoginCookieName})
|
||||||
if cookieStr := c.Req.Header.Get("Cookie"); cookieStr != "" {
|
if cookieStr := c.Req.Header.Get("Cookie"); cookieStr != "" {
|
||||||
req.Headers["Cookie"] = cookieStr
|
req.Headers["Cookie"] = cookieStr
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/query"
|
"github.com/grafana/grafana/pkg/services/query"
|
||||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/util/errutil"
|
"github.com/grafana/grafana/pkg/util/errutil"
|
||||||
"github.com/grafana/grafana/pkg/web/webtest"
|
"github.com/grafana/grafana/pkg/web/webtest"
|
||||||
)
|
)
|
||||||
@ -72,7 +73,7 @@ func (ts *fakeOAuthTokenService) InvalidateOAuthTokens(ctx context.Context, usr
|
|||||||
// `/ds/query` endpoint test
|
// `/ds/query` endpoint test
|
||||||
func TestAPIEndpoint_Metrics_QueryMetricsV2(t *testing.T) {
|
func TestAPIEndpoint_Metrics_QueryMetricsV2(t *testing.T) {
|
||||||
qds := query.ProvideService(
|
qds := query.ProvideService(
|
||||||
nil,
|
setting.NewCfg(),
|
||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
&fakePluginRequestValidator{},
|
&fakePluginRequestValidator{},
|
||||||
@ -121,7 +122,7 @@ func TestAPIEndpoint_Metrics_QueryMetricsV2(t *testing.T) {
|
|||||||
|
|
||||||
func TestAPIEndpoint_Metrics_PluginDecryptionFailure(t *testing.T) {
|
func TestAPIEndpoint_Metrics_PluginDecryptionFailure(t *testing.T) {
|
||||||
qds := query.ProvideService(
|
qds := query.ProvideService(
|
||||||
nil,
|
setting.NewCfg(),
|
||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
&fakePluginRequestValidator{},
|
&fakePluginRequestValidator{},
|
||||||
@ -284,7 +285,7 @@ func TestDataSourceQueryError(t *testing.T) {
|
|||||||
err := r.Add(context.Background(), p)
|
err := r.Add(context.Background(), p)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
hs.queryDataService = query.ProvideService(
|
hs.queryDataService = query.ProvideService(
|
||||||
nil,
|
setting.NewCfg(),
|
||||||
&fakeDatasources.FakeCacheService{},
|
&fakeDatasources.FakeCacheService{},
|
||||||
nil,
|
nil,
|
||||||
&fakePluginRequestValidator{},
|
&fakePluginRequestValidator{},
|
||||||
|
@ -14,6 +14,7 @@ import (
|
|||||||
|
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||||
|
"github.com/grafana/grafana/pkg/services/contexthandler"
|
||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
"github.com/grafana/grafana/pkg/services/datasources"
|
||||||
"github.com/grafana/grafana/pkg/util/proxyutil"
|
"github.com/grafana/grafana/pkg/util/proxyutil"
|
||||||
"github.com/grafana/grafana/pkg/web"
|
"github.com/grafana/grafana/pkg/web"
|
||||||
@ -117,7 +118,15 @@ func (hs *HTTPServer) makePluginResourceRequest(w http.ResponseWriter, req *http
|
|||||||
hs.log.Warn("failed to unpack JSONData in datasource instance settings", "err", err)
|
hs.log.Warn("failed to unpack JSONData in datasource instance settings", "err", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
proxyutil.ClearCookieHeader(req, keepCookieModel.KeepCookies)
|
|
||||||
|
list := contexthandler.AuthHTTPHeaderListFromContext(req.Context())
|
||||||
|
if list != nil {
|
||||||
|
for _, name := range list.Items {
|
||||||
|
req.Header.Del(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyutil.ClearCookieHeader(req, keepCookieModel.KeepCookies, []string{hs.Cfg.LoginCookieName})
|
||||||
proxyutil.PrepareProxyRequest(req)
|
proxyutil.PrepareProxyRequest(req)
|
||||||
|
|
||||||
body, err := io.ReadAll(req.Body)
|
body, err := io.ReadAll(req.Body)
|
||||||
|
@ -224,7 +224,7 @@ func (proxy *DataSourceProxy) director(req *http.Request) {
|
|||||||
|
|
||||||
applyUserHeader(proxy.cfg.SendUserHeader, req, proxy.ctx.SignedInUser)
|
applyUserHeader(proxy.cfg.SendUserHeader, req, proxy.ctx.SignedInUser)
|
||||||
|
|
||||||
proxyutil.ClearCookieHeader(req, proxy.ds.AllowedCookies())
|
proxyutil.ClearCookieHeader(req, proxy.ds.AllowedCookies(), []string{proxy.cfg.LoginCookieName})
|
||||||
req.Header.Set("User-Agent", fmt.Sprintf("Grafana/%s", setting.BuildVersion))
|
req.Header.Set("User-Agent", fmt.Sprintf("Grafana/%s", setting.BuildVersion))
|
||||||
|
|
||||||
jsonData := make(map[string]interface{})
|
jsonData := make(map[string]interface{})
|
||||||
|
@ -23,6 +23,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
|
"github.com/grafana/grafana/pkg/services/contexthandler"
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsettings"
|
"github.com/grafana/grafana/pkg/services/pluginsettings"
|
||||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||||
@ -313,6 +314,12 @@ func TestMakePluginResourceRequest(t *testing.T) {
|
|||||||
pluginClient: &fakePluginClient{},
|
pluginClient: &fakePluginClient{},
|
||||||
}
|
}
|
||||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
|
||||||
|
const customHeader = "X-CUSTOM"
|
||||||
|
req.Header.Set(customHeader, "val")
|
||||||
|
ctx := contexthandler.WithAuthHTTPHeader(req.Context(), customHeader)
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
resp := httptest.NewRecorder()
|
resp := httptest.NewRecorder()
|
||||||
pCtx := backend.PluginContext{}
|
pCtx := backend.PluginContext{}
|
||||||
err := hs.makePluginResourceRequest(resp, req, pCtx)
|
err := hs.makePluginResourceRequest(resp, req, pCtx)
|
||||||
@ -325,6 +332,7 @@ func TestMakePluginResourceRequest(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
require.Equal(t, "sandbox", resp.Header().Get("Content-Security-Policy"))
|
require.Equal(t, "sandbox", resp.Header().Get("Content-Security-Policy"))
|
||||||
|
require.Empty(t, req.Header.Get(customHeader))
|
||||||
}
|
}
|
||||||
|
|
||||||
func callGetPluginAsset(sc *scenarioContext) {
|
func callGetPluginAsset(sc *scenarioContext) {
|
||||||
|
@ -13,6 +13,7 @@ func TestForwardedCookiesMiddleware(t *testing.T) {
|
|||||||
tcs := []struct {
|
tcs := []struct {
|
||||||
desc string
|
desc string
|
||||||
allowedCookies []string
|
allowedCookies []string
|
||||||
|
disallowedCookies []string
|
||||||
expectedCookieHeader string
|
expectedCookieHeader string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@ -30,6 +31,12 @@ func TestForwardedCookiesMiddleware(t *testing.T) {
|
|||||||
allowedCookies: []string{"c1", "c3"},
|
allowedCookies: []string{"c1", "c3"},
|
||||||
expectedCookieHeader: "c1=1; c3=3",
|
expectedCookieHeader: "c1=1; c3=3",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
desc: "When provided with allowed and not allowed cookies should populate Cookie header",
|
||||||
|
allowedCookies: []string{"c1", "c3"},
|
||||||
|
disallowedCookies: []string{"c1"},
|
||||||
|
expectedCookieHeader: "c3=3",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tcs {
|
for _, tc := range tcs {
|
||||||
@ -41,7 +48,7 @@ func TestForwardedCookiesMiddleware(t *testing.T) {
|
|||||||
{Name: "c2", Value: "2"},
|
{Name: "c2", Value: "2"},
|
||||||
{Name: "c3", Value: "3"},
|
{Name: "c3", Value: "3"},
|
||||||
}
|
}
|
||||||
mw := httpclientprovider.ForwardedCookiesMiddleware(forwarded, tc.allowedCookies)
|
mw := httpclientprovider.ForwardedCookiesMiddleware(forwarded, tc.allowedCookies, tc.disallowedCookies)
|
||||||
opts := httpclient.Options{}
|
opts := httpclient.Options{}
|
||||||
rt := mw.CreateMiddleware(opts, finalRoundTripper)
|
rt := mw.CreateMiddleware(opts, finalRoundTripper)
|
||||||
require.NotNil(t, rt)
|
require.NotNil(t, rt)
|
||||||
|
@ -11,13 +11,13 @@ const ForwardedCookiesMiddlewareName = "forwarded-cookies"
|
|||||||
|
|
||||||
// ForwardedCookiesMiddleware middleware that sets Cookie header on the
|
// ForwardedCookiesMiddleware middleware that sets Cookie header on the
|
||||||
// outgoing request, if forwarded cookies configured/provided.
|
// outgoing request, if forwarded cookies configured/provided.
|
||||||
func ForwardedCookiesMiddleware(forwardedCookies []*http.Cookie, allowedCookies []string) httpclient.Middleware {
|
func ForwardedCookiesMiddleware(forwardedCookies []*http.Cookie, allowedCookies []string, disallowedCookies []string) httpclient.Middleware {
|
||||||
return httpclient.NamedMiddlewareFunc(ForwardedCookiesMiddlewareName, func(opts httpclient.Options, next http.RoundTripper) http.RoundTripper {
|
return httpclient.NamedMiddlewareFunc(ForwardedCookiesMiddlewareName, func(opts httpclient.Options, next http.RoundTripper) http.RoundTripper {
|
||||||
return httpclient.RoundTripperFunc(func(req *http.Request) (*http.Response, error) {
|
return httpclient.RoundTripperFunc(func(req *http.Request) (*http.Response, error) {
|
||||||
for _, cookie := range forwardedCookies {
|
for _, cookie := range forwardedCookies {
|
||||||
req.AddCookie(cookie)
|
req.AddCookie(cookie)
|
||||||
}
|
}
|
||||||
proxyutil.ClearCookieHeader(req, allowedCookies)
|
proxyutil.ClearCookieHeader(req, allowedCookies, disallowedCookies)
|
||||||
return next.RoundTrip(req)
|
return next.RoundTrip(req)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -38,6 +38,9 @@ func TestMiddlewareBasicAuth(t *testing.T) {
|
|||||||
assert.True(t, sc.context.IsSignedIn)
|
assert.True(t, sc.context.IsSignedIn)
|
||||||
assert.Equal(t, orgID, sc.context.OrgID)
|
assert.Equal(t, orgID, sc.context.OrgID)
|
||||||
assert.Equal(t, org.RoleEditor, sc.context.OrgRole)
|
assert.Equal(t, org.RoleEditor, sc.context.OrgRole)
|
||||||
|
list := contexthandler.AuthHTTPHeaderListFromContext(sc.context.Req.Context())
|
||||||
|
require.NotNil(t, list)
|
||||||
|
require.EqualValues(t, []string{"Authorization"}, list.Items)
|
||||||
}, configure)
|
}, configure)
|
||||||
|
|
||||||
middlewareScenario(t, "Handle auth", func(t *testing.T, sc *scenarioContext) {
|
middlewareScenario(t, "Handle auth", func(t *testing.T, sc *scenarioContext) {
|
||||||
@ -71,6 +74,9 @@ func TestMiddlewareBasicAuth(t *testing.T) {
|
|||||||
|
|
||||||
assert.True(t, sc.context.IsSignedIn)
|
assert.True(t, sc.context.IsSignedIn)
|
||||||
assert.Equal(t, id, sc.context.UserID)
|
assert.Equal(t, id, sc.context.UserID)
|
||||||
|
list := contexthandler.AuthHTTPHeaderListFromContext(sc.context.Req.Context())
|
||||||
|
require.NotNil(t, list)
|
||||||
|
require.EqualValues(t, []string{"Authorization"}, list.Items)
|
||||||
}, configure)
|
}, configure)
|
||||||
|
|
||||||
middlewareScenario(t, "Should return error if user is not found", func(t *testing.T, sc *scenarioContext) {
|
middlewareScenario(t, "Should return error if user is not found", func(t *testing.T, sc *scenarioContext) {
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/contexthandler"
|
"github.com/grafana/grafana/pkg/services/contexthandler"
|
||||||
@ -75,6 +76,9 @@ func TestMiddlewareJWTAuth(t *testing.T) {
|
|||||||
assert.Equal(t, orgID, sc.context.OrgID)
|
assert.Equal(t, orgID, sc.context.OrgID)
|
||||||
assert.Equal(t, id, sc.context.UserID)
|
assert.Equal(t, id, sc.context.UserID)
|
||||||
assert.Equal(t, myUsername, sc.context.Login)
|
assert.Equal(t, myUsername, sc.context.Login)
|
||||||
|
list := contexthandler.AuthHTTPHeaderListFromContext(sc.context.Req.Context())
|
||||||
|
require.NotNil(t, list)
|
||||||
|
require.EqualValues(t, []string{sc.cfg.JWTAuthHeaderName}, list.Items)
|
||||||
}, configure, configureUsernameClaim)
|
}, configure, configureUsernameClaim)
|
||||||
|
|
||||||
middlewareScenario(t, "Valid token with bearer in authorization header", func(t *testing.T, sc *scenarioContext) {
|
middlewareScenario(t, "Valid token with bearer in authorization header", func(t *testing.T, sc *scenarioContext) {
|
||||||
|
@ -538,6 +538,11 @@ func TestMiddlewareContext(t *testing.T) {
|
|||||||
assert.True(t, sc.context.IsSignedIn)
|
assert.True(t, sc.context.IsSignedIn)
|
||||||
assert.Equal(t, userID, sc.context.UserID)
|
assert.Equal(t, userID, sc.context.UserID)
|
||||||
assert.Equal(t, orgID, sc.context.OrgID)
|
assert.Equal(t, orgID, sc.context.OrgID)
|
||||||
|
list := contexthandler.AuthHTTPHeaderListFromContext(sc.context.Req.Context())
|
||||||
|
require.NotNil(t, list)
|
||||||
|
require.Contains(t, list.Items, sc.cfg.AuthProxyHeaderName)
|
||||||
|
require.Contains(t, list.Items, "X-WEBAUTH-GROUPS")
|
||||||
|
require.Contains(t, list.Items, "X-WEBAUTH-ROLE")
|
||||||
}, func(cfg *setting.Cfg) {
|
}, func(cfg *setting.Cfg) {
|
||||||
configure(cfg)
|
configure(cfg)
|
||||||
cfg.LDAPEnabled = false
|
cfg.LDAPEnabled = false
|
||||||
|
@ -298,7 +298,7 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Load an unsigned plugin with modified signature (production)",
|
name: "Load a plugin with v1 manifest should return signatureInvalid",
|
||||||
class: plugins.External,
|
class: plugins.External,
|
||||||
cfg: &config.Cfg{},
|
cfg: &config.Cfg{},
|
||||||
pluginPaths: []string{"../testdata/lacking-files"},
|
pluginPaths: []string{"../testdata/lacking-files"},
|
||||||
@ -306,12 +306,12 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
pluginErrors: map[string]*plugins.Error{
|
pluginErrors: map[string]*plugins.Error{
|
||||||
"test-datasource": {
|
"test-datasource": {
|
||||||
PluginID: "test-datasource",
|
PluginID: "test-datasource",
|
||||||
ErrorCode: "signatureModified",
|
ErrorCode: "signatureInvalid",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Load an unsigned plugin with modified signature using PluginsAllowUnsigned config (production) still includes a signing error",
|
name: "Load a plugin with v1 manifest using PluginsAllowUnsigned config (production) should return signatureInvali",
|
||||||
class: plugins.External,
|
class: plugins.External,
|
||||||
cfg: &config.Cfg{
|
cfg: &config.Cfg{
|
||||||
PluginsAllowUnsigned: []string{"test-datasource"},
|
PluginsAllowUnsigned: []string{"test-datasource"},
|
||||||
@ -321,7 +321,7 @@ func TestLoader_Load(t *testing.T) {
|
|||||||
pluginErrors: map[string]*plugins.Error{
|
pluginErrors: map[string]*plugins.Error{
|
||||||
"test-datasource": {
|
"test-datasource": {
|
||||||
PluginID: "test-datasource",
|
PluginID: "test-datasource",
|
||||||
ErrorCode: "signatureModified",
|
ErrorCode: "signatureInvalid",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -132,6 +132,12 @@ func Calculate(mlog log.Logger, plugin *plugins.Plugin) (plugins.Signature, erro
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !manifest.isV2() {
|
||||||
|
return plugins.Signature{
|
||||||
|
Status: plugins.SignatureInvalid,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Make sure the versions all match
|
// Make sure the versions all match
|
||||||
if manifest.Plugin != plugin.ID || manifest.Version != plugin.Info.Version {
|
if manifest.Plugin != plugin.ID || manifest.Version != plugin.Info.Version {
|
||||||
return plugins.Signature{
|
return plugins.Signature{
|
||||||
@ -167,7 +173,6 @@ func Calculate(mlog log.Logger, plugin *plugins.Plugin) (plugins.Signature, erro
|
|||||||
manifestFiles[p] = struct{}{}
|
manifestFiles[p] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
if manifest.isV2() {
|
|
||||||
// Track files missing from the manifest
|
// Track files missing from the manifest
|
||||||
var unsignedFiles []string
|
var unsignedFiles []string
|
||||||
for _, f := range pluginFiles {
|
for _, f := range pluginFiles {
|
||||||
@ -182,7 +187,6 @@ func Calculate(mlog log.Logger, plugin *plugins.Plugin) (plugins.Signature, erro
|
|||||||
Status: plugins.SignatureModified,
|
Status: plugins.SignatureModified,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
mlog.Debug("Plugin signature valid", "id", plugin.ID)
|
mlog.Debug("Plugin signature valid", "id", plugin.ID)
|
||||||
return plugins.Signature{
|
return plugins.Signature{
|
||||||
|
@ -142,6 +142,9 @@ func (h *ContextHandler) initContextWithJWT(ctx *models.ReqContext, orgId int64)
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newCtx := WithAuthHTTPHeader(ctx.Req.Context(), h.Cfg.JWTAuthHeaderName)
|
||||||
|
*ctx.Req = *ctx.Req.WithContext(newCtx)
|
||||||
|
|
||||||
ctx.SignedInUser = queryResult
|
ctx.SignedInUser = queryResult
|
||||||
ctx.IsSignedIn = true
|
ctx.IsSignedIn = true
|
||||||
|
|
||||||
|
@ -267,6 +267,9 @@ func (h *ContextHandler) initContextWithAPIKey(reqContext *models.ReqContext) bo
|
|||||||
_, span := h.tracer.Start(reqContext.Req.Context(), "initContextWithAPIKey")
|
_, span := h.tracer.Start(reqContext.Req.Context(), "initContextWithAPIKey")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
|
ctx := WithAuthHTTPHeader(reqContext.Req.Context(), "Authorization")
|
||||||
|
*reqContext.Req = *reqContext.Req.WithContext(ctx)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
apikey *apikey.APIKey
|
apikey *apikey.APIKey
|
||||||
errKey error
|
errKey error
|
||||||
@ -356,7 +359,7 @@ func (h *ContextHandler) initContextWithBasicAuth(reqContext *models.ReqContext,
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, span := h.tracer.Start(reqContext.Req.Context(), "initContextWithBasicAuth")
|
_, span := h.tracer.Start(reqContext.Req.Context(), "initContextWithBasicAuth")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
username, password, err := util.DecodeBasicAuthHeader(header)
|
username, password, err := util.DecodeBasicAuthHeader(header)
|
||||||
@ -365,12 +368,15 @@ func (h *ContextHandler) initContextWithBasicAuth(reqContext *models.ReqContext,
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx := WithAuthHTTPHeader(reqContext.Req.Context(), "Authorization")
|
||||||
|
*reqContext.Req = *reqContext.Req.WithContext(ctx)
|
||||||
|
|
||||||
authQuery := models.LoginUserQuery{
|
authQuery := models.LoginUserQuery{
|
||||||
Username: username,
|
Username: username,
|
||||||
Password: password,
|
Password: password,
|
||||||
Cfg: h.Cfg,
|
Cfg: h.Cfg,
|
||||||
}
|
}
|
||||||
if err := h.authenticator.AuthenticateUser(reqContext.Req.Context(), &authQuery); err != nil {
|
if err := h.authenticator.AuthenticateUser(ctx, &authQuery); err != nil {
|
||||||
reqContext.Logger.Debug(
|
reqContext.Logger.Debug(
|
||||||
"Failed to authorize the user",
|
"Failed to authorize the user",
|
||||||
"username", username,
|
"username", username,
|
||||||
@ -651,6 +657,15 @@ func (h *ContextHandler) initContextWithAuthProxy(reqContext *models.ReqContext,
|
|||||||
|
|
||||||
logger.Debug("Successfully got user info", "userID", user.UserID, "username", user.Login)
|
logger.Debug("Successfully got user info", "userID", user.UserID, "username", user.Login)
|
||||||
|
|
||||||
|
ctx := WithAuthHTTPHeader(reqContext.Req.Context(), h.Cfg.AuthProxyHeaderName)
|
||||||
|
for _, header := range h.Cfg.AuthProxyHeaders {
|
||||||
|
if header != "" {
|
||||||
|
ctx = WithAuthHTTPHeader(ctx, header)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*reqContext.Req = *reqContext.Req.WithContext(ctx)
|
||||||
|
|
||||||
// Add user info to context
|
// Add user info to context
|
||||||
reqContext.SignedInUser = user
|
reqContext.SignedInUser = user
|
||||||
reqContext.IsSignedIn = true
|
reqContext.IsSignedIn = true
|
||||||
@ -670,3 +685,38 @@ func (h *ContextHandler) initContextWithAuthProxy(reqContext *models.ReqContext,
|
|||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type authHTTPHeaderListContextKey struct{}
|
||||||
|
|
||||||
|
var authHTTPHeaderListKey = authHTTPHeaderListContextKey{}
|
||||||
|
|
||||||
|
// AuthHTTPHeaderList used to record HTTP headers that being when verifying authentication
|
||||||
|
// of an incoming HTTP request.
|
||||||
|
type AuthHTTPHeaderList struct {
|
||||||
|
Items []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAuthHTTPHeader returns a copy of parent in which the named HTTP header will be included
|
||||||
|
// and later retrievable by AuthHTTPHeaderListFromContext.
|
||||||
|
func WithAuthHTTPHeader(parent context.Context, name string) context.Context {
|
||||||
|
list := AuthHTTPHeaderListFromContext(parent)
|
||||||
|
|
||||||
|
if list == nil {
|
||||||
|
list = &AuthHTTPHeaderList{
|
||||||
|
Items: []string{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
list.Items = append(list.Items, name)
|
||||||
|
|
||||||
|
return context.WithValue(parent, authHTTPHeaderListKey, list)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthHTTPHeaderListFromContext returns the AuthHTTPHeaderList in a context.Context, if any,
|
||||||
|
// and will include any HTTP headers used when verifying authentication of an incoming HTTP request.
|
||||||
|
func AuthHTTPHeaderListFromContext(c context.Context) *AuthHTTPHeaderList {
|
||||||
|
if list, ok := c.Value(authHTTPHeaderListKey).(*AuthHTTPHeaderList); ok {
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -192,7 +192,7 @@ func (s *Service) handleQuerySingleDatasource(ctx context.Context, user *user.Si
|
|||||||
middlewares := []httpclient.Middleware{}
|
middlewares := []httpclient.Middleware{}
|
||||||
if parsedReq.httpRequest != nil {
|
if parsedReq.httpRequest != nil {
|
||||||
middlewares = append(middlewares,
|
middlewares = append(middlewares,
|
||||||
httpclientprovider.ForwardedCookiesMiddleware(parsedReq.httpRequest.Cookies(), ds.AllowedCookies()),
|
httpclientprovider.ForwardedCookiesMiddleware(parsedReq.httpRequest.Cookies(), ds.AllowedCookies(), []string{s.cfg.LoginCookieName}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -209,7 +209,7 @@ func (s *Service) handleQuerySingleDatasource(ctx context.Context, user *user.Si
|
|||||||
}
|
}
|
||||||
|
|
||||||
if parsedReq.httpRequest != nil {
|
if parsedReq.httpRequest != nil {
|
||||||
proxyutil.ClearCookieHeader(parsedReq.httpRequest, ds.AllowedCookies())
|
proxyutil.ClearCookieHeader(parsedReq.httpRequest, ds.AllowedCookies(), []string{s.cfg.LoginCookieName})
|
||||||
if cookieStr := parsedReq.httpRequest.Header.Get("Cookie"); cookieStr != "" {
|
if cookieStr := parsedReq.httpRequest.Header.Get("Cookie"); cookieStr != "" {
|
||||||
req.Headers["Cookie"] = cookieStr
|
req.Headers["Cookie"] = cookieStr
|
||||||
}
|
}
|
||||||
|
@ -353,6 +353,29 @@ func TestQueryData(t *testing.T) {
|
|||||||
|
|
||||||
require.Equal(t, map[string]string{"Cookie": "bar=rab; foo=oof"}, tc.pluginContext.req.Headers)
|
require.Equal(t, map[string]string{"Cookie": "bar=rab; foo=oof"}, tc.pluginContext.req.Headers)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("it doesn't adds cookie header to the request when keepCookies configured with login cookie name", func(t *testing.T) {
|
||||||
|
tc := setup(t)
|
||||||
|
tc.queryService.cfg.LoginCookieName = "grafana_session"
|
||||||
|
json, err := simplejson.NewJson([]byte(`{"keepCookies": [ "grafana_session", "bar" ]}`))
|
||||||
|
require.NoError(t, err)
|
||||||
|
tc.dataSourceCache.ds.JsonData = json
|
||||||
|
|
||||||
|
metricReq := metricRequest()
|
||||||
|
httpReq, err := http.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
httpReq.AddCookie(&http.Cookie{Name: "a"})
|
||||||
|
httpReq.AddCookie(&http.Cookie{Name: "bar", Value: "rab"})
|
||||||
|
httpReq.AddCookie(&http.Cookie{Name: "b"})
|
||||||
|
httpReq.AddCookie(&http.Cookie{Name: "foo", Value: "oof"})
|
||||||
|
httpReq.AddCookie(&http.Cookie{Name: "c"})
|
||||||
|
httpReq.AddCookie(&http.Cookie{Name: tc.queryService.cfg.LoginCookieName, Value: "val"})
|
||||||
|
metricReq.HTTPRequest = httpReq
|
||||||
|
_, err = tc.queryService.QueryData(context.Background(), tc.signedInUser, true, metricReq)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, map[string]string{"Cookie": "bar=rab"}, tc.pluginContext.req.Headers)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func setup(t *testing.T) *testContext {
|
func setup(t *testing.T) *testContext {
|
||||||
@ -372,7 +395,7 @@ func setup(t *testing.T) *testContext {
|
|||||||
SimulatePluginFailure: false,
|
SimulatePluginFailure: false,
|
||||||
}
|
}
|
||||||
exprService := expr.ProvideService(&setting.Cfg{ExpressionsEnabled: true}, pc, fakeDatasourceService)
|
exprService := expr.ProvideService(&setting.Cfg{ExpressionsEnabled: true}, pc, fakeDatasourceService)
|
||||||
queryService := ProvideService(nil, dc, exprService, rv, ds, pc, tc) // provider belonging to this package
|
queryService := ProvideService(setting.NewCfg(), dc, exprService, rv, ds, pc, tc) // provider belonging to this package
|
||||||
return &testContext{
|
return &testContext{
|
||||||
pluginContext: pc,
|
pluginContext: pc,
|
||||||
secretStore: ss,
|
secretStore: ss,
|
||||||
|
@ -3,6 +3,7 @@ package proxyutil
|
|||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sort"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PrepareProxyRequest prepares a request for being proxied.
|
// PrepareProxyRequest prepares a request for being proxied.
|
||||||
@ -26,19 +27,31 @@ func PrepareProxyRequest(req *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClearCookieHeader clear cookie header, except for cookies specified to be kept.
|
// ClearCookieHeader clear cookie header, except for cookies specified to be kept (keepCookiesNames) if not in skipCookiesNames.
|
||||||
func ClearCookieHeader(req *http.Request, keepCookiesNames []string) {
|
func ClearCookieHeader(req *http.Request, keepCookiesNames []string, skipCookiesNames []string) {
|
||||||
var keepCookies []*http.Cookie
|
keepCookies := map[string]*http.Cookie{}
|
||||||
for _, c := range req.Cookies() {
|
for _, c := range req.Cookies() {
|
||||||
for _, v := range keepCookiesNames {
|
for _, v := range keepCookiesNames {
|
||||||
if c.Name == v {
|
if c.Name == v {
|
||||||
keepCookies = append(keepCookies, c)
|
keepCookies[c.Name] = c
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, v := range skipCookiesNames {
|
||||||
|
delete(keepCookies, v)
|
||||||
|
}
|
||||||
|
|
||||||
req.Header.Del("Cookie")
|
req.Header.Del("Cookie")
|
||||||
for _, c := range keepCookies {
|
|
||||||
|
sortedCookies := []string{}
|
||||||
|
for name := range keepCookies {
|
||||||
|
sortedCookies = append(sortedCookies, name)
|
||||||
|
}
|
||||||
|
sort.Strings(sortedCookies)
|
||||||
|
|
||||||
|
for _, name := range sortedCookies {
|
||||||
|
c := keepCookies[name]
|
||||||
req.AddCookie(c)
|
req.AddCookie(c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@ func TestClearCookieHeader(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
req.AddCookie(&http.Cookie{Name: "cookie"})
|
req.AddCookie(&http.Cookie{Name: "cookie"})
|
||||||
|
|
||||||
ClearCookieHeader(req, nil)
|
ClearCookieHeader(req, nil, nil)
|
||||||
require.NotContains(t, req.Header, "Cookie")
|
require.NotContains(t, req.Header, "Cookie")
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -60,8 +60,20 @@ func TestClearCookieHeader(t *testing.T) {
|
|||||||
req.AddCookie(&http.Cookie{Name: "cookie2"})
|
req.AddCookie(&http.Cookie{Name: "cookie2"})
|
||||||
req.AddCookie(&http.Cookie{Name: "cookie3"})
|
req.AddCookie(&http.Cookie{Name: "cookie3"})
|
||||||
|
|
||||||
ClearCookieHeader(req, []string{"cookie1", "cookie3"})
|
ClearCookieHeader(req, []string{"cookie1", "cookie3"}, nil)
|
||||||
require.Contains(t, req.Header, "Cookie")
|
require.Contains(t, req.Header, "Cookie")
|
||||||
require.Equal(t, "cookie1=; cookie3=", req.Header.Get("Cookie"))
|
require.Equal(t, "cookie1=; cookie3=", req.Header.Get("Cookie"))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("Clear cookie header with cookies to keep and skip should clear Cookie header and keep cookies", func(t *testing.T) {
|
||||||
|
req, err := http.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
req.AddCookie(&http.Cookie{Name: "cookie1"})
|
||||||
|
req.AddCookie(&http.Cookie{Name: "cookie2"})
|
||||||
|
req.AddCookie(&http.Cookie{Name: "cookie3"})
|
||||||
|
|
||||||
|
ClearCookieHeader(req, []string{"cookie1", "cookie3"}, []string{"cookie3"})
|
||||||
|
require.Contains(t, req.Header, "Cookie")
|
||||||
|
require.Equal(t, "cookie1=", req.Header.Get("Cookie"))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
glog "github.com/grafana/grafana/pkg/infra/log"
|
glog "github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
"github.com/grafana/grafana/pkg/services/contexthandler"
|
||||||
)
|
)
|
||||||
|
|
||||||
// StatusClientClosedRequest A non-standard status code introduced by nginx
|
// StatusClientClosedRequest A non-standard status code introduced by nginx
|
||||||
@ -66,6 +67,13 @@ func NewReverseProxy(logger glog.Logger, director func(*http.Request), opts ...R
|
|||||||
// wrapDirector wraps a director and adds additional functionality.
|
// wrapDirector wraps a director and adds additional functionality.
|
||||||
func wrapDirector(d func(*http.Request)) func(req *http.Request) {
|
func wrapDirector(d func(*http.Request)) func(req *http.Request) {
|
||||||
return func(req *http.Request) {
|
return func(req *http.Request) {
|
||||||
|
list := contexthandler.AuthHTTPHeaderListFromContext(req.Context())
|
||||||
|
if list != nil {
|
||||||
|
for _, name := range list.Items {
|
||||||
|
req.Header.Del(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
d(req)
|
d(req)
|
||||||
PrepareProxyRequest(req)
|
PrepareProxyRequest(req)
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
"github.com/grafana/grafana/pkg/services/contexthandler"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -30,6 +31,11 @@ func TestReverseProxy(t *testing.T) {
|
|||||||
req.Header.Set("Referer", "https://test.com/api")
|
req.Header.Set("Referer", "https://test.com/api")
|
||||||
req.RemoteAddr = "10.0.0.1"
|
req.RemoteAddr = "10.0.0.1"
|
||||||
|
|
||||||
|
const customHeader = "X-CUSTOM"
|
||||||
|
req.Header.Set(customHeader, "val")
|
||||||
|
ctx := contexthandler.WithAuthHTTPHeader(req.Context(), customHeader)
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
rp := NewReverseProxy(log.New("test"), func(req *http.Request) {
|
rp := NewReverseProxy(log.New("test"), func(req *http.Request) {
|
||||||
req.Header.Set("X-KEY", "value")
|
req.Header.Set("X-KEY", "value")
|
||||||
})
|
})
|
||||||
@ -49,6 +55,7 @@ func TestReverseProxy(t *testing.T) {
|
|||||||
require.Empty(t, resp.Cookies())
|
require.Empty(t, resp.Cookies())
|
||||||
require.Equal(t, "sandbox", resp.Header.Get("Content-Security-Policy"))
|
require.Equal(t, "sandbox", resp.Header.Get("Content-Security-Policy"))
|
||||||
require.NoError(t, resp.Body.Close())
|
require.NoError(t, resp.Body.Close())
|
||||||
|
require.Empty(t, actualReq.Header.Get(customHeader))
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("When proxying a request using WithModifyResponse should call it before default ModifyResponse func", func(t *testing.T) {
|
t.Run("When proxying a request using WithModifyResponse should call it before default ModifyResponse func", func(t *testing.T) {
|
||||||
|
Loading…
Reference in New Issue
Block a user