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>
pull/29775/head
Arve Knudsen 5 years ago committed by GitHub
parent d0f52d5334
commit 12661e8a9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      pkg/api/admin_users_test.go
  2. 2
      pkg/api/api.go
  3. 2
      pkg/api/common.go
  4. 61
      pkg/api/common_test.go
  5. 6
      pkg/api/frontendsettings.go
  6. 30
      pkg/api/frontendsettings_test.go
  7. 40
      pkg/api/http_server.go
  8. 4
      pkg/api/index.go
  9. 8
      pkg/api/ldap_debug.go
  10. 18
      pkg/api/ldap_debug_test.go
  11. 21
      pkg/api/login.go
  12. 8
      pkg/api/login_oauth.go
  13. 4
      pkg/api/login_test.go
  14. 11
      pkg/infra/remotecache/remotecache.go
  15. 8
      pkg/login/auth.go
  16. 20
      pkg/login/auth_test.go
  17. 9
      pkg/login/brute_force_login_protection.go
  18. 30
      pkg/login/brute_force_login_protection_test.go
  19. 2
      pkg/login/ldap_login.go
  20. 16
      pkg/login/ldap_login_test.go
  21. 24
      pkg/middleware/auth.go
  22. 133
      pkg/middleware/auth_proxy.go
  23. 26
      pkg/middleware/auth_test.go
  24. 6
      pkg/middleware/cookies/cookies.go
  25. 4
      pkg/middleware/logger.go
  26. 287
      pkg/middleware/middleware.go
  27. 36
      pkg/middleware/middleware_basic_auth_test.go
  28. 369
      pkg/middleware/middleware_test.go
  29. 7
      pkg/middleware/rate_limit_test.go
  30. 6
      pkg/middleware/recovery.go
  31. 12
      pkg/middleware/recovery_test.go
  32. 31
      pkg/middleware/render_auth.go
  33. 7
      pkg/middleware/testing.go
  34. 2
      pkg/models/user_auth.go
  35. 45
      pkg/registry/di.go
  36. 8
      pkg/services/auth/auth_token.go
  37. 9
      pkg/services/auth/testing.go
  38. 81
      pkg/services/contexthandler/auth_proxy_test.go
  39. 96
      pkg/services/contexthandler/authproxy/authproxy.go
  40. 107
      pkg/services/contexthandler/authproxy/authproxy_test.go
  41. 448
      pkg/services/contexthandler/contexthandler.go
  42. 126
      pkg/services/contexthandler/contexthandler_test.go
  43. 8
      pkg/services/ldap/settings.go
  44. 7
      pkg/services/rendering/rendering.go
  45. 6
      pkg/services/rendering/rendering_test.go
  46. 4
      pkg/services/sqlstore/preferences.go
  47. 3
      pkg/services/sqlstore/preferences_test.go
  48. 28
      pkg/services/sqlstore/sqlstore.go
  49. 164
      pkg/setting/setting.go
  50. 2
      pkg/setting/setting_quota.go
  51. 10
      pkg/setting/setting_test.go

@ -22,7 +22,7 @@ const (
func TestAdminAPIEndpoint(t *testing.T) {
const role = models.ROLE_ADMIN
t.Run("Given a server admin attempts to remove themself as an admin", func(t *testing.T) {
t.Run("Given a server admin attempts to remove themselves as an admin", func(t *testing.T) {
updateCmd := dtos.AdminUpdateUserPermissionsForm{
IsGrafanaAdmin: false,
}

@ -18,7 +18,7 @@ func (hs *HTTPServer) registerRoutes() {
reqEditorRole := middleware.ReqEditorRole
reqOrgAdmin := middleware.ReqOrgAdmin
reqCanAccessTeams := middleware.AdminOrFeatureEnabled(hs.Cfg.EditorsCanAdmin)
reqSnapshotPublicModeOrSignedIn := middleware.SnapshotPublicModeOrSignedIn()
reqSnapshotPublicModeOrSignedIn := middleware.SnapshotPublicModeOrSignedIn(hs.Cfg)
redirectFromLegacyDashboardURL := middleware.RedirectFromLegacyDashboardURL()
redirectFromLegacyDashboardSoloURL := middleware.RedirectFromLegacyDashboardSoloURL()
redirectFromLegacyPanelEditURL := middleware.RedirectFromLegacyPanelEditURL()

@ -85,7 +85,7 @@ func Success(message string) *NormalResponse {
return JSON(200, resp)
}
// Error create a erroneous response
// Error creates an error response.
func Error(status int, message string, err error) *NormalResponse {
data := make(map[string]interface{})

@ -8,9 +8,14 @@ import (
"testing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/infra/remotecache"
"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/contexthandler"
"github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/require"
"gopkg.in/macaron.v1"
)
@ -141,20 +146,68 @@ func (sc *scenarioContext) exec() {
type scenarioFunc func(c *scenarioContext)
type handlerFunc func(c *models.ReqContext) Response
func getContextHandler(t *testing.T) *contexthandler.ContextHandler {
t.Helper()
sqlStore := sqlstore.InitTestDB(t)
remoteCacheSvc := &remotecache.RemoteCache{}
cfg := setting.NewCfg()
cfg.RemoteCacheOptions = &setting.RemoteCacheOptions{
Name: "database",
}
userAuthTokenSvc := auth.NewFakeUserAuthTokenService()
renderSvc := &fakeRenderService{}
ctxHdlr := &contexthandler.ContextHandler{}
err := registry.BuildServiceGraph([]interface{}{cfg}, []*registry.Descriptor{
{
Name: sqlstore.ServiceName,
Instance: sqlStore,
},
{
Name: remotecache.ServiceName,
Instance: remoteCacheSvc,
},
{
Name: auth.ServiceName,
Instance: userAuthTokenSvc,
},
{
Name: rendering.ServiceName,
Instance: renderSvc,
},
{
Name: contexthandler.ServiceName,
Instance: ctxHdlr,
},
})
require.NoError(t, err)
return ctxHdlr
}
func setupScenarioContext(t *testing.T, url string) *scenarioContext {
sc := &scenarioContext{
url: url,
t: t,
}
viewsPath, _ := filepath.Abs("../../public/views")
viewsPath, err := filepath.Abs("../../public/views")
require.NoError(t, err)
sc.m = macaron.New()
sc.m.Use(macaron.Renderer(macaron.RenderOptions{
Directory: viewsPath,
Delims: macaron.Delims{Left: "[[", Right: "]]"},
}))
sc.m.Use(middleware.GetContextHandler(nil, nil, nil))
sc.m.Use(getContextHandler(t).Middleware)
return sc
}
type fakeRenderService struct {
rendering.Service
}
func (s *fakeRenderService) Init() error {
return nil
}

@ -193,11 +193,11 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
"datasources": dataSources,
"minRefreshInterval": setting.MinRefreshInterval,
"panels": panels,
"appUrl": setting.AppUrl,
"appSubUrl": setting.AppSubUrl,
"appUrl": hs.Cfg.AppURL,
"appSubUrl": hs.Cfg.AppSubURL,
"allowOrgCreate": (setting.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin,
"authProxyEnabled": setting.AuthProxyEnabled,
"ldapEnabled": setting.LDAPEnabled,
"ldapEnabled": hs.Cfg.LDAPEnabled,
"alertingEnabled": setting.AlertingEnabled,
"alertingErrorOrTimeout": setting.AlertingErrorOrTimeout,
"alertingNoDataOrNullValues": setting.AlertingNoDataOrNullValues,

@ -18,7 +18,6 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/middleware"
"gopkg.in/macaron.v1"
"github.com/grafana/grafana/pkg/setting"
@ -53,7 +52,7 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg) (*macaron.Macaron, *HT
}
m := macaron.New()
m.Use(middleware.GetContextHandler(nil, nil, nil))
m.Use(getContextHandler(t).Middleware)
m.Use(macaron.Renderer(macaron.RenderOptions{
Directory: filepath.Join(setting.StaticRootPath, "views"),
IndentJSON: true,
@ -84,10 +83,12 @@ func TestHTTPServer_GetFrontendSettings_hideVersionAnonyomus(t *testing.T) {
setting.Env = "testing"
tests := []struct {
desc string
hideVersion bool
expected settings
}{
{
desc: "Not hiding version",
hideVersion: false,
expected: settings{
BuildInfo: buildInfo{
@ -98,6 +99,7 @@ func TestHTTPServer_GetFrontendSettings_hideVersionAnonyomus(t *testing.T) {
},
},
{
desc: "Hiding version",
hideVersion: true,
expected: settings{
BuildInfo: buildInfo{
@ -110,16 +112,18 @@ func TestHTTPServer_GetFrontendSettings_hideVersionAnonyomus(t *testing.T) {
}
for _, test := range tests {
hs.Cfg.AnonymousHideVersion = test.hideVersion
expected := test.expected
recorder := httptest.NewRecorder()
m.ServeHTTP(recorder, req)
got := settings{}
err := json.Unmarshal(recorder.Body.Bytes(), &got)
require.NoError(t, err)
require.GreaterOrEqual(t, 400, recorder.Code, "status codes higher than 400 indicates a failure")
assert.EqualValues(t, expected, got)
t.Run(test.desc, func(t *testing.T) {
hs.Cfg.AnonymousHideVersion = test.hideVersion
expected := test.expected
recorder := httptest.NewRecorder()
m.ServeHTTP(recorder, req)
got := settings{}
err := json.Unmarshal(recorder.Body.Bytes(), &got)
require.NoError(t, err)
require.GreaterOrEqual(t, 400, recorder.Code, "status codes higher than 400 indicate a failure")
assert.EqualValues(t, expected, got)
})
}
}

@ -29,6 +29,7 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/contexthandler"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/hooks"
"github.com/grafana/grafana/pkg/services/login"
@ -75,6 +76,7 @@ type HTTPServer struct {
SearchService *search.SearchService `inject:""`
ShortURLService *shorturls.ShortURLService `inject:""`
Live *live.GrafanaLive `inject:""`
ContextHandler *contexthandler.ContextHandler `inject:""`
Listener net.Listener
}
@ -100,7 +102,7 @@ func (hs *HTTPServer) Run(ctx context.Context) error {
Addr: net.JoinHostPort(setting.HttpAddr, setting.HttpPort),
Handler: hs.macaron,
}
switch setting.Protocol {
switch hs.Cfg.Protocol {
case setting.HTTP2Scheme:
if err := hs.configureHttp2(); err != nil {
return err
@ -118,7 +120,7 @@ func (hs *HTTPServer) Run(ctx context.Context) error {
}
hs.log.Info("HTTP Server Listen", "address", listener.Addr().String(), "protocol",
setting.Protocol, "subUrl", setting.AppSubUrl, "socket", setting.SocketPath)
hs.Cfg.Protocol, "subUrl", hs.Cfg.AppSubURL, "socket", hs.Cfg.SocketPath)
var wg sync.WaitGroup
wg.Add(1)
@ -133,7 +135,7 @@ func (hs *HTTPServer) Run(ctx context.Context) error {
}
}()
switch setting.Protocol {
switch hs.Cfg.Protocol {
case setting.HTTPScheme, setting.SocketScheme:
if err := hs.httpSrv.Serve(listener); err != nil {
if errors.Is(err, http.ErrServerClosed) {
@ -151,7 +153,7 @@ func (hs *HTTPServer) Run(ctx context.Context) error {
return err
}
default:
panic(fmt.Sprintf("Unhandled protocol %q", setting.Protocol))
panic(fmt.Sprintf("Unhandled protocol %q", hs.Cfg.Protocol))
}
wg.Wait()
@ -164,7 +166,7 @@ func (hs *HTTPServer) getListener() (net.Listener, error) {
return hs.Listener, nil
}
switch setting.Protocol {
switch hs.Cfg.Protocol {
case setting.HTTPScheme, setting.HTTPSScheme, setting.HTTP2Scheme:
listener, err := net.Listen("tcp", hs.httpSrv.Addr)
if err != nil {
@ -172,21 +174,21 @@ func (hs *HTTPServer) getListener() (net.Listener, error) {
}
return listener, nil
case setting.SocketScheme:
listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: setting.SocketPath, Net: "unix"})
listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: hs.Cfg.SocketPath, Net: "unix"})
if err != nil {
return nil, errutil.Wrapf(err, "failed to open listener for socket %s", setting.SocketPath)
return nil, errutil.Wrapf(err, "failed to open listener for socket %s", hs.Cfg.SocketPath)
}
// Make socket writable by group
// nolint:gosec
if err := os.Chmod(setting.SocketPath, 0660); err != nil {
if err := os.Chmod(hs.Cfg.SocketPath, 0660); err != nil {
return nil, errutil.Wrapf(err, "failed to change socket permissions")
}
return listener, nil
default:
hs.log.Error("Invalid protocol", "protocol", setting.Protocol)
return nil, fmt.Errorf("invalid protocol %q", setting.Protocol)
hs.log.Error("Invalid protocol", "protocol", hs.Cfg.Protocol)
return nil, fmt.Errorf("invalid protocol %q", hs.Cfg.Protocol)
}
}
@ -271,7 +273,7 @@ func (hs *HTTPServer) configureHttp2() error {
}
func (hs *HTTPServer) newMacaron() *macaron.Macaron {
macaron.Env = setting.Env
macaron.Env = hs.Cfg.Env
m := macaron.New()
// automatically set HEAD for every GET
@ -294,13 +296,13 @@ func (hs *HTTPServer) applyRoutes() {
func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
m := hs.macaron
m.Use(middleware.Logger())
m.Use(middleware.Logger(hs.Cfg))
if setting.EnableGzip {
m.Use(middleware.Gziper())
}
m.Use(middleware.Recovery())
m.Use(middleware.Recovery(hs.Cfg))
for _, route := range plugins.StaticRoutes {
pluginRoute := path.Join("/public/plugins/", route.PluginId)
@ -316,7 +318,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
hs.mapStatic(m, hs.Cfg.ImagesDir, "", "/public/img/attachments")
}
m.Use(middleware.AddDefaultResponseHeaders())
m.Use(middleware.AddDefaultResponseHeaders(hs.Cfg))
if setting.ServeFromSubPath && setting.AppSubUrl != "" {
m.SetURLPrefix(setting.AppSubUrl)
@ -334,16 +336,12 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
m.Use(hs.apiHealthHandler)
m.Use(hs.metricsEndpoint)
m.Use(middleware.GetContextHandler(
hs.AuthTokenService,
hs.RemoteCacheService,
hs.RenderService,
))
m.Use(hs.ContextHandler.Middleware)
m.Use(middleware.OrgRedirect())
// needs to be after context handler
if setting.EnforceDomain {
m.Use(middleware.ValidateHostHeader(setting.Domain))
m.Use(middleware.ValidateHostHeader(hs.Cfg.Domain))
}
m.Use(middleware.HandleNoCacheHeader())
@ -433,7 +431,7 @@ func (hs *HTTPServer) mapStatic(m *macaron.Macaron, rootDir string, dir string,
}
}
if setting.Env == setting.Dev {
if hs.Cfg.Env == setting.Dev {
headers = func(c *macaron.Context) {
c.Resp.Header().Set("Cache-Control", "max-age=0, must-revalidate, no-cache")
}

@ -300,7 +300,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
{Text: "Stats", Id: "server-stats", Url: setting.AppSubUrl + "/admin/stats", Icon: "graph-bar"},
}
if setting.LDAPEnabled {
if hs.Cfg.LDAPEnabled {
adminNavLinks = append(adminNavLinks, &dtos.NavLink{
Text: "LDAP", Id: "ldap", Url: setting.AppSubUrl + "/admin/ldap", Icon: "book",
})
@ -371,7 +371,7 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat
// special case when doing localhost call from image renderer
if c.IsRenderCall && !hs.Cfg.ServeFromSubPath {
appURL = fmt.Sprintf("%s://localhost:%s", setting.Protocol, setting.HttpPort)
appURL = fmt.Sprintf("%s://localhost:%s", hs.Cfg.Protocol, setting.HttpPort)
appSubURL = ""
settings["appSubUrl"] = ""
}

@ -116,7 +116,7 @@ func (hs *HTTPServer) GetLDAPStatus(c *models.ReqContext) Response {
return Error(http.StatusBadRequest, "LDAP is not enabled", nil)
}
ldapConfig, err := getLDAPConfig()
ldapConfig, err := getLDAPConfig(hs.Cfg)
if err != nil {
return Error(http.StatusBadRequest, "Failed to obtain the LDAP configuration. Please verify the configuration and try again", err)
@ -158,7 +158,7 @@ func (hs *HTTPServer) PostSyncUserWithLDAP(c *models.ReqContext) Response {
return Error(http.StatusBadRequest, "LDAP is not enabled", nil)
}
ldapConfig, err := getLDAPConfig()
ldapConfig, err := getLDAPConfig(hs.Cfg)
if err != nil {
return Error(http.StatusBadRequest, "Failed to obtain the LDAP configuration. Please verify the configuration and try again", err)
}
@ -217,7 +217,7 @@ func (hs *HTTPServer) PostSyncUserWithLDAP(c *models.ReqContext) Response {
upsertCmd := &models.UpsertUserCommand{
ReqContext: c,
ExternalUser: user,
SignupAllowed: setting.LDAPAllowSignup,
SignupAllowed: hs.Cfg.LDAPAllowSignup,
}
err = bus.Dispatch(upsertCmd)
@ -235,7 +235,7 @@ func (hs *HTTPServer) GetUserFromLDAP(c *models.ReqContext) Response {
return Error(http.StatusBadRequest, "LDAP is not enabled", nil)
}
ldapConfig, err := getLDAPConfig()
ldapConfig, err := getLDAPConfig(hs.Cfg)
if err != nil {
return Error(http.StatusBadRequest, "Failed to obtain the LDAP configuration", err)

@ -74,7 +74,7 @@ func getUserFromLDAPContext(t *testing.T, requestURL string) *scenarioContext {
}
func TestGetUserFromLDAPAPIEndpoint_UserNotFound(t *testing.T) {
getLDAPConfig = func() (*ldap.Config, error) {
getLDAPConfig = func(*setting.Cfg) (*ldap.Config, error) {
return &ldap.Config{}, nil
}
@ -131,7 +131,7 @@ func TestGetUserFromLDAPAPIEndpoint_OrgNotfound(t *testing.T) {
return nil
})
getLDAPConfig = func() (*ldap.Config, error) {
getLDAPConfig = func(*setting.Cfg) (*ldap.Config, error) {
return &ldap.Config{}, nil
}
@ -193,7 +193,7 @@ func TestGetUserFromLDAPAPIEndpoint(t *testing.T) {
return nil
})
getLDAPConfig = func() (*ldap.Config, error) {
getLDAPConfig = func(*setting.Cfg) (*ldap.Config, error) {
return &ldap.Config{}, nil
}
@ -273,7 +273,7 @@ func TestGetUserFromLDAPAPIEndpoint_WithTeamHandler(t *testing.T) {
return nil
})
getLDAPConfig = func() (*ldap.Config, error) {
getLDAPConfig = func(*setting.Cfg) (*ldap.Config, error) {
return &ldap.Config{}, nil
}
@ -349,7 +349,7 @@ func TestGetLDAPStatusAPIEndpoint(t *testing.T) {
{Host: "10.0.0.5", Port: 361, Available: false, Error: errors.New("something is awfully wrong")},
}
getLDAPConfig = func() (*ldap.Config, error) {
getLDAPConfig = func(*setting.Cfg) (*ldap.Config, error) {
return &ldap.Config{}, nil
}
@ -412,7 +412,7 @@ func postSyncUserWithLDAPContext(t *testing.T, requestURL string, preHook func(t
func TestPostSyncUserWithLDAPAPIEndpoint_Success(t *testing.T) {
sc := postSyncUserWithLDAPContext(t, "/api/admin/ldap/sync/34", func(t *testing.T) {
getLDAPConfig = func() (*ldap.Config, error) {
getLDAPConfig = func(*setting.Cfg) (*ldap.Config, error) {
return &ldap.Config{}, nil
}
@ -457,7 +457,7 @@ func TestPostSyncUserWithLDAPAPIEndpoint_Success(t *testing.T) {
func TestPostSyncUserWithLDAPAPIEndpoint_WhenUserNotFound(t *testing.T) {
sc := postSyncUserWithLDAPContext(t, "/api/admin/ldap/sync/34", func(t *testing.T) {
getLDAPConfig = func() (*ldap.Config, error) {
getLDAPConfig = func(*setting.Cfg) (*ldap.Config, error) {
return &ldap.Config{}, nil
}
@ -485,7 +485,7 @@ func TestPostSyncUserWithLDAPAPIEndpoint_WhenUserNotFound(t *testing.T) {
func TestPostSyncUserWithLDAPAPIEndpoint_WhenGrafanaAdmin(t *testing.T) {
sc := postSyncUserWithLDAPContext(t, "/api/admin/ldap/sync/34", func(t *testing.T) {
getLDAPConfig = func() (*ldap.Config, error) {
getLDAPConfig = func(*setting.Cfg) (*ldap.Config, error) {
return &ldap.Config{}, nil
}
@ -528,7 +528,7 @@ func TestPostSyncUserWithLDAPAPIEndpoint_WhenGrafanaAdmin(t *testing.T) {
func TestPostSyncUserWithLDAPAPIEndpoint_WhenUserNotInLDAP(t *testing.T) {
sc := postSyncUserWithLDAPContext(t, "/api/admin/ldap/sync/34", func(t *testing.T) {
getLDAPConfig = func() (*ldap.Config, error) {
getLDAPConfig = func(*setting.Cfg) (*ldap.Config, error) {
return &ldap.Config{}, nil
}

@ -13,7 +13,7 @@ import (
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/infra/network"
"github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/middleware"
"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"
@ -61,12 +61,12 @@ func (hs *HTTPServer) ValidateRedirectTo(redirectTo string) error {
return nil
}
func (hs *HTTPServer) CookieOptionsFromCfg() middleware.CookieOptions {
func (hs *HTTPServer) CookieOptionsFromCfg() cookies.CookieOptions {
path := "/"
if len(hs.Cfg.AppSubURL) > 0 {
path = hs.Cfg.AppSubURL
}
return middleware.CookieOptions{
return cookies.CookieOptions{
Path: path,
Secure: hs.Cfg.CookieSecure,
SameSiteDisabled: hs.Cfg.CookieSameSiteDisabled,
@ -101,7 +101,7 @@ func (hs *HTTPServer) LoginView(c *models.ReqContext) {
// therefore the loginError should be passed to the view data
// and the view should return immediately before attempting
// to login again via OAuth and enter to a redirect loop
middleware.DeleteCookie(c.Resp, LoginErrorCookieName, hs.CookieOptionsFromCfg)
cookies.DeleteCookie(c.Resp, LoginErrorCookieName, hs.CookieOptionsFromCfg)
viewData.Settings["loginError"] = loginError
c.HTML(200, getViewIndex(), viewData)
return
@ -113,7 +113,7 @@ func (hs *HTTPServer) LoginView(c *models.ReqContext) {
if c.IsSignedIn {
// Assign login token to auth proxy users if enable_login_token = true
if setting.AuthProxyEnabled && setting.AuthProxyEnableLoginToken {
if hs.Cfg.AuthProxyEnabled && hs.Cfg.AuthProxyEnableLoginToken {
user := &models.User{Id: c.SignedInUser.UserId, Email: c.SignedInUser.Email, Login: c.SignedInUser.Login}
err := hs.loginUserWithUser(user, c)
if err != nil {
@ -129,7 +129,7 @@ func (hs *HTTPServer) LoginView(c *models.ReqContext) {
log.Debugf("Ignored invalid redirect_to cookie value: %v", redirectTo)
redirectTo = hs.Cfg.AppSubURL + "/"
}
middleware.DeleteCookie(c.Resp, "redirect_to", hs.CookieOptionsFromCfg)
cookies.DeleteCookie(c.Resp, "redirect_to", hs.CookieOptionsFromCfg)
c.Redirect(redirectTo)
return
}
@ -196,6 +196,7 @@ func (hs *HTTPServer) LoginPost(c *models.ReqContext, cmd dtos.LoginCommand) Res
Username: cmd.User,
Password: cmd.Password,
IpAddress: c.Req.RemoteAddr,
Cfg: hs.Cfg,
}
err := bus.Dispatch(authQuery)
@ -236,7 +237,7 @@ func (hs *HTTPServer) LoginPost(c *models.ReqContext, cmd dtos.LoginCommand) Res
} else {
log.Infof("Ignored invalid redirect_to cookie value: %v", redirectTo)
}
middleware.DeleteCookie(c.Resp, "redirect_to", hs.CookieOptionsFromCfg)
cookies.DeleteCookie(c.Resp, "redirect_to", hs.CookieOptionsFromCfg)
}
metrics.MApiLoginPost.Inc()
@ -263,7 +264,7 @@ func (hs *HTTPServer) loginUserWithUser(user *models.User, c *models.ReqContext)
}
hs.log.Info("Successful Login", "User", user.Email)
middleware.WriteSessionCookie(c, userToken.UnhashedToken, hs.Cfg.LoginMaxLifetime)
cookies.WriteSessionCookie(c, hs.Cfg, userToken.UnhashedToken, hs.Cfg.LoginMaxLifetime)
return nil
}
@ -278,7 +279,7 @@ func (hs *HTTPServer) Logout(c *models.ReqContext) {
hs.log.Error("failed to revoke auth token", "error", err)
}
middleware.WriteSessionCookie(c, "", -1)
cookies.WriteSessionCookie(c, hs.Cfg, "", -1)
if setting.SignoutRedirectUrl != "" {
c.Redirect(setting.SignoutRedirectUrl)
@ -309,7 +310,7 @@ func (hs *HTTPServer) trySetEncryptedCookie(ctx *models.ReqContext, cookieName s
return err
}
middleware.WriteCookie(ctx.Resp, cookieName, hex.EncodeToString(encryptedError), 60, hs.CookieOptionsFromCfg)
cookies.WriteCookie(ctx.Resp, cookieName, hex.EncodeToString(encryptedError), 60, hs.CookieOptionsFromCfg)
return nil
}

@ -18,7 +18,7 @@ import (
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/middleware/cookies"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
)
@ -81,7 +81,7 @@ func (hs *HTTPServer) OAuthLogin(ctx *models.ReqContext) {
}
hashedState := hashStatecode(state, setting.OAuthService.OAuthInfos[name].ClientSecret)
middleware.WriteCookie(ctx.Resp, OauthStateCookieName, hashedState, hs.Cfg.OAuthCookieMaxAge, hs.CookieOptionsFromCfg)
cookies.WriteCookie(ctx.Resp, OauthStateCookieName, hashedState, hs.Cfg.OAuthCookieMaxAge, hs.CookieOptionsFromCfg)
if setting.OAuthService.OAuthInfos[name].HostedDomain == "" {
ctx.Redirect(connect.AuthCodeURL(state, oauth2.AccessTypeOnline))
} else {
@ -93,7 +93,7 @@ func (hs *HTTPServer) OAuthLogin(ctx *models.ReqContext) {
cookieState := ctx.GetCookie(OauthStateCookieName)
// delete cookie
middleware.DeleteCookie(ctx.Resp, OauthStateCookieName, hs.CookieOptionsFromCfg)
cookies.DeleteCookie(ctx.Resp, OauthStateCookieName, hs.CookieOptionsFromCfg)
if cookieState == "" {
hs.handleOAuthLoginError(ctx, loginInfo, LoginError{
@ -192,7 +192,7 @@ func (hs *HTTPServer) OAuthLogin(ctx *models.ReqContext) {
if redirectTo, err := url.QueryUnescape(ctx.GetCookie("redirect_to")); err == nil && len(redirectTo) > 0 {
if err := hs.ValidateRedirectTo(redirectTo); err == nil {
middleware.DeleteCookie(ctx.Resp, "redirect_to", hs.CookieOptionsFromCfg)
cookies.DeleteCookie(ctx.Resp, "redirect_to", hs.CookieOptionsFromCfg)
ctx.Redirect(redirectTo)
return
}

@ -592,8 +592,8 @@ func setupAuthProxyLoginTest(t *testing.T, enableLoginToken bool) *scenarioConte
setting.OAuthService = &setting.OAuther{}
setting.OAuthService.OAuthInfos = make(map[string]*setting.OAuthInfo)
setting.AuthProxyEnabled = true
setting.AuthProxyEnableLoginToken = enableLoginToken
hs.Cfg.AuthProxyEnabled = true
hs.Cfg.AuthProxyEnableLoginToken = enableLoginToken
sc.m.Get(sc.url, sc.defaultHandler)
sc.fakeReqNoAssertions("GET", sc.url).exec()

@ -23,8 +23,17 @@ var (
defaultMaxCacheExpiration = time.Hour * 24
)
const (
ServiceName = "RemoteCache"
)
func init() {
registry.RegisterService(&RemoteCache{})
rc := &RemoteCache{}
registry.Register(&registry.Descriptor{
Name: ServiceName,
Instance: rc,
InitPriority: registry.Medium,
})
}
// CacheStorage allows the caller to set, get and delete items in the cache.

@ -25,12 +25,12 @@ var (
var loginLogger = log.New("login")
func Init() {
bus.AddHandler("auth", AuthenticateUser)
bus.AddHandler("auth", authenticateUser)
}
// AuthenticateUser authenticates the user via username & password
func AuthenticateUser(query *models.LoginUserQuery) error {
if err := validateLoginAttempts(query.Username); err != nil {
// authenticateUser authenticates the user via username & password
func authenticateUser(query *models.LoginUserQuery) error {
if err := validateLoginAttempts(query); err != nil {
return err
}

@ -21,7 +21,7 @@ func TestAuthenticateUser(t *testing.T) {
Username: "user",
Password: "",
}
err := AuthenticateUser(&loginQuery)
err := authenticateUser(&loginQuery)
Convey("login should fail", func() {
So(sc.grafanaLoginWasCalled, ShouldBeFalse)
@ -37,7 +37,7 @@ func TestAuthenticateUser(t *testing.T) {
mockLoginUsingLDAP(true, nil, sc)
mockSaveInvalidLoginAttempt(sc)
err := AuthenticateUser(sc.loginUserQuery)
err := authenticateUser(sc.loginUserQuery)
Convey("it should result in", func() {
So(err, ShouldEqual, ErrTooManyLoginAttempts)
@ -55,7 +55,7 @@ func TestAuthenticateUser(t *testing.T) {
mockLoginUsingLDAP(true, ErrInvalidCredentials, sc)
mockSaveInvalidLoginAttempt(sc)
err := AuthenticateUser(sc.loginUserQuery)
err := authenticateUser(sc.loginUserQuery)
Convey("it should result in", func() {
So(err, ShouldEqual, nil)
@ -74,7 +74,7 @@ func TestAuthenticateUser(t *testing.T) {
mockLoginUsingLDAP(true, ErrInvalidCredentials, sc)
mockSaveInvalidLoginAttempt(sc)
err := AuthenticateUser(sc.loginUserQuery)
err := authenticateUser(sc.loginUserQuery)
Convey("it should result in", func() {
So(err, ShouldEqual, customErr)
@ -92,7 +92,7 @@ func TestAuthenticateUser(t *testing.T) {
mockLoginUsingLDAP(false, nil, sc)
mockSaveInvalidLoginAttempt(sc)
err := AuthenticateUser(sc.loginUserQuery)
err := authenticateUser(sc.loginUserQuery)
Convey("it should result in", func() {
So(err, ShouldEqual, models.ErrUserNotFound)
@ -110,7 +110,7 @@ func TestAuthenticateUser(t *testing.T) {
mockLoginUsingLDAP(true, ldap.ErrInvalidCredentials, sc)
mockSaveInvalidLoginAttempt(sc)
err := AuthenticateUser(sc.loginUserQuery)
err := authenticateUser(sc.loginUserQuery)
Convey("it should result in", func() {
So(err, ShouldEqual, ErrInvalidCredentials)
@ -128,7 +128,7 @@ func TestAuthenticateUser(t *testing.T) {
mockLoginUsingLDAP(true, nil, sc)
mockSaveInvalidLoginAttempt(sc)
err := AuthenticateUser(sc.loginUserQuery)
err := authenticateUser(sc.loginUserQuery)
Convey("it should result in", func() {
So(err, ShouldBeNil)
@ -147,7 +147,7 @@ func TestAuthenticateUser(t *testing.T) {
mockLoginUsingLDAP(true, customErr, sc)
mockSaveInvalidLoginAttempt(sc)
err := AuthenticateUser(sc.loginUserQuery)
err := authenticateUser(sc.loginUserQuery)
Convey("it should result in", func() {
So(err, ShouldEqual, customErr)
@ -165,7 +165,7 @@ func TestAuthenticateUser(t *testing.T) {
mockLoginUsingLDAP(true, ldap.ErrInvalidCredentials, sc)
mockSaveInvalidLoginAttempt(sc)
err := AuthenticateUser(sc.loginUserQuery)
err := authenticateUser(sc.loginUserQuery)
Convey("it should result in", func() {
So(err, ShouldEqual, ErrInvalidCredentials)
@ -203,7 +203,7 @@ func mockLoginUsingLDAP(enabled bool, err error, sc *authScenarioContext) {
}
func mockLoginAttemptValidation(err error, sc *authScenarioContext) {
validateLoginAttempts = func(username string) error {
validateLoginAttempts = func(*models.LoginUserQuery) error {
sc.loginAttemptValidationWasCalled = true
return err
}

@ -5,7 +5,6 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
)
var (
@ -13,13 +12,13 @@ var (
loginAttemptsWindow = time.Minute * 5
)
var validateLoginAttempts = func(username string) error {
if setting.DisableBruteForceLoginProtection {
var validateLoginAttempts = func(query *models.LoginUserQuery) error {
if query.Cfg.DisableBruteForceLoginProtection {
return nil
}
loginAttemptCountQuery := models.GetUserLoginAttemptCountQuery{
Username: username,
Username: query.Username,
Since: time.Now().Add(-loginAttemptsWindow),
}
@ -35,7 +34,7 @@ var validateLoginAttempts = func(username string) error {
}
var saveInvalidLoginAttempt = func(query *models.LoginUserQuery) error {
if setting.DisableBruteForceLoginProtection {
if query.Cfg.DisableBruteForceLoginProtection {
return nil
}

@ -12,11 +12,16 @@ import (
func TestLoginAttemptsValidation(t *testing.T) {
Convey("Validate login attempts", t, func() {
Convey("Given brute force login protection enabled", func() {
setting.DisableBruteForceLoginProtection = false
cfg := setting.NewCfg()
cfg.DisableBruteForceLoginProtection = false
query := &models.LoginUserQuery{
Username: "user",
Cfg: cfg,
}
Convey("When user login attempt count equals max-1 ", func() {
withLoginAttempts(maxInvalidLoginAttempts - 1)
err := validateLoginAttempts("user")
err := validateLoginAttempts(query)
Convey("it should not result in error", func() {
So(err, ShouldBeNil)
@ -25,7 +30,7 @@ func TestLoginAttemptsValidation(t *testing.T) {
Convey("When user login attempt count equals max ", func() {
withLoginAttempts(maxInvalidLoginAttempts)
err := validateLoginAttempts("user")
err := validateLoginAttempts(query)
Convey("it should result in too many login attempts error", func() {
So(err, ShouldEqual, ErrTooManyLoginAttempts)
@ -34,7 +39,7 @@ func TestLoginAttemptsValidation(t *testing.T) {
Convey("When user login attempt count is greater than max ", func() {
withLoginAttempts(maxInvalidLoginAttempts + 5)
err := validateLoginAttempts("user")
err := validateLoginAttempts(query)
Convey("it should result in too many login attempts error", func() {
So(err, ShouldEqual, ErrTooManyLoginAttempts)
@ -54,6 +59,7 @@ func TestLoginAttemptsValidation(t *testing.T) {
Username: "user",
Password: "pwd",
IpAddress: "192.168.1.1:56433",
Cfg: setting.NewCfg(),
})
So(err, ShouldBeNil)
@ -66,11 +72,16 @@ func TestLoginAttemptsValidation(t *testing.T) {
})
Convey("Given brute force login protection disabled", func() {
setting.DisableBruteForceLoginProtection = true
cfg := setting.NewCfg()
cfg.DisableBruteForceLoginProtection = true
query := &models.LoginUserQuery{
Username: "user",
Cfg: cfg,
}
Convey("When user login attempt count equals max-1 ", func() {
withLoginAttempts(maxInvalidLoginAttempts - 1)
err := validateLoginAttempts("user")
err := validateLoginAttempts(query)
Convey("it should not result in error", func() {
So(err, ShouldBeNil)
@ -79,7 +90,7 @@ func TestLoginAttemptsValidation(t *testing.T) {
Convey("When user login attempt count equals max ", func() {
withLoginAttempts(maxInvalidLoginAttempts)
err := validateLoginAttempts("user")
err := validateLoginAttempts(query)
Convey("it should not result in error", func() {
So(err, ShouldBeNil)
@ -88,7 +99,7 @@ func TestLoginAttemptsValidation(t *testing.T) {
Convey("When user login attempt count is greater than max ", func() {
withLoginAttempts(maxInvalidLoginAttempts + 5)
err := validateLoginAttempts("user")
err := validateLoginAttempts(query)
Convey("it should not result in error", func() {
So(err, ShouldBeNil)
@ -97,7 +108,7 @@ func TestLoginAttemptsValidation(t *testing.T) {
Convey("When saving invalid login attempt", func() {
defer bus.ClearBusHandlers()
createLoginAttemptCmd := (*models.CreateLoginAttemptCommand)(nil)
var createLoginAttemptCmd *models.CreateLoginAttemptCommand
bus.AddHandler("test", func(cmd *models.CreateLoginAttemptCommand) error {
createLoginAttemptCmd = cmd
@ -108,6 +119,7 @@ func TestLoginAttemptsValidation(t *testing.T) {
Username: "user",
Password: "pwd",
IpAddress: "192.168.1.1:56433",
Cfg: cfg,
})
So(err, ShouldBeNil)

@ -33,7 +33,7 @@ var loginUsingLDAP = func(query *models.LoginUserQuery) (bool, error) {
return false, nil
}
config, err := getLDAPConfig()
config, err := getLDAPConfig(query.Cfg)
if err != nil {
return true, errutil.Wrap("Failed to get LDAP config", err)
}

@ -20,7 +20,7 @@ func TestLDAPLogin(t *testing.T) {
LDAPLoginScenario("When login", func(sc *LDAPLoginScenarioContext) {
sc.withLoginResult(false)
getLDAPConfig = func() (*ldap.Config, error) {
getLDAPConfig = func(*setting.Cfg) (*ldap.Config, error) {
config := &ldap.Config{
Servers: []*ldap.ServerConfig{},
}
@ -150,7 +150,14 @@ func LDAPLoginScenario(desc string, fn LDAPLoginScenarioFunc) {
LDAPAuthenticatorMock: mock,
}
getLDAPConfig = func() (*ldap.Config, error) {
origNewLDAP := newLDAP
origGetLDAPConfig := getLDAPConfig
defer func() {
newLDAP = origNewLDAP
getLDAPConfig = origGetLDAPConfig
}()
getLDAPConfig = func(*setting.Cfg) (*ldap.Config, error) {
config := &ldap.Config{
Servers: []*ldap.ServerConfig{
{
@ -166,11 +173,6 @@ func LDAPLoginScenario(desc string, fn LDAPLoginScenarioFunc) {
return mock
}
defer func() {
newLDAP = multildap.New
getLDAPConfig = multildap.GetConfig
}()
fn(sc)
})
}

@ -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
}

@ -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,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
}
}

@ -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)
}

@ -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()
}
rr := httptest.NewRecorder()
req, err := http.NewRequestWithContext(ctx, "", "", nil)
if err != nil {
return nil, nil, err
cfg.RemoteCacheOptions = &setting.RemoteCacheOptions{
Name: "database",
}
reqContext := &models.ReqContext{
Context: &macaron.Context{
Req: macaron.Request{
Request: req,
},
userAuthTokenSvc := auth.NewFakeUserAuthTokenService()
renderSvc := &fakeRenderService{}
ctxHdlr := &contexthandler.ContextHandler{}
err := registry.BuildServiceGraph([]interface{}{cfg}, []*registry.Descriptor{
{
Name: sqlstore.ServiceName,
Instance: sqlStore,
},
Logger: log.New("testlogger"),
}
mw := mockWriter{rr}
reqContext.Resp = mw
{
Name: remotecache.ServiceName,
Instance: remoteCacheSvc,
},
{
Name: auth.ServiceName,
Instance: userAuthTokenSvc,
},
{
Name: rendering.ServiceName,
Instance: renderSvc,
},
{
Name: contexthandler.ServiceName,
Instance: ctxHdlr,
},
})
require.NoError(t, err)
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,
})
}

@ -3,6 +3,7 @@ package models
import (
"time"
"github.com/grafana/grafana/pkg/setting"
"golang.org/x/oauth2"
)
@ -84,6 +85,7 @@ type LoginUserQuery struct {
User *User
IpAddress string
AuthModule string
Cfg *setting.Cfg
}
type GetUserByAuthInfoQuery struct {

@ -0,0 +1,45 @@
package registry
import (
"fmt"
"github.com/facebookgo/inject"
)
// BuildServiceGraph builds a graph of services and their dependencies.
// The services are initialized after the graph is built.
func BuildServiceGraph(objs []interface{}, services []*Descriptor) error {
if services == nil {
services = GetServices()
}
for _, service := range services {
objs = append(objs, service.Instance)
}
serviceGraph := inject.Graph{}
// Provide services and their dependencies to the graph.
for _, obj := range objs {
if err := serviceGraph.Provide(&inject.Object{Value: obj}); err != nil {
return fmt.Errorf("failed to provide object to the graph: %w", err)
}
}
// Resolve services and their dependencies.
if err := serviceGraph.Populate(); err != nil {
return fmt.Errorf("failed to populate service dependencies: %w", err)
}
// Initialize services.
for _, service := range services {
if IsDisabled(service.Instance) {
continue
}
if err := service.Instance.Init(); err != nil {
return fmt.Errorf("service init failed: %w", err)
}
}
return nil
}

@ -18,8 +18,14 @@ import (
"github.com/grafana/grafana/pkg/util"
)
const ServiceName = "UserAuthTokenService"
func init() {
registry.RegisterService(&UserAuthTokenService{})
registry.Register(&registry.Descriptor{
Name: ServiceName,
Instance: &UserAuthTokenService{},
InitPriority: registry.Medium,
})
}
var getTime = time.Now

@ -57,8 +57,13 @@ func NewFakeUserAuthTokenService() *FakeUserAuthTokenService {
}
}
func (s *FakeUserAuthTokenService) CreateToken(ctx context.Context, userId int64, clientIP net.IP,
userAgent string) (*models.UserToken, error) {
// Init initializes the service.
// Required for dependency injection.
func (s *FakeUserAuthTokenService) Init() error {
return nil
}
func (s *FakeUserAuthTokenService) CreateToken(ctx context.Context, userId int64, clientIP net.IP, userAgent string) (*models.UserToken, error) {
return s.CreateTokenProvider(context.Background(), userId, clientIP, userAgent)
}

@ -1,4 +1,4 @@
package middleware
package contexthandler
import (
"fmt"
@ -8,8 +8,12 @@ import (
"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/registry"
"github.com/grafana/grafana/pkg/services/auth"
"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/stretchr/testify/require"
macaron "gopkg.in/macaron.v1"
@ -41,25 +45,16 @@ func TestInitContextWithAuthProxy_CachedInvalidUserID(t *testing.T) {
}
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"
svc := getContextHandler(t)
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{
@ -69,20 +64,72 @@ func TestInitContextWithAuthProxy_CachedInvalidUserID(t *testing.T) {
},
Logger: log.New("Test"),
}
req.Header.Add(setting.AuthProxyHeaderName, name)
req.Header.Set(svc.Cfg.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)
err = svc.RemoteCache.Set(key, int64(33), 0)
require.NoError(t, err)
authEnabled := initContextWithAuthProxy(store, ctx, orgID)
authEnabled := svc.initContextWithAuthProxy(ctx, orgID)
require.True(t, authEnabled)
require.Equal(t, userID, ctx.SignedInUser.UserId)
require.True(t, ctx.IsSignedIn)
i, err := store.Get(key)
i, err := svc.RemoteCache.Get(key)
require.NoError(t, err)
require.Equal(t, userID, i.(int64))
}
type fakeRenderService struct {
rendering.Service
}
func (s *fakeRenderService) Init() error {
return nil
}
func getContextHandler(t *testing.T) *ContextHandler {
t.Helper()
sqlStore := sqlstore.InitTestDB(t)
remoteCacheSvc := &remotecache.RemoteCache{}
cfg := setting.NewCfg()
cfg.RemoteCacheOptions = &setting.RemoteCacheOptions{
Name: "database",
}
cfg.AuthProxyHeaderName = "X-Killa"
cfg.AuthProxyEnabled = true
cfg.AuthProxyHeaderProperty = "username"
userAuthTokenSvc := auth.NewFakeUserAuthTokenService()
renderSvc := &fakeRenderService{}
svc := &ContextHandler{}
err := registry.BuildServiceGraph([]interface{}{cfg}, []*registry.Descriptor{
{
Name: sqlstore.ServiceName,
Instance: sqlStore,
},
{
Name: remotecache.ServiceName,
Instance: remoteCacheSvc,
},
{
Name: auth.ServiceName,
Instance: userAuthTokenSvc,
},
{
Name: rendering.ServiceName,
Instance: renderSvc,
},
{
Name: ServiceName,
Instance: svc,
},
})
require.NoError(t, err)
return svc
}

@ -32,7 +32,13 @@ const (
var getLDAPConfig = ldap.GetConfig
// isLDAPEnabled checks if LDAP is enabled
var isLDAPEnabled = ldap.IsEnabled
var isLDAPEnabled = func(cfg *setting.Cfg) bool {
if cfg != nil {
return cfg.LDAPEnabled
}
return setting.LDAPEnabled
}
// newLDAP creates multiple LDAP instance
var newLDAP = multildap.New
@ -42,18 +48,11 @@ 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
cfg *setting.Cfg
remoteCache *remotecache.RemoteCache
ctx *models.ReqContext
orgID int64
header string
}
// Error auth proxy specific error
@ -77,35 +76,27 @@ func (err Error) Error() string {
// Options for the AuthProxy
type Options struct {
Store *remotecache.RemoteCache
Ctx *models.ReqContext
OrgID int64
RemoteCache *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)
func New(cfg *setting.Cfg, options *Options) *AuthProxy {
header := options.Ctx.Req.Header.Get(cfg.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,
remoteCache: options.RemoteCache,
cfg: cfg,
ctx: options.Ctx,
orgID: options.OrgID,
header: header,
}
}
// IsEnabled checks if the proxy auth is enabled
func (auth *AuthProxy) IsEnabled() bool {
// Bail if the setting is not enabled
return auth.enabled
return auth.cfg.AuthProxyEnabled
}
// HasHeader checks if the we have specified header
@ -113,15 +104,15 @@ func (auth *AuthProxy) HasHeader() bool {
return len(auth.header) != 0
}
// IsAllowedIP compares presented IP with the whitelist one
// IsAllowedIP returns whether provided IP is allowed.
func (auth *AuthProxy) IsAllowedIP() error {
ip := auth.ctx.Req.RemoteAddr
if len(strings.TrimSpace(auth.whitelistIP)) == 0 {
if len(strings.TrimSpace(auth.cfg.AuthProxyWhitelist)) == 0 {
return nil
}
proxies := strings.Split(auth.whitelistIP, ",")
proxies := strings.Split(auth.cfg.AuthProxyWhitelist, ",")
var proxyObjs []*net.IPNet
for _, proxy := range proxies {
result, err := coerceProxyAddress(proxy)
@ -181,7 +172,7 @@ func (auth *AuthProxy) Login(logger log.Logger, ignoreCache bool) (int64, error)
}
}
if isLDAPEnabled() {
if isLDAPEnabled(auth.cfg) {
id, err := auth.LoginViaLDAP()
if err != nil {
if errors.Is(err, ldap.ErrInvalidCredentials) {
@ -205,7 +196,7 @@ func (auth *AuthProxy) Login(logger log.Logger, ignoreCache bool) (int64, error)
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)
userID, err := auth.remoteCache.Get(cacheKey)
if err != nil {
logger.Debug("Failed getting user ID via auth cache", "error", err)
return 0, err
@ -219,7 +210,7 @@ func (auth *AuthProxy) GetUserViaCache(logger log.Logger) (int64, error) {
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 {
if err := auth.remoteCache.Delete(cacheKey); err != nil {
return err
}
@ -229,12 +220,13 @@ func (auth *AuthProxy) RemoveUserFromCache(logger log.Logger) error {
// LoginViaLDAP logs in user via LDAP request
func (auth *AuthProxy) LoginViaLDAP() (int64, error) {
config, err := getLDAPConfig()
config, err := getLDAPConfig(auth.cfg)
if err != nil {
return 0, newError("failed to get LDAP config", err)
}
extUser, _, err := newLDAP(config.Servers).User(auth.header)
mldap := newLDAP(config.Servers)
extUser, _, err := mldap.User(auth.header)
if err != nil {
return 0, err
}
@ -242,7 +234,7 @@ func (auth *AuthProxy) LoginViaLDAP() (int64, error) {
// Have to sync grafana and LDAP user during log in
upsert := &models.UpsertUserCommand{
ReqContext: auth.ctx,
SignupAllowed: auth.LDAPAllowSignup,
SignupAllowed: auth.cfg.LDAPAllowSignup,
ExternalUser: extUser,
}
if err := bus.Dispatch(upsert); err != nil {
@ -259,7 +251,7 @@ func (auth *AuthProxy) LoginViaHeader() (int64, error) {
AuthId: auth.header,
}
switch auth.headerType {
switch auth.cfg.AuthProxyHeaderProperty {
case "username":
extUser.Login = auth.header
@ -284,7 +276,7 @@ func (auth *AuthProxy) LoginViaHeader() (int64, error) {
upsert := &models.UpsertUserCommand{
ReqContext: auth.ctx,
SignupAllowed: setting.AuthProxyAutoSignUp,
SignupAllowed: auth.cfg.AuthProxyAutoSignUp,
ExternalUser: extUser,
}
@ -299,8 +291,7 @@ func (auth *AuthProxy) LoginViaHeader() (int64, error) {
// 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]
h := auth.cfg.AuthProxyHeaders[field]
if h == "" {
continue
}
@ -311,8 +302,8 @@ func (auth *AuthProxy) headersIterator(fn func(field string, header string)) {
}
}
// GetSignedUser gets full signed user info.
func (auth *AuthProxy) GetSignedUser(userID int64) (*models.SignedInUser, error) {
// GetSignedUser gets full signed in user info.
func (auth *AuthProxy) GetSignedInUser(userID int64) (*models.SignedInUser, error) {
query := &models.GetSignedInUserQuery{
OrgId: auth.orgID,
UserId: userID,
@ -330,14 +321,14 @@ func (auth *AuthProxy) Remember(id int64) error {
key := auth.getKey()
// Check if user already in cache
userID, _ := auth.store.Get(key)
userID, _ := auth.remoteCache.Get(key)
if userID != nil {
return nil
}
expiration := time.Duration(auth.cacheTTL) * time.Minute
expiration := time.Duration(auth.cfg.AuthProxySyncTTL) * time.Minute
err := auth.store.Set(key, id, expiration)
err := auth.remoteCache.Set(key, id, expiration)
if err != nil {
return err
}
@ -353,5 +344,8 @@ func coerceProxyAddress(proxyAddr string) (*net.IPNet, error) {
}
_, network, err := net.ParseCIDR(proxyAddr)
return network, err
if err != nil {
return nil, fmt.Errorf("could not parse the network: %w", err)
}
return network, nil
}

@ -47,9 +47,22 @@ func (m *fakeMultiLDAP) User(login string) (
return result, ldap.ServerConfig{}, nil
}
func prepareMiddleware(t *testing.T, req *http.Request, store *remotecache.RemoteCache) *AuthProxy {
const hdrName = "markelog"
func prepareMiddleware(t *testing.T, remoteCache *remotecache.RemoteCache, cb func(*http.Request, *setting.Cfg)) *AuthProxy {
t.Helper()
cfg := setting.NewCfg()
cfg.AuthProxyHeaderName = "X-Killa"
req, err := http.NewRequest("POST", "http://example.com", nil)
require.NoError(t, err)
req.Header.Set(cfg.AuthProxyHeaderName, hdrName)
if cb != nil {
cb(req, cfg)
}
ctx := &models.ReqContext{
Context: &macaron.Context{
Req: macaron.Request{
@ -58,10 +71,10 @@ func prepareMiddleware(t *testing.T, req *http.Request, store *remotecache.Remot
},
}
auth := New(&Options{
Store: store,
Ctx: ctx,
OrgID: 4,
auth := New(cfg, &Options{
RemoteCache: remoteCache,
Ctx: ctx,
OrgID: 4,
})
return auth
@ -69,24 +82,17 @@ func prepareMiddleware(t *testing.T, req *http.Request, store *remotecache.Remot
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)
cache := remotecache.NewFakeStore(t)
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)
key := fmt.Sprintf(CachePrefix, HashCacheKey(hdrName))
err := cache.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())
auth := prepareMiddleware(t, cache, nil)
assert.Equal(t, key, auth.getKey())
gotID, err := auth.Login(logger, false)
require.NoError(t, err)
@ -96,15 +102,16 @@ func TestMiddlewareContext(t *testing.T) {
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)
const group = "grafana-core-team"
key := fmt.Sprintf(CachePrefix, HashCacheKey(name+"-"+group))
err := store.Set(key, id, 0)
key := fmt.Sprintf(CachePrefix, HashCacheKey(hdrName+"-"+group))
err := cache.Set(key, id, 0)
require.NoError(t, err)
auth := prepareMiddleware(t, req, store)
auth := prepareMiddleware(t, cache, func(req *http.Request, cfg *setting.Cfg) {
req.Header.Set("X-WEBAUTH-GROUPS", group)
cfg.AuthProxyHeaders = map[string]string{"Groups": "X-WEBAUTH-GROUPS"}
})
assert.Equal(t, "auth-proxy-sync-ttl:14f69b7023baa0ac98c96b31cec07bc0", auth.getKey())
gotID, err := auth.Login(logger, false)
@ -115,12 +122,6 @@ func TestMiddlewareContext(t *testing.T) {
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
@ -133,7 +134,16 @@ func TestMiddlewareContext_ldap(t *testing.T) {
return nil
})
isLDAPEnabled = func() bool {
origIsLDAPEnabled := isLDAPEnabled
origGetLDAPConfig := getLDAPConfig
origNewLDAP := newLDAP
t.Cleanup(func() {
newLDAP = origNewLDAP
isLDAPEnabled = origIsLDAPEnabled
getLDAPConfig = origGetLDAPConfig
})
isLDAPEnabled = func(*setting.Cfg) bool {
return true
}
@ -141,7 +151,7 @@ func TestMiddlewareContext_ldap(t *testing.T) {
ID: id,
}
getLDAPConfig = func() (*ldap.Config, error) {
getLDAPConfig = func(*setting.Cfg) (*ldap.Config, error) {
config := &ldap.Config{
Servers: []*ldap.ServerConfig{
{
@ -156,15 +166,9 @@ func TestMiddlewareContext_ldap(t *testing.T) {
return stub
}
defer func() {
newLDAP = multildap.New
isLDAPEnabled = ldap.IsEnabled
getLDAPConfig = ldap.GetConfig
}()
store := remotecache.NewFakeStore(t)
cache := remotecache.NewFakeStore(t)
auth := prepareMiddleware(t, req, store)
auth := prepareMiddleware(t, cache, nil)
gotID, err := auth.Login(logger, false)
require.NoError(t, err)
@ -173,25 +177,28 @@ func TestMiddlewareContext_ldap(t *testing.T) {
assert.True(t, stub.userCalled)
})
t.Run("Gets nice error if ldap is enabled but not configured", func(t *testing.T) {
t.Run("Gets nice error if LDAP is enabled, but not configured", func(t *testing.T) {
const id int64 = 42
isLDAPEnabled = func() bool {
origIsLDAPEnabled := isLDAPEnabled
origNewLDAP := newLDAP
origGetLDAPConfig := getLDAPConfig
t.Cleanup(func() {
isLDAPEnabled = origIsLDAPEnabled
newLDAP = origNewLDAP
getLDAPConfig = origGetLDAPConfig
})
isLDAPEnabled = func(*setting.Cfg) bool {
return true
}
getLDAPConfig = func() (*ldap.Config, error) {
getLDAPConfig = func(*setting.Cfg) (*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)
cache := remotecache.NewFakeStore(t)
auth := prepareMiddleware(t, req, store)
auth := prepareMiddleware(t, cache, nil)
stub := &fakeMultiLDAP{
ID: id,

@ -0,0 +1,448 @@
// Package contexthandler contains the ContextHandler service.
package contexthandler
import (
"context"
"errors"
"strconv"
"strings"
"time"
"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/middleware/cookies"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/contexthandler/authproxy"
"github.com/grafana/grafana/pkg/services/login"
"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"
"gopkg.in/macaron.v1"
)
const (
InvalidUsernamePassword = "invalid username or password"
InvalidAPIKey = "invalid API key"
)
const ServiceName = "ContextHandler"
func init() {
registry.Register(&registry.Descriptor{
Name: ServiceName,
Instance: &ContextHandler{},
InitPriority: registry.High,
})
}
// ContextHandler is a middleware.
type ContextHandler struct {
Cfg *setting.Cfg `inject:""`
AuthTokenService models.UserTokenService `inject:""`
RemoteCache *remotecache.RemoteCache `inject:""`
RenderService rendering.Service `inject:""`
SQLStore *sqlstore.SQLStore `inject:""`
// GetTime returns the current time.
// Stubbable by tests.
GetTime func() time.Time
}
// Init initializes the service.
func (h *ContextHandler) Init() error {
return nil
}
// Middleware provides a middleware to initialize the Macaron context.
func (h *ContextHandler) Middleware(c *macaron.Context) {
ctx := &models.ReqContext{
Context: c,
SignedInUser: &models.SignedInUser{},
IsSignedIn: false,
AllowAnonymous: false,
SkipCache: false,
Logger: log.New("context"),
}
const headerName = "X-Grafana-Org-Id"
orgID := int64(0)
orgIDHeader := ctx.Req.Header.Get(headerName)
if orgIDHeader != "" {
id, err := strconv.ParseInt(orgIDHeader, 10, 64)
if err == nil {
orgID = id
} else {
ctx.Logger.Debug("Received invalid header", "header", headerName, "value", orgIDHeader)
}
}
// 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 h.initContextWithRenderAuth(ctx):
case h.initContextWithAPIKey(ctx):
case h.initContextWithBasicAuth(ctx, orgID):
case h.initContextWithAuthProxy(ctx, orgID):
case h.initContextWithToken(ctx, orgID):
case h.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 (h *ContextHandler) initContextWithAnonymousUser(ctx *models.ReqContext) bool {
if !h.Cfg.AnonymousEnabled {
return false
}
orgQuery := models.GetOrgByNameQuery{Name: h.Cfg.AnonymousOrgName}
if err := bus.Dispatch(&orgQuery); err != nil {
log.Errorf(3, "Anonymous access organization error: '%s': %s", h.Cfg.AnonymousOrgName, err)
return false
}
ctx.IsSignedIn = false
ctx.AllowAnonymous = true
ctx.SignedInUser = &models.SignedInUser{IsAnonymous: true}
ctx.OrgRole = models.RoleType(h.Cfg.AnonymousOrgRole)
ctx.OrgId = orgQuery.Result.Id
ctx.OrgName = orgQuery.Result.Name
return true
}
func (h *ContextHandler) initContextWithAPIKey(ctx *models.ReqContext) bool {
header := ctx.Req.Header.Get("Authorization")
parts := strings.SplitN(header, " ", 2)
var keyString string
if len(parts) == 2 && parts[0] == "Bearer" {
keyString = parts[1]
} else {
username, password, err := util.DecodeBasicAuthHeader(header)
if err == nil && username == "api_key" {
keyString = password
}
}
if keyString == "" {
return false
}
// base64 decode key
decoded, err := apikeygen.Decode(keyString)
if err != nil {
ctx.JsonApiErr(401, InvalidAPIKey, err)
return true
}
// fetch key
keyQuery := models.GetApiKeyByNameQuery{KeyName: decoded.Name, OrgId: decoded.OrgId}
if err := bus.Dispatch(&keyQuery); err != nil {
ctx.JsonApiErr(401, InvalidAPIKey, 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, InvalidAPIKey, err)
return true
}
// check for expiration
getTime := h.GetTime
if getTime == nil {
getTime = time.Now
}
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 (h *ContextHandler) initContextWithBasicAuth(ctx *models.ReqContext, orgID int64) bool {
if !h.Cfg.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,
Cfg: h.Cfg,
}
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, InvalidUsernamePassword, 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, InvalidUsernamePassword, err)
return true
}
ctx.SignedInUser = query.Result
ctx.IsSignedIn = true
return true
}
func (h *ContextHandler) initContextWithToken(ctx *models.ReqContext, orgID int64) bool {
if h.Cfg.LoginCookieName == "" {
return false
}
rawToken := ctx.GetCookie(h.Cfg.LoginCookieName)
if rawToken == "" {
return false
}
token, err := h.AuthTokenService.LookupToken(ctx.Req.Context(), rawToken)
if err != nil {
ctx.Logger.Error("Failed to look up user based on cookie", "error", err)
cookies.WriteSessionCookie(ctx, h.Cfg, "", -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(h.rotateEndOfRequestFunc(ctx, h.AuthTokenService, token))
return true
}
func (h *ContextHandler) 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 {
cookies.WriteSessionCookie(ctx, h.Cfg, token.UnhashedToken, h.Cfg.LoginMaxLifetime)
}
}
}
func (h *ContextHandler) initContextWithRenderAuth(ctx *models.ReqContext) bool {
key := ctx.GetCookie("renderKey")
if key == "" {
return false
}
renderUser, exists := h.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
}
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
}
func handleError(ctx *models.ReqContext, err error, statusCode int, cb func(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 (h *ContextHandler) initContextWithAuthProxy(ctx *models.ReqContext, orgID int64) bool {
username := ctx.Req.Header.Get(h.Cfg.AuthProxyHeaderName)
auth := authproxy.New(h.Cfg, &authproxy.Options{
RemoteCache: h.RemoteCache,
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, err := auth.GetSignedInUser(id)
if err != 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.GetSignedInUser(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", err.Error(),
"error", details,
)
})
return true
}
return true
}

@ -0,0 +1,126 @@
package contexthandler
import (
"context"
"net"
"net/http"
"net/http/httptest"
"testing"
"github.com/grafana/grafana/pkg/components/gtime"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
macaron "gopkg.in/macaron.v1"
)
func TestDontRotateTokensOnCancelledRequests(t *testing.T) {
ctxHdlr := getContextHandler(t)
ctx, cancel := context.WithCancel(context.Background())
reqContext, _, err := initTokenRotationScenario(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 := ctxHdlr.rotateEndOfRequestFunc(reqContext, uts, token)
cancel()
fn(reqContext.Resp)
assert.Equal(t, 0, tryRotateCallCount, "Token rotation was attempted")
}
func TestTokenRotationAtEndOfRequest(t *testing.T) {
ctxHdlr := getContextHandler(t)
reqContext, rr, err := initTokenRotationScenario(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"}
ctxHdlr.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 initTokenRotationScenario(ctx context.Context, t *testing.T) (*models.ReqContext, *httptest.ResponseRecorder, error) {
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
}
rr := httptest.NewRecorder()
req, err := http.NewRequestWithContext(ctx, "", "", nil)
if err != nil {
return nil, nil, err
}
reqContext := &models.ReqContext{
Context: &macaron.Context{
Req: macaron.Request{
Request: req,
},
},
Logger: log.New("testlogger"),
}
mw := mockWriter{rr}
reqContext.Resp = mw
return reqContext, rr, nil
}
type mockWriter struct {
*httptest.ResponseRecorder
}
func (mw mockWriter) Flush() {}
func (mw mockWriter) Status() int { return 0 }
func (mw mockWriter) Size() int { return 0 }
func (mw mockWriter) Written() bool { return false }
func (mw mockWriter) Before(macaron.BeforeFunc) {}
func (mw mockWriter) Push(target string, opts *http.PushOptions) error {
return nil
}

@ -94,8 +94,12 @@ var config *Config
// GetConfig returns the LDAP config if LDAP is enabled otherwise it returns nil. It returns either cached value of
// the config or it reads it and caches it first.
func GetConfig() (*Config, error) {
if !IsEnabled() {
func GetConfig(cfg *setting.Cfg) (*Config, error) {
if cfg != nil {
if !cfg.LDAPEnabled {
return nil, nil
}
} else if !IsEnabled() {
return nil, nil
}

@ -25,12 +25,13 @@ import (
func init() {
remotecache.Register(&RenderUser{})
registry.Register(&registry.Descriptor{
Name: "RenderingService",
Name: ServiceName,
Instance: &RenderingService{},
InitPriority: registry.High,
})
}
const ServiceName = "RenderingService"
const renderKeyPrefix = "render-%s"
type RenderUser struct {
@ -226,8 +227,8 @@ func (rs *RenderingService) getURL(path string) string {
return fmt.Sprintf("%s%s&render=1", rs.Cfg.RendererCallbackUrl, path)
}
protocol := setting.Protocol
switch setting.Protocol {
protocol := rs.Cfg.Protocol
switch protocol {
case setting.HTTPScheme:
protocol = "http"
case setting.HTTP2Scheme, setting.HTTPSScheme:

@ -28,7 +28,7 @@ func TestGetUrl(t *testing.T) {
t.Run("And protocol HTTP configured should return expected path", func(t *testing.T) {
rs.Cfg.ServeFromSubPath = false
rs.Cfg.AppSubURL = ""
setting.Protocol = setting.HTTPScheme
rs.Cfg.Protocol = setting.HTTPScheme
url := rs.getURL(path)
require.Equal(t, "http://localhost:3000/"+path+"&render=1", url)
@ -43,7 +43,7 @@ func TestGetUrl(t *testing.T) {
t.Run("And protocol HTTPS configured should return expected path", func(t *testing.T) {
rs.Cfg.ServeFromSubPath = false
rs.Cfg.AppSubURL = ""
setting.Protocol = setting.HTTPSScheme
rs.Cfg.Protocol = setting.HTTPSScheme
url := rs.getURL(path)
require.Equal(t, "https://localhost:3000/"+path+"&render=1", url)
})
@ -51,7 +51,7 @@ func TestGetUrl(t *testing.T) {
t.Run("And protocol HTTP2 configured should return expected path", func(t *testing.T) {
rs.Cfg.ServeFromSubPath = false
rs.Cfg.AppSubURL = ""
setting.Protocol = setting.HTTP2Scheme
rs.Cfg.Protocol = setting.HTTP2Scheme
url := rs.getURL(path)
require.Equal(t, "https://localhost:3000/"+path+"&render=1", url)
})

@ -6,8 +6,6 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
)
func (ss *SQLStore) addPreferencesQueryAndCommandHandlers() {
@ -42,7 +40,7 @@ func (ss *SQLStore) GetPreferencesWithDefaults(query *models.GetPreferencesWithD
}
res := &models.Preferences{
Theme: setting.DefaultTheme,
Theme: ss.Cfg.DefaultTheme,
Timezone: ss.Cfg.DateFormats.DefaultTimezone,
HomeDashboardId: 0,
}

@ -6,7 +6,6 @@ import (
"testing"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/require"
)
@ -14,7 +13,7 @@ func TestPreferencesDataAccess(t *testing.T) {
ss := InitTestDB(t)
t.Run("GetPreferencesWithDefaults with no saved preferences should return defaults", func(t *testing.T) {
setting.DefaultTheme = "light"
ss.Cfg.DefaultTheme = "light"
ss.Cfg.DateFormats.DefaultTimezone = "UTC"
query := &models.GetPreferencesWithDefaultsQuery{User: &models.SignedInUser{}}

@ -39,16 +39,21 @@ var (
// ContextSessionKey is used as key to save values in `context.Context`
type ContextSessionKey struct{}
const ServiceName = "SqlStore"
const InitPriority = registry.High
func init() {
ss := &SQLStore{}
// This change will make xorm use an empty default schema for postgres and
// by that mimic the functionality of how it was functioning before
// xorm's changes above.
xorm.DefaultPostgresSchema = ""
registry.Register(&registry.Descriptor{
Name: "SQLStore",
Instance: &SQLStore{},
InitPriority: registry.High,
Name: ServiceName,
Instance: ss,
InitPriority: InitPriority,
})
}
@ -113,13 +118,20 @@ func (ss *SQLStore) Init() error {
func (ss *SQLStore) ensureMainOrgAndAdminUser() error {
err := ss.InTransaction(context.Background(), func(ctx context.Context) error {
systemUserCountQuery := models.GetSystemUserCountStatsQuery{}
err := bus.DispatchCtx(ctx, &systemUserCountQuery)
var stats models.SystemUserCountStats
err := ss.WithDbSession(ctx, func(sess *DBSession) error {
var rawSql = `SELECT COUNT(id) AS Count FROM ` + dialect.Quote("user")
if _, err := sess.SQL(rawSql).Get(&stats); err != nil {
return fmt.Errorf("could not determine if admin user exists: %w", err)
}
return nil
})
if err != nil {
return fmt.Errorf("could not determine if admin user exists: %w", err)
return err
}
if systemUserCountQuery.Result.Count > 0 {
if stats.Count > 0 {
return nil
}
@ -351,7 +363,7 @@ func InitTestDB(t ITestDB) *SQLStore {
testSQLStore = &SQLStore{}
testSQLStore.Bus = bus.New()
testSQLStore.CacheService = localcache.New(5*time.Minute, 10*time.Minute)
testSQLStore.skipEnsureDefaultOrgAndUser = true
testSQLStore.skipEnsureDefaultOrgAndUser = false
dbType := migrator.SQLite

@ -47,7 +47,7 @@ var (
// This constant corresponds to the default value for ldap_sync_ttl in .ini files
// it is used for comparison and has to be kept in sync
const (
AuthProxySyncTTL = 60
authProxySyncTTL = 60
)
var (
@ -75,12 +75,8 @@ var (
CustomInitPath = "conf/custom.ini"
// HTTP server options
Protocol Scheme
Domain string
HttpAddr, HttpPort string
CertFile, KeyFile string
SocketPath string
RouterLogging bool
DataProxyLogging bool
DataProxyTimeout int
DataProxyTLSHandshakeTimeout int
@ -93,28 +89,19 @@ var (
EnforceDomain bool
// Security settings.
SecretKey string
DisableGravatar bool
EmailCodeValidMinutes int
DataProxyWhiteList map[string]bool
DisableBruteForceLoginProtection bool
CookieSecure bool
CookieSameSiteDisabled bool
CookieSameSiteMode http.SameSite
AllowEmbedding bool
XSSProtectionHeader bool
ContentTypeProtectionHeader bool
StrictTransportSecurity bool
StrictTransportSecurityMaxAge int
StrictTransportSecurityPreload bool
StrictTransportSecuritySubDomains bool
SecretKey string
DisableGravatar bool
EmailCodeValidMinutes int
DataProxyWhiteList map[string]bool
CookieSecure bool
CookieSameSiteDisabled bool
CookieSameSiteMode http.SameSite
// Snapshots
ExternalSnapshotUrl string
ExternalSnapshotName string
ExternalEnabled bool
SnapShotRemoveExpired bool
SnapshotPublicMode bool
// Dashboard history
DashboardVersionsToKeep int
@ -129,7 +116,6 @@ var (
VerifyEmailEnabled bool
LoginHint string
PasswordHint string
DefaultTheme string
DisableLoginForm bool
DisableSignoutMenu bool
SignoutRedirectUrl string
@ -139,7 +125,7 @@ var (
OAuthAutoLogin bool
ViewersCanEdit bool
// Http auth
// HTTP auth
AdminUser string
AdminPassword string
LoginCookieName string
@ -147,18 +133,10 @@ var (
SigV4AuthEnabled bool
AnonymousEnabled bool
AnonymousOrgName string
AnonymousOrgRole string
// Auth proxy settings
AuthProxyEnabled bool
AuthProxyHeaderName string
AuthProxyHeaderProperty string
AuthProxyAutoSignUp bool
AuthProxyEnableLoginToken bool
AuthProxySyncTtl int
AuthProxyWhitelist string
AuthProxyHeaders map[string]string
AuthProxyEnabled bool
AuthProxyHeaderProperty string
// Basic Auth
BasicAuthEnabled bool
@ -224,6 +202,9 @@ type Cfg struct {
ServeFromSubPath bool
StaticRootPath string
Protocol Scheme
SocketPath string
RouterLogging bool
Domain string
// build
BuildVersion string
@ -251,11 +232,18 @@ type Cfg struct {
RendererConcurrentRequestLimit int
// Security
DisableInitAdminCreation bool
DisableBruteForceLoginProtection bool
CookieSecure bool
CookieSameSiteDisabled bool
CookieSameSiteMode http.SameSite
DisableInitAdminCreation bool
DisableBruteForceLoginProtection bool
CookieSecure bool
CookieSameSiteDisabled bool
CookieSameSiteMode http.SameSite
AllowEmbedding bool
XSSProtectionHeader bool
ContentTypeProtectionHeader bool
StrictTransportSecurity bool
StrictTransportSecurityMaxAge int
StrictTransportSecurityPreload bool
StrictTransportSecuritySubDomains bool
TempDataLifetime time.Duration
PluginsEnableAlpha bool
@ -282,6 +270,17 @@ type Cfg struct {
LoginMaxLifetime time.Duration
TokenRotationIntervalMinutes int
SigV4AuthEnabled bool
BasicAuthEnabled bool
// Auth proxy settings
AuthProxyEnabled bool
AuthProxyHeaderName string
AuthProxyHeaderProperty string
AuthProxyAutoSignUp bool
AuthProxyEnableLoginToken bool
AuthProxyWhitelist string
AuthProxyHeaders map[string]string
AuthProxySyncTTL int
// OAuth
OAuthCookieMaxAge int
@ -302,6 +301,9 @@ type Cfg struct {
// Use to enable new features which may still be in alpha/beta stage.
FeatureToggles map[string]bool
AnonymousEnabled bool
AnonymousOrgName string
AnonymousOrgRole string
AnonymousHideVersion bool
DateFormats DateFormats
@ -317,6 +319,21 @@ type Cfg struct {
// Sentry config
Sentry Sentry
// Snapshots
SnapshotPublicMode bool
ErrTemplateName string
Env string
// LDAP
LDAPEnabled bool
LDAPAllowSignup bool
Quota QuotaSettings
DefaultTheme string
}
// IsExpressionsEnabled returns whether the expressions feature is enabled.
@ -707,9 +724,12 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
cfg.IsEnterprise = IsEnterprise
cfg.Packaging = Packaging
cfg.ErrTemplateName = ErrTemplateName
ApplicationName = "Grafana"
Env = valueAsString(iniFile.Section(""), "app_mode", "development")
cfg.Env = Env
InstanceName = valueAsString(iniFile.Section(""), "instance_name", "unknown_instance_name")
plugins := valueAsString(iniFile.Section("paths"), "plugins", "")
PluginsPath = makeAbsolute(plugins, HomePath)
@ -736,7 +756,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
return err
}
if err := readSnapshotsSettings(iniFile); err != nil {
if err := readSnapshotsSettings(cfg, iniFile); err != nil {
return err
}
@ -789,7 +809,6 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
cfg.PluginsAllowUnsigned = append(cfg.PluginsAllowUnsigned, plug)
}
cfg.MarketplaceURL = pluginsSection.Key("marketplace_url").MustString("https://grafana.com/grafana/plugins/")
cfg.Protocol = Protocol
// Read and populate feature toggles list
featureTogglesSection := iniFile.Section("feature_toggles")
@ -858,8 +877,10 @@ func (cfg *Cfg) readLDAPConfig() {
LDAPConfigFile = ldapSec.Key("config_file").String()
LDAPSyncCron = ldapSec.Key("sync_cron").String()
LDAPEnabled = ldapSec.Key("enabled").MustBool(false)
cfg.LDAPEnabled = LDAPEnabled
LDAPActiveSyncEnabled = ldapSec.Key("active_sync_enabled").MustBool(false)
LDAPAllowSignup = ldapSec.Key("allow_sign_up").MustBool(true)
cfg.LDAPAllowSignup = LDAPAllowSignup
}
func (cfg *Cfg) readSessionConfig() {
@ -910,7 +931,7 @@ func (cfg *Cfg) LogConfigSources() {
cfg.Logger.Info("Path Logs", "path", cfg.LogsPath)
cfg.Logger.Info("Path Plugins", "path", PluginsPath)
cfg.Logger.Info("Path Provisioning", "path", cfg.ProvisioningPath)
cfg.Logger.Info("App mode " + Env)
cfg.Logger.Info("App mode " + cfg.Env)
}
type DynamicSection struct {
@ -949,7 +970,6 @@ func readSecuritySettings(iniFile *ini.File, cfg *Cfg) error {
SecretKey = valueAsString(security, "secret_key", "")
DisableGravatar = security.Key("disable_gravatar").MustBool(true)
cfg.DisableBruteForceLoginProtection = security.Key("disable_brute_force_login_protection").MustBool(false)
DisableBruteForceLoginProtection = cfg.DisableBruteForceLoginProtection
CookieSecure = security.Key("cookie_secure").MustBool(false)
cfg.CookieSecure = CookieSecure
@ -974,14 +994,14 @@ func readSecuritySettings(iniFile *ini.File, cfg *Cfg) error {
cfg.CookieSameSiteMode = CookieSameSiteMode
}
}
AllowEmbedding = security.Key("allow_embedding").MustBool(false)
cfg.AllowEmbedding = security.Key("allow_embedding").MustBool(false)
ContentTypeProtectionHeader = security.Key("x_content_type_options").MustBool(true)
XSSProtectionHeader = security.Key("x_xss_protection").MustBool(true)
StrictTransportSecurity = security.Key("strict_transport_security").MustBool(false)
StrictTransportSecurityMaxAge = security.Key("strict_transport_security_max_age_seconds").MustInt(86400)
StrictTransportSecurityPreload = security.Key("strict_transport_security_preload").MustBool(false)
StrictTransportSecuritySubDomains = security.Key("strict_transport_security_subdomains").MustBool(false)
cfg.ContentTypeProtectionHeader = security.Key("x_content_type_options").MustBool(true)
cfg.XSSProtectionHeader = security.Key("x_xss_protection").MustBool(true)
cfg.StrictTransportSecurity = security.Key("strict_transport_security").MustBool(false)
cfg.StrictTransportSecurityMaxAge = security.Key("strict_transport_security_max_age_seconds").MustInt(86400)
cfg.StrictTransportSecurityPreload = security.Key("strict_transport_security_preload").MustBool(false)
cfg.StrictTransportSecuritySubDomains = security.Key("strict_transport_security_subdomains").MustBool(false)
// read data source proxy whitelist
DataProxyWhiteList = make(map[string]bool)
@ -1054,41 +1074,45 @@ func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) {
// anonymous access
AnonymousEnabled = iniFile.Section("auth.anonymous").Key("enabled").MustBool(false)
AnonymousOrgName = valueAsString(iniFile.Section("auth.anonymous"), "org_name", "")
AnonymousOrgRole = valueAsString(iniFile.Section("auth.anonymous"), "org_role", "")
cfg.AnonymousEnabled = AnonymousEnabled
cfg.AnonymousOrgName = valueAsString(iniFile.Section("auth.anonymous"), "org_name", "")
cfg.AnonymousOrgRole = valueAsString(iniFile.Section("auth.anonymous"), "org_role", "")
cfg.AnonymousHideVersion = iniFile.Section("auth.anonymous").Key("hide_version").MustBool(false)
// basic auth
authBasic := iniFile.Section("auth.basic")
BasicAuthEnabled = authBasic.Key("enabled").MustBool(true)
cfg.BasicAuthEnabled = BasicAuthEnabled
authProxy := iniFile.Section("auth.proxy")
AuthProxyEnabled = authProxy.Key("enabled").MustBool(false)
cfg.AuthProxyEnabled = AuthProxyEnabled
AuthProxyHeaderName = valueAsString(authProxy, "header_name", "")
cfg.AuthProxyHeaderName = valueAsString(authProxy, "header_name", "")
AuthProxyHeaderProperty = valueAsString(authProxy, "header_property", "")
AuthProxyAutoSignUp = authProxy.Key("auto_sign_up").MustBool(true)
AuthProxyEnableLoginToken = authProxy.Key("enable_login_token").MustBool(false)
cfg.AuthProxyHeaderProperty = AuthProxyHeaderProperty
cfg.AuthProxyAutoSignUp = authProxy.Key("auto_sign_up").MustBool(true)
cfg.AuthProxyEnableLoginToken = authProxy.Key("enable_login_token").MustBool(false)
ldapSyncVal := authProxy.Key("ldap_sync_ttl").MustInt()
syncVal := authProxy.Key("sync_ttl").MustInt()
if ldapSyncVal != AuthProxySyncTTL {
AuthProxySyncTtl = ldapSyncVal
if ldapSyncVal != authProxySyncTTL {
cfg.AuthProxySyncTTL = ldapSyncVal
cfg.Logger.Warn("[Deprecated] the configuration setting 'ldap_sync_ttl' is deprecated, please use 'sync_ttl' instead")
} else {
AuthProxySyncTtl = syncVal
cfg.AuthProxySyncTTL = syncVal
}
AuthProxyWhitelist = valueAsString(authProxy, "whitelist", "")
cfg.AuthProxyWhitelist = valueAsString(authProxy, "whitelist", "")
AuthProxyHeaders = make(map[string]string)
cfg.AuthProxyHeaders = make(map[string]string)
headers := valueAsString(authProxy, "headers", "")
for _, propertyAndHeader := range util.SplitString(headers) {
split := strings.SplitN(propertyAndHeader, ":", 2)
if len(split) == 2 {
AuthProxyHeaders[split[0]] = split[1]
cfg.AuthProxyHeaders[split[0]] = split[1]
}
}
@ -1106,7 +1130,7 @@ func readUserSettings(iniFile *ini.File, cfg *Cfg) error {
LoginHint = valueAsString(users, "login_hint", "")
PasswordHint = valueAsString(users, "password_hint", "")
DefaultTheme = valueAsString(users, "default_theme", "")
cfg.DefaultTheme = valueAsString(users, "default_theme", "")
ExternalUserMngLinkUrl = valueAsString(users, "external_manage_link_url", "")
ExternalUserMngLinkName = valueAsString(users, "external_manage_link_name", "")
ExternalUserMngInfo = valueAsString(users, "external_manage_info", "")
@ -1178,7 +1202,7 @@ func readAlertingSettings(iniFile *ini.File) error {
return nil
}
func readSnapshotsSettings(iniFile *ini.File) error {
func readSnapshotsSettings(cfg *Cfg, iniFile *ini.File) error {
snapshots := iniFile.Section("snapshots")
ExternalSnapshotUrl = valueAsString(snapshots, "external_snapshot_url", "")
@ -1186,7 +1210,7 @@ func readSnapshotsSettings(iniFile *ini.File) error {
ExternalEnabled = snapshots.Key("external_enabled").MustBool(true)
SnapShotRemoveExpired = snapshots.Key("snapshot_remove_expired").MustBool(true)
SnapshotPublicMode = snapshots.Key("public_mode").MustBool(false)
cfg.SnapshotPublicMode = snapshots.Key("public_mode").MustBool(false)
return nil
}
@ -1204,28 +1228,28 @@ func readServerSettings(iniFile *ini.File, cfg *Cfg) error {
cfg.AppSubURL = AppSubUrl
cfg.ServeFromSubPath = ServeFromSubPath
Protocol = HTTPScheme
cfg.Protocol = HTTPScheme
protocolStr := valueAsString(server, "protocol", "http")
if protocolStr == "https" {
Protocol = HTTPSScheme
cfg.Protocol = HTTPSScheme
CertFile = server.Key("cert_file").String()
KeyFile = server.Key("cert_key").String()
}
if protocolStr == "h2" {
Protocol = HTTP2Scheme
cfg.Protocol = HTTP2Scheme
CertFile = server.Key("cert_file").String()
KeyFile = server.Key("cert_key").String()
}
if protocolStr == "socket" {
Protocol = SocketScheme
SocketPath = server.Key("socket").String()
cfg.Protocol = SocketScheme
cfg.SocketPath = server.Key("socket").String()
}
Domain = valueAsString(server, "domain", "localhost")
cfg.Domain = valueAsString(server, "domain", "localhost")
HttpAddr = valueAsString(server, "http_addr", DefaultHTTPAddr)
HttpPort = valueAsString(server, "http_port", "3000")
RouterLogging = server.Key("router_logging").MustBool(false)
cfg.RouterLogging = server.Key("router_logging").MustBool(false)
EnableGzip = server.Key("enable_gzip").MustBool(false)
EnforceDomain = server.Key("enforce_domain").MustBool(false)

@ -86,4 +86,6 @@ func (cfg *Cfg) readQuotaSettings() {
ApiKey: quota.Key("global_api_key").MustInt64(-1),
Session: quota.Key("global_session").MustInt64(-1),
}
cfg.Quota = Quota
}

@ -133,7 +133,7 @@ func TestLoadingSettings(t *testing.T) {
})
So(err, ShouldBeNil)
So(Domain, ShouldEqual, "test2")
So(cfg.Domain, ShouldEqual, "test2")
})
Convey("Defaults can be overridden in specified config file", func() {
@ -239,7 +239,7 @@ func TestLoadingSettings(t *testing.T) {
})
So(err, ShouldBeNil)
So(AuthProxySyncTtl, ShouldEqual, 2)
So(cfg.AuthProxySyncTTL, ShouldEqual, 2)
})
Convey("Only ldap_sync_ttl should return the value ldap_sync_ttl", func() {
@ -250,7 +250,7 @@ func TestLoadingSettings(t *testing.T) {
})
So(err, ShouldBeNil)
So(AuthProxySyncTtl, ShouldEqual, 5)
So(cfg.AuthProxySyncTTL, ShouldEqual, 5)
})
Convey("ldap_sync should override ldap_sync_ttl that is default value", func() {
@ -261,7 +261,7 @@ func TestLoadingSettings(t *testing.T) {
})
So(err, ShouldBeNil)
So(AuthProxySyncTtl, ShouldEqual, 5)
So(cfg.AuthProxySyncTTL, ShouldEqual, 5)
})
Convey("ldap_sync should not override ldap_sync_ttl that is different from default value", func() {
@ -272,7 +272,7 @@ func TestLoadingSettings(t *testing.T) {
})
So(err, ShouldBeNil)
So(AuthProxySyncTtl, ShouldEqual, 12)
So(cfg.AuthProxySyncTTL, ShouldEqual, 12)
})
})

Loading…
Cancel
Save