mirror of
https://github.com/grafana/grafana.git
synced 2024-11-27 11:20:27 -06:00
844b194f5b
Grafana itself may not be serving content over HTTPS, but it may be behind a transparent proxy which does. Fixes #26770. Based on #26868.
746 lines
25 KiB
Go
746 lines
25 KiB
Go
package middleware
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/grafana/grafana-plugin-sdk-go/backend/gtime"
|
|
"github.com/grafana/grafana/pkg/api/dtos"
|
|
"github.com/grafana/grafana/pkg/bus"
|
|
"github.com/grafana/grafana/pkg/infra/fs"
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
"github.com/grafana/grafana/pkg/infra/remotecache"
|
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
|
"github.com/grafana/grafana/pkg/login"
|
|
"github.com/grafana/grafana/pkg/models"
|
|
"github.com/grafana/grafana/pkg/services/auth"
|
|
"github.com/grafana/grafana/pkg/services/contexthandler"
|
|
"github.com/grafana/grafana/pkg/services/contexthandler/authproxy"
|
|
"github.com/grafana/grafana/pkg/services/rendering"
|
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
"github.com/grafana/grafana/pkg/util"
|
|
"github.com/grafana/grafana/pkg/web"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func fakeGetTime() func() time.Time {
|
|
var timeSeed int64
|
|
return func() time.Time {
|
|
fakeNow := time.Unix(timeSeed, 0)
|
|
timeSeed++
|
|
return fakeNow
|
|
}
|
|
}
|
|
|
|
func TestMiddleWareSecurityHeaders(t *testing.T) {
|
|
middlewareScenario(t, "middleware should get correct x-xss-protection header", func(t *testing.T, sc *scenarioContext) {
|
|
sc.fakeReq("GET", "/api/").exec()
|
|
assert.Equal(t, "1; mode=block", sc.resp.Header().Get("X-XSS-Protection"))
|
|
}, func(cfg *setting.Cfg) {
|
|
cfg.XSSProtectionHeader = true
|
|
})
|
|
|
|
middlewareScenario(t, "middleware should not get x-xss-protection when disabled", func(t *testing.T, sc *scenarioContext) {
|
|
sc.fakeReq("GET", "/api/").exec()
|
|
assert.Empty(t, sc.resp.Header().Get("X-XSS-Protection"))
|
|
}, func(cfg *setting.Cfg) {
|
|
cfg.XSSProtectionHeader = false
|
|
})
|
|
|
|
middlewareScenario(t, "middleware should add correct Strict-Transport-Security header", func(t *testing.T, sc *scenarioContext) {
|
|
sc.fakeReq("GET", "/api/").exec()
|
|
assert.Equal(t, "max-age=64000", sc.resp.Header().Get("Strict-Transport-Security"))
|
|
sc.cfg.StrictTransportSecurityPreload = true
|
|
sc.fakeReq("GET", "/api/").exec()
|
|
assert.Equal(t, "max-age=64000; preload", sc.resp.Header().Get("Strict-Transport-Security"))
|
|
sc.cfg.StrictTransportSecuritySubDomains = true
|
|
sc.fakeReq("GET", "/api/").exec()
|
|
assert.Equal(t, "max-age=64000; preload; includeSubDomains", sc.resp.Header().Get("Strict-Transport-Security"))
|
|
}, func(cfg *setting.Cfg) {
|
|
cfg.StrictTransportSecurity = true
|
|
cfg.StrictTransportSecurityMaxAge = 64000
|
|
})
|
|
}
|
|
|
|
func TestMiddlewareContext(t *testing.T) {
|
|
const noCache = "no-cache"
|
|
|
|
middlewareScenario(t, "middleware should add context to injector", func(t *testing.T, sc *scenarioContext) {
|
|
sc.fakeReq("GET", "/").exec()
|
|
assert.NotNil(t, sc.context)
|
|
})
|
|
|
|
middlewareScenario(t, "Default middleware should allow get request", func(t *testing.T, sc *scenarioContext) {
|
|
sc.fakeReq("GET", "/").exec()
|
|
assert.Equal(t, 200, sc.resp.Code)
|
|
})
|
|
|
|
middlewareScenario(t, "middleware should add Cache-Control header for requests to API", func(t *testing.T, sc *scenarioContext) {
|
|
sc.fakeReq("GET", "/api/search").exec()
|
|
assert.Equal(t, noCache, sc.resp.Header().Get("Cache-Control"))
|
|
assert.Equal(t, noCache, sc.resp.Header().Get("Pragma"))
|
|
assert.Equal(t, "-1", sc.resp.Header().Get("Expires"))
|
|
})
|
|
|
|
middlewareScenario(t, "middleware should not add Cache-Control header for requests to datasource proxy API", func(
|
|
t *testing.T, sc *scenarioContext) {
|
|
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"))
|
|
})
|
|
|
|
middlewareScenario(t, "middleware should add Cache-Control header for requests with HTML response", func(
|
|
t *testing.T, sc *scenarioContext) {
|
|
sc.handlerFunc = func(c *models.ReqContext) {
|
|
t.Log("Handler called")
|
|
data := &dtos.IndexViewData{
|
|
User: &dtos.CurrentUser{},
|
|
Settings: map[string]interface{}{},
|
|
NavTree: []*dtos.NavLink{},
|
|
}
|
|
t.Log("Calling HTML", "data", data)
|
|
c.HTML(200, "index-template", data)
|
|
t.Log("Returned HTML with code 200")
|
|
}
|
|
sc.fakeReq("GET", "/").exec()
|
|
require.Equal(t, 200, sc.resp.Code)
|
|
assert.Equal(t, noCache, sc.resp.Header().Get("Cache-Control"))
|
|
assert.Equal(t, noCache, sc.resp.Header().Get("Pragma"))
|
|
assert.Equal(t, "-1", sc.resp.Header().Get("Expires"))
|
|
})
|
|
|
|
middlewareScenario(t, "middleware should add X-Frame-Options header with deny for request when not allowing embedding", func(
|
|
t *testing.T, sc *scenarioContext) {
|
|
sc.fakeReq("GET", "/api/search").exec()
|
|
assert.Equal(t, "deny", sc.resp.Header().Get("X-Frame-Options"))
|
|
})
|
|
|
|
middlewareScenario(t, "middleware should not add X-Frame-Options header for request when allowing embedding", func(
|
|
t *testing.T, sc *scenarioContext) {
|
|
sc.fakeReq("GET", "/api/search").exec()
|
|
assert.Empty(t, sc.resp.Header().Get("X-Frame-Options"))
|
|
}, func(cfg *setting.Cfg) {
|
|
cfg.AllowEmbedding = true
|
|
})
|
|
|
|
middlewareScenario(t, "Invalid api key", func(t *testing.T, sc *scenarioContext) {
|
|
sc.apiKey = "invalid_key_test"
|
|
sc.fakeReq("GET", "/").exec()
|
|
|
|
assert.Empty(t, sc.resp.Header().Get("Set-Cookie"))
|
|
assert.Equal(t, 401, sc.resp.Code)
|
|
assert.Equal(t, contexthandler.InvalidAPIKey, sc.respJson["message"])
|
|
})
|
|
|
|
middlewareScenario(t, "Valid API key", func(t *testing.T, sc *scenarioContext) {
|
|
const orgID int64 = 12
|
|
keyhash, err := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd")
|
|
require.NoError(t, err)
|
|
|
|
bus.AddHandler("test", func(ctx context.Context, query *models.GetApiKeyByNameQuery) error {
|
|
query.Result = &models.ApiKey{OrgId: orgID, Role: models.ROLE_EDITOR, Key: keyhash}
|
|
return nil
|
|
})
|
|
|
|
sc.fakeReq("GET", "/").withValidApiKey().exec()
|
|
|
|
require.Equal(t, 200, sc.resp.Code)
|
|
|
|
assert.True(t, sc.context.IsSignedIn)
|
|
assert.Equal(t, orgID, sc.context.OrgId)
|
|
assert.Equal(t, models.ROLE_EDITOR, sc.context.OrgRole)
|
|
})
|
|
|
|
middlewareScenario(t, "Valid API key, but does not match DB hash", func(t *testing.T, sc *scenarioContext) {
|
|
const keyhash = "Something_not_matching"
|
|
|
|
bus.AddHandler("test", func(ctx context.Context, query *models.GetApiKeyByNameQuery) error {
|
|
query.Result = &models.ApiKey{OrgId: 12, Role: models.ROLE_EDITOR, Key: keyhash}
|
|
return nil
|
|
})
|
|
|
|
sc.fakeReq("GET", "/").withValidApiKey().exec()
|
|
|
|
assert.Equal(t, 401, sc.resp.Code)
|
|
assert.Equal(t, contexthandler.InvalidAPIKey, sc.respJson["message"])
|
|
})
|
|
|
|
middlewareScenario(t, "Valid API key, but expired", func(t *testing.T, sc *scenarioContext) {
|
|
sc.contextHandler.GetTime = fakeGetTime()
|
|
|
|
keyhash, err := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd")
|
|
require.NoError(t, err)
|
|
|
|
bus.AddHandler("test", func(ctx context.Context, query *models.GetApiKeyByNameQuery) error {
|
|
// api key expired one second before
|
|
expires := sc.contextHandler.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()
|
|
|
|
assert.Equal(t, 401, sc.resp.Code)
|
|
assert.Equal(t, "Expired API key", sc.respJson["message"])
|
|
})
|
|
|
|
middlewareScenario(t, "Non-expired auth token in cookie which is not being rotated", func(
|
|
t *testing.T, sc *scenarioContext) {
|
|
const userID int64 = 12
|
|
|
|
sc.withTokenSessionCookie("token")
|
|
|
|
bus.AddHandler("test", func(ctx context.Context, query *models.GetSignedInUserQuery) error {
|
|
query.Result = &models.SignedInUser{OrgId: 2, UserId: userID}
|
|
return nil
|
|
})
|
|
|
|
sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) {
|
|
return &models.UserToken{
|
|
UserId: userID,
|
|
UnhashedToken: unhashedToken,
|
|
}, nil
|
|
}
|
|
|
|
sc.fakeReq("GET", "/").exec()
|
|
|
|
require.NotNil(t, sc.context)
|
|
require.NotNil(t, sc.context.UserToken)
|
|
assert.True(t, sc.context.IsSignedIn)
|
|
assert.Equal(t, userID, sc.context.UserId)
|
|
assert.Equal(t, userID, sc.context.UserToken.UserId)
|
|
assert.Equal(t, "token", sc.context.UserToken.UnhashedToken)
|
|
assert.Empty(t, sc.resp.Header().Get("Set-Cookie"))
|
|
})
|
|
|
|
middlewareScenario(t, "Non-expired auth token in cookie which is being rotated", func(t *testing.T, sc *scenarioContext) {
|
|
const userID int64 = 12
|
|
|
|
sc.withTokenSessionCookie("token")
|
|
|
|
bus.AddHandler("test", func(ctx context.Context, query *models.GetSignedInUserQuery) error {
|
|
query.Result = &models.SignedInUser{OrgId: 2, UserId: userID}
|
|
return nil
|
|
})
|
|
|
|
sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) {
|
|
return &models.UserToken{
|
|
UserId: userID,
|
|
UnhashedToken: "",
|
|
}, nil
|
|
}
|
|
|
|
sc.userAuthTokenService.TryRotateTokenProvider = func(ctx context.Context, userToken *models.UserToken,
|
|
clientIP net.IP, userAgent string) (bool, error) {
|
|
userToken.UnhashedToken = "rotated"
|
|
return true, nil
|
|
}
|
|
|
|
maxAge := int(sc.cfg.LoginMaxLifetime.Seconds())
|
|
|
|
sameSiteModes := []http.SameSite{
|
|
http.SameSiteNoneMode,
|
|
http.SameSiteLaxMode,
|
|
http.SameSiteStrictMode,
|
|
}
|
|
for _, sameSiteMode := range sameSiteModes {
|
|
t.Run(fmt.Sprintf("Same site mode %d", sameSiteMode), func(t *testing.T) {
|
|
origCookieSameSiteMode := setting.CookieSameSiteMode
|
|
t.Cleanup(func() {
|
|
setting.CookieSameSiteMode = origCookieSameSiteMode
|
|
})
|
|
setting.CookieSameSiteMode = sameSiteMode
|
|
|
|
expectedCookiePath := "/"
|
|
if len(sc.cfg.AppSubURL) > 0 {
|
|
expectedCookiePath = sc.cfg.AppSubURL
|
|
}
|
|
expectedCookie := &http.Cookie{
|
|
Name: sc.cfg.LoginCookieName,
|
|
Value: "rotated",
|
|
Path: expectedCookiePath,
|
|
HttpOnly: true,
|
|
MaxAge: maxAge,
|
|
Secure: setting.CookieSecure,
|
|
SameSite: sameSiteMode,
|
|
}
|
|
|
|
sc.fakeReq("GET", "/").exec()
|
|
|
|
assert.True(t, sc.context.IsSignedIn)
|
|
assert.Equal(t, userID, sc.context.UserId)
|
|
assert.Equal(t, userID, sc.context.UserToken.UserId)
|
|
assert.Equal(t, "rotated", sc.context.UserToken.UnhashedToken)
|
|
assert.Equal(t, expectedCookie.String(), sc.resp.Header().Get("Set-Cookie"))
|
|
})
|
|
}
|
|
|
|
t.Run("Should not set cookie with SameSite attribute when setting.CookieSameSiteDisabled is true", func(t *testing.T) {
|
|
origCookieSameSiteDisabled := setting.CookieSameSiteDisabled
|
|
origCookieSameSiteMode := setting.CookieSameSiteMode
|
|
t.Cleanup(func() {
|
|
setting.CookieSameSiteDisabled = origCookieSameSiteDisabled
|
|
setting.CookieSameSiteMode = origCookieSameSiteMode
|
|
})
|
|
setting.CookieSameSiteDisabled = true
|
|
setting.CookieSameSiteMode = http.SameSiteLaxMode
|
|
|
|
expectedCookiePath := "/"
|
|
if len(sc.cfg.AppSubURL) > 0 {
|
|
expectedCookiePath = sc.cfg.AppSubURL
|
|
}
|
|
expectedCookie := &http.Cookie{
|
|
Name: sc.cfg.LoginCookieName,
|
|
Value: "rotated",
|
|
Path: expectedCookiePath,
|
|
HttpOnly: true,
|
|
MaxAge: maxAge,
|
|
Secure: setting.CookieSecure,
|
|
}
|
|
|
|
sc.fakeReq("GET", "/").exec()
|
|
assert.Equal(t, expectedCookie.String(), sc.resp.Header().Get("Set-Cookie"))
|
|
})
|
|
})
|
|
|
|
middlewareScenario(t, "Invalid/expired auth token in cookie", func(t *testing.T, 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()
|
|
|
|
assert.False(t, sc.context.IsSignedIn)
|
|
assert.Equal(t, int64(0), sc.context.UserId)
|
|
assert.Nil(t, sc.context.UserToken)
|
|
})
|
|
|
|
middlewareScenario(t, "When anonymous access is enabled", func(t *testing.T, sc *scenarioContext) {
|
|
org, err := sc.sqlStore.CreateOrgWithMember(sc.cfg.AnonymousOrgName, 1)
|
|
require.NoError(t, err)
|
|
|
|
sc.fakeReq("GET", "/").exec()
|
|
|
|
assert.Equal(t, int64(0), sc.context.UserId)
|
|
assert.Equal(t, org.Id, sc.context.OrgId)
|
|
assert.Equal(t, models.ROLE_EDITOR, sc.context.OrgRole)
|
|
assert.False(t, sc.context.IsSignedIn)
|
|
}, func(cfg *setting.Cfg) {
|
|
cfg.AnonymousEnabled = true
|
|
cfg.AnonymousOrgName = "test"
|
|
cfg.AnonymousOrgRole = string(models.ROLE_EDITOR)
|
|
})
|
|
|
|
t.Run("auth_proxy", func(t *testing.T) {
|
|
const userID int64 = 33
|
|
const orgID int64 = 4
|
|
const defaultOrgId int64 = 1
|
|
const orgRole = "Admin"
|
|
|
|
configure := func(cfg *setting.Cfg) {
|
|
cfg.AuthProxyEnabled = true
|
|
cfg.AuthProxyAutoSignUp = true
|
|
cfg.LDAPEnabled = true
|
|
cfg.AuthProxyHeaderName = "X-WEBAUTH-USER"
|
|
cfg.AuthProxyHeaderProperty = "username"
|
|
cfg.AuthProxyHeaders = map[string]string{"Groups": "X-WEBAUTH-GROUPS", "Role": "X-WEBAUTH-ROLE"}
|
|
}
|
|
|
|
const hdrName = "markelog"
|
|
const group = "grafana-core-team"
|
|
|
|
middlewareScenario(t, "Should not sync the user if it's in the cache", func(t *testing.T, sc *scenarioContext) {
|
|
bus.AddHandler("test", func(ctx context.Context, query *models.GetSignedInUserQuery) error {
|
|
query.Result = &models.SignedInUser{OrgId: orgID, UserId: query.UserId}
|
|
return nil
|
|
})
|
|
|
|
h, err := authproxy.HashCacheKey(hdrName + "-" + group)
|
|
require.NoError(t, err)
|
|
key := fmt.Sprintf(authproxy.CachePrefix, h)
|
|
err = sc.remoteCacheService.Set(context.Background(), key, userID, 0)
|
|
require.NoError(t, err)
|
|
sc.fakeReq("GET", "/")
|
|
|
|
sc.req.Header.Set(sc.cfg.AuthProxyHeaderName, hdrName)
|
|
sc.req.Header.Set("X-WEBAUTH-GROUPS", group)
|
|
sc.exec()
|
|
|
|
assert.True(t, sc.context.IsSignedIn)
|
|
assert.Equal(t, userID, sc.context.UserId)
|
|
assert.Equal(t, orgID, sc.context.OrgId)
|
|
}, configure)
|
|
|
|
middlewareScenario(t, "Should respect auto signup option", func(t *testing.T, sc *scenarioContext) {
|
|
var actualAuthProxyAutoSignUp *bool = nil
|
|
|
|
bus.AddHandler("test", func(ctx context.Context, cmd *models.UpsertUserCommand) error {
|
|
actualAuthProxyAutoSignUp = &cmd.SignupAllowed
|
|
return login.ErrInvalidCredentials
|
|
})
|
|
|
|
sc.fakeReq("GET", "/")
|
|
sc.req.Header.Set(sc.cfg.AuthProxyHeaderName, hdrName)
|
|
sc.exec()
|
|
|
|
assert.False(t, *actualAuthProxyAutoSignUp)
|
|
assert.Equal(t, 407, sc.resp.Code)
|
|
assert.Nil(t, sc.context)
|
|
}, func(cfg *setting.Cfg) {
|
|
configure(cfg)
|
|
cfg.LDAPEnabled = false
|
|
cfg.AuthProxyAutoSignUp = false
|
|
})
|
|
|
|
middlewareScenario(t, "Should create an user from a header", func(t *testing.T, sc *scenarioContext) {
|
|
bus.AddHandler("test", func(ctx context.Context, query *models.GetSignedInUserQuery) error {
|
|
if query.UserId > 0 {
|
|
query.Result = &models.SignedInUser{OrgId: orgID, UserId: userID}
|
|
return nil
|
|
}
|
|
return models.ErrUserNotFound
|
|
})
|
|
|
|
bus.AddHandler("test", func(ctx context.Context, cmd *models.UpsertUserCommand) error {
|
|
cmd.Result = &models.User{Id: userID}
|
|
return nil
|
|
})
|
|
|
|
sc.fakeReq("GET", "/")
|
|
sc.req.Header.Set(sc.cfg.AuthProxyHeaderName, hdrName)
|
|
sc.exec()
|
|
|
|
assert.True(t, sc.context.IsSignedIn)
|
|
assert.Equal(t, userID, sc.context.UserId)
|
|
assert.Equal(t, orgID, sc.context.OrgId)
|
|
}, func(cfg *setting.Cfg) {
|
|
configure(cfg)
|
|
cfg.LDAPEnabled = false
|
|
cfg.AuthProxyAutoSignUp = true
|
|
})
|
|
|
|
middlewareScenario(t, "Should assign role from header to default org", func(t *testing.T, sc *scenarioContext) {
|
|
var storedRoleInfo map[int64]models.RoleType = nil
|
|
bus.AddHandler("test", func(ctx context.Context, query *models.GetSignedInUserQuery) error {
|
|
if query.UserId > 0 {
|
|
query.Result = &models.SignedInUser{OrgId: defaultOrgId, UserId: userID, OrgRole: storedRoleInfo[defaultOrgId]}
|
|
return nil
|
|
}
|
|
return models.ErrUserNotFound
|
|
})
|
|
|
|
bus.AddHandler("test", func(ctx context.Context, cmd *models.UpsertUserCommand) error {
|
|
cmd.Result = &models.User{Id: userID}
|
|
storedRoleInfo = cmd.ExternalUser.OrgRoles
|
|
return nil
|
|
})
|
|
|
|
sc.fakeReq("GET", "/")
|
|
sc.req.Header.Set(sc.cfg.AuthProxyHeaderName, hdrName)
|
|
sc.req.Header.Set("X-WEBAUTH-ROLE", orgRole)
|
|
sc.exec()
|
|
|
|
assert.True(t, sc.context.IsSignedIn)
|
|
assert.Equal(t, userID, sc.context.UserId)
|
|
assert.Equal(t, defaultOrgId, sc.context.OrgId)
|
|
assert.Equal(t, orgRole, string(sc.context.OrgRole))
|
|
}, func(cfg *setting.Cfg) {
|
|
configure(cfg)
|
|
cfg.LDAPEnabled = false
|
|
cfg.AuthProxyAutoSignUp = true
|
|
})
|
|
|
|
middlewareScenario(t, "Should NOT assign role from header to non-default org", func(t *testing.T, sc *scenarioContext) {
|
|
var storedRoleInfo map[int64]models.RoleType = nil
|
|
bus.AddHandler("test", func(ctx context.Context, query *models.GetSignedInUserQuery) error {
|
|
if query.UserId > 0 {
|
|
query.Result = &models.SignedInUser{OrgId: orgID, UserId: userID, OrgRole: storedRoleInfo[orgID]}
|
|
return nil
|
|
}
|
|
return models.ErrUserNotFound
|
|
})
|
|
|
|
bus.AddHandler("test", func(ctx context.Context, cmd *models.UpsertUserCommand) error {
|
|
cmd.Result = &models.User{Id: userID}
|
|
storedRoleInfo = cmd.ExternalUser.OrgRoles
|
|
return nil
|
|
})
|
|
|
|
sc.fakeReq("GET", "/")
|
|
sc.req.Header.Set(sc.cfg.AuthProxyHeaderName, hdrName)
|
|
sc.req.Header.Set("X-WEBAUTH-ROLE", "Admin")
|
|
sc.req.Header.Set("X-Grafana-Org-Id", strconv.FormatInt(orgID, 10))
|
|
sc.exec()
|
|
|
|
assert.True(t, sc.context.IsSignedIn)
|
|
assert.Equal(t, userID, sc.context.UserId)
|
|
assert.Equal(t, orgID, sc.context.OrgId)
|
|
|
|
// For non-default org, the user role should be empty
|
|
assert.Equal(t, "", string(sc.context.OrgRole))
|
|
}, func(cfg *setting.Cfg) {
|
|
configure(cfg)
|
|
cfg.LDAPEnabled = false
|
|
cfg.AuthProxyAutoSignUp = true
|
|
})
|
|
|
|
middlewareScenario(t, "Should use organisation specified by targetOrgId parameter", func(t *testing.T, sc *scenarioContext) {
|
|
bus.AddHandler("test", func(ctx context.Context, query *models.GetSignedInUserQuery) error {
|
|
if query.UserId > 0 {
|
|
query.Result = &models.SignedInUser{OrgId: query.OrgId, UserId: userID}
|
|
return nil
|
|
}
|
|
return models.ErrUserNotFound
|
|
})
|
|
|
|
bus.AddHandler("test", func(ctx context.Context, cmd *models.UpsertUserCommand) error {
|
|
cmd.Result = &models.User{Id: userID}
|
|
return nil
|
|
})
|
|
|
|
targetOrgID := 123
|
|
sc.fakeReq("GET", fmt.Sprintf("/?targetOrgId=%d", targetOrgID))
|
|
sc.req.Header.Set(sc.cfg.AuthProxyHeaderName, hdrName)
|
|
sc.exec()
|
|
|
|
assert.True(t, sc.context.IsSignedIn)
|
|
assert.Equal(t, userID, sc.context.UserId)
|
|
assert.Equal(t, int64(targetOrgID), sc.context.OrgId)
|
|
}, func(cfg *setting.Cfg) {
|
|
configure(cfg)
|
|
cfg.LDAPEnabled = false
|
|
cfg.AuthProxyAutoSignUp = true
|
|
})
|
|
|
|
middlewareScenario(t, "Request body should not be read in default context handler", func(t *testing.T, sc *scenarioContext) {
|
|
sc.fakeReq("POST", "/?targetOrgId=123")
|
|
body := "key=value"
|
|
sc.req.Body = io.NopCloser(strings.NewReader(body))
|
|
|
|
sc.handlerFunc = func(c *models.ReqContext) {
|
|
t.Log("Handler called")
|
|
defer func() {
|
|
err := c.Req.Body.Close()
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
bodyAfterHandler, e := io.ReadAll(c.Req.Body)
|
|
require.NoError(t, e)
|
|
require.Equal(t, body, string(bodyAfterHandler))
|
|
}
|
|
|
|
sc.req.Header.Set(sc.cfg.AuthProxyHeaderName, hdrName)
|
|
sc.req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
sc.req.Header.Set("Content-Length", strconv.Itoa(len(body)))
|
|
sc.m.Post("/", sc.defaultHandler)
|
|
sc.exec()
|
|
})
|
|
|
|
middlewareScenario(t, "Should get an existing user from header", func(t *testing.T, sc *scenarioContext) {
|
|
const userID int64 = 12
|
|
const orgID int64 = 2
|
|
|
|
bus.AddHandler("test", func(ctx context.Context, query *models.GetSignedInUserQuery) error {
|
|
query.Result = &models.SignedInUser{OrgId: orgID, UserId: userID}
|
|
return nil
|
|
})
|
|
|
|
bus.AddHandler("test", func(ctx context.Context, cmd *models.UpsertUserCommand) error {
|
|
cmd.Result = &models.User{Id: userID}
|
|
return nil
|
|
})
|
|
|
|
sc.fakeReq("GET", "/")
|
|
sc.req.Header.Set(sc.cfg.AuthProxyHeaderName, hdrName)
|
|
sc.exec()
|
|
|
|
assert.True(t, sc.context.IsSignedIn)
|
|
assert.Equal(t, userID, sc.context.UserId)
|
|
assert.Equal(t, orgID, sc.context.OrgId)
|
|
}, func(cfg *setting.Cfg) {
|
|
configure(cfg)
|
|
cfg.LDAPEnabled = false
|
|
})
|
|
|
|
middlewareScenario(t, "Should allow the request from whitelist IP", func(t *testing.T, sc *scenarioContext) {
|
|
bus.AddHandler("test", func(ctx context.Context, query *models.GetSignedInUserQuery) error {
|
|
query.Result = &models.SignedInUser{OrgId: orgID, UserId: userID}
|
|
return nil
|
|
})
|
|
|
|
bus.AddHandler("test", func(ctx context.Context, cmd *models.UpsertUserCommand) error {
|
|
cmd.Result = &models.User{Id: userID}
|
|
return nil
|
|
})
|
|
|
|
sc.fakeReq("GET", "/")
|
|
sc.req.Header.Set(sc.cfg.AuthProxyHeaderName, hdrName)
|
|
sc.req.RemoteAddr = "[2001::23]:12345"
|
|
sc.exec()
|
|
|
|
assert.True(t, sc.context.IsSignedIn)
|
|
assert.Equal(t, userID, sc.context.UserId)
|
|
assert.Equal(t, orgID, sc.context.OrgId)
|
|
}, func(cfg *setting.Cfg) {
|
|
configure(cfg)
|
|
cfg.AuthProxyWhitelist = "192.168.1.0/24, 2001::0/120"
|
|
cfg.LDAPEnabled = false
|
|
})
|
|
|
|
middlewareScenario(t, "Should not allow the request from whitelisted IP", func(t *testing.T, sc *scenarioContext) {
|
|
bus.AddHandler("test", func(ctx context.Context, query *models.GetSignedInUserQuery) error {
|
|
query.Result = &models.SignedInUser{OrgId: orgID, UserId: userID}
|
|
return nil
|
|
})
|
|
|
|
bus.AddHandler("test", func(ctx context.Context, cmd *models.UpsertUserCommand) error {
|
|
cmd.Result = &models.User{Id: userID}
|
|
return nil
|
|
})
|
|
|
|
sc.fakeReq("GET", "/")
|
|
sc.req.Header.Set(sc.cfg.AuthProxyHeaderName, hdrName)
|
|
sc.req.RemoteAddr = "[2001::23]:12345"
|
|
sc.exec()
|
|
|
|
assert.Equal(t, 407, sc.resp.Code)
|
|
assert.Nil(t, sc.context)
|
|
}, func(cfg *setting.Cfg) {
|
|
configure(cfg)
|
|
cfg.AuthProxyWhitelist = "8.8.8.8"
|
|
cfg.LDAPEnabled = false
|
|
})
|
|
|
|
middlewareScenario(t, "Should return 407 status code if LDAP says no", func(t *testing.T, sc *scenarioContext) {
|
|
bus.AddHandler("LDAP", func(ctx context.Context, cmd *models.UpsertUserCommand) error {
|
|
return errors.New("Do not add user")
|
|
})
|
|
|
|
sc.fakeReq("GET", "/")
|
|
sc.req.Header.Set(sc.cfg.AuthProxyHeaderName, hdrName)
|
|
sc.exec()
|
|
|
|
assert.Equal(t, 407, sc.resp.Code)
|
|
assert.Nil(t, sc.context)
|
|
}, configure)
|
|
|
|
middlewareScenario(t, "Should return 407 status code if there is cache mishap", func(t *testing.T, sc *scenarioContext) {
|
|
bus.AddHandler("Do not have the user", func(ctx context.Context, query *models.GetSignedInUserQuery) error {
|
|
return errors.New("Do not add user")
|
|
})
|
|
|
|
sc.fakeReq("GET", "/")
|
|
sc.req.Header.Set(sc.cfg.AuthProxyHeaderName, hdrName)
|
|
sc.exec()
|
|
|
|
assert.Equal(t, 407, sc.resp.Code)
|
|
assert.Nil(t, sc.context)
|
|
}, configure)
|
|
})
|
|
}
|
|
|
|
func middlewareScenario(t *testing.T, desc string, fn scenarioFunc, cbs ...func(*setting.Cfg)) {
|
|
t.Helper()
|
|
|
|
t.Run(desc, func(t *testing.T) {
|
|
t.Cleanup(bus.ClearBusHandlers)
|
|
|
|
logger := log.New("test")
|
|
|
|
loginMaxLifetime, err := gtime.ParseDuration("30d")
|
|
require.NoError(t, err)
|
|
cfg := setting.NewCfg()
|
|
cfg.LoginCookieName = "grafana_session"
|
|
cfg.LoginMaxLifetime = loginMaxLifetime
|
|
// Required when rendering errors
|
|
cfg.ErrTemplateName = "error-template"
|
|
for _, cb := range cbs {
|
|
cb(cfg)
|
|
}
|
|
|
|
sc := &scenarioContext{t: t, cfg: cfg}
|
|
|
|
viewsPath, err := filepath.Abs("../../public/views")
|
|
require.NoError(t, err)
|
|
exists, err := fs.Exists(viewsPath)
|
|
require.NoError(t, err)
|
|
require.Truef(t, exists, "Views directory should exist at %q", viewsPath)
|
|
|
|
sc.m = web.New()
|
|
sc.m.Use(AddDefaultResponseHeaders(cfg))
|
|
sc.m.UseMiddleware(AddCSPHeader(cfg, logger))
|
|
sc.m.UseMiddleware(web.Renderer(viewsPath, "[[", "]]"))
|
|
|
|
ctxHdlr := getContextHandler(t, cfg)
|
|
sc.sqlStore = ctxHdlr.SQLStore
|
|
sc.contextHandler = ctxHdlr
|
|
sc.m.Use(ctxHdlr.Middleware)
|
|
sc.m.Use(OrgRedirect(sc.cfg))
|
|
|
|
sc.userAuthTokenService = ctxHdlr.AuthTokenService.(*auth.FakeUserAuthTokenService)
|
|
sc.jwtAuthService = ctxHdlr.JWTAuthService.(*models.FakeJWTService)
|
|
sc.remoteCacheService = ctxHdlr.RemoteCache
|
|
|
|
sc.defaultHandler = func(c *models.ReqContext) {
|
|
require.NotNil(t, c)
|
|
t.Log("Default HTTP handler called")
|
|
sc.context = c
|
|
if sc.handlerFunc != nil {
|
|
sc.handlerFunc(sc.context)
|
|
} else {
|
|
t.Log("Returning JSON OK")
|
|
resp := make(map[string]interface{})
|
|
resp["message"] = "OK"
|
|
c.JSON(200, resp)
|
|
}
|
|
}
|
|
|
|
sc.m.Get("/", sc.defaultHandler)
|
|
|
|
fn(t, sc)
|
|
})
|
|
}
|
|
|
|
func getContextHandler(t *testing.T, cfg *setting.Cfg) *contexthandler.ContextHandler {
|
|
t.Helper()
|
|
|
|
sqlStore := sqlstore.InitTestDB(t)
|
|
if cfg == nil {
|
|
cfg = setting.NewCfg()
|
|
}
|
|
cfg.RemoteCacheOptions = &setting.RemoteCacheOptions{
|
|
Name: "database",
|
|
}
|
|
remoteCacheSvc, err := remotecache.ProvideService(cfg, sqlStore)
|
|
require.NoError(t, err)
|
|
userAuthTokenSvc := auth.NewFakeUserAuthTokenService()
|
|
renderSvc := &fakeRenderService{}
|
|
authJWTSvc := models.NewFakeJWTService()
|
|
tracer, err := tracing.InitializeTracerForTest()
|
|
require.NoError(t, err)
|
|
return contexthandler.ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc, renderSvc, sqlStore, tracer)
|
|
}
|
|
|
|
type fakeRenderService struct {
|
|
rendering.Service
|
|
}
|
|
|
|
func (s *fakeRenderService) Init() error {
|
|
return nil
|
|
}
|