2015-05-01 04:55:59 -05:00
|
|
|
package middleware
|
|
|
|
|
|
|
|
import (
|
2022-10-18 11:17:28 -05:00
|
|
|
"errors"
|
2015-05-01 04:55:59 -05:00
|
|
|
"net/http"
|
2015-05-01 15:26:16 -05:00
|
|
|
"path/filepath"
|
2015-05-01 04:55:59 -05:00
|
|
|
"testing"
|
|
|
|
|
2022-09-27 06:58:49 -05:00
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
|
2023-02-02 07:36:16 -06:00
|
|
|
"github.com/grafana/grafana-plugin-sdk-go/backend/gtime"
|
|
|
|
|
2019-05-06 02:22:59 -05:00
|
|
|
"github.com/grafana/grafana/pkg/api/dtos"
|
2020-12-15 12:09:04 -06:00
|
|
|
"github.com/grafana/grafana/pkg/infra/fs"
|
2021-01-12 00:42:32 -06:00
|
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
2022-01-20 04:10:12 -06:00
|
|
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
2023-08-09 01:54:52 -05:00
|
|
|
"github.com/grafana/grafana/pkg/services/authn"
|
2022-12-02 08:10:03 -06:00
|
|
|
"github.com/grafana/grafana/pkg/services/authn/authntest"
|
2020-12-11 04:44:44 -06:00
|
|
|
"github.com/grafana/grafana/pkg/services/contexthandler"
|
2023-01-27 01:50:36 -06:00
|
|
|
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
2022-11-14 09:47:46 -06:00
|
|
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
2022-09-22 15:04:48 -05:00
|
|
|
"github.com/grafana/grafana/pkg/services/navtree"
|
2022-08-04 08:44:14 -05:00
|
|
|
"github.com/grafana/grafana/pkg/services/user/usertest"
|
2015-05-02 02:24:56 -05:00
|
|
|
"github.com/grafana/grafana/pkg/setting"
|
2021-10-11 07:30:59 -05:00
|
|
|
"github.com/grafana/grafana/pkg/web"
|
2015-05-01 04:55:59 -05:00
|
|
|
)
|
|
|
|
|
2019-06-18 13:24:23 -05:00
|
|
|
func TestMiddleWareSecurityHeaders(t *testing.T) {
|
2020-12-04 04:09:32 -06:00
|
|
|
middlewareScenario(t, "middleware should get correct x-xss-protection header", func(t *testing.T, sc *scenarioContext) {
|
2020-12-03 01:28:54 -06:00
|
|
|
sc.fakeReq("GET", "/api/").exec()
|
|
|
|
assert.Equal(t, "1; mode=block", sc.resp.Header().Get("X-XSS-Protection"))
|
2020-12-11 04:44:44 -06:00
|
|
|
}, func(cfg *setting.Cfg) {
|
|
|
|
cfg.XSSProtectionHeader = true
|
2020-12-03 01:28:54 -06:00
|
|
|
})
|
2019-06-18 13:24:23 -05:00
|
|
|
|
2020-12-04 04:09:32 -06:00
|
|
|
middlewareScenario(t, "middleware should not get x-xss-protection when disabled", func(t *testing.T, sc *scenarioContext) {
|
2020-12-03 01:28:54 -06:00
|
|
|
sc.fakeReq("GET", "/api/").exec()
|
|
|
|
assert.Empty(t, sc.resp.Header().Get("X-XSS-Protection"))
|
2020-12-11 04:44:44 -06:00
|
|
|
}, func(cfg *setting.Cfg) {
|
|
|
|
cfg.XSSProtectionHeader = false
|
2020-12-03 01:28:54 -06:00
|
|
|
})
|
2019-06-18 13:24:23 -05:00
|
|
|
|
2020-12-04 04:09:32 -06:00
|
|
|
middlewareScenario(t, "middleware should add correct Strict-Transport-Security header", func(t *testing.T, sc *scenarioContext) {
|
2020-12-03 01:28:54 -06:00
|
|
|
sc.fakeReq("GET", "/api/").exec()
|
|
|
|
assert.Equal(t, "max-age=64000", sc.resp.Header().Get("Strict-Transport-Security"))
|
2020-12-11 04:44:44 -06:00
|
|
|
sc.cfg.StrictTransportSecurityPreload = true
|
2020-12-03 01:28:54 -06:00
|
|
|
sc.fakeReq("GET", "/api/").exec()
|
|
|
|
assert.Equal(t, "max-age=64000; preload", sc.resp.Header().Get("Strict-Transport-Security"))
|
2020-12-11 04:44:44 -06:00
|
|
|
sc.cfg.StrictTransportSecuritySubDomains = true
|
2020-12-03 01:28:54 -06:00
|
|
|
sc.fakeReq("GET", "/api/").exec()
|
|
|
|
assert.Equal(t, "max-age=64000; preload; includeSubDomains", sc.resp.Header().Get("Strict-Transport-Security"))
|
2020-12-11 04:44:44 -06:00
|
|
|
}, func(cfg *setting.Cfg) {
|
|
|
|
cfg.StrictTransportSecurity = true
|
|
|
|
cfg.StrictTransportSecurityMaxAge = 64000
|
2019-06-18 13:24:23 -05:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-11-16 11:11:26 -06:00
|
|
|
func TestMiddleWareContentSecurityPolicyHeaders(t *testing.T) {
|
|
|
|
policy := `script-src 'self' 'strict-dynamic' 'nonce-[^']+';connect-src 'self' ws://localhost:3000/ wss://localhost:3000/;`
|
|
|
|
|
|
|
|
middlewareScenario(t, "middleware should add Content-Security-Policy", func(t *testing.T, sc *scenarioContext) {
|
|
|
|
sc.fakeReq("GET", "/api/").exec()
|
|
|
|
assert.Regexp(t, policy, sc.resp.Header().Get("Content-Security-Policy"))
|
|
|
|
}, func(cfg *setting.Cfg) {
|
|
|
|
cfg.CSPEnabled = true
|
|
|
|
cfg.CSPTemplate = "script-src 'self' 'strict-dynamic' $NONCE;connect-src 'self' ws://$ROOT_PATH wss://$ROOT_PATH;"
|
|
|
|
cfg.AppURL = "http://localhost:3000/"
|
|
|
|
})
|
|
|
|
|
|
|
|
middlewareScenario(t, "middleware should add Content-Security-Policy-Report-Only", func(t *testing.T, sc *scenarioContext) {
|
|
|
|
sc.fakeReq("GET", "/api/").exec()
|
|
|
|
assert.Regexp(t, policy, sc.resp.Header().Get("Content-Security-Policy-Report-Only"))
|
|
|
|
}, func(cfg *setting.Cfg) {
|
|
|
|
cfg.CSPReportOnlyEnabled = true
|
|
|
|
cfg.CSPReportOnlyTemplate = "script-src 'self' 'strict-dynamic' $NONCE;connect-src 'self' ws://$ROOT_PATH wss://$ROOT_PATH;"
|
|
|
|
cfg.AppURL = "http://localhost:3000/"
|
|
|
|
})
|
|
|
|
|
|
|
|
middlewareScenario(t, "middleware can add both CSP and CSP-Report-Only", func(t *testing.T, sc *scenarioContext) {
|
|
|
|
sc.fakeReq("GET", "/api/").exec()
|
|
|
|
|
|
|
|
cspHeader := sc.resp.Header().Get("Content-Security-Policy")
|
|
|
|
cspReportOnlyHeader := sc.resp.Header().Get("Content-Security-Policy-Report-Only")
|
|
|
|
|
|
|
|
assert.Regexp(t, policy, cspHeader)
|
|
|
|
assert.Regexp(t, policy, cspReportOnlyHeader)
|
|
|
|
|
|
|
|
// assert CSP-Report-Only reuses the same nonce as CSP
|
|
|
|
assert.Equal(t, cspHeader, cspReportOnlyHeader)
|
|
|
|
}, func(cfg *setting.Cfg) {
|
|
|
|
cfg.CSPEnabled = true
|
|
|
|
cfg.CSPTemplate = "script-src 'self' 'strict-dynamic' $NONCE;connect-src 'self' ws://$ROOT_PATH wss://$ROOT_PATH;"
|
|
|
|
cfg.CSPReportOnlyEnabled = true
|
|
|
|
cfg.CSPReportOnlyTemplate = "script-src 'self' 'strict-dynamic' $NONCE;connect-src 'self' ws://$ROOT_PATH wss://$ROOT_PATH;"
|
|
|
|
cfg.AppURL = "http://localhost:3000/"
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2015-05-01 09:23:36 -05:00
|
|
|
func TestMiddlewareContext(t *testing.T) {
|
2023-01-25 08:09:27 -06:00
|
|
|
const noStore = "no-store"
|
2015-05-01 04:55:59 -05:00
|
|
|
|
2020-12-04 04:09:32 -06:00
|
|
|
middlewareScenario(t, "middleware should add context to injector", func(t *testing.T, sc *scenarioContext) {
|
2020-12-03 01:28:54 -06:00
|
|
|
sc.fakeReq("GET", "/").exec()
|
|
|
|
assert.NotNil(t, sc.context)
|
|
|
|
})
|
2015-05-01 04:55:59 -05:00
|
|
|
|
2020-12-04 04:09:32 -06:00
|
|
|
middlewareScenario(t, "Default middleware should allow get request", func(t *testing.T, sc *scenarioContext) {
|
2020-12-03 01:28:54 -06:00
|
|
|
sc.fakeReq("GET", "/").exec()
|
|
|
|
assert.Equal(t, 200, sc.resp.Code)
|
|
|
|
})
|
2015-05-01 09:23:36 -05:00
|
|
|
|
2020-12-04 04:09:32 -06:00
|
|
|
middlewareScenario(t, "middleware should add Cache-Control header for requests to API", func(t *testing.T, sc *scenarioContext) {
|
2020-12-03 01:28:54 -06:00
|
|
|
sc.fakeReq("GET", "/api/search").exec()
|
2023-01-25 08:09:27 -06:00
|
|
|
assert.Equal(t, noStore, sc.resp.Header().Get("Cache-Control"))
|
|
|
|
assert.Empty(t, sc.resp.Header().Get("Pragma"))
|
|
|
|
assert.Empty(t, sc.resp.Header().Get("Expires"))
|
2020-12-03 01:28:54 -06:00
|
|
|
})
|
2017-07-04 09:33:37 -05:00
|
|
|
|
2023-02-02 14:34:55 -06:00
|
|
|
middlewareScenario(t, "middleware should pass cache-control on resources with private cache control", func(t *testing.T, sc *scenarioContext) {
|
|
|
|
sc = sc.fakeReq("GET", "/api/datasources/1/resources/foo")
|
2023-02-27 14:55:22 -06:00
|
|
|
sc.resp.Header().Add("Cache-Control", "private, max-age=86400")
|
2023-02-02 14:34:55 -06:00
|
|
|
sc.resp.Header().Add("X-Grafana-Cache", "true")
|
|
|
|
sc.exec()
|
2023-02-27 14:55:22 -06:00
|
|
|
assert.Equal(t, "private, max-age=86400", sc.resp.Header().Get("Cache-Control"))
|
2023-02-02 14:34:55 -06:00
|
|
|
})
|
|
|
|
|
|
|
|
middlewareScenario(t, "middleware should not pass cache-control on resources with public cache control", func(t *testing.T, sc *scenarioContext) {
|
|
|
|
sc = sc.fakeReq("GET", "/api/datasources/1/resources/foo")
|
2023-02-27 14:55:22 -06:00
|
|
|
sc.resp.Header().Add("Cache-Control", "public, max-age=86400, private")
|
2023-02-02 14:34:55 -06:00
|
|
|
sc.resp.Header().Add("X-Grafana-Cache", "true")
|
|
|
|
sc.exec()
|
|
|
|
assert.Equal(t, noStore, sc.resp.Header().Get("Cache-Control"))
|
|
|
|
})
|
|
|
|
|
2020-12-04 04:09:32 -06:00
|
|
|
middlewareScenario(t, "middleware should not add Cache-Control header for requests to datasource proxy API", func(
|
|
|
|
t *testing.T, sc *scenarioContext) {
|
2020-12-03 01:28:54 -06:00
|
|
|
sc.fakeReq("GET", "/api/datasources/proxy/1/test").exec()
|
|
|
|
assert.Empty(t, sc.resp.Header().Get("Cache-Control"))
|
|
|
|
assert.Empty(t, sc.resp.Header().Get("Pragma"))
|
|
|
|
assert.Empty(t, sc.resp.Header().Get("Expires"))
|
|
|
|
})
|
2019-05-06 02:22:59 -05:00
|
|
|
|
2020-12-15 12:09:04 -06:00
|
|
|
middlewareScenario(t, "middleware should add Cache-Control header for requests with HTML response", func(
|
2020-12-04 04:09:32 -06:00
|
|
|
t *testing.T, sc *scenarioContext) {
|
2023-01-27 01:50:36 -06:00
|
|
|
sc.handlerFunc = func(c *contextmodel.ReqContext) {
|
2020-12-15 12:09:04 -06:00
|
|
|
t.Log("Handler called")
|
2020-12-03 01:28:54 -06:00
|
|
|
data := &dtos.IndexViewData{
|
|
|
|
User: &dtos.CurrentUser{},
|
2023-01-31 13:14:15 -06:00
|
|
|
Settings: &dtos.FrontendSettingsDTO{},
|
2022-09-28 01:29:35 -05:00
|
|
|
NavTree: &navtree.NavTreeRoot{},
|
2020-12-03 01:28:54 -06:00
|
|
|
}
|
2021-08-10 06:29:46 -05:00
|
|
|
t.Log("Calling HTML", "data", data)
|
2022-04-15 07:01:58 -05:00
|
|
|
c.HTML(http.StatusOK, "index-template", data)
|
2020-12-15 12:09:04 -06:00
|
|
|
t.Log("Returned HTML with code 200")
|
|
|
|
}
|
2020-12-03 01:28:54 -06:00
|
|
|
sc.fakeReq("GET", "/").exec()
|
2020-12-15 12:09:04 -06:00
|
|
|
require.Equal(t, 200, sc.resp.Code)
|
2023-01-25 08:09:27 -06:00
|
|
|
assert.Equal(t, noStore, sc.resp.Header().Get("Cache-Control"))
|
|
|
|
assert.Empty(t, sc.resp.Header().Get("Pragma"))
|
|
|
|
assert.Empty(t, sc.resp.Header().Get("Expires"))
|
2020-12-03 01:28:54 -06:00
|
|
|
})
|
2017-07-04 09:33:37 -05:00
|
|
|
|
2020-12-04 04:09:32 -06:00
|
|
|
middlewareScenario(t, "middleware should add X-Frame-Options header with deny for request when not allowing embedding", func(
|
|
|
|
t *testing.T, sc *scenarioContext) {
|
2020-12-03 01:28:54 -06:00
|
|
|
sc.fakeReq("GET", "/api/search").exec()
|
|
|
|
assert.Equal(t, "deny", sc.resp.Header().Get("X-Frame-Options"))
|
|
|
|
})
|
2019-05-06 02:56:23 -05:00
|
|
|
|
2020-12-04 04:09:32 -06:00
|
|
|
middlewareScenario(t, "middleware should not add X-Frame-Options header for request when allowing embedding", func(
|
|
|
|
t *testing.T, sc *scenarioContext) {
|
2020-12-03 01:28:54 -06:00
|
|
|
sc.fakeReq("GET", "/api/search").exec()
|
|
|
|
assert.Empty(t, sc.resp.Header().Get("X-Frame-Options"))
|
2020-12-11 04:44:44 -06:00
|
|
|
}, func(cfg *setting.Cfg) {
|
|
|
|
cfg.AllowEmbedding = true
|
2020-12-03 01:28:54 -06:00
|
|
|
})
|
2019-05-06 02:56:23 -05:00
|
|
|
|
2022-11-30 11:12:34 -06:00
|
|
|
middlewareScenario(t, "middleware should add custom response headers", func(t *testing.T, sc *scenarioContext) {
|
|
|
|
sc.fakeReq("GET", "/api/").exec()
|
|
|
|
assert.Regexp(t, "test", sc.resp.Header().Get("X-Custom-Header"))
|
|
|
|
assert.Regexp(t, "other-test", sc.resp.Header().Get("X-Other-Header"))
|
|
|
|
}, func(cfg *setting.Cfg) {
|
|
|
|
cfg.CustomResponseHeaders = map[string]string{
|
|
|
|
"X-Custom-Header": "test",
|
|
|
|
"X-Other-Header": "other-test",
|
|
|
|
}
|
|
|
|
})
|
2015-05-01 04:55:59 -05:00
|
|
|
}
|
2015-05-02 02:24:56 -05:00
|
|
|
|
2020-12-11 04:44:44 -06:00
|
|
|
func middlewareScenario(t *testing.T, desc string, fn scenarioFunc, cbs ...func(*setting.Cfg)) {
|
2020-12-03 01:28:54 -06:00
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
t.Run(desc, func(t *testing.T) {
|
2021-01-12 00:42:32 -06:00
|
|
|
logger := log.New("test")
|
|
|
|
|
2020-12-11 04:44:44 -06:00
|
|
|
loginMaxLifetime, err := gtime.ParseDuration("30d")
|
2020-11-02 12:26:19 -06:00
|
|
|
require.NoError(t, err)
|
2020-12-11 04:44:44 -06:00
|
|
|
cfg := setting.NewCfg()
|
|
|
|
cfg.LoginCookieName = "grafana_session"
|
|
|
|
cfg.LoginMaxLifetime = loginMaxLifetime
|
2020-12-15 12:09:04 -06:00
|
|
|
// Required when rendering errors
|
|
|
|
cfg.ErrTemplateName = "error-template"
|
2020-12-11 04:44:44 -06:00
|
|
|
for _, cb := range cbs {
|
|
|
|
cb(cfg)
|
|
|
|
}
|
2019-02-05 14:14:23 -06:00
|
|
|
|
2020-12-11 04:44:44 -06:00
|
|
|
sc := &scenarioContext{t: t, cfg: cfg}
|
2020-02-17 10:31:44 -06:00
|
|
|
viewsPath, err := filepath.Abs("../../public/views")
|
|
|
|
require.NoError(t, err)
|
2020-12-15 12:09:04 -06:00
|
|
|
exists, err := fs.Exists(viewsPath)
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Truef(t, exists, "Views directory should exist at %q", viewsPath)
|
2015-05-02 02:24:56 -05:00
|
|
|
|
2021-10-11 07:30:59 -05:00
|
|
|
sc.m = web.New()
|
2022-11-30 11:12:34 -06:00
|
|
|
sc.m.Use(AddCustomResponseHeaders(cfg))
|
2020-12-11 04:44:44 -06:00
|
|
|
sc.m.Use(AddDefaultResponseHeaders(cfg))
|
2022-11-16 11:11:26 -06:00
|
|
|
sc.m.UseMiddleware(ContentSecurityPolicy(cfg, logger))
|
2021-10-11 07:30:59 -05:00
|
|
|
sc.m.UseMiddleware(web.Renderer(viewsPath, "[[", "]]"))
|
2015-05-02 02:24:56 -05:00
|
|
|
|
2023-08-09 01:54:52 -05:00
|
|
|
// defalut to not authenticated request
|
|
|
|
sc.authnService = &authntest.FakeService{ExpectedErr: errors.New("no auth")}
|
2022-08-16 09:08:59 -05:00
|
|
|
sc.userService = usertest.NewUserServiceFake()
|
2023-08-09 01:54:52 -05:00
|
|
|
|
|
|
|
ctxHdlr := getContextHandler(t, cfg, sc.authnService)
|
2020-12-11 04:44:44 -06:00
|
|
|
sc.m.Use(ctxHdlr.Middleware)
|
2022-09-27 06:58:49 -05:00
|
|
|
sc.m.Use(OrgRedirect(sc.cfg, sc.userService))
|
2015-05-02 02:24:56 -05:00
|
|
|
|
2023-01-27 01:50:36 -06:00
|
|
|
sc.defaultHandler = func(c *contextmodel.ReqContext) {
|
2020-12-11 04:44:44 -06:00
|
|
|
require.NotNil(t, c)
|
|
|
|
t.Log("Default HTTP handler called")
|
2015-05-02 02:24:56 -05:00
|
|
|
sc.context = c
|
|
|
|
if sc.handlerFunc != nil {
|
|
|
|
sc.handlerFunc(sc.context)
|
2022-08-09 07:58:50 -05:00
|
|
|
if !c.Resp.Written() {
|
|
|
|
c.Resp.WriteHeader(http.StatusOK)
|
|
|
|
}
|
2020-01-15 06:03:12 -06:00
|
|
|
} else {
|
2020-12-15 12:09:04 -06:00
|
|
|
t.Log("Returning JSON OK")
|
2023-08-30 10:46:47 -05:00
|
|
|
resp := make(map[string]any)
|
2020-11-17 04:51:31 -06:00
|
|
|
resp["message"] = "OK"
|
2022-04-15 07:01:58 -05:00
|
|
|
c.JSON(http.StatusOK, resp)
|
2015-05-02 02:24:56 -05:00
|
|
|
}
|
2015-05-02 05:06:58 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
sc.m.Get("/", sc.defaultHandler)
|
2015-05-02 02:24:56 -05:00
|
|
|
|
2020-12-04 04:09:32 -06:00
|
|
|
fn(t, sc)
|
2015-05-02 02:24:56 -05:00
|
|
|
})
|
|
|
|
}
|
2020-02-17 10:31:44 -06:00
|
|
|
|
2023-08-09 01:54:52 -05:00
|
|
|
func getContextHandler(t *testing.T, cfg *setting.Cfg, authnService authn.Service) *contexthandler.ContextHandler {
|
2020-12-03 01:28:54 -06:00
|
|
|
t.Helper()
|
|
|
|
|
2023-09-27 02:51:57 -05:00
|
|
|
tracer := tracing.InitializeTracerForTest()
|
2023-08-09 08:17:59 -05:00
|
|
|
return contexthandler.ProvideService(cfg, tracer, featuremgmt.WithFeatures(), authnService)
|
2020-06-29 12:15:11 -05:00
|
|
|
}
|