grafana/pkg/middleware/middleware_test.go
Marcus Efraimsson d0a80c59f3
Rendering: Store render key in remote cache (#22031)
By storing render key in remote cache it will enable
image renderer to use public facing url or load
balancer url to render images and thereby remove
the requirement of image renderer having to use the
url of the originating Grafana instance when running
HA setup (multiple Grafana instances).

Fixes #17704
Ref grafana/grafana-image-renderer#91
2020-02-19 19:47:39 +01:00

663 lines
21 KiB
Go

package middleware
import (
"context"
"encoding/base32"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"time"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/macaron.v1"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/remotecache"
authproxy "github.com/grafana/grafana/pkg/middleware/auth_proxy"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
const errorTemplate = "error-template"
func mockGetTime() {
var timeSeed int64
getTime = func() time.Time {
fakeNow := time.Unix(timeSeed, 0)
timeSeed++
return fakeNow
}
}
func resetGetTime() {
getTime = time.Now
}
func TestMiddleWareSecurityHeaders(t *testing.T) {
setting.ERR_TEMPLATE_NAME = errorTemplate
Convey("Given the grafana middleware", t, func() {
middlewareScenario(t, "middleware should get correct x-xss-protection header", func(sc *scenarioContext) {
setting.XSSProtectionHeader = true
sc.fakeReq("GET", "/api/").exec()
So(sc.resp.Header().Get("X-XSS-Protection"), ShouldEqual, "1; mode=block")
})
middlewareScenario(t, "middleware should not get x-xss-protection when disabled", func(sc *scenarioContext) {
setting.XSSProtectionHeader = false
sc.fakeReq("GET", "/api/").exec()
So(sc.resp.Header().Get("X-XSS-Protection"), ShouldBeEmpty)
})
middlewareScenario(t, "middleware should add correct Strict-Transport-Security header", func(sc *scenarioContext) {
setting.StrictTransportSecurity = true
setting.Protocol = setting.HTTPS
setting.StrictTransportSecurityMaxAge = 64000
sc.fakeReq("GET", "/api/").exec()
So(sc.resp.Header().Get("Strict-Transport-Security"), ShouldEqual, "max-age=64000")
setting.StrictTransportSecurityPreload = true
sc.fakeReq("GET", "/api/").exec()
So(sc.resp.Header().Get("Strict-Transport-Security"), ShouldEqual, "max-age=64000; preload")
setting.StrictTransportSecuritySubDomains = true
sc.fakeReq("GET", "/api/").exec()
So(sc.resp.Header().Get("Strict-Transport-Security"), ShouldEqual, "max-age=64000; preload; includeSubDomains")
})
})
}
func TestMiddlewareContext(t *testing.T) {
setting.ERR_TEMPLATE_NAME = errorTemplate
Convey("Given the grafana middleware", t, func() {
middlewareScenario(t, "middleware should add context to injector", func(sc *scenarioContext) {
sc.fakeReq("GET", "/").exec()
So(sc.context, ShouldNotBeNil)
})
middlewareScenario(t, "Default middleware should allow get request", func(sc *scenarioContext) {
sc.fakeReq("GET", "/").exec()
So(sc.resp.Code, ShouldEqual, 200)
})
middlewareScenario(t, "middleware should add Cache-Control header for requests to API", func(sc *scenarioContext) {
sc.fakeReq("GET", "/api/search").exec()
So(sc.resp.Header().Get("Cache-Control"), ShouldEqual, "no-cache")
So(sc.resp.Header().Get("Pragma"), ShouldEqual, "no-cache")
So(sc.resp.Header().Get("Expires"), ShouldEqual, "-1")
})
middlewareScenario(t, "middleware should not add Cache-Control header for requests to datasource proxy API", func(sc *scenarioContext) {
sc.fakeReq("GET", "/api/datasources/proxy/1/test").exec()
So(sc.resp.Header().Get("Cache-Control"), ShouldBeEmpty)
So(sc.resp.Header().Get("Pragma"), ShouldBeEmpty)
So(sc.resp.Header().Get("Expires"), ShouldBeEmpty)
})
middlewareScenario(t, "middleware should add Cache-Control header for requests with html response", func(sc *scenarioContext) {
sc.handler(func(c *models.ReqContext) {
data := &dtos.IndexViewData{
User: &dtos.CurrentUser{},
Settings: map[string]interface{}{},
NavTree: []*dtos.NavLink{},
}
c.HTML(200, "index-template", data)
})
sc.fakeReq("GET", "/").exec()
So(sc.resp.Code, ShouldEqual, 200)
So(sc.resp.Header().Get("Cache-Control"), ShouldEqual, "no-cache")
So(sc.resp.Header().Get("Pragma"), ShouldEqual, "no-cache")
So(sc.resp.Header().Get("Expires"), ShouldEqual, "-1")
})
middlewareScenario(t, "middleware should add X-Frame-Options header with deny for request when not allowing embedding", func(sc *scenarioContext) {
sc.fakeReq("GET", "/api/search").exec()
So(sc.resp.Header().Get("X-Frame-Options"), ShouldEqual, "deny")
})
middlewareScenario(t, "middleware should not add X-Frame-Options header for request when allowing embedding", func(sc *scenarioContext) {
setting.AllowEmbedding = true
sc.fakeReq("GET", "/api/search").exec()
So(sc.resp.Header().Get("X-Frame-Options"), ShouldBeEmpty)
})
middlewareScenario(t, "Invalid api key", func(sc *scenarioContext) {
sc.apiKey = "invalid_key_test"
sc.fakeReq("GET", "/").exec()
Convey("Should not init session", func() {
So(sc.resp.Header().Get("Set-Cookie"), ShouldBeEmpty)
})
Convey("Should return 401", func() {
So(sc.resp.Code, ShouldEqual, 401)
So(sc.respJson["message"], ShouldEqual, errStringInvalidAPIKey)
})
})
middlewareScenario(t, "Valid api key", func(sc *scenarioContext) {
keyhash, err := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd")
So(err, ShouldBeNil)
bus.AddHandler("test", func(query *models.GetApiKeyByNameQuery) error {
query.Result = &models.ApiKey{OrgId: 12, Role: models.ROLE_EDITOR, Key: keyhash}
return nil
})
sc.fakeReq("GET", "/").withValidApiKey().exec()
Convey("Should return 200", func() {
So(sc.resp.Code, ShouldEqual, 200)
})
Convey("Should init middleware context", func() {
So(sc.context.IsSignedIn, ShouldEqual, true)
So(sc.context.OrgId, ShouldEqual, 12)
So(sc.context.OrgRole, ShouldEqual, models.ROLE_EDITOR)
})
})
middlewareScenario(t, "Valid api key, but does not match db hash", func(sc *scenarioContext) {
keyhash := "Something_not_matching"
bus.AddHandler("test", func(query *models.GetApiKeyByNameQuery) error {
query.Result = &models.ApiKey{OrgId: 12, Role: models.ROLE_EDITOR, Key: keyhash}
return nil
})
sc.fakeReq("GET", "/").withValidApiKey().exec()
Convey("Should return api key invalid", func() {
So(sc.resp.Code, ShouldEqual, 401)
So(sc.respJson["message"], ShouldEqual, errStringInvalidAPIKey)
})
})
middlewareScenario(t, "Valid api key, but expired", func(sc *scenarioContext) {
mockGetTime()
defer resetGetTime()
keyhash, err := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd")
So(err, ShouldBeNil)
bus.AddHandler("test", func(query *models.GetApiKeyByNameQuery) error {
// api key expired one second before
expires := getTime().Add(-1 * time.Second).Unix()
query.Result = &models.ApiKey{OrgId: 12, Role: models.ROLE_EDITOR, Key: keyhash,
Expires: &expires}
return nil
})
sc.fakeReq("GET", "/").withValidApiKey().exec()
Convey("Should return 401", func() {
So(sc.resp.Code, ShouldEqual, 401)
So(sc.respJson["message"], ShouldEqual, "Expired API key")
})
})
middlewareScenario(t, "Non-expired auth token in cookie which not are being rotated", func(sc *scenarioContext) {
sc.withTokenSessionCookie("token")
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
query.Result = &models.SignedInUser{OrgId: 2, UserId: 12}
return nil
})
sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) {
return &models.UserToken{
UserId: 12,
UnhashedToken: unhashedToken,
}, nil
}
sc.fakeReq("GET", "/").exec()
Convey("Should init context with user info", func() {
So(sc.context.IsSignedIn, ShouldBeTrue)
So(sc.context.UserId, ShouldEqual, 12)
So(sc.context.UserToken.UserId, ShouldEqual, 12)
So(sc.context.UserToken.UnhashedToken, ShouldEqual, "token")
})
Convey("Should not set cookie", func() {
So(sc.resp.Header().Get("Set-Cookie"), ShouldEqual, "")
})
})
middlewareScenario(t, "Non-expired auth token in cookie which are being rotated", func(sc *scenarioContext) {
sc.withTokenSessionCookie("token")
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
query.Result = &models.SignedInUser{OrgId: 2, UserId: 12}
return nil
})
sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) {
return &models.UserToken{
UserId: 12,
UnhashedToken: "",
}, nil
}
sc.userAuthTokenService.TryRotateTokenProvider = func(ctx context.Context, userToken *models.UserToken, clientIP, userAgent string) (bool, error) {
userToken.UnhashedToken = "rotated"
return true, nil
}
maxAgeHours := (time.Duration(setting.LoginMaxLifetimeDays) * 24 * time.Hour)
maxAge := (maxAgeHours + time.Hour).Seconds()
sameSitePolicies := []http.SameSite{
http.SameSiteNoneMode,
http.SameSiteLaxMode,
http.SameSiteStrictMode,
}
for _, sameSitePolicy := range sameSitePolicies {
setting.CookieSameSiteMode = sameSitePolicy
expectedCookie := &http.Cookie{
Name: setting.LoginCookieName,
Value: "rotated",
Path: setting.AppSubUrl + "/",
HttpOnly: true,
MaxAge: int(maxAge),
Secure: setting.CookieSecure,
SameSite: sameSitePolicy,
}
sc.fakeReq("GET", "/").exec()
Convey(fmt.Sprintf("Should init context with user info and setting.SameSite=%v", sameSitePolicy), func() {
So(sc.context.IsSignedIn, ShouldBeTrue)
So(sc.context.UserId, ShouldEqual, 12)
So(sc.context.UserToken.UserId, ShouldEqual, 12)
So(sc.context.UserToken.UnhashedToken, ShouldEqual, "rotated")
})
Convey(fmt.Sprintf("Should set cookie with setting.SameSite=%v", sameSitePolicy), func() {
So(sc.resp.Header().Get("Set-Cookie"), ShouldEqual, expectedCookie.String())
})
}
Convey("Should not set cookie with SameSite attribute when setting.CookieSameSiteDisabled is true", func() {
setting.CookieSameSiteDisabled = true
setting.CookieSameSiteMode = http.SameSiteLaxMode
expectedCookie := &http.Cookie{
Name: setting.LoginCookieName,
Value: "rotated",
Path: setting.AppSubUrl + "/",
HttpOnly: true,
MaxAge: int(maxAge),
Secure: setting.CookieSecure,
}
sc.fakeReq("GET", "/").exec()
So(sc.resp.Header().Get("Set-Cookie"), ShouldEqual, expectedCookie.String())
})
})
middlewareScenario(t, "Invalid/expired auth token in cookie", func(sc *scenarioContext) {
sc.withTokenSessionCookie("token")
sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) {
return nil, models.ErrUserTokenNotFound
}
sc.fakeReq("GET", "/").exec()
Convey("Should not init context with user info", func() {
So(sc.context.IsSignedIn, ShouldBeFalse)
So(sc.context.UserId, ShouldEqual, 0)
So(sc.context.UserToken, ShouldBeNil)
})
})
middlewareScenario(t, "When anonymous access is enabled", func(sc *scenarioContext) {
setting.AnonymousEnabled = true
setting.AnonymousOrgName = "test"
setting.AnonymousOrgRole = string(models.ROLE_EDITOR)
bus.AddHandler("test", func(query *models.GetOrgByNameQuery) error {
So(query.Name, ShouldEqual, "test")
query.Result = &models.Org{Id: 2, Name: "test"}
return nil
})
sc.fakeReq("GET", "/").exec()
Convey("Should init context with org info", func() {
So(sc.context.UserId, ShouldEqual, 0)
So(sc.context.OrgId, ShouldEqual, 2)
So(sc.context.OrgRole, ShouldEqual, models.ROLE_EDITOR)
})
Convey("context signed in should be false", func() {
So(sc.context.IsSignedIn, ShouldBeFalse)
})
})
Convey("auth_proxy", func() {
setting.AuthProxyEnabled = true
setting.AuthProxyWhitelist = ""
setting.AuthProxyAutoSignUp = true
setting.LDAPEnabled = true
setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
setting.AuthProxyHeaderProperty = "username"
setting.AuthProxyHeaders = map[string]string{"Groups": "X-WEBAUTH-GROUPS"}
name := "markelog"
group := "grafana-core-team"
middlewareScenario(t, "Should not sync the user if it's in the cache", func(sc *scenarioContext) {
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
query.Result = &models.SignedInUser{OrgId: 4, UserId: query.UserId}
return nil
})
key := fmt.Sprintf(authproxy.CachePrefix, base32.StdEncoding.EncodeToString([]byte(name+"-"+group)))
err := sc.remoteCacheService.Set(key, int64(33), 0)
So(err, ShouldBeNil)
sc.fakeReq("GET", "/")
sc.req.Header.Add(setting.AuthProxyHeaderName, name)
sc.req.Header.Add("X-WEBAUTH-GROUPS", group)
sc.exec()
Convey("Should init user via cache", func() {
So(sc.context.IsSignedIn, ShouldBeTrue)
So(sc.context.UserId, ShouldEqual, 33)
So(sc.context.OrgId, ShouldEqual, 4)
})
})
middlewareScenario(t, "Should respect auto signup option", func(sc *scenarioContext) {
setting.LDAPEnabled = false
setting.AuthProxyAutoSignUp = false
var actualAuthProxyAutoSignUp *bool = nil
bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error {
actualAuthProxyAutoSignUp = &cmd.SignupAllowed
return login.ErrInvalidCredentials
})
sc.fakeReq("GET", "/")
sc.req.Header.Add(setting.AuthProxyHeaderName, name)
sc.exec()
assert.False(t, *actualAuthProxyAutoSignUp)
assert.Equal(t, sc.resp.Code, 407)
assert.Nil(t, sc.context)
})
middlewareScenario(t, "Should create an user from a header", func(sc *scenarioContext) {
setting.LDAPEnabled = false
setting.AuthProxyAutoSignUp = true
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
if query.UserId > 0 {
query.Result = &models.SignedInUser{OrgId: 4, UserId: 33}
return nil
}
return models.ErrUserNotFound
})
bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error {
cmd.Result = &models.User{Id: 33}
return nil
})
sc.fakeReq("GET", "/")
sc.req.Header.Add(setting.AuthProxyHeaderName, name)
sc.exec()
Convey("Should create user from header info", func() {
So(sc.context.IsSignedIn, ShouldBeTrue)
So(sc.context.UserId, ShouldEqual, 33)
So(sc.context.OrgId, ShouldEqual, 4)
})
})
middlewareScenario(t, "Should get an existing user from header", func(sc *scenarioContext) {
setting.LDAPEnabled = false
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
query.Result = &models.SignedInUser{OrgId: 2, UserId: 12}
return nil
})
bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error {
cmd.Result = &models.User{Id: 12}
return nil
})
sc.fakeReq("GET", "/")
sc.req.Header.Add(setting.AuthProxyHeaderName, name)
sc.exec()
Convey("Should init context with user info", func() {
So(sc.context.IsSignedIn, ShouldBeTrue)
So(sc.context.UserId, ShouldEqual, 12)
So(sc.context.OrgId, ShouldEqual, 2)
})
})
middlewareScenario(t, "Should allow the request from whitelist IP", func(sc *scenarioContext) {
setting.AuthProxyWhitelist = "192.168.1.0/24, 2001::0/120"
setting.LDAPEnabled = false
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
query.Result = &models.SignedInUser{OrgId: 4, UserId: 33}
return nil
})
bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error {
cmd.Result = &models.User{Id: 33}
return nil
})
sc.fakeReq("GET", "/")
sc.req.Header.Add(setting.AuthProxyHeaderName, name)
sc.req.RemoteAddr = "[2001::23]:12345"
sc.exec()
Convey("Should init context with user info", func() {
So(sc.context.IsSignedIn, ShouldBeTrue)
So(sc.context.UserId, ShouldEqual, 33)
So(sc.context.OrgId, ShouldEqual, 4)
})
})
middlewareScenario(t, "Should not allow the request from whitelist IP", func(sc *scenarioContext) {
setting.AuthProxyWhitelist = "8.8.8.8"
setting.LDAPEnabled = false
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
query.Result = &models.SignedInUser{OrgId: 4, UserId: 33}
return nil
})
bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error {
cmd.Result = &models.User{Id: 33}
return nil
})
sc.fakeReq("GET", "/")
sc.req.Header.Add(setting.AuthProxyHeaderName, name)
sc.req.RemoteAddr = "[2001::23]:12345"
sc.exec()
Convey("Should return 407 status code", func() {
So(sc.resp.Code, ShouldEqual, 407)
So(sc.context, ShouldBeNil)
})
})
middlewareScenario(t, "Should return 407 status code if LDAP says no", func(sc *scenarioContext) {
bus.AddHandler("LDAP", func(cmd *models.UpsertUserCommand) error {
return errors.New("Do not add user")
})
sc.fakeReq("GET", "/")
sc.req.Header.Add(setting.AuthProxyHeaderName, name)
sc.exec()
Convey("Should return 407 status code", func() {
So(sc.resp.Code, ShouldEqual, 407)
So(sc.context, ShouldBeNil)
})
})
middlewareScenario(t, "Should return 407 status code if there is cache mishap", func(sc *scenarioContext) {
bus.AddHandler("Do not have the user", func(query *models.GetSignedInUserQuery) error {
return errors.New("Do not add user")
})
sc.fakeReq("GET", "/")
sc.req.Header.Add(setting.AuthProxyHeaderName, name)
sc.exec()
Convey("Should return 407 status code", func() {
So(sc.resp.Code, ShouldEqual, 407)
So(sc.context, ShouldBeNil)
})
})
})
})
}
func middlewareScenario(t *testing.T, desc string, fn scenarioFunc) {
Convey(desc, func() {
defer bus.ClearBusHandlers()
setting.LoginCookieName = "grafana_session"
setting.LoginMaxLifetimeDays = 30
sc := &scenarioContext{}
viewsPath, err := filepath.Abs("../../public/views")
require.NoError(t, err)
sc.m = macaron.New()
sc.m.Use(AddDefaultResponseHeaders())
sc.m.Use(macaron.Renderer(macaron.RenderOptions{
Directory: viewsPath,
Delims: macaron.Delims{Left: "[[", Right: "]]"},
}))
sc.userAuthTokenService = auth.NewFakeUserAuthTokenService()
sc.remoteCacheService = remotecache.NewFakeStore(t)
sc.m.Use(GetContextHandler(sc.userAuthTokenService, sc.remoteCacheService, nil))
sc.m.Use(OrgRedirect())
sc.defaultHandler = func(c *models.ReqContext) {
sc.context = c
if sc.handlerFunc != nil {
sc.handlerFunc(sc.context)
} else {
c.JsonOK("OK")
}
}
sc.m.Get("/", sc.defaultHandler)
fn(sc)
})
}
func TestDontRotateTokensOnCancelledRequests(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
reqContext, _, err := initTokenRotationTest(ctx)
require.NoError(t, err)
tryRotateCallCount := 0
uts := &auth.FakeUserAuthTokenService{
TryRotateTokenProvider: func(ctx context.Context, token *models.UserToken, clientIP, userAgent string) (bool, error) {
tryRotateCallCount++
return false, nil
},
}
token := &models.UserToken{AuthToken: "oldtoken"}
fn := rotateEndOfRequestFunc(reqContext, uts, token)
cancel()
fn(reqContext.Resp)
assert.Equal(t, 0, tryRotateCallCount, "Token rotation was attempted")
}
func TestTokenRotationAtEndOfRequest(t *testing.T) {
reqContext, rr, err := initTokenRotationTest(context.Background())
require.NoError(t, err)
uts := &auth.FakeUserAuthTokenService{
TryRotateTokenProvider: func(ctx context.Context, token *models.UserToken, clientIP, userAgent string) (bool, error) {
newToken, err := util.RandomHex(16)
require.NoError(t, err)
token.AuthToken = newToken
return true, nil
},
}
token := &models.UserToken{AuthToken: "oldtoken"}
rotateEndOfRequestFunc(reqContext, uts, token)(reqContext.Resp)
foundLoginCookie := false
for _, c := range rr.Result().Cookies() {
if c.Name == "login_token" {
foundLoginCookie = true
require.NotEqual(t, token.AuthToken, c.Value, "Auth token is still the same")
}
}
assert.True(t, foundLoginCookie, "Could not find cookie")
}
func initTokenRotationTest(ctx context.Context) (*models.ReqContext, *httptest.ResponseRecorder, error) {
setting.LoginCookieName = "login_token"
setting.LoginMaxLifetimeDays = 7
rr := httptest.NewRecorder()
req, err := http.NewRequestWithContext(ctx, "", "", nil)
if err != nil {
return nil, nil, err
}
reqContext := &models.ReqContext{
Context: &macaron.Context{
Req: macaron.Request{
Request: req,
},
},
Logger: log.New("testlogger"),
}
mw := mockWriter{rr}
reqContext.Resp = mw
return reqContext, rr, nil
}
type mockWriter struct {
*httptest.ResponseRecorder
}
func (mw mockWriter) Flush() {}
func (mw mockWriter) Status() int { return 0 }
func (mw mockWriter) Size() int { return 0 }
func (mw mockWriter) Written() bool { return false }
func (mw mockWriter) Before(macaron.BeforeFunc) {}