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:
co-authored by
Emil Tullsted
Will Browne
parent
d0f52d5334
commit
12661e8a9d
+4
-20
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user