Move middleware context handler logic to service (#29605)

* middleware: Move context handler to own service

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

Co-authored-by: Emil Tullsted <sakjur@users.noreply.github.com>
Co-authored-by: Will Browne <wbrowne@users.noreply.github.com>
This commit is contained in:
Arve Knudsen
2020-12-11 11:44:44 +01:00
committed by GitHub
co-authored by Emil Tullsted Will Browne
parent d0f52d5334
commit 12661e8a9d
51 changed files with 1321 additions and 1079 deletions
+4 -20
View File
@@ -8,9 +8,9 @@ import (
macaron "gopkg.in/macaron.v1"
"github.com/grafana/grafana/pkg/middleware/cookies"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
type AuthOptions struct {
@@ -18,22 +18,6 @@ type AuthOptions struct {
ReqSignedIn bool
}
func getApiKey(c *models.ReqContext) string {
header := c.Req.Header.Get("Authorization")
parts := strings.SplitN(header, " ", 2)
if len(parts) == 2 && parts[0] == "Bearer" {
key := parts[1]
return key
}
username, password, err := util.DecodeBasicAuthHeader(header)
if err == nil && username == "api_key" {
return password
}
return ""
}
func accessForbidden(c *models.ReqContext) {
if c.IsApiRequest() {
c.JsonApiErr(403, "Permission denied", nil)
@@ -57,7 +41,7 @@ func notAuthorized(c *models.ReqContext) {
// remove any forceLogin=true params
redirectTo = removeForceLoginParams(redirectTo)
WriteCookie(c.Resp, "redirect_to", url.QueryEscape(redirectTo), 0, nil)
cookies.WriteCookie(c.Resp, "redirect_to", url.QueryEscape(redirectTo), 0, nil)
c.Redirect(setting.AppSubUrl + "/login")
}
@@ -135,9 +119,9 @@ func AdminOrFeatureEnabled(enabled bool) macaron.Handler {
}
}
func SnapshotPublicModeOrSignedIn() macaron.Handler {
func SnapshotPublicModeOrSignedIn(cfg *setting.Cfg) macaron.Handler {
return func(c *models.ReqContext) {
if setting.SnapshotPublicMode {
if cfg.SnapshotPublicMode {
return
}
-133
View File
@@ -1,133 +0,0 @@
package middleware
import (
"errors"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/middleware/authproxy"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
)
var header = setting.AuthProxyHeaderName
func logUserIn(auth *authproxy.AuthProxy, username string, logger log.Logger, ignoreCache bool) (int64, error) {
logger.Debug("Trying to log user in", "username", username, "ignoreCache", ignoreCache)
// Try to log in user via various providers
id, err := auth.Login(logger, ignoreCache)
if err != nil {
details := err
var e authproxy.Error
if errors.As(err, &e) {
details = e.DetailsError
}
logger.Error("Failed to login", "username", username, "message", err.Error(), "error", details,
"ignoreCache", ignoreCache)
return 0, err
}
return id, nil
}
// handleError calls ctx.Handle with the error message and the underlying error.
// If the error is of type authproxy.Error, its DetailsError is unwrapped and passed to ctx.Handle.
// If a callback is provided, it's called with either err.DetailsError, if err is of type
// authproxy.Error, otherwise err itself.
func handleError(ctx *models.ReqContext, err error, statusCode int, cb func(err error)) {
details := err
var e authproxy.Error
if errors.As(err, &e) {
details = e.DetailsError
}
ctx.Handle(statusCode, err.Error(), details)
if cb != nil {
cb(details)
}
}
func initContextWithAuthProxy(store *remotecache.RemoteCache, ctx *models.ReqContext, orgID int64) bool {
username := ctx.Req.Header.Get(header)
auth := authproxy.New(&authproxy.Options{
Store: store,
Ctx: ctx,
OrgID: orgID,
})
logger := log.New("auth.proxy")
// Bail if auth proxy is not enabled
if !auth.IsEnabled() {
return false
}
// If there is no header - we can't move forward
if !auth.HasHeader() {
return false
}
// Check if allowed to continue with this IP
if err := auth.IsAllowedIP(); err != nil {
handleError(ctx, err, 407, func(details error) {
logger.Error("Failed to check whitelisted IP addresses", "message", err.Error(), "error", details)
})
return true
}
id, err := logUserIn(auth, username, logger, false)
if err != nil {
handleError(ctx, err, 407, nil)
return true
}
logger.Debug("Got user ID, getting full user info", "userID", id)
user, e := auth.GetSignedUser(id)
if e != nil {
// The reason we couldn't find the user corresponding to the ID might be that the ID was found from a stale
// cache entry. For example, if a user is deleted via the API, corresponding cache entries aren't invalidated
// because cache keys are computed from request header values and not just the user ID. Meaning that
// we can't easily derive cache keys to invalidate when deleting a user. To work around this, we try to
// log the user in again without the cache.
logger.Debug("Failed to get user info given ID, retrying without cache", "userID", id)
if err := auth.RemoveUserFromCache(logger); err != nil {
if !errors.Is(err, remotecache.ErrCacheItemNotFound) {
logger.Error("Got unexpected error when removing user from auth cache", "error", err)
}
}
id, err = logUserIn(auth, username, logger, true)
if err != nil {
handleError(ctx, err, 407, nil)
return true
}
user, err = auth.GetSignedUser(id)
if err != nil {
handleError(ctx, err, 407, nil)
return true
}
}
logger.Debug("Successfully got user info", "userID", user.UserId, "username", user.Login)
// Add user info to context
ctx.SignedInUser = user
ctx.IsSignedIn = true
// Remember user data in cache
if err := auth.Remember(id); err != nil {
handleError(ctx, err, 500, func(details error) {
logger.Error(
"Failed to store user in cache",
"username", username,
"message", e.Error(),
"error", details,
)
})
return true
}
return true
}
-88
View File
@@ -1,88 +0,0 @@
package middleware
import (
"fmt"
"net/http"
"testing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/middleware/authproxy"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/require"
macaron "gopkg.in/macaron.v1"
)
// Test initContextWithAuthProxy with a cached user ID that is no longer valid.
//
// In this case, the cache entry should be ignored/cleared and another attempt should be done to sign the user
// in without cache.
func TestInitContextWithAuthProxy_CachedInvalidUserID(t *testing.T) {
const name = "markelog"
const userID = int64(1)
const orgID = int64(4)
upsertHandler := func(cmd *models.UpsertUserCommand) error {
require.Equal(t, name, cmd.ExternalUser.Login)
cmd.Result = &models.User{Id: userID}
return nil
}
getUserHandler := func(cmd *models.GetSignedInUserQuery) error {
// Simulate that the cached user ID is stale
if cmd.UserId != userID {
return models.ErrUserNotFound
}
cmd.Result = &models.SignedInUser{
UserId: userID,
OrgId: orgID,
}
return nil
}
origHeaderName := setting.AuthProxyHeaderName
origEnabled := setting.AuthProxyEnabled
origHeaderProperty := setting.AuthProxyHeaderProperty
bus.AddHandler("", upsertHandler)
bus.AddHandler("", getUserHandler)
t.Cleanup(func() {
setting.AuthProxyHeaderName = origHeaderName
setting.AuthProxyEnabled = origEnabled
setting.AuthProxyHeaderProperty = origHeaderProperty
bus.ClearBusHandlers()
})
setting.AuthProxyHeaderName = "X-Killa"
setting.AuthProxyEnabled = true
setting.AuthProxyHeaderProperty = "username"
req, err := http.NewRequest("POST", "http://example.com", nil)
require.NoError(t, err)
store := remotecache.NewFakeStore(t)
ctx := &models.ReqContext{
Context: &macaron.Context{
Req: macaron.Request{
Request: req,
},
Data: map[string]interface{}{},
},
Logger: log.New("Test"),
}
req.Header.Add(setting.AuthProxyHeaderName, name)
key := fmt.Sprintf(authproxy.CachePrefix, authproxy.HashCacheKey(name))
t.Logf("Injecting stale user ID in cache with key %q", key)
err = store.Set(key, int64(33), 0)
require.NoError(t, err)
authEnabled := initContextWithAuthProxy(store, ctx, orgID)
require.True(t, authEnabled)
require.Equal(t, userID, ctx.SignedInUser.UserId)
require.True(t, ctx.IsSignedIn)
i, err := store.Get(key)
require.NoError(t, err)
require.Equal(t, userID, i.(int64))
}
+10 -16
View File
@@ -33,16 +33,10 @@ func TestMiddlewareAuth(t *testing.T) {
t.Run("Anonymous auth enabled", func(t *testing.T) {
const orgID int64 = 1
origEnabled := setting.AnonymousEnabled
t.Cleanup(func() {
setting.AnonymousEnabled = origEnabled
})
origName := setting.AnonymousOrgName
t.Cleanup(func() {
setting.AnonymousOrgName = origName
})
setting.AnonymousEnabled = true
setting.AnonymousOrgName = "test"
configure := func(cfg *setting.Cfg) {
cfg.AnonymousEnabled = true
cfg.AnonymousOrgName = "test"
}
middlewareScenario(t, "ReqSignIn true and request with forceLogin in query string", func(
t *testing.T, sc *scenarioContext) {
@@ -59,7 +53,7 @@ func TestMiddlewareAuth(t *testing.T) {
location, ok := sc.resp.Header()["Location"]
assert.True(t, ok)
assert.Equal(t, "/login", location[0])
})
}, configure)
middlewareScenario(t, "ReqSignIn true and request with same org provided in query string", func(
t *testing.T, sc *scenarioContext) {
@@ -73,7 +67,7 @@ func TestMiddlewareAuth(t *testing.T) {
sc.fakeReq("GET", fmt.Sprintf("/secure?orgId=%d", orgID)).exec()
assert.Equal(t, 200, sc.resp.Code)
})
}, configure)
middlewareScenario(t, "ReqSignIn true and request with different org provided in query string", func(
t *testing.T, sc *scenarioContext) {
@@ -90,20 +84,20 @@ func TestMiddlewareAuth(t *testing.T) {
location, ok := sc.resp.Header()["Location"]
assert.True(t, ok)
assert.Equal(t, "/login", location[0])
})
}, configure)
})
middlewareScenario(t, "Snapshot public mode disabled and unauthenticated request should return 401", func(
t *testing.T, sc *scenarioContext) {
sc.m.Get("/api/snapshot", SnapshotPublicModeOrSignedIn(), sc.defaultHandler)
sc.m.Get("/api/snapshot", SnapshotPublicModeOrSignedIn(sc.cfg), sc.defaultHandler)
sc.fakeReq("GET", "/api/snapshot").exec()
assert.Equal(t, 401, sc.resp.Code)
})
middlewareScenario(t, "Snapshot public mode enabled and unauthenticated request should return 200", func(
t *testing.T, sc *scenarioContext) {
setting.SnapshotPublicMode = true
sc.m.Get("/api/snapshot", SnapshotPublicModeOrSignedIn(), sc.defaultHandler)
sc.cfg.SnapshotPublicMode = true
sc.m.Get("/api/snapshot", SnapshotPublicModeOrSignedIn(sc.cfg), sc.defaultHandler)
sc.fakeReq("GET", "/api/snapshot").exec()
assert.Equal(t, 200, sc.resp.Code)
})
-357
View File
@@ -1,357 +0,0 @@
package authproxy
import (
"encoding/hex"
"errors"
"fmt"
"hash/fnv"
"net"
"net/mail"
"path"
"reflect"
"strings"
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/ldap"
"github.com/grafana/grafana/pkg/services/multildap"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
const (
// CachePrefix is a prefix for the cache key
CachePrefix = "auth-proxy-sync-ttl:%s"
)
// getLDAPConfig gets LDAP config
var getLDAPConfig = ldap.GetConfig
// isLDAPEnabled checks if LDAP is enabled
var isLDAPEnabled = ldap.IsEnabled
// newLDAP creates multiple LDAP instance
var newLDAP = multildap.New
// supportedHeaders states the supported headers configuration fields
var supportedHeaderFields = []string{"Name", "Email", "Login", "Groups"}
// AuthProxy struct
type AuthProxy struct {
store *remotecache.RemoteCache
ctx *models.ReqContext
orgID int64
header string
enabled bool
LDAPAllowSignup bool
AuthProxyAutoSignUp bool
whitelistIP string
headerType string
headers map[string]string
cacheTTL int
}
// Error auth proxy specific error
type Error struct {
Message string
DetailsError error
}
// newError returns an Error.
func newError(message string, err error) Error {
return Error{
Message: message,
DetailsError: err,
}
}
// Error returns the error message.
func (err Error) Error() string {
return err.Message
}
// Options for the AuthProxy
type Options struct {
Store *remotecache.RemoteCache
Ctx *models.ReqContext
OrgID int64
}
// New instance of the AuthProxy
func New(options *Options) *AuthProxy {
header := options.Ctx.Req.Header.Get(setting.AuthProxyHeaderName)
return &AuthProxy{
store: options.Store,
ctx: options.Ctx,
orgID: options.OrgID,
header: header,
enabled: setting.AuthProxyEnabled,
headerType: setting.AuthProxyHeaderProperty,
headers: setting.AuthProxyHeaders,
whitelistIP: setting.AuthProxyWhitelist,
cacheTTL: setting.AuthProxySyncTtl,
LDAPAllowSignup: setting.LDAPAllowSignup,
AuthProxyAutoSignUp: setting.AuthProxyAutoSignUp,
}
}
// IsEnabled checks if the proxy auth is enabled
func (auth *AuthProxy) IsEnabled() bool {
// Bail if the setting is not enabled
return auth.enabled
}
// HasHeader checks if the we have specified header
func (auth *AuthProxy) HasHeader() bool {
return len(auth.header) != 0
}
// IsAllowedIP compares presented IP with the whitelist one
func (auth *AuthProxy) IsAllowedIP() error {
ip := auth.ctx.Req.RemoteAddr
if len(strings.TrimSpace(auth.whitelistIP)) == 0 {
return nil
}
proxies := strings.Split(auth.whitelistIP, ",")
var proxyObjs []*net.IPNet
for _, proxy := range proxies {
result, err := coerceProxyAddress(proxy)
if err != nil {
return newError("could not get the network", err)
}
proxyObjs = append(proxyObjs, result)
}
sourceIP, _, err := net.SplitHostPort(ip)
if err != nil {
return newError("could not parse address", err)
}
sourceObj := net.ParseIP(sourceIP)
for _, proxyObj := range proxyObjs {
if proxyObj.Contains(sourceObj) {
return nil
}
}
return newError("proxy authentication required", fmt.Errorf(
"request for user (%s) from %s is not from the authentication proxy", auth.header,
sourceIP,
))
}
func HashCacheKey(key string) string {
hasher := fnv.New128a()
// according to the documentation, Hash.Write cannot error, but linter is complaining
hasher.Write([]byte(key)) // nolint: errcheck
return hex.EncodeToString(hasher.Sum(nil))
}
// getKey forms a key for the cache based on the headers received as part of the authentication flow.
// Our configuration supports multiple headers. The main header contains the email or username.
// And the additional ones that allow us to specify extra attributes: Name, Email or Groups.
func (auth *AuthProxy) getKey() string {
key := strings.TrimSpace(auth.header) // start the key with the main header
auth.headersIterator(func(_, header string) {
key = strings.Join([]string{key, header}, "-") // compose the key with any additional headers
})
hashedKey := HashCacheKey(key)
return fmt.Sprintf(CachePrefix, hashedKey)
}
// Login logs in user ID by whatever means possible.
func (auth *AuthProxy) Login(logger log.Logger, ignoreCache bool) (int64, error) {
if !ignoreCache {
// Error here means absent cache - we don't need to handle that
id, err := auth.GetUserViaCache(logger)
if err == nil && id != 0 {
return id, nil
}
}
if isLDAPEnabled() {
id, err := auth.LoginViaLDAP()
if err != nil {
if errors.Is(err, ldap.ErrInvalidCredentials) {
return 0, newError("proxy authentication required", ldap.ErrInvalidCredentials)
}
return 0, newError("failed to get the user", err)
}
return id, nil
}
id, err := auth.LoginViaHeader()
if err != nil {
return 0, newError("failed to log in as user, specified in auth proxy header", err)
}
return id, nil
}
// GetUserViaCache gets user ID from cache.
func (auth *AuthProxy) GetUserViaCache(logger log.Logger) (int64, error) {
cacheKey := auth.getKey()
logger.Debug("Getting user ID via auth cache", "cacheKey", cacheKey)
userID, err := auth.store.Get(cacheKey)
if err != nil {
logger.Debug("Failed getting user ID via auth cache", "error", err)
return 0, err
}
logger.Debug("Successfully got user ID via auth cache", "id", userID)
return userID.(int64), nil
}
// RemoveUserFromCache removes user from cache.
func (auth *AuthProxy) RemoveUserFromCache(logger log.Logger) error {
cacheKey := auth.getKey()
logger.Debug("Removing user from auth cache", "cacheKey", cacheKey)
if err := auth.store.Delete(cacheKey); err != nil {
return err
}
logger.Debug("Successfully removed user from auth cache", "cacheKey", cacheKey)
return nil
}
// LoginViaLDAP logs in user via LDAP request
func (auth *AuthProxy) LoginViaLDAP() (int64, error) {
config, err := getLDAPConfig()
if err != nil {
return 0, newError("failed to get LDAP config", err)
}
extUser, _, err := newLDAP(config.Servers).User(auth.header)
if err != nil {
return 0, err
}
// Have to sync grafana and LDAP user during log in
upsert := &models.UpsertUserCommand{
ReqContext: auth.ctx,
SignupAllowed: auth.LDAPAllowSignup,
ExternalUser: extUser,
}
if err := bus.Dispatch(upsert); err != nil {
return 0, err
}
return upsert.Result.Id, nil
}
// LoginViaHeader logs in user from the header only
func (auth *AuthProxy) LoginViaHeader() (int64, error) {
extUser := &models.ExternalUserInfo{
AuthModule: "authproxy",
AuthId: auth.header,
}
switch auth.headerType {
case "username":
extUser.Login = auth.header
emailAddr, emailErr := mail.ParseAddress(auth.header) // only set Email if it can be parsed as an email address
if emailErr == nil {
extUser.Email = emailAddr.Address
}
case "email":
extUser.Email = auth.header
extUser.Login = auth.header
default:
return 0, fmt.Errorf("auth proxy header property invalid")
}
auth.headersIterator(func(field string, header string) {
if field == "Groups" {
extUser.Groups = util.SplitString(header)
} else {
reflect.ValueOf(extUser).Elem().FieldByName(field).SetString(header)
}
})
upsert := &models.UpsertUserCommand{
ReqContext: auth.ctx,
SignupAllowed: setting.AuthProxyAutoSignUp,
ExternalUser: extUser,
}
err := bus.Dispatch(upsert)
if err != nil {
return 0, err
}
return upsert.Result.Id, nil
}
// headersIterator iterates over all non-empty supported additional headers
func (auth *AuthProxy) headersIterator(fn func(field string, header string)) {
for _, field := range supportedHeaderFields {
h := auth.headers[field]
if h == "" {
continue
}
if value := auth.ctx.Req.Header.Get(h); value != "" {
fn(field, strings.TrimSpace(value))
}
}
}
// GetSignedUser gets full signed user info.
func (auth *AuthProxy) GetSignedUser(userID int64) (*models.SignedInUser, error) {
query := &models.GetSignedInUserQuery{
OrgId: auth.orgID,
UserId: userID,
}
if err := bus.Dispatch(query); err != nil {
return nil, err
}
return query.Result, nil
}
// Remember user in cache
func (auth *AuthProxy) Remember(id int64) error {
key := auth.getKey()
// Check if user already in cache
userID, _ := auth.store.Get(key)
if userID != nil {
return nil
}
expiration := time.Duration(auth.cacheTTL) * time.Minute
err := auth.store.Set(key, id, expiration)
if err != nil {
return err
}
return nil
}
// coerceProxyAddress gets network of the presented CIDR notation
func coerceProxyAddress(proxyAddr string) (*net.IPNet, error) {
proxyAddr = strings.TrimSpace(proxyAddr)
if !strings.Contains(proxyAddr, "/") {
proxyAddr = path.Join(proxyAddr, "32")
}
_, network, err := net.ParseCIDR(proxyAddr)
return network, err
}
-210
View File
@@ -1,210 +0,0 @@
package authproxy
import (
"errors"
"fmt"
"net/http"
"testing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/ldap"
"github.com/grafana/grafana/pkg/services/multildap"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/macaron.v1"
)
type fakeMultiLDAP struct {
multildap.MultiLDAP
ID int64
userCalled bool
loginCalled bool
}
func (m *fakeMultiLDAP) Login(query *models.LoginUserQuery) (
*models.ExternalUserInfo, error,
) {
m.loginCalled = true
result := &models.ExternalUserInfo{
UserId: m.ID,
}
return result, nil
}
func (m *fakeMultiLDAP) User(login string) (
*models.ExternalUserInfo,
ldap.ServerConfig,
error,
) {
m.userCalled = true
result := &models.ExternalUserInfo{
UserId: m.ID,
}
return result, ldap.ServerConfig{}, nil
}
func prepareMiddleware(t *testing.T, req *http.Request, store *remotecache.RemoteCache) *AuthProxy {
t.Helper()
ctx := &models.ReqContext{
Context: &macaron.Context{
Req: macaron.Request{
Request: req,
},
},
}
auth := New(&Options{
Store: store,
Ctx: ctx,
OrgID: 4,
})
return auth
}
func TestMiddlewareContext(t *testing.T) {
logger := log.New("test")
req, err := http.NewRequest("POST", "http://example.com", nil)
require.NoError(t, err)
setting.AuthProxyHeaderName = "X-Killa"
store := remotecache.NewFakeStore(t)
name := "markelog"
req.Header.Add(setting.AuthProxyHeaderName, name)
t.Run("When the cache only contains the main header with a simple cache key", func(t *testing.T) {
const id int64 = 33
// Set cache key
key := fmt.Sprintf(CachePrefix, HashCacheKey(name))
err := store.Set(key, id, 0)
require.NoError(t, err)
// Set up the middleware
auth := prepareMiddleware(t, req, store)
assert.Equal(t, "auth-proxy-sync-ttl:0a7f3374e9659b10980fd66247b0cf2f", auth.getKey())
gotID, err := auth.Login(logger, false)
require.NoError(t, err)
assert.Equal(t, id, gotID)
})
t.Run("When the cache key contains additional headers", func(t *testing.T) {
const id int64 = 33
setting.AuthProxyHeaders = map[string]string{"Groups": "X-WEBAUTH-GROUPS"}
group := "grafana-core-team"
req.Header.Add("X-WEBAUTH-GROUPS", group)
key := fmt.Sprintf(CachePrefix, HashCacheKey(name+"-"+group))
err := store.Set(key, id, 0)
require.NoError(t, err)
auth := prepareMiddleware(t, req, store)
assert.Equal(t, "auth-proxy-sync-ttl:14f69b7023baa0ac98c96b31cec07bc0", auth.getKey())
gotID, err := auth.Login(logger, false)
require.NoError(t, err)
assert.Equal(t, id, gotID)
})
}
func TestMiddlewareContext_ldap(t *testing.T) {
logger := log.New("test")
req, err := http.NewRequest("POST", "http://example.com", nil)
require.NoError(t, err)
setting.AuthProxyHeaderName = "X-Killa"
const headerName = "markelog"
req.Header.Add(setting.AuthProxyHeaderName, headerName)
t.Run("Logs in via LDAP", func(t *testing.T) {
const id int64 = 42
bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error {
cmd.Result = &models.User{
Id: id,
}
return nil
})
isLDAPEnabled = func() bool {
return true
}
stub := &fakeMultiLDAP{
ID: id,
}
getLDAPConfig = func() (*ldap.Config, error) {
config := &ldap.Config{
Servers: []*ldap.ServerConfig{
{
SearchBaseDNs: []string{"BaseDNHere"},
},
},
}
return config, nil
}
newLDAP = func(servers []*ldap.ServerConfig) multildap.IMultiLDAP {
return stub
}
defer func() {
newLDAP = multildap.New
isLDAPEnabled = ldap.IsEnabled
getLDAPConfig = ldap.GetConfig
}()
store := remotecache.NewFakeStore(t)
auth := prepareMiddleware(t, req, store)
gotID, err := auth.Login(logger, false)
require.NoError(t, err)
assert.Equal(t, id, gotID)
assert.True(t, stub.userCalled)
})
t.Run("Gets nice error if ldap is enabled but not configured", func(t *testing.T) {
const id int64 = 42
isLDAPEnabled = func() bool {
return true
}
getLDAPConfig = func() (*ldap.Config, error) {
return nil, errors.New("something went wrong")
}
defer func() {
newLDAP = multildap.New
isLDAPEnabled = ldap.IsEnabled
getLDAPConfig = ldap.GetConfig
}()
store := remotecache.NewFakeStore(t)
auth := prepareMiddleware(t, req, store)
stub := &fakeMultiLDAP{
ID: id,
}
newLDAP = func(servers []*ldap.ServerConfig) multildap.IMultiLDAP {
return stub
}
gotID, err := auth.Login(logger, false)
require.EqualError(t, err, "failed to get the user")
assert.NotEqual(t, id, gotID)
assert.False(t, stub.loginCalled)
})
}
@@ -1,4 +1,4 @@
package middleware
package cookies
import (
"net/http"
@@ -55,8 +55,8 @@ func WriteCookie(w http.ResponseWriter, name string, value string, maxAge int, g
http.SetCookie(w, &cookie)
}
func WriteSessionCookie(ctx *models.ReqContext, value string, maxLifetime time.Duration) {
if setting.Env == setting.Dev {
func WriteSessionCookie(ctx *models.ReqContext, cfg *setting.Cfg, value string, maxLifetime time.Duration) {
if cfg.Env == setting.Dev {
ctx.Logger.Info("New token", "unhashed token", value)
}
+2 -2
View File
@@ -26,7 +26,7 @@ import (
"gopkg.in/macaron.v1"
)
func Logger() macaron.Handler {
func Logger(cfg *setting.Cfg) macaron.Handler {
return func(res http.ResponseWriter, req *http.Request, c *macaron.Context) {
start := time.Now()
c.Data["perfmon.start"] = start
@@ -43,7 +43,7 @@ func Logger() macaron.Handler {
status := rw.Status()
if status == 200 || status == 304 {
if !setting.RouterLogging {
if !cfg.RouterLogging {
return
}
}
+15 -272
View File
@@ -1,32 +1,13 @@
package middleware
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"time"
macaron "gopkg.in/macaron.v1"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/apikeygen"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/network"
"github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
var getTime = time.Now
const (
errStringInvalidUsernamePassword = "Invalid username or password"
errStringInvalidAPIKey = "Invalid API key"
)
var (
@@ -39,244 +20,7 @@ var (
ReqOrgAdmin = RoleAuth(models.ROLE_ADMIN)
)
func GetContextHandler(
ats models.UserTokenService,
remoteCache *remotecache.RemoteCache,
renderService rendering.Service,
) macaron.Handler {
return func(c *macaron.Context) {
ctx := &models.ReqContext{
Context: c,
SignedInUser: &models.SignedInUser{},
IsSignedIn: false,
AllowAnonymous: false,
SkipCache: false,
Logger: log.New("context"),
}
orgID := int64(0)
orgIDHeader := ctx.Req.Header.Get("X-Grafana-Org-Id")
if orgIDHeader != "" {
orgIDParsed, err := strconv.ParseInt(orgIDHeader, 10, 64)
if err == nil {
orgID = orgIDParsed
}
}
// the order in which these are tested are important
// look for api key in Authorization header first
// then init session and look for userId in session
// then look for api key in session (special case for render calls via api)
// then test if anonymous access is enabled
switch {
case initContextWithRenderAuth(ctx, renderService):
case initContextWithApiKey(ctx):
case initContextWithBasicAuth(ctx, orgID):
case initContextWithAuthProxy(remoteCache, ctx, orgID):
case initContextWithToken(ats, ctx, orgID):
case initContextWithAnonymousUser(ctx):
}
ctx.Logger = log.New("context", "userId", ctx.UserId, "orgId", ctx.OrgId, "uname", ctx.Login)
ctx.Data["ctx"] = ctx
c.Map(ctx)
// update last seen every 5min
if ctx.ShouldUpdateLastSeenAt() {
ctx.Logger.Debug("Updating last user_seen_at", "user_id", ctx.UserId)
if err := bus.Dispatch(&models.UpdateUserLastSeenAtCommand{UserId: ctx.UserId}); err != nil {
ctx.Logger.Error("Failed to update last_seen_at", "error", err)
}
}
}
}
func initContextWithAnonymousUser(ctx *models.ReqContext) bool {
if !setting.AnonymousEnabled {
return false
}
orgQuery := models.GetOrgByNameQuery{Name: setting.AnonymousOrgName}
if err := bus.Dispatch(&orgQuery); err != nil {
log.Errorf(3, "Anonymous access organization error: '%s': %s", setting.AnonymousOrgName, err)
return false
}
ctx.IsSignedIn = false
ctx.AllowAnonymous = true
ctx.SignedInUser = &models.SignedInUser{IsAnonymous: true}
ctx.OrgRole = models.RoleType(setting.AnonymousOrgRole)
ctx.OrgId = orgQuery.Result.Id
ctx.OrgName = orgQuery.Result.Name
return true
}
func initContextWithApiKey(ctx *models.ReqContext) bool {
var keyString string
if keyString = getApiKey(ctx); keyString == "" {
return false
}
// base64 decode key
decoded, err := apikeygen.Decode(keyString)
if err != nil {
ctx.JsonApiErr(401, errStringInvalidAPIKey, err)
return true
}
// fetch key
keyQuery := models.GetApiKeyByNameQuery{KeyName: decoded.Name, OrgId: decoded.OrgId}
if err := bus.Dispatch(&keyQuery); err != nil {
ctx.JsonApiErr(401, errStringInvalidAPIKey, err)
return true
}
apikey := keyQuery.Result
// validate api key
isValid, err := apikeygen.IsValid(decoded, apikey.Key)
if err != nil {
ctx.JsonApiErr(500, "Validating API key failed", err)
return true
}
if !isValid {
ctx.JsonApiErr(401, errStringInvalidAPIKey, err)
return true
}
// check for expiration
if apikey.Expires != nil && *apikey.Expires <= getTime().Unix() {
ctx.JsonApiErr(401, "Expired API key", err)
return true
}
ctx.IsSignedIn = true
ctx.SignedInUser = &models.SignedInUser{}
ctx.OrgRole = apikey.Role
ctx.ApiKeyId = apikey.Id
ctx.OrgId = apikey.OrgId
return true
}
func initContextWithBasicAuth(ctx *models.ReqContext, orgId int64) bool {
if !setting.BasicAuthEnabled {
return false
}
header := ctx.Req.Header.Get("Authorization")
if header == "" {
return false
}
username, password, err := util.DecodeBasicAuthHeader(header)
if err != nil {
ctx.JsonApiErr(401, "Invalid Basic Auth Header", err)
return true
}
authQuery := models.LoginUserQuery{
Username: username,
Password: password,
}
if err := bus.Dispatch(&authQuery); err != nil {
ctx.Logger.Debug(
"Failed to authorize the user",
"username", username,
"err", err,
)
if errors.Is(err, models.ErrUserNotFound) {
err = login.ErrInvalidCredentials
}
ctx.JsonApiErr(401, errStringInvalidUsernamePassword, err)
return true
}
user := authQuery.User
query := models.GetSignedInUserQuery{UserId: user.Id, OrgId: orgId}
if err := bus.Dispatch(&query); err != nil {
ctx.Logger.Error(
"Failed at user signed in",
"id", user.Id,
"org", orgId,
)
ctx.JsonApiErr(401, errStringInvalidUsernamePassword, err)
return true
}
ctx.SignedInUser = query.Result
ctx.IsSignedIn = true
return true
}
func initContextWithToken(authTokenService models.UserTokenService, ctx *models.ReqContext, orgID int64) bool {
if setting.LoginCookieName == "" {
return false
}
rawToken := ctx.GetCookie(setting.LoginCookieName)
if rawToken == "" {
return false
}
token, err := authTokenService.LookupToken(ctx.Req.Context(), rawToken)
if err != nil {
ctx.Logger.Error("Failed to look up user based on cookie", "error", err)
WriteSessionCookie(ctx, "", -1)
return false
}
query := models.GetSignedInUserQuery{UserId: token.UserId, OrgId: orgID}
if err := bus.Dispatch(&query); err != nil {
ctx.Logger.Error("Failed to get user with id", "userId", token.UserId, "error", err)
return false
}
ctx.SignedInUser = query.Result
ctx.IsSignedIn = true
ctx.UserToken = token
// Rotate the token just before we write response headers to ensure there is no delay between
// the new token being generated and the client receiving it.
ctx.Resp.Before(rotateEndOfRequestFunc(ctx, authTokenService, token))
return true
}
func rotateEndOfRequestFunc(ctx *models.ReqContext, authTokenService models.UserTokenService, token *models.UserToken) macaron.BeforeFunc {
return func(w macaron.ResponseWriter) {
// if response has already been written, skip.
if w.Written() {
return
}
// if the request is cancelled by the client we should not try
// to rotate the token since the client would not accept any result.
if errors.Is(ctx.Context.Req.Context().Err(), context.Canceled) {
return
}
addr := ctx.RemoteAddr()
ip, err := network.GetIPFromAddress(addr)
if err != nil {
ctx.Logger.Debug("Failed to get client IP address", "addr", addr, "err", err)
ip = nil
}
rotated, err := authTokenService.TryRotateToken(ctx.Req.Context(), token, ip, ctx.Req.UserAgent())
if err != nil {
ctx.Logger.Error("Failed to rotate token", "error", err)
return
}
if rotated {
WriteSessionCookie(ctx, token.UnhashedToken, setting.LoginMaxLifetime)
}
}
}
func AddDefaultResponseHeaders() macaron.Handler {
func AddDefaultResponseHeaders(cfg *setting.Cfg) macaron.Handler {
return func(ctx *macaron.Context) {
ctx.Resp.Before(func(w macaron.ResponseWriter) {
// if response has already been written, skip.
@@ -285,47 +29,46 @@ func AddDefaultResponseHeaders() macaron.Handler {
}
if !strings.HasPrefix(ctx.Req.URL.Path, "/api/datasources/proxy/") {
AddNoCacheHeaders(ctx.Resp)
addNoCacheHeaders(ctx.Resp)
}
if !setting.AllowEmbedding {
AddXFrameOptionsDenyHeader(w)
if !cfg.AllowEmbedding {
addXFrameOptionsDenyHeader(w)
}
AddSecurityHeaders(w)
addSecurityHeaders(w, cfg)
})
}
}
// AddSecurityHeaders adds various HTTP(S) response headers that enable various security protections behaviors in the client's browser.
func AddSecurityHeaders(w macaron.ResponseWriter) {
if (setting.Protocol == setting.HTTPSScheme || setting.Protocol == setting.HTTP2Scheme) &&
setting.StrictTransportSecurity {
strictHeaderValues := []string{fmt.Sprintf("max-age=%v", setting.StrictTransportSecurityMaxAge)}
if setting.StrictTransportSecurityPreload {
// addSecurityHeaders adds HTTP(S) response headers that enable various security protections in the client's browser.
func addSecurityHeaders(w macaron.ResponseWriter, cfg *setting.Cfg) {
if (cfg.Protocol == setting.HTTPSScheme || cfg.Protocol == setting.HTTP2Scheme) && cfg.StrictTransportSecurity {
strictHeaderValues := []string{fmt.Sprintf("max-age=%v", cfg.StrictTransportSecurityMaxAge)}
if cfg.StrictTransportSecurityPreload {
strictHeaderValues = append(strictHeaderValues, "preload")
}
if setting.StrictTransportSecuritySubDomains {
if cfg.StrictTransportSecuritySubDomains {
strictHeaderValues = append(strictHeaderValues, "includeSubDomains")
}
w.Header().Add("Strict-Transport-Security", strings.Join(strictHeaderValues, "; "))
}
if setting.ContentTypeProtectionHeader {
if cfg.ContentTypeProtectionHeader {
w.Header().Add("X-Content-Type-Options", "nosniff")
}
if setting.XSSProtectionHeader {
if cfg.XSSProtectionHeader {
w.Header().Add("X-XSS-Protection", "1; mode=block")
}
}
func AddNoCacheHeaders(w macaron.ResponseWriter) {
func addNoCacheHeaders(w macaron.ResponseWriter) {
w.Header().Add("Cache-Control", "no-cache")
w.Header().Add("Pragma", "no-cache")
w.Header().Add("Expires", "-1")
}
func AddXFrameOptionsDenyHeader(w macaron.ResponseWriter) {
func addXFrameOptionsDenyHeader(w macaron.ResponseWriter) {
w.Header().Add("X-Frame-Options", "deny")
}
+16 -20
View File
@@ -7,6 +7,7 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/contexthandler"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/stretchr/testify/assert"
@@ -14,19 +15,13 @@ import (
)
func TestMiddlewareBasicAuth(t *testing.T) {
var origBasicAuthEnabled = setting.BasicAuthEnabled
var origDisableBruteForceLoginProtection = setting.DisableBruteForceLoginProtection
t.Cleanup(func() {
setting.BasicAuthEnabled = origBasicAuthEnabled
setting.DisableBruteForceLoginProtection = origDisableBruteForceLoginProtection
})
setting.BasicAuthEnabled = true
setting.DisableBruteForceLoginProtection = true
bus.ClearBusHandlers()
const id int64 = 12
configure := func(cfg *setting.Cfg) {
cfg.BasicAuthEnabled = true
cfg.DisableBruteForceLoginProtection = true
}
middlewareScenario(t, "Valid API key", func(t *testing.T, sc *scenarioContext) {
const orgID int64 = 2
keyhash, err := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd")
@@ -44,16 +39,15 @@ func TestMiddlewareBasicAuth(t *testing.T) {
assert.True(t, sc.context.IsSignedIn)
assert.Equal(t, orgID, sc.context.OrgId)
assert.Equal(t, models.ROLE_EDITOR, sc.context.OrgRole)
})
}, configure)
middlewareScenario(t, "Handle auth", func(t *testing.T, sc *scenarioContext) {
const password = "MyPass"
const salt = "Salt"
const orgID int64 = 2
t.Cleanup(bus.ClearBusHandlers)
bus.AddHandler("grafana-auth", func(query *models.LoginUserQuery) error {
t.Log("Handling LoginUserQuery")
encoded, err := util.EncodePassword(password, salt)
if err != nil {
return err
@@ -66,6 +60,7 @@ func TestMiddlewareBasicAuth(t *testing.T) {
})
bus.AddHandler("get-sign-user", func(query *models.GetSignedInUserQuery) error {
t.Log("Handling GetSignedInUserQuery")
query.Result = &models.SignedInUser{OrgId: orgID, UserId: id}
return nil
})
@@ -76,7 +71,7 @@ func TestMiddlewareBasicAuth(t *testing.T) {
assert.True(t, sc.context.IsSignedIn)
assert.Equal(t, orgID, sc.context.OrgId)
assert.Equal(t, id, sc.context.UserId)
})
}, configure)
middlewareScenario(t, "Auth sequence", func(t *testing.T, sc *scenarioContext) {
const password = "MyPass"
@@ -104,10 +99,11 @@ func TestMiddlewareBasicAuth(t *testing.T) {
authHeader := util.GetBasicAuthHeader("myUser", password)
sc.fakeReq("GET", "/").withAuthorizationHeader(authHeader).exec()
require.NotNil(t, sc.context)
assert.True(t, sc.context.IsSignedIn)
assert.Equal(t, id, sc.context.UserId)
})
}, configure)
middlewareScenario(t, "Should return error if user is not found", func(t *testing.T, sc *scenarioContext) {
sc.fakeReq("GET", "/")
@@ -118,8 +114,8 @@ func TestMiddlewareBasicAuth(t *testing.T) {
require.Error(t, err)
assert.Equal(t, 401, sc.resp.Code)
assert.Equal(t, errStringInvalidUsernamePassword, sc.respJson["message"])
})
assert.Equal(t, contexthandler.InvalidUsernamePassword, sc.respJson["message"])
}, configure)
middlewareScenario(t, "Should return error if user & password do not match", func(t *testing.T, sc *scenarioContext) {
bus.AddHandler("user-query", func(loginUserQuery *models.GetUserByLoginQuery) error {
@@ -134,6 +130,6 @@ func TestMiddlewareBasicAuth(t *testing.T) {
require.Error(t, err)
assert.Equal(t, 401, sc.resp.Code)
assert.Equal(t, errStringInvalidUsernamePassword, sc.respJson["message"])
})
assert.Equal(t, contexthandler.InvalidUsernamePassword, sc.respJson["message"])
}, configure)
}
+125 -244
View File
@@ -6,7 +6,6 @@ import (
"fmt"
"net"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"time"
@@ -18,31 +17,30 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/gtime"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/middleware/authproxy"
"github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/login"
"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"
)
const errorTemplate = "error-template"
func mockGetTime() {
func fakeGetTime() func() time.Time {
var timeSeed int64
getTime = func() time.Time {
return func() time.Time {
fakeNow := time.Unix(timeSeed, 0)
timeSeed++
return fakeNow
}
}
func resetGetTime() {
getTime = time.Now
}
func TestMiddleWareSecurityHeaders(t *testing.T) {
origErrTemplateName := setting.ErrTemplateName
t.Cleanup(func() {
@@ -51,46 +49,32 @@ func TestMiddleWareSecurityHeaders(t *testing.T) {
setting.ErrTemplateName = errorTemplate
middlewareScenario(t, "middleware should get correct x-xss-protection header", func(t *testing.T, sc *scenarioContext) {
origXSSProtectionHeader := setting.XSSProtectionHeader
t.Cleanup(func() {
setting.XSSProtectionHeader = origXSSProtectionHeader
})
setting.XSSProtectionHeader = true
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) {
origXSSProtectionHeader := setting.XSSProtectionHeader
t.Cleanup(func() {
setting.XSSProtectionHeader = origXSSProtectionHeader
})
setting.XSSProtectionHeader = false
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) {
origStrictTransportSecurity := setting.StrictTransportSecurity
origProtocol := setting.Protocol
origStrictTransportSecurityMaxAge := setting.StrictTransportSecurityMaxAge
t.Cleanup(func() {
setting.StrictTransportSecurity = origStrictTransportSecurity
setting.Protocol = origProtocol
setting.StrictTransportSecurityMaxAge = origStrictTransportSecurityMaxAge
})
setting.StrictTransportSecurity = true
setting.Protocol = setting.HTTPSScheme
setting.StrictTransportSecurityMaxAge = 64000
sc.fakeReq("GET", "/api/").exec()
assert.Equal(t, "max-age=64000", sc.resp.Header().Get("Strict-Transport-Security"))
setting.StrictTransportSecurityPreload = true
sc.cfg.StrictTransportSecurityPreload = true
sc.fakeReq("GET", "/api/").exec()
assert.Equal(t, "max-age=64000; preload", sc.resp.Header().Get("Strict-Transport-Security"))
setting.StrictTransportSecuritySubDomains = true
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.Protocol = setting.HTTPSScheme
cfg.StrictTransportSecurity = true
cfg.StrictTransportSecurityMaxAge = 64000
})
}
@@ -151,13 +135,10 @@ func TestMiddlewareContext(t *testing.T) {
middlewareScenario(t, "middleware should not add X-Frame-Options header for request when allowing embedding", func(
t *testing.T, sc *scenarioContext) {
origAllowEmbedding := setting.AllowEmbedding
t.Cleanup(func() {
setting.AllowEmbedding = origAllowEmbedding
})
setting.AllowEmbedding = true
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) {
@@ -166,7 +147,7 @@ func TestMiddlewareContext(t *testing.T) {
assert.Empty(t, sc.resp.Header().Get("Set-Cookie"))
assert.Equal(t, 401, sc.resp.Code)
assert.Equal(t, errStringInvalidAPIKey, sc.respJson["message"])
assert.Equal(t, contexthandler.InvalidAPIKey, sc.respJson["message"])
})
middlewareScenario(t, "Valid api key", func(t *testing.T, sc *scenarioContext) {
@@ -199,19 +180,18 @@ func TestMiddlewareContext(t *testing.T) {
sc.fakeReq("GET", "/").withValidApiKey().exec()
assert.Equal(t, 401, sc.resp.Code)
assert.Equal(t, errStringInvalidAPIKey, sc.respJson["message"])
assert.Equal(t, contexthandler.InvalidAPIKey, sc.respJson["message"])
})
middlewareScenario(t, "Valid api key, but expired", func(t *testing.T, sc *scenarioContext) {
mockGetTime()
defer resetGetTime()
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(query *models.GetApiKeyByNameQuery) error {
// api key expired one second before
expires := getTime().Add(-1 * time.Second).Unix()
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
@@ -223,7 +203,7 @@ func TestMiddlewareContext(t *testing.T) {
assert.Equal(t, "Expired API key", sc.respJson["message"])
})
middlewareScenario(t, "Non-expired auth token in cookie which not are being rotated", func(
middlewareScenario(t, "Non-expired auth token in cookie which is not being rotated", func(
t *testing.T, sc *scenarioContext) {
const userID int64 = 12
@@ -357,18 +337,6 @@ func TestMiddlewareContext(t *testing.T) {
middlewareScenario(t, "When anonymous access is enabled", func(t *testing.T, sc *scenarioContext) {
const orgID int64 = 2
origAnonymousEnabled := setting.AnonymousEnabled
origAnonymousOrgName := setting.AnonymousOrgName
origAnonymousOrgRole := setting.AnonymousOrgRole
t.Cleanup(func() {
setting.AnonymousEnabled = origAnonymousEnabled
setting.AnonymousOrgName = origAnonymousOrgName
setting.AnonymousOrgRole = origAnonymousOrgRole
})
setting.AnonymousEnabled = true
setting.AnonymousOrgName = "test"
setting.AnonymousOrgRole = string(models.ROLE_EDITOR)
bus.AddHandler("test", func(query *models.GetOrgByNameQuery) error {
assert.Equal(t, "test", query.Name)
@@ -382,35 +350,24 @@ func TestMiddlewareContext(t *testing.T) {
assert.Equal(t, orgID, 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
origAuthProxyEnabled := setting.AuthProxyEnabled
origAuthProxyWhitelist := setting.AuthProxyWhitelist
origAuthProxyAutoSignUp := setting.AuthProxyAutoSignUp
origLDAPEnabled := setting.LDAPEnabled
origAuthProxyHeaderName := setting.AuthProxyHeaderName
origAuthProxyHeaderProperty := setting.AuthProxyHeaderProperty
origAuthProxyHeaders := setting.AuthProxyHeaders
t.Cleanup(func() {
setting.AuthProxyEnabled = origAuthProxyEnabled
setting.AuthProxyWhitelist = origAuthProxyWhitelist
setting.AuthProxyAutoSignUp = origAuthProxyAutoSignUp
setting.LDAPEnabled = origLDAPEnabled
setting.AuthProxyHeaderName = origAuthProxyHeaderName
setting.AuthProxyHeaderProperty = origAuthProxyHeaderProperty
setting.AuthProxyHeaders = origAuthProxyHeaders
})
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"}
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"}
}
const hdrName = "markelog"
const group = "grafana-core-team"
@@ -426,25 +383,16 @@ func TestMiddlewareContext(t *testing.T) {
require.NoError(t, err)
sc.fakeReq("GET", "/")
sc.req.Header.Set(setting.AuthProxyHeaderName, hdrName)
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) {
origLDAPEnabled = setting.LDAPEnabled
origAuthProxyAutoSignUp = setting.AuthProxyAutoSignUp
t.Cleanup(func() {
setting.LDAPEnabled = origLDAPEnabled
setting.AuthProxyAutoSignUp = origAuthProxyAutoSignUp
})
setting.LDAPEnabled = false
setting.AuthProxyAutoSignUp = false
var actualAuthProxyAutoSignUp *bool = nil
bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error {
@@ -453,24 +401,19 @@ func TestMiddlewareContext(t *testing.T) {
})
sc.fakeReq("GET", "/")
sc.req.Header.Set(setting.AuthProxyHeaderName, hdrName)
sc.req.Header.Set(sc.cfg.AuthProxyHeaderName, hdrName)
sc.exec()
assert.False(t, *actualAuthProxyAutoSignUp)
assert.Equal(t, sc.resp.Code, 407)
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) {
origLDAPEnabled = setting.LDAPEnabled
origAuthProxyAutoSignUp = setting.AuthProxyAutoSignUp
t.Cleanup(func() {
setting.LDAPEnabled = origLDAPEnabled
setting.AuthProxyAutoSignUp = origAuthProxyAutoSignUp
})
setting.LDAPEnabled = false
setting.AuthProxyAutoSignUp = true
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
if query.UserId > 0 {
query.Result = &models.SignedInUser{OrgId: orgID, UserId: userID}
@@ -485,24 +428,22 @@ func TestMiddlewareContext(t *testing.T) {
})
sc.fakeReq("GET", "/")
sc.req.Header.Set(setting.AuthProxyHeaderName, hdrName)
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 get an existing user from header", func(t *testing.T, sc *scenarioContext) {
const userID int64 = 12
const orgID int64 = 2
origLDAPEnabled = setting.LDAPEnabled
t.Cleanup(func() {
setting.LDAPEnabled = origLDAPEnabled
})
setting.LDAPEnabled = false
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
query.Result = &models.SignedInUser{OrgId: orgID, UserId: userID}
return nil
@@ -514,24 +455,18 @@ func TestMiddlewareContext(t *testing.T) {
})
sc.fakeReq("GET", "/")
sc.req.Header.Set(setting.AuthProxyHeaderName, hdrName)
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) {
origAuthProxyWhitelist = setting.AuthProxyWhitelist
origLDAPEnabled = setting.LDAPEnabled
t.Cleanup(func() {
setting.AuthProxyWhitelist = origAuthProxyWhitelist
setting.LDAPEnabled = origLDAPEnabled
})
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: orgID, UserId: userID}
return nil
@@ -543,25 +478,20 @@ func TestMiddlewareContext(t *testing.T) {
})
sc.fakeReq("GET", "/")
sc.req.Header.Set(setting.AuthProxyHeaderName, hdrName)
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 whitelist IP", func(t *testing.T, sc *scenarioContext) {
origAuthProxyWhitelist = setting.AuthProxyWhitelist
origLDAPEnabled = setting.LDAPEnabled
t.Cleanup(func() {
setting.AuthProxyWhitelist = origAuthProxyWhitelist
setting.LDAPEnabled = origLDAPEnabled
})
setting.AuthProxyWhitelist = "8.8.8.8"
setting.LDAPEnabled = false
middlewareScenario(t, "Should not allow the request from whitelisted IP", func(t *testing.T, sc *scenarioContext) {
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
query.Result = &models.SignedInUser{OrgId: orgID, UserId: userID}
return nil
@@ -573,12 +503,16 @@ func TestMiddlewareContext(t *testing.T) {
})
sc.fakeReq("GET", "/")
sc.req.Header.Set(setting.AuthProxyHeaderName, hdrName)
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) {
@@ -587,12 +521,12 @@ func TestMiddlewareContext(t *testing.T) {
})
sc.fakeReq("GET", "/")
sc.req.Header.Set(setting.AuthProxyHeaderName, hdrName)
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(query *models.GetSignedInUserQuery) error {
@@ -600,52 +534,53 @@ func TestMiddlewareContext(t *testing.T) {
})
sc.fakeReq("GET", "/")
sc.req.Header.Set(setting.AuthProxyHeaderName, hdrName)
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) {
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)
origLoginCookieName := setting.LoginCookieName
origLoginMaxLifetime := setting.LoginMaxLifetime
t.Cleanup(func() {
setting.LoginCookieName = origLoginCookieName
setting.LoginMaxLifetime = origLoginMaxLifetime
})
setting.LoginCookieName = "grafana_session"
var err error
setting.LoginMaxLifetime, err = gtime.ParseDuration("30d")
loginMaxLifetime, err := gtime.ParseDuration("30d")
require.NoError(t, err)
cfg := setting.NewCfg()
cfg.LoginCookieName = "grafana_session"
cfg.LoginMaxLifetime = loginMaxLifetime
for _, cb := range cbs {
cb(cfg)
}
sc := &scenarioContext{t: t}
sc := &scenarioContext{t: t, cfg: cfg}
viewsPath, err := filepath.Abs("../../public/views")
require.NoError(t, err)
sc.m = macaron.New()
sc.m.Use(AddDefaultResponseHeaders())
sc.m.Use(AddDefaultResponseHeaders(cfg))
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))
ctxHdlr := getContextHandler(t, cfg)
sc.contextHandler = ctxHdlr
sc.m.Use(ctxHdlr.Middleware)
sc.m.Use(OrgRedirect())
sc.userAuthTokenService = ctxHdlr.AuthTokenService.(*auth.FakeUserAuthTokenService)
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)
@@ -662,106 +597,52 @@ func middlewareScenario(t *testing.T, desc string, fn scenarioFunc) {
})
}
func TestDontRotateTokensOnCancelledRequests(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
reqContext, _, err := initTokenRotationTest(ctx, t)
require.NoError(t, err)
tryRotateCallCount := 0
uts := &auth.FakeUserAuthTokenService{
TryRotateTokenProvider: func(ctx context.Context, token *models.UserToken, clientIP net.IP,
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(), t)
require.NoError(t, err)
uts := &auth.FakeUserAuthTokenService{
TryRotateTokenProvider: func(ctx context.Context, token *models.UserToken, clientIP net.IP,
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
resp := rr.Result()
defer resp.Body.Close()
for _, c := range resp.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, t *testing.T) (*models.ReqContext, *httptest.ResponseRecorder, error) {
func getContextHandler(t *testing.T, cfg *setting.Cfg) *contexthandler.ContextHandler {
t.Helper()
origLoginCookieName := setting.LoginCookieName
origLoginMaxLifetime := setting.LoginMaxLifetime
t.Cleanup(func() {
setting.LoginCookieName = origLoginCookieName
setting.LoginMaxLifetime = origLoginMaxLifetime
})
setting.LoginCookieName = "login_token"
var err error
setting.LoginMaxLifetime, err = gtime.ParseDuration("7d")
if err != nil {
return nil, nil, err
sqlStore := sqlstore.InitTestDB(t)
remoteCacheSvc := &remotecache.RemoteCache{}
if cfg == nil {
cfg = setting.NewCfg()
}
cfg.RemoteCacheOptions = &setting.RemoteCacheOptions{
Name: "database",
}
userAuthTokenSvc := auth.NewFakeUserAuthTokenService()
renderSvc := &fakeRenderService{}
ctxHdlr := &contexthandler.ContextHandler{}
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,
},
err := registry.BuildServiceGraph([]interface{}{cfg}, []*registry.Descriptor{
{
Name: sqlstore.ServiceName,
Instance: sqlStore,
},
Logger: log.New("testlogger"),
}
{
Name: remotecache.ServiceName,
Instance: remoteCacheSvc,
},
{
Name: auth.ServiceName,
Instance: userAuthTokenSvc,
},
{
Name: rendering.ServiceName,
Instance: renderSvc,
},
{
Name: contexthandler.ServiceName,
Instance: ctxHdlr,
},
})
require.NoError(t, err)
mw := mockWriter{rr}
reqContext.Resp = mw
return reqContext, rr, nil
return ctxHdlr
}
type mockWriter struct {
*httptest.ResponseRecorder
type fakeRenderService struct {
rendering.Service
}
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) {}
func (mw mockWriter) Push(target string, opts *http.PushOptions) error {
func (s *fakeRenderService) Init() error {
return nil
}
+6 -1
View File
@@ -7,6 +7,7 @@ import (
"time"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -18,6 +19,8 @@ type advanceTimeFunc func(deltaTime time.Duration)
type rateLimiterScenarioFunc func(c execFunc, t advanceTimeFunc)
func rateLimiterScenario(t *testing.T, desc string, rps int, burst int, fn rateLimiterScenarioFunc) {
t.Helper()
t.Run(desc, func(t *testing.T) {
defaultHandler := func(c *models.ReqContext) {
resp := make(map[string]interface{})
@@ -26,12 +29,14 @@ func rateLimiterScenario(t *testing.T, desc string, rps int, burst int, fn rateL
}
currentTime := time.Now()
cfg := setting.NewCfg()
m := macaron.New()
m.Use(macaron.Renderer(macaron.RenderOptions{
Directory: "",
Delims: macaron.Delims{Left: "[[", Right: "]]"},
}))
m.Use(GetContextHandler(nil, nil, nil))
m.Use(getContextHandler(t, cfg).Middleware)
m.Get("/foo", RateLimit(rps, burst, func() time.Time { return currentTime }), defaultHandler)
fn(func() *httptest.ResponseRecorder {
+3 -3
View File
@@ -103,7 +103,7 @@ func function(pc uintptr) []byte {
// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one.
// While Martini is in development mode, Recovery will also output the panic as HTML.
func Recovery() macaron.Handler {
func Recovery(cfg *setting.Cfg) macaron.Handler {
return func(c *macaron.Context) {
defer func() {
if r := recover(); r != nil {
@@ -134,7 +134,7 @@ func Recovery() macaron.Handler {
c.Data["Title"] = "Server Error"
c.Data["AppSubUrl"] = setting.AppSubUrl
c.Data["Theme"] = setting.DefaultTheme
c.Data["Theme"] = cfg.DefaultTheme
if setting.Env == setting.Dev {
if err, ok := r.(error); ok {
@@ -158,7 +158,7 @@ func Recovery() macaron.Handler {
c.JSON(500, resp)
} else {
c.HTML(500, setting.ErrTemplateName)
c.HTML(500, cfg.ErrTemplateName)
}
}
}()
+7 -5
View File
@@ -16,8 +16,6 @@ import (
)
func TestRecoveryMiddleware(t *testing.T) {
setting.ErrTemplateName = "error-template"
t.Run("Given an API route that panics", func(t *testing.T) {
apiURL := "/api/whatever"
recoveryScenario(t, "recovery middleware should return json", apiURL, func(t *testing.T, sc *scenarioContext) {
@@ -52,18 +50,21 @@ func recoveryScenario(t *testing.T, desc string, url string, fn scenarioFunc) {
t.Run(desc, func(t *testing.T) {
defer bus.ClearBusHandlers()
cfg := setting.NewCfg()
cfg.ErrTemplateName = "error-template"
sc := &scenarioContext{
t: t,
url: url,
cfg: cfg,
}
viewsPath, err := filepath.Abs("../../public/views")
require.NoError(t, err)
sc.m = macaron.New()
sc.m.Use(Recovery())
sc.m.Use(Recovery(cfg))
sc.m.Use(AddDefaultResponseHeaders())
sc.m.Use(AddDefaultResponseHeaders(cfg))
sc.m.Use(macaron.Renderer(macaron.RenderOptions{
Directory: viewsPath,
Delims: macaron.Delims{Left: "[[", Right: "]]"},
@@ -72,7 +73,8 @@ func recoveryScenario(t *testing.T, desc string, url string, fn scenarioFunc) {
sc.userAuthTokenService = auth.NewFakeUserAuthTokenService()
sc.remoteCacheService = remotecache.NewFakeStore(t)
sc.m.Use(GetContextHandler(sc.userAuthTokenService, sc.remoteCacheService, nil))
contextHandler := getContextHandler(t, nil)
sc.m.Use(contextHandler.Middleware)
// mock out gc goroutine
sc.m.Use(OrgRedirect())
-31
View File
@@ -1,31 +0,0 @@
package middleware
import (
"time"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/rendering"
)
func initContextWithRenderAuth(ctx *models.ReqContext, renderService rendering.Service) bool {
key := ctx.GetCookie("renderKey")
if key == "" {
return false
}
renderUser, exists := renderService.GetRenderUser(key)
if !exists {
ctx.JsonApiErr(401, "Invalid Render Key", nil)
return true
}
ctx.IsSignedIn = true
ctx.SignedInUser = &models.SignedInUser{
OrgId: renderUser.OrgID,
UserId: renderUser.UserID,
OrgRole: models.RoleType(renderUser.OrgRole),
}
ctx.IsRenderCall = true
ctx.LastSeenAt = time.Now()
return true
}
+5 -2
View File
@@ -11,6 +11,7 @@ import (
"github.com/grafana/grafana/pkg/infra/remotecache"
"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/setting"
"github.com/stretchr/testify/require"
)
@@ -29,6 +30,8 @@ type scenarioContext struct {
url string
userAuthTokenService *auth.FakeUserAuthTokenService
remoteCacheService *remotecache.RemoteCache
cfg *setting.Cfg
contextHandler *contexthandler.ContextHandler
req *http.Request
}
@@ -94,9 +97,9 @@ func (sc *scenarioContext) exec() {
}
if sc.tokenSessionCookie != "" {
sc.t.Log(`Adding cookie`, "name", setting.LoginCookieName, "value", sc.tokenSessionCookie)
sc.t.Log(`Adding cookie`, "name", sc.cfg.LoginCookieName, "value", sc.tokenSessionCookie)
sc.req.AddCookie(&http.Cookie{
Name: setting.LoginCookieName,
Name: sc.cfg.LoginCookieName,
Value: sc.tokenSessionCookie,
})
}