From dd2d206d99f7ea8341c1f7242d3026205539aac3 Mon Sep 17 00:00:00 2001 From: Arve Knudsen Date: Tue, 15 Dec 2020 19:09:04 +0100 Subject: [PATCH] Backend: Remove more globals (#29644) Signed-off-by: Arve Knudsen --- pkg/api/api.go | 4 +- pkg/api/common_test.go | 18 +- pkg/api/dashboard_test.go | 13 +- pkg/api/frontendsettings_test.go | 2 +- pkg/api/http_server.go | 4 +- pkg/api/index.go | 4 +- pkg/api/ldap_debug.go | 3 +- pkg/api/ldap_debug_test.go | 18 +- pkg/api/login.go | 4 +- pkg/api/login_oauth.go | 2 +- pkg/api/login_test.go | 73 ++-- pkg/api/render.go | 18 +- pkg/middleware/cookies/cookies.go | 2 +- pkg/middleware/dashboard_redirect.go | 38 +- pkg/middleware/dashboard_redirect_test.go | 115 +++--- pkg/middleware/{util.go => gziper.go} | 0 pkg/middleware/middleware_test.go | 70 ++-- pkg/middleware/org_redirect.go | 4 +- pkg/middleware/quota.go | 4 +- pkg/middleware/quota_test.go | 375 ++++++++++-------- pkg/middleware/recovery_test.go | 6 +- pkg/middleware/testing.go | 10 +- pkg/middleware/validate_host.go | 6 +- pkg/models/context.go | 6 +- pkg/models/dashboards.go | 6 +- pkg/models/quotas.go | 45 --- .../contexthandler/authproxy/authproxy.go | 4 +- pkg/services/contexthandler/contexthandler.go | 14 +- .../contexthandler/contexthandler_test.go | 18 +- pkg/services/quota/quota.go | 53 ++- pkg/services/sqlstore/org.go | 12 - pkg/services/sqlstore/sqlstore.go | 31 +- pkg/setting/setting.go | 20 +- pkg/setting/setting_test.go | 4 +- pkg/tsdb/cloudwatch/cloudwatch.go | 4 +- 35 files changed, 526 insertions(+), 484 deletions(-) rename pkg/middleware/{util.go => gziper.go} (100%) diff --git a/pkg/api/api.go b/pkg/api/api.go index 9049f70cc6d..22805e73178 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -24,8 +24,8 @@ func (hs *HTTPServer) registerRoutes() { reqCanAccessTeams := middleware.AdminOrFeatureEnabled(hs.Cfg.EditorsCanAdmin) reqSnapshotPublicModeOrSignedIn := middleware.SnapshotPublicModeOrSignedIn(hs.Cfg) redirectFromLegacyDashboardURL := middleware.RedirectFromLegacyDashboardURL() - redirectFromLegacyDashboardSoloURL := middleware.RedirectFromLegacyDashboardSoloURL() - redirectFromLegacyPanelEditURL := middleware.RedirectFromLegacyPanelEditURL() + redirectFromLegacyDashboardSoloURL := middleware.RedirectFromLegacyDashboardSoloURL(hs.Cfg) + redirectFromLegacyPanelEditURL := middleware.RedirectFromLegacyPanelEditURL(hs.Cfg) quota := middleware.Quota(hs.QuotaService) bind := binding.Bind diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index 71d878368d4..f6e0b9e9677 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/infra/fs" "github.com/grafana/grafana/pkg/infra/remotecache" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/registry" @@ -26,7 +27,7 @@ func loggedInUserScenario(t *testing.T, desc string, url string, fn scenarioFunc func loggedInUserScenarioWithRole(t *testing.T, desc string, method string, url string, routePattern string, role models.RoleType, fn scenarioFunc) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { - defer bus.ClearBusHandlers() + t.Cleanup(bus.ClearBusHandlers) sc := setupScenarioContext(t, url) sc.defaultHandler = Wrap(func(c *models.ReqContext) Response { @@ -129,6 +130,7 @@ func (sc *scenarioContext) fakeReqNoAssertionsWithCookie(method, url string, coo type scenarioContext struct { t *testing.T + cfg *setting.Cfg m *macaron.Macaron context *models.ReqContext resp *httptest.ResponseRecorder @@ -146,12 +148,15 @@ func (sc *scenarioContext) exec() { type scenarioFunc func(c *scenarioContext) type handlerFunc func(c *models.ReqContext) Response -func getContextHandler(t *testing.T) *contexthandler.ContextHandler { +func getContextHandler(t *testing.T, cfg *setting.Cfg) *contexthandler.ContextHandler { t.Helper() + if cfg == nil { + cfg = setting.NewCfg() + } + sqlStore := sqlstore.InitTestDB(t) remoteCacheSvc := &remotecache.RemoteCache{} - cfg := setting.NewCfg() cfg.RemoteCacheOptions = &setting.RemoteCacheOptions{ Name: "database", } @@ -187,19 +192,24 @@ func getContextHandler(t *testing.T) *contexthandler.ContextHandler { } func setupScenarioContext(t *testing.T, url string) *scenarioContext { + cfg := setting.NewCfg() sc := &scenarioContext{ url: url, t: t, + cfg: cfg, } viewsPath, err := filepath.Abs("../../public/views") require.NoError(t, err) + exists, err := fs.Exists(viewsPath) + require.NoError(t, err) + require.Truef(t, exists, "Views should be in %q", viewsPath) sc.m = macaron.New() sc.m.Use(macaron.Renderer(macaron.RenderOptions{ Directory: viewsPath, Delims: macaron.Delims{Left: "[[", Right: "]]"}, })) - sc.m.Use(getContextHandler(t).Middleware) + sc.m.Use(getContextHandler(t, cfg).Middleware) return sc } diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index edc1b47c5ea..38dba127b26 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -15,6 +15,7 @@ import ( "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/live" "github.com/grafana/grafana/pkg/services/provisioning" + "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -1177,11 +1178,15 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { t.Cleanup(bus.ClearBusHandlers) + cfg := setting.NewCfg() hs := HTTPServer{ Bus: bus.GetBus(), - Cfg: setting.NewCfg(), + Cfg: cfg, ProvisioningService: provisioning.NewProvisioningServiceMock(), Live: &live.GrafanaLive{Cfg: setting.NewCfg()}, + QuotaService: "a.QuotaService{ + Cfg: cfg, + }, } sc := setupScenarioContext(t, url) @@ -1238,11 +1243,13 @@ func restoreDashboardVersionScenario(t *testing.T, desc string, url string, rout t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { defer bus.ClearBusHandlers() + cfg := setting.NewCfg() hs := HTTPServer{ - Cfg: setting.NewCfg(), + Cfg: cfg, Bus: bus.GetBus(), ProvisioningService: provisioning.NewProvisioningServiceMock(), - Live: &live.GrafanaLive{Cfg: setting.NewCfg()}, + Live: &live.GrafanaLive{Cfg: cfg}, + QuotaService: "a.QuotaService{Cfg: cfg}, } sc := setupScenarioContext(t, url) diff --git a/pkg/api/frontendsettings_test.go b/pkg/api/frontendsettings_test.go index 94f731d66f6..1e9bf4e1e09 100644 --- a/pkg/api/frontendsettings_test.go +++ b/pkg/api/frontendsettings_test.go @@ -52,7 +52,7 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg) (*macaron.Macaron, *HT } m := macaron.New() - m.Use(getContextHandler(t).Middleware) + m.Use(getContextHandler(t, cfg).Middleware) m.Use(macaron.Renderer(macaron.RenderOptions{ Directory: filepath.Join(setting.StaticRootPath, "views"), IndentJSON: true, diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index c0c02d765d9..8b71fda5e30 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -337,11 +337,11 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() { m.Use(hs.metricsEndpoint) m.Use(hs.ContextHandler.Middleware) - m.Use(middleware.OrgRedirect()) + m.Use(middleware.OrgRedirect(hs.Cfg)) // needs to be after context handler if setting.EnforceDomain { - m.Use(middleware.ValidateHostHeader(hs.Cfg.Domain)) + m.Use(middleware.ValidateHostHeader(hs.Cfg)) } m.Use(middleware.HandleNoCacheHeader()) diff --git a/pkg/api/index.go b/pkg/api/index.go index bbc982a4a2e..4a427c45c97 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -458,7 +458,7 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat func (hs *HTTPServer) Index(c *models.ReqContext) { data, err := hs.setIndexViewData(c) if err != nil { - c.Handle(500, "Failed to get settings", err) + c.Handle(hs.Cfg, 500, "Failed to get settings", err) return } c.HTML(200, "index", data) @@ -472,7 +472,7 @@ func (hs *HTTPServer) NotFoundHandler(c *models.ReqContext) { data, err := hs.setIndexViewData(c) if err != nil { - c.Handle(500, "Failed to get settings", err) + c.Handle(hs.Cfg, 500, "Failed to get settings", err) return } diff --git a/pkg/api/ldap_debug.go b/pkg/api/ldap_debug.go index 361f6d8ce53..1276cf0ceff 100644 --- a/pkg/api/ldap_debug.go +++ b/pkg/api/ldap_debug.go @@ -11,7 +11,6 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/ldap" "github.com/grafana/grafana/pkg/services/multildap" - "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -187,7 +186,7 @@ func (hs *HTTPServer) PostSyncUserWithLDAP(c *models.ReqContext) Response { user, _, err := ldapServer.User(query.Result.Login) if err != nil { if errors.Is(err, multildap.ErrDidNotFindUser) { // User was not in the LDAP server - we need to take action: - if setting.AdminUser == query.Result.Login { // User is *the* Grafana Admin. We cannot disable it. + if hs.Cfg.AdminUser == query.Result.Login { // User is *the* Grafana Admin. We cannot disable it. errMsg := fmt.Sprintf(`Refusing to sync grafana super admin "%s" - it would be disabled`, query.Result.Login) ldapLogger.Error(errMsg) return Error(http.StatusBadRequest, errMsg, err) diff --git a/pkg/api/ldap_debug_test.go b/pkg/api/ldap_debug_test.go index a929c06e640..9eb7e1304db 100644 --- a/pkg/api/ldap_debug_test.go +++ b/pkg/api/ldap_debug_test.go @@ -375,7 +375,7 @@ func TestGetLDAPStatusAPIEndpoint(t *testing.T) { // PostSyncUserWithLDAP tests // *** -func postSyncUserWithLDAPContext(t *testing.T, requestURL string, preHook func(t *testing.T)) *scenarioContext { +func postSyncUserWithLDAPContext(t *testing.T, requestURL string, preHook func(*testing.T, *scenarioContext)) *scenarioContext { t.Helper() sc := setupScenarioContext(t, requestURL) @@ -387,7 +387,7 @@ func postSyncUserWithLDAPContext(t *testing.T, requestURL string, preHook func(t setting.LDAPEnabled = true hs := &HTTPServer{ - Cfg: setting.NewCfg(), + Cfg: sc.cfg, AuthTokenService: auth.NewFakeUserAuthTokenService(), } @@ -402,7 +402,7 @@ func postSyncUserWithLDAPContext(t *testing.T, requestURL string, preHook func(t req, err := http.NewRequest(http.MethodPost, requestURL, nil) require.NoError(t, err) - preHook(t) + preHook(t, sc) sc.req = req sc.exec() @@ -411,7 +411,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) { + sc := postSyncUserWithLDAPContext(t, "/api/admin/ldap/sync/34", func(t *testing.T, sc *scenarioContext) { getLDAPConfig = func(*setting.Cfg) (*ldap.Config, error) { return &ldap.Config{}, nil } @@ -456,7 +456,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) { + sc := postSyncUserWithLDAPContext(t, "/api/admin/ldap/sync/34", func(t *testing.T, sc *scenarioContext) { getLDAPConfig = func(*setting.Cfg) (*ldap.Config, error) { return &ldap.Config{}, nil } @@ -484,7 +484,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) { + sc := postSyncUserWithLDAPContext(t, "/api/admin/ldap/sync/34", func(t *testing.T, sc *scenarioContext) { getLDAPConfig = func(*setting.Cfg) (*ldap.Config, error) { return &ldap.Config{}, nil } @@ -495,9 +495,7 @@ func TestPostSyncUserWithLDAPAPIEndpoint_WhenGrafanaAdmin(t *testing.T) { userSearchError = multildap.ErrDidNotFindUser - admin := setting.AdminUser - t.Cleanup(func() { setting.AdminUser = admin }) - setting.AdminUser = "ldap-daniel" + sc.cfg.AdminUser = "ldap-daniel" bus.AddHandler("test", func(q *models.GetUserByIdQuery) error { require.Equal(t, q.Id, int64(34)) @@ -527,7 +525,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) { + sc := postSyncUserWithLDAPContext(t, "/api/admin/ldap/sync/34", func(t *testing.T, sc *scenarioContext) { getLDAPConfig = func(*setting.Cfg) (*ldap.Config, error) { return &ldap.Config{}, nil } diff --git a/pkg/api/login.go b/pkg/api/login.go index 517fc231b4a..415b1909dda 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -77,7 +77,7 @@ func (hs *HTTPServer) CookieOptionsFromCfg() cookies.CookieOptions { func (hs *HTTPServer) LoginView(c *models.ReqContext) { viewData, err := setIndexViewData(hs, c) if err != nil { - c.Handle(500, "Failed to get settings", err) + c.Handle(hs.Cfg, 500, "Failed to get settings", err) return } @@ -117,7 +117,7 @@ func (hs *HTTPServer) LoginView(c *models.ReqContext) { user := &models.User{Id: c.SignedInUser.UserId, Email: c.SignedInUser.Email, Login: c.SignedInUser.Login} err := hs.loginUserWithUser(user, c) if err != nil { - c.Handle(500, "Failed to sign in user", err) + c.Handle(hs.Cfg, 500, "Failed to sign in user", err) return } } diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go index fe4b425dc5a..1fce9b6f610 100644 --- a/pkg/api/login_oauth.go +++ b/pkg/api/login_oauth.go @@ -277,7 +277,7 @@ type LoginError struct { } func (hs *HTTPServer) handleOAuthLoginError(ctx *models.ReqContext, info models.LoginInfo, err LoginError) { - ctx.Handle(err.HttpStatus, err.PublicMessage, err.Err) + ctx.Handle(hs.Cfg, err.HttpStatus, err.PublicMessage, err.Err) info.Error = err.Err if info.Error == nil { diff --git a/pkg/api/login_test.go b/pkg/api/login_test.go index 1403fd478f2..8c67fc2eec8 100644 --- a/pkg/api/login_test.go +++ b/pkg/api/login_test.go @@ -25,7 +25,11 @@ import ( "github.com/stretchr/testify/require" ) -func mockSetIndexViewData() { +func fakeSetIndexViewData(t *testing.T) { + origSetIndexViewData := setIndexViewData + t.Cleanup(func() { + setIndexViewData = origSetIndexViewData + }) setIndexViewData = func(*HTTPServer, *models.ReqContext) (*dtos.IndexViewData, error) { data := &dtos.IndexViewData{ User: &dtos.CurrentUser{}, @@ -36,22 +40,16 @@ func mockSetIndexViewData() { } } -func resetSetIndexViewData() { - setIndexViewData = (*HTTPServer).setIndexViewData -} - -func mockViewIndex() { +func fakeViewIndex(t *testing.T) { + origGetViewIndex := getViewIndex + t.Cleanup(func() { + getViewIndex = origGetViewIndex + }) getViewIndex = func() string { return "index-template" } } -func resetViewIndex() { - getViewIndex = func() string { - return ViewIndex - } -} - func getBody(resp *httptest.ResponseRecorder) (string, error) { responseData, err := ioutil.ReadAll(resp.Body) if err != nil { @@ -86,16 +84,15 @@ type redirectCase struct { redirectURL string } -func TestLoginErrorCookieApiEndpoint(t *testing.T) { - mockSetIndexViewData() - defer resetSetIndexViewData() +func TestLoginErrorCookieAPIEndpoint(t *testing.T) { + fakeSetIndexViewData(t) - mockViewIndex() - defer resetViewIndex() + fakeViewIndex(t) sc := setupScenarioContext(t, "/login") + cfg := setting.NewCfg() hs := &HTTPServer{ - Cfg: setting.NewCfg(), + Cfg: cfg, License: &licensing.OSSLicensingService{}, } @@ -103,7 +100,7 @@ func TestLoginErrorCookieApiEndpoint(t *testing.T) { hs.LoginView(c) }) - setting.LoginCookieName = "grafana_session" + cfg.LoginCookieName = "grafana_session" setting.SecretKey = "login_testing" setting.OAuthService = &setting.OAuther{} @@ -142,11 +139,9 @@ func TestLoginErrorCookieApiEndpoint(t *testing.T) { } func TestLoginViewRedirect(t *testing.T) { - mockSetIndexViewData() - defer resetSetIndexViewData() + fakeSetIndexViewData(t) - mockViewIndex() - defer resetViewIndex() + fakeViewIndex(t) sc := setupScenarioContext(t, "/login") hs := &HTTPServer{ Cfg: setting.NewCfg(), @@ -318,11 +313,9 @@ func TestLoginViewRedirect(t *testing.T) { } func TestLoginPostRedirect(t *testing.T) { - mockSetIndexViewData() - defer resetSetIndexViewData() + fakeSetIndexViewData(t) - mockViewIndex() - defer resetViewIndex() + fakeViewIndex(t) sc := setupScenarioContext(t, "/login") hs := &HTTPServer{ log: &FakeLogger{}, @@ -478,8 +471,7 @@ func TestLoginPostRedirect(t *testing.T) { } func TestLoginOAuthRedirect(t *testing.T) { - mockSetIndexViewData() - defer resetSetIndexViewData() + fakeSetIndexViewData(t) sc := setupScenarioContext(t, "/login") hs := &HTTPServer{ @@ -511,11 +503,9 @@ func TestLoginOAuthRedirect(t *testing.T) { } func TestLoginInternal(t *testing.T) { - mockSetIndexViewData() - defer resetSetIndexViewData() + fakeSetIndexViewData(t) - mockViewIndex() - defer resetViewIndex() + fakeViewIndex(t) sc := setupScenarioContext(t, "/login") hs := &HTTPServer{ Cfg: setting.NewCfg(), @@ -559,24 +549,23 @@ func TestAuthProxyLoginEnableLoginTokenDisabled(t *testing.T) { func TestAuthProxyLoginWithEnableLoginToken(t *testing.T) { sc := setupAuthProxyLoginTest(t, true) + require.Equal(t, sc.resp.Code, 302) - assert.Equal(t, sc.resp.Code, 302) location, ok := sc.resp.Header()["Location"] assert.True(t, ok) assert.Equal(t, location[0], "/") - - setCookie, ok := sc.resp.Header()["Set-Cookie"] - assert.True(t, ok, "Set-Cookie exists") + setCookie := sc.resp.Header()["Set-Cookie"] + require.NotNil(t, setCookie, "Set-Cookie should exist") assert.Equal(t, "grafana_session=; Path=/; Max-Age=0; HttpOnly", setCookie[0]) } func setupAuthProxyLoginTest(t *testing.T, enableLoginToken bool) *scenarioContext { - mockSetIndexViewData() - defer resetSetIndexViewData() + fakeSetIndexViewData(t) sc := setupScenarioContext(t, "/login") + sc.cfg.LoginCookieName = "grafana_session" hs := &HTTPServer{ - Cfg: setting.NewCfg(), + Cfg: sc.cfg, License: &licensing.OSSLicensingService{}, AuthTokenService: auth.NewFakeUserAuthTokenService(), log: log.New("hello"), @@ -592,8 +581,8 @@ func setupAuthProxyLoginTest(t *testing.T, enableLoginToken bool) *scenarioConte setting.OAuthService = &setting.OAuther{} setting.OAuthService.OAuthInfos = make(map[string]*setting.OAuthInfo) - hs.Cfg.AuthProxyEnabled = true - hs.Cfg.AuthProxyEnableLoginToken = enableLoginToken + sc.cfg.AuthProxyEnabled = true + sc.cfg.AuthProxyEnableLoginToken = enableLoginToken sc.m.Get(sc.url, sc.defaultHandler) sc.fakeReqNoAssertions("GET", sc.url).exec() diff --git a/pkg/api/render.go b/pkg/api/render.go index 593c0bcb6dc..6940ff8ac4b 100644 --- a/pkg/api/render.go +++ b/pkg/api/render.go @@ -17,7 +17,7 @@ import ( func (hs *HTTPServer) RenderToPng(c *models.ReqContext) { queryReader, err := util.NewURLQueryReader(c.Req.URL) if err != nil { - c.Handle(400, "Render parameters error", err) + c.Handle(hs.Cfg, 400, "Render parameters error", err) return } @@ -25,25 +25,25 @@ func (hs *HTTPServer) RenderToPng(c *models.ReqContext) { width, err := strconv.Atoi(queryReader.Get("width", "800")) if err != nil { - c.Handle(400, "Render parameters error", fmt.Errorf("cannot parse width as int: %s", err)) + c.Handle(hs.Cfg, 400, "Render parameters error", fmt.Errorf("cannot parse width as int: %s", err)) return } height, err := strconv.Atoi(queryReader.Get("height", "400")) if err != nil { - c.Handle(400, "Render parameters error", fmt.Errorf("cannot parse height as int: %s", err)) + c.Handle(hs.Cfg, 400, "Render parameters error", fmt.Errorf("cannot parse height as int: %s", err)) return } timeout, err := strconv.Atoi(queryReader.Get("timeout", "60")) if err != nil { - c.Handle(400, "Render parameters error", fmt.Errorf("cannot parse timeout as int: %s", err)) + c.Handle(hs.Cfg, 400, "Render parameters error", fmt.Errorf("cannot parse timeout as int: %s", err)) return } scale, err := strconv.ParseFloat(queryReader.Get("scale", "1"), 64) if err != nil { - c.Handle(400, "Render parameters error", fmt.Errorf("cannot parse scale as float: %s", err)) + c.Handle(hs.Cfg, 400, "Render parameters error", fmt.Errorf("cannot parse scale as float: %s", err)) return } @@ -69,19 +69,19 @@ func (hs *HTTPServer) RenderToPng(c *models.ReqContext) { }) if err != nil { if errors.Is(err, rendering.ErrTimeout) { - c.Handle(500, err.Error(), err) + c.Handle(hs.Cfg, 500, err.Error(), err) return } if errors.Is(err, rendering.ErrPhantomJSNotInstalled) { if strings.HasPrefix(runtime.GOARCH, "arm") { - c.Handle(500, "Rendering failed - PhantomJS isn't included in arm build per default", err) + c.Handle(hs.Cfg, 500, "Rendering failed - PhantomJS isn't included in arm build per default", err) } else { - c.Handle(500, "Rendering failed - PhantomJS isn't installed correctly", err) + c.Handle(hs.Cfg, 500, "Rendering failed - PhantomJS isn't installed correctly", err) } return } - c.Handle(500, "Rendering failed.", err) + c.Handle(hs.Cfg, 500, "Rendering failed.", err) return } diff --git a/pkg/middleware/cookies/cookies.go b/pkg/middleware/cookies/cookies.go index b819320758d..87bd59e362c 100644 --- a/pkg/middleware/cookies/cookies.go +++ b/pkg/middleware/cookies/cookies.go @@ -67,5 +67,5 @@ func WriteSessionCookie(ctx *models.ReqContext, cfg *setting.Cfg, value string, maxAge = int(maxLifetime.Seconds()) } - WriteCookie(ctx.Resp, setting.LoginCookieName, url.QueryEscape(value), maxAge, nil) + WriteCookie(ctx.Resp, cfg.LoginCookieName, url.QueryEscape(value), maxAge, nil) } diff --git a/pkg/middleware/dashboard_redirect.go b/pkg/middleware/dashboard_redirect.go index 4f0b16df994..734e7215eb8 100644 --- a/pkg/middleware/dashboard_redirect.go +++ b/pkg/middleware/dashboard_redirect.go @@ -7,12 +7,11 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" - "gopkg.in/macaron.v1" ) func getDashboardURLBySlug(orgID int64, slug string) (string, error) { + // TODO: Drop bus call query := models.GetDashboardQuery{Slug: slug, OrgId: orgID} - if err := bus.Dispatch(&query); err != nil { return "", models.ErrDashboardNotFound } @@ -20,7 +19,7 @@ func getDashboardURLBySlug(orgID int64, slug string) (string, error) { return models.GetDashboardUrl(query.Result.Uid, query.Result.Slug), nil } -func RedirectFromLegacyDashboardURL() macaron.Handler { +func RedirectFromLegacyDashboardURL() func(c *models.ReqContext) { return func(c *models.ReqContext) { slug := c.Params("slug") @@ -36,47 +35,50 @@ func RedirectFromLegacyDashboardURL() macaron.Handler { // In Grafana v7.0 we changed panel edit & view query parameters. // This middleware tries to detect those old url parameters and direct to the new url query params -func RedirectFromLegacyPanelEditURL() macaron.Handler { +func RedirectFromLegacyPanelEditURL(cfg *setting.Cfg) func(c *models.ReqContext) { return func(c *models.ReqContext) { queryParams := c.Req.URL.Query() - panelId, hasPanelId := queryParams["panelId"] + panelID, hasPanelID := queryParams["panelId"] _, hasFullscreen := queryParams["fullscreen"] _, hasEdit := queryParams["edit"] - if hasPanelId && hasFullscreen { + if hasPanelID && hasFullscreen { delete(queryParams, "panelId") delete(queryParams, "fullscreen") delete(queryParams, "edit") if hasEdit { - queryParams["editPanel"] = panelId + queryParams["editPanel"] = panelID } else { - queryParams["viewPanel"] = panelId + queryParams["viewPanel"] = panelID } - newURL := setting.ToAbsUrl(fmt.Sprintf("%s?%s", strings.TrimPrefix(c.Req.URL.Path, "/"), queryParams.Encode())) + newURL := fmt.Sprintf("%s%s?%s", cfg.AppURL, strings.TrimPrefix(c.Req.URL.Path, "/"), queryParams.Encode()) c.Redirect(newURL, 301) } } } -func RedirectFromLegacyDashboardSoloURL() macaron.Handler { +func RedirectFromLegacyDashboardSoloURL(cfg *setting.Cfg) func(c *models.ReqContext) { return func(c *models.ReqContext) { slug := c.Params("slug") renderRequest := c.QueryBool("render") if slug != "" { - if url, err := getDashboardURLBySlug(c.OrgId, slug); err == nil { - if renderRequest && strings.Contains(url, setting.AppSubUrl) { - url = strings.Replace(url, setting.AppSubUrl, "", 1) - } - - url = strings.Replace(url, "/d/", "/d-solo/", 1) - url = fmt.Sprintf("%s?%s", url, c.Req.URL.RawQuery) - c.Redirect(url, 301) + url, err := getDashboardURLBySlug(c.OrgId, slug) + if err != nil { return } + + if renderRequest && strings.Contains(url, cfg.AppSubURL) { + url = strings.Replace(url, cfg.AppSubURL, "", 1) + } + + url = strings.Replace(url, "/d/", "/d-solo/", 1) + url = fmt.Sprintf("%s?%s", url, c.Req.URL.RawQuery) + c.Redirect(url, 301) + return } } } diff --git a/pkg/middleware/dashboard_redirect_test.go b/pkg/middleware/dashboard_redirect_test.go index 3e4f9d5e915..3d2fab37cf7 100644 --- a/pkg/middleware/dashboard_redirect_test.go +++ b/pkg/middleware/dashboard_redirect_test.go @@ -7,74 +7,79 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/util" - . "github.com/smartystreets/goconvey/convey" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestMiddlewareDashboardRedirect(t *testing.T) { - Convey("Given the dashboard redirect middleware", t, func() { - bus.ClearBusHandlers() - redirectFromLegacyDashboardUrl := RedirectFromLegacyDashboardURL() - redirectFromLegacyDashboardSoloUrl := RedirectFromLegacyDashboardSoloURL() + bus.ClearBusHandlers() + fakeDash := models.NewDashboard("Child dash") + fakeDash.Id = 1 + fakeDash.FolderId = 1 + fakeDash.HasAcl = false + fakeDash.Uid = util.GenerateShortUID() - fakeDash := models.NewDashboard("Child dash") - fakeDash.Id = 1 - fakeDash.FolderId = 1 - fakeDash.HasAcl = false - fakeDash.Uid = util.GenerateShortUID() + middlewareScenario(t, "GET dashboard by legacy url", func(t *testing.T, sc *scenarioContext) { + redirectFromLegacyDashboardURL := RedirectFromLegacyDashboardURL() - middlewareScenario(t, "GET dashboard by legacy url", func(t *testing.T, sc *scenarioContext) { - bus.AddHandler("test", func(query *models.GetDashboardQuery) error { - query.Result = fakeDash - return nil - }) - - sc.m.Get("/dashboard/db/:slug", redirectFromLegacyDashboardUrl, sc.defaultHandler) - - sc.fakeReqWithParams("GET", "/dashboard/db/dash?orgId=1&panelId=2", map[string]string{}).exec() - - assert.Equal(t, 301, sc.resp.Code) - // nolint:bodyclose - resp := sc.resp.Result() - t.Cleanup(func() { - err := resp.Body.Close() - assert.NoError(t, err) - }) - redirectURL, err := resp.Location() - require.NoError(t, err) - assert.Equal(t, models.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug), redirectURL.Path) - assert.Equal(t, 2, len(redirectURL.Query())) + bus.AddHandler("test", func(query *models.GetDashboardQuery) error { + t.Log("Returning fake dashboard") + query.Result = fakeDash + return nil }) - middlewareScenario(t, "GET dashboard solo by legacy url", func(t *testing.T, sc *scenarioContext) { - bus.AddHandler("test", func(query *models.GetDashboardQuery) error { - query.Result = fakeDash - return nil - }) + sc.handlerFunc = redirectFromLegacyDashboardURL + sc.m.Get("/dashboard/db/:slug", sc.defaultHandler) + sc.fakeReqWithParams("GET", "/dashboard/db/dash?orgId=1&panelId=2", map[string]string{}).exec() - sc.m.Get("/dashboard-solo/db/:slug", redirectFromLegacyDashboardSoloUrl, sc.defaultHandler) - - sc.fakeReqWithParams("GET", "/dashboard-solo/db/dash?orgId=1&panelId=2", map[string]string{}).exec() - - assert.Equal(t, 301, sc.resp.Code) - // nolint:bodyclose - resp := sc.resp.Result() - t.Cleanup(func() { - err := resp.Body.Close() - assert.NoError(t, err) - }) - redirectURL, err := resp.Location() - require.NoError(t, err) - expectedURL := models.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug) - expectedURL = strings.Replace(expectedURL, "/d/", "/d-solo/", 1) - assert.Equal(t, expectedURL, redirectURL.Path) - assert.Equal(t, 2, len(redirectURL.Query())) + assert.Equal(t, 301, sc.resp.Code) + // nolint:bodyclose + resp := sc.resp.Result() + t.Cleanup(func() { + err := resp.Body.Close() + assert.NoError(t, err) }) + redirectURL, err := resp.Location() + require.NoError(t, err) + assert.Equal(t, models.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug), redirectURL.Path) + assert.Len(t, redirectURL.Query(), 2) }) - middlewareScenario(t, "GET dashboard by legacy edit url", func(t *testing.T, sc *scenarioContext) { - sc.m.Get("/d/:uid/:slug", RedirectFromLegacyPanelEditURL(), sc.defaultHandler) + middlewareScenario(t, "GET dashboard solo by legacy url", func(t *testing.T, sc *scenarioContext) { + redirectFromLegacyDashboardSoloURL := RedirectFromLegacyDashboardSoloURL(sc.cfg) + + bus.AddHandler("test", func(query *models.GetDashboardQuery) error { + t.Log("Returning fake dashboard") + query.Result = fakeDash + return nil + }) + + sc.handlerFunc = redirectFromLegacyDashboardSoloURL + sc.m.Get("/dashboard-solo/db/:slug", sc.defaultHandler) + + sc.fakeReqWithParams("GET", "/dashboard-solo/db/dash?orgId=1&panelId=2", map[string]string{}).exec() + + require.Equal(t, 301, sc.resp.Code) + // nolint:bodyclose + resp := sc.resp.Result() + t.Cleanup(func() { + err := resp.Body.Close() + assert.NoError(t, err) + }) + redirectURL, err := resp.Location() + require.NoError(t, err) + // XXX: Should this be called path?? + expectedURL := models.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug) + expectedURL = strings.Replace(expectedURL, "/d/", "/d-solo/", 1) + assert.Equal(t, expectedURL, redirectURL.Path) + assert.Len(t, redirectURL.Query(), 2) + }) +} + +func TestMiddlewareDashboardRedirect_legacyEditPanel(t *testing.T) { + middlewareScenario(t, "GET dashboard by legacy edit URL", func(t *testing.T, sc *scenarioContext) { + sc.handlerFunc = RedirectFromLegacyPanelEditURL(sc.cfg) + sc.m.Get("/d/:uid/:slug", sc.defaultHandler) sc.fakeReqWithParams("GET", "/d/asd/dash?orgId=1&panelId=12&fullscreen&edit", map[string]string{}).exec() diff --git a/pkg/middleware/util.go b/pkg/middleware/gziper.go similarity index 100% rename from pkg/middleware/util.go rename to pkg/middleware/gziper.go diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index df286edc82a..f21f6b97145 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -17,6 +17,7 @@ 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/fs" "github.com/grafana/grafana/pkg/infra/remotecache" "github.com/grafana/grafana/pkg/login" "github.com/grafana/grafana/pkg/models" @@ -30,8 +31,6 @@ import ( "github.com/grafana/grafana/pkg/util" ) -const errorTemplate = "error-template" - func fakeGetTime() func() time.Time { var timeSeed int64 return func() time.Time { @@ -42,12 +41,6 @@ func fakeGetTime() func() time.Time { } func TestMiddleWareSecurityHeaders(t *testing.T) { - origErrTemplateName := setting.ErrTemplateName - t.Cleanup(func() { - setting.ErrTemplateName = origErrTemplateName - }) - setting.ErrTemplateName = errorTemplate - middlewareScenario(t, "middleware should get correct x-xss-protection header", func(t *testing.T, sc *scenarioContext) { sc.fakeReq("GET", "/api/").exec() assert.Equal(t, "1; mode=block", sc.resp.Header().Get("X-XSS-Protection")) @@ -79,11 +72,7 @@ func TestMiddleWareSecurityHeaders(t *testing.T) { } func TestMiddlewareContext(t *testing.T) { - origErrTemplateName := setting.ErrTemplateName - t.Cleanup(func() { - setting.ErrTemplateName = origErrTemplateName - }) - setting.ErrTemplateName = errorTemplate + const noCache = "no-cache" middlewareScenario(t, "middleware should add context to injector", func(t *testing.T, sc *scenarioContext) { sc.fakeReq("GET", "/").exec() @@ -97,8 +86,8 @@ func TestMiddlewareContext(t *testing.T) { middlewareScenario(t, "middleware should add Cache-Control header for requests to API", func(t *testing.T, sc *scenarioContext) { sc.fakeReq("GET", "/api/search").exec() - assert.Equal(t, "no-cache", sc.resp.Header().Get("Cache-Control")) - assert.Equal(t, "no-cache", sc.resp.Header().Get("Pragma")) + assert.Equal(t, noCache, sc.resp.Header().Get("Cache-Control")) + assert.Equal(t, noCache, sc.resp.Header().Get("Pragma")) assert.Equal(t, "-1", sc.resp.Header().Get("Expires")) }) @@ -110,20 +99,23 @@ func TestMiddlewareContext(t *testing.T) { assert.Empty(t, sc.resp.Header().Get("Expires")) }) - middlewareScenario(t, "middleware should add Cache-Control header for requests with html response", func( + middlewareScenario(t, "middleware should add Cache-Control header for requests with HTML response", func( t *testing.T, sc *scenarioContext) { - sc.handler(func(c *models.ReqContext) { + sc.handlerFunc = func(c *models.ReqContext) { + t.Log("Handler called") data := &dtos.IndexViewData{ User: &dtos.CurrentUser{}, Settings: map[string]interface{}{}, NavTree: []*dtos.NavLink{}, } + t.Log("Calling HTML", "data", data, "render", c.Render) c.HTML(200, "index-template", data) - }) + t.Log("Returned HTML with code 200") + } sc.fakeReq("GET", "/").exec() - assert.Equal(t, 200, sc.resp.Code) - assert.Equal(t, "no-cache", sc.resp.Header().Get("Cache-Control")) - assert.Equal(t, "no-cache", sc.resp.Header().Get("Pragma")) + require.Equal(t, 200, sc.resp.Code) + assert.Equal(t, noCache, sc.resp.Header().Get("Cache-Control")) + assert.Equal(t, noCache, sc.resp.Header().Get("Pragma")) assert.Equal(t, "-1", sc.resp.Header().Get("Expires")) }) @@ -150,7 +142,7 @@ func TestMiddlewareContext(t *testing.T) { assert.Equal(t, contexthandler.InvalidAPIKey, sc.respJson["message"]) }) - middlewareScenario(t, "Valid api key", func(t *testing.T, sc *scenarioContext) { + middlewareScenario(t, "Valid API key", func(t *testing.T, sc *scenarioContext) { const orgID int64 = 12 keyhash, err := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd") require.NoError(t, err) @@ -162,15 +154,15 @@ func TestMiddlewareContext(t *testing.T) { sc.fakeReq("GET", "/").withValidApiKey().exec() - assert.Equal(t, 200, sc.resp.Code) + require.Equal(t, 200, sc.resp.Code) assert.True(t, sc.context.IsSignedIn) assert.Equal(t, orgID, sc.context.OrgId) assert.Equal(t, models.ROLE_EDITOR, sc.context.OrgRole) }) - middlewareScenario(t, "Valid api key, but does not match db hash", func(t *testing.T, sc *scenarioContext) { - keyhash := "Something_not_matching" + middlewareScenario(t, "Valid API key, but does not match DB hash", func(t *testing.T, sc *scenarioContext) { + const keyhash = "Something_not_matching" bus.AddHandler("test", func(query *models.GetApiKeyByNameQuery) error { query.Result = &models.ApiKey{OrgId: 12, Role: models.ROLE_EDITOR, Key: keyhash} @@ -223,14 +215,16 @@ func TestMiddlewareContext(t *testing.T) { sc.fakeReq("GET", "/").exec() + require.NotNil(t, sc.context) + require.NotNil(t, sc.context.UserToken) assert.True(t, sc.context.IsSignedIn) assert.Equal(t, userID, sc.context.UserId) assert.Equal(t, userID, sc.context.UserToken.UserId) assert.Equal(t, "token", sc.context.UserToken.UnhashedToken) - assert.Equal(t, "", sc.resp.Header().Get("Set-Cookie")) + assert.Empty(t, sc.resp.Header().Get("Set-Cookie")) }) - middlewareScenario(t, "Non-expired auth token in cookie which are being rotated", func(t *testing.T, sc *scenarioContext) { + middlewareScenario(t, "Non-expired auth token in cookie which is being rotated", func(t *testing.T, sc *scenarioContext) { const userID int64 = 12 sc.withTokenSessionCookie("token") @@ -253,7 +247,7 @@ func TestMiddlewareContext(t *testing.T) { return true, nil } - maxAge := int(setting.LoginMaxLifetime.Seconds()) + maxAge := int(sc.cfg.LoginMaxLifetime.Seconds()) sameSiteModes := []http.SameSite{ http.SameSiteNoneMode, @@ -269,11 +263,11 @@ func TestMiddlewareContext(t *testing.T) { setting.CookieSameSiteMode = sameSiteMode expectedCookiePath := "/" - if len(setting.AppSubUrl) > 0 { - expectedCookiePath = setting.AppSubUrl + if len(sc.cfg.AppSubURL) > 0 { + expectedCookiePath = sc.cfg.AppSubURL } expectedCookie := &http.Cookie{ - Name: setting.LoginCookieName, + Name: sc.cfg.LoginCookieName, Value: "rotated", Path: expectedCookiePath, HttpOnly: true, @@ -303,11 +297,11 @@ func TestMiddlewareContext(t *testing.T) { setting.CookieSameSiteMode = http.SameSiteLaxMode expectedCookiePath := "/" - if len(setting.AppSubUrl) > 0 { - expectedCookiePath = setting.AppSubUrl + if len(sc.cfg.AppSubURL) > 0 { + expectedCookiePath = sc.cfg.AppSubURL } expectedCookie := &http.Cookie{ - Name: setting.LoginCookieName, + Name: sc.cfg.LoginCookieName, Value: "rotated", Path: expectedCookiePath, HttpOnly: true, @@ -556,6 +550,8 @@ func middlewareScenario(t *testing.T, desc string, fn scenarioFunc, cbs ...func( cfg := setting.NewCfg() cfg.LoginCookieName = "grafana_session" cfg.LoginMaxLifetime = loginMaxLifetime + // Required when rendering errors + cfg.ErrTemplateName = "error-template" for _, cb := range cbs { cb(cfg) } @@ -564,6 +560,9 @@ func middlewareScenario(t *testing.T, desc string, fn scenarioFunc, cbs ...func( viewsPath, err := filepath.Abs("../../public/views") require.NoError(t, err) + exists, err := fs.Exists(viewsPath) + require.NoError(t, err) + require.Truef(t, exists, "Views directory should exist at %q", viewsPath) sc.m = macaron.New() sc.m.Use(AddDefaultResponseHeaders(cfg)) @@ -575,7 +574,7 @@ func middlewareScenario(t *testing.T, desc string, fn scenarioFunc, cbs ...func( ctxHdlr := getContextHandler(t, cfg) sc.contextHandler = ctxHdlr sc.m.Use(ctxHdlr.Middleware) - sc.m.Use(OrgRedirect()) + sc.m.Use(OrgRedirect(sc.cfg)) sc.userAuthTokenService = ctxHdlr.AuthTokenService.(*auth.FakeUserAuthTokenService) sc.remoteCacheService = ctxHdlr.RemoteCache @@ -587,6 +586,7 @@ func middlewareScenario(t *testing.T, desc string, fn scenarioFunc, cbs ...func( if sc.handlerFunc != nil { sc.handlerFunc(sc.context) } else { + t.Log("Returning JSON OK") resp := make(map[string]interface{}) resp["message"] = "OK" c.JSON(200, resp) diff --git a/pkg/middleware/org_redirect.go b/pkg/middleware/org_redirect.go index fe7e168fa6c..cb1813ade7f 100644 --- a/pkg/middleware/org_redirect.go +++ b/pkg/middleware/org_redirect.go @@ -14,7 +14,7 @@ import ( // OrgRedirect changes org and redirects users if the // querystring `orgId` doesn't match the active org. -func OrgRedirect() macaron.Handler { +func OrgRedirect(cfg *setting.Cfg) macaron.Handler { return func(res http.ResponseWriter, req *http.Request, c *macaron.Context) { orgIdValue := req.URL.Query().Get("orgId") orgId, err := strconv.ParseInt(orgIdValue, 10, 64) @@ -43,7 +43,7 @@ func OrgRedirect() macaron.Handler { return } - newURL := setting.ToAbsUrl(fmt.Sprintf("%s?%s", strings.TrimPrefix(c.Req.URL.Path, "/"), c.Req.URL.Query().Encode())) + newURL := fmt.Sprintf("%s%s?%s", cfg.AppURL, strings.TrimPrefix(c.Req.URL.Path, "/"), c.Req.URL.Query().Encode()) c.Redirect(newURL, 302) } } diff --git a/pkg/middleware/quota.go b/pkg/middleware/quota.go index 69895c26b55..72bcc2e4e53 100644 --- a/pkg/middleware/quota.go +++ b/pkg/middleware/quota.go @@ -10,13 +10,13 @@ import ( ) // Quota returns a function that returns a function used to call quotaservice based on target name -func Quota(quotaService *quota.QuotaService) func(target string) macaron.Handler { +func Quota(quotaService *quota.QuotaService) func(string) macaron.Handler { //https://open.spotify.com/track/7bZSoBEAEEUsGEuLOf94Jm?si=T1Tdju5qRSmmR0zph_6RBw fuuuuunky return func(target string) macaron.Handler { return func(c *models.ReqContext) { limitReached, err := quotaService.QuotaReached(c, target) if err != nil { - c.JsonApiErr(500, "failed to get quota", err) + c.JsonApiErr(500, "Failed to get quota", err) return } if limitReached { diff --git a/pkg/middleware/quota_test.go b/pkg/middleware/quota_test.go index bf2696d5f04..be0fbe60a1e 100644 --- a/pkg/middleware/quota_test.go +++ b/pkg/middleware/quota_test.go @@ -10,11 +10,220 @@ import ( "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/assert" + macaron "gopkg.in/macaron.v1" ) func TestMiddlewareQuota(t *testing.T) { - setting.AnonymousEnabled = false - setting.Quota = setting.QuotaSettings{ + t.Run("With user not logged in", func(t *testing.T) { + middlewareScenario(t, "and global quota not reached", func(t *testing.T, sc *scenarioContext) { + bus.AddHandler("globalQuota", func(query *models.GetGlobalQuotaByTargetQuery) error { + query.Result = &models.GlobalQuotaDTO{ + Target: query.Target, + Limit: query.Default, + Used: 4, + } + return nil + }) + + quotaHandler := getQuotaHandler(sc, "user") + + sc.m.Get("/user", quotaHandler, sc.defaultHandler) + sc.fakeReq("GET", "/user").exec() + assert.Equal(t, 200, sc.resp.Code) + }, configure) + + middlewareScenario(t, "and global quota reached", func(t *testing.T, sc *scenarioContext) { + bus.AddHandler("globalQuota", func(query *models.GetGlobalQuotaByTargetQuery) error { + query.Result = &models.GlobalQuotaDTO{ + Target: query.Target, + Limit: query.Default, + Used: 4, + } + return nil + }) + + quotaHandler := getQuotaHandler(sc, "user") + sc.m.Get("/user", quotaHandler, sc.defaultHandler) + sc.fakeReq("GET", "/user").exec() + assert.Equal(t, 403, sc.resp.Code) + }, func(cfg *setting.Cfg) { + configure(cfg) + + cfg.Quota.Global.User = 4 + }) + + middlewareScenario(t, "and global session quota not reached", func(t *testing.T, sc *scenarioContext) { + bus.AddHandler("globalQuota", func(query *models.GetGlobalQuotaByTargetQuery) error { + query.Result = &models.GlobalQuotaDTO{ + Target: query.Target, + Limit: query.Default, + Used: 4, + } + return nil + }) + + quotaHandler := getQuotaHandler(sc, "session") + sc.m.Get("/user", quotaHandler, sc.defaultHandler) + sc.fakeReq("GET", "/user").exec() + assert.Equal(t, 200, sc.resp.Code) + }, func(cfg *setting.Cfg) { + configure(cfg) + + cfg.Quota.Global.Session = 10 + }) + + middlewareScenario(t, "and global session quota reached", func(t *testing.T, sc *scenarioContext) { + quotaHandler := getQuotaHandler(sc, "session") + sc.m.Get("/user", quotaHandler, sc.defaultHandler) + sc.fakeReq("GET", "/user").exec() + assert.Equal(t, 403, sc.resp.Code) + }, func(cfg *setting.Cfg) { + configure(cfg) + + cfg.Quota.Global.Session = 1 + }) + }) + + t.Run("with user logged in", func(t *testing.T) { + const quotaUsed = 4 + + setUp := func(sc *scenarioContext) { + sc.withTokenSessionCookie("token") + bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error { + query.Result = &models.SignedInUser{OrgId: 2, UserId: 12} + return nil + }) + + sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) { + return &models.UserToken{ + UserId: 12, + UnhashedToken: "", + }, nil + } + + bus.AddHandler("globalQuota", func(query *models.GetGlobalQuotaByTargetQuery) error { + query.Result = &models.GlobalQuotaDTO{ + Target: query.Target, + Limit: query.Default, + Used: quotaUsed, + } + return nil + }) + + bus.AddHandler("userQuota", func(query *models.GetUserQuotaByTargetQuery) error { + query.Result = &models.UserQuotaDTO{ + Target: query.Target, + Limit: query.Default, + Used: quotaUsed, + } + return nil + }) + + bus.AddHandler("orgQuota", func(query *models.GetOrgQuotaByTargetQuery) error { + query.Result = &models.OrgQuotaDTO{ + Target: query.Target, + Limit: query.Default, + Used: quotaUsed, + } + return nil + }) + } + + middlewareScenario(t, "global datasource quota reached", func(t *testing.T, sc *scenarioContext) { + setUp(sc) + + quotaHandler := getQuotaHandler(sc, "data_source") + sc.m.Get("/ds", quotaHandler, sc.defaultHandler) + sc.fakeReq("GET", "/ds").exec() + assert.Equal(t, 403, sc.resp.Code) + }, func(cfg *setting.Cfg) { + configure(cfg) + + cfg.Quota.Global.DataSource = quotaUsed + }) + + middlewareScenario(t, "user Org quota not reached", func(t *testing.T, sc *scenarioContext) { + setUp(sc) + + quotaHandler := getQuotaHandler(sc, "org") + + sc.m.Get("/org", quotaHandler, sc.defaultHandler) + sc.fakeReq("GET", "/org").exec() + assert.Equal(t, 200, sc.resp.Code) + }, func(cfg *setting.Cfg) { + configure(cfg) + + cfg.Quota.User.Org = quotaUsed + 1 + }) + + middlewareScenario(t, "user Org quota reached", func(t *testing.T, sc *scenarioContext) { + setUp(sc) + + quotaHandler := getQuotaHandler(sc, "org") + sc.m.Get("/org", quotaHandler, sc.defaultHandler) + sc.fakeReq("GET", "/org").exec() + assert.Equal(t, 403, sc.resp.Code) + }, func(cfg *setting.Cfg) { + configure(cfg) + + cfg.Quota.User.Org = quotaUsed + }) + + middlewareScenario(t, "org dashboard quota not reached", func(t *testing.T, sc *scenarioContext) { + setUp(sc) + + quotaHandler := getQuotaHandler(sc, "dashboard") + sc.m.Get("/dashboard", quotaHandler, sc.defaultHandler) + sc.fakeReq("GET", "/dashboard").exec() + assert.Equal(t, 200, sc.resp.Code) + }, func(cfg *setting.Cfg) { + configure(cfg) + + cfg.Quota.Org.Dashboard = quotaUsed + 1 + }) + + middlewareScenario(t, "org dashboard quota reached", func(t *testing.T, sc *scenarioContext) { + setUp(sc) + + quotaHandler := getQuotaHandler(sc, "dashboard") + sc.m.Get("/dashboard", quotaHandler, sc.defaultHandler) + sc.fakeReq("GET", "/dashboard").exec() + assert.Equal(t, 403, sc.resp.Code) + }, func(cfg *setting.Cfg) { + configure(cfg) + + cfg.Quota.Org.Dashboard = quotaUsed + }) + + middlewareScenario(t, "org dashboard quota reached, but quotas disabled", func(t *testing.T, sc *scenarioContext) { + setUp(sc) + + quotaHandler := getQuotaHandler(sc, "dashboard") + sc.m.Get("/dashboard", quotaHandler, sc.defaultHandler) + sc.fakeReq("GET", "/dashboard").exec() + assert.Equal(t, 200, sc.resp.Code) + }, func(cfg *setting.Cfg) { + configure(cfg) + + cfg.Quota.Org.Dashboard = quotaUsed + cfg.Quota.Enabled = false + }) + }) +} + +func getQuotaHandler(sc *scenarioContext, target string) macaron.Handler { + fakeAuthTokenService := auth.NewFakeUserAuthTokenService() + qs := "a.QuotaService{ + AuthTokenService: fakeAuthTokenService, + Cfg: sc.cfg, + } + + return Quota(qs)(target) +} + +func configure(cfg *setting.Cfg) { + cfg.AnonymousEnabled = false + cfg.Quota = setting.QuotaSettings{ Enabled: true, Org: &setting.OrgQuota{ User: 5, @@ -34,166 +243,4 @@ func TestMiddlewareQuota(t *testing.T) { Session: 5, }, } - - fakeAuthTokenService := auth.NewFakeUserAuthTokenService() - qs := "a.QuotaService{ - AuthTokenService: fakeAuthTokenService, - } - quotaFn := Quota(qs) - - t.Run("With user not logged in", func(t *testing.T) { - middlewareScenario(t, "and global quota not reached", func(t *testing.T, sc *scenarioContext) { - bus.AddHandler("globalQuota", func(query *models.GetGlobalQuotaByTargetQuery) error { - query.Result = &models.GlobalQuotaDTO{ - Target: query.Target, - Limit: query.Default, - Used: 4, - } - return nil - }) - - sc.m.Get("/user", quotaFn("user"), sc.defaultHandler) - sc.fakeReq("GET", "/user").exec() - assert.Equal(t, 200, sc.resp.Code) - }) - - middlewareScenario(t, "and global quota reached", func(t *testing.T, sc *scenarioContext) { - bus.AddHandler("globalQuota", func(query *models.GetGlobalQuotaByTargetQuery) error { - query.Result = &models.GlobalQuotaDTO{ - Target: query.Target, - Limit: query.Default, - Used: 4, - } - return nil - }) - - origUser := setting.Quota.Global.User - t.Cleanup(func() { - setting.Quota.Global.User = origUser - }) - setting.Quota.Global.User = 4 - - sc.m.Get("/user", quotaFn("user"), sc.defaultHandler) - sc.fakeReq("GET", "/user").exec() - assert.Equal(t, 403, sc.resp.Code) - }) - - middlewareScenario(t, "and global session quota not reached", func(t *testing.T, sc *scenarioContext) { - bus.AddHandler("globalQuota", func(query *models.GetGlobalQuotaByTargetQuery) error { - query.Result = &models.GlobalQuotaDTO{ - Target: query.Target, - Limit: query.Default, - Used: 4, - } - return nil - }) - - origSession := setting.Quota.Global.Session - t.Cleanup(func() { - setting.Quota.Global.Session = origSession - }) - setting.Quota.Global.Session = 10 - - sc.m.Get("/user", quotaFn("session"), sc.defaultHandler) - sc.fakeReq("GET", "/user").exec() - assert.Equal(t, 200, sc.resp.Code) - }) - - middlewareScenario(t, "and global session quota reached", func(t *testing.T, sc *scenarioContext) { - origSession := setting.Quota.Global.Session - t.Cleanup(func() { - setting.Quota.Global.Session = origSession - }) - setting.Quota.Global.Session = 1 - - sc.m.Get("/user", quotaFn("session"), sc.defaultHandler) - sc.fakeReq("GET", "/user").exec() - assert.Equal(t, 403, sc.resp.Code) - }) - }) - - middlewareScenario(t, "with user logged in", func(t *testing.T, sc *scenarioContext) { - sc.withTokenSessionCookie("token") - bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error { - query.Result = &models.SignedInUser{OrgId: 2, UserId: 12} - return nil - }) - - sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) { - return &models.UserToken{ - UserId: 12, - UnhashedToken: "", - }, nil - } - - bus.AddHandler("globalQuota", func(query *models.GetGlobalQuotaByTargetQuery) error { - query.Result = &models.GlobalQuotaDTO{ - Target: query.Target, - Limit: query.Default, - Used: 4, - } - return nil - }) - - bus.AddHandler("userQuota", func(query *models.GetUserQuotaByTargetQuery) error { - query.Result = &models.UserQuotaDTO{ - Target: query.Target, - Limit: query.Default, - Used: 4, - } - return nil - }) - - bus.AddHandler("orgQuota", func(query *models.GetOrgQuotaByTargetQuery) error { - query.Result = &models.OrgQuotaDTO{ - Target: query.Target, - Limit: query.Default, - Used: 4, - } - return nil - }) - - t.Run("global datasource quota reached", func(t *testing.T) { - setting.Quota.Global.DataSource = 4 - sc.m.Get("/ds", quotaFn("data_source"), sc.defaultHandler) - sc.fakeReq("GET", "/ds").exec() - assert.Equal(t, 403, sc.resp.Code) - }) - - t.Run("user Org quota not reached", func(t *testing.T) { - setting.Quota.User.Org = 5 - sc.m.Get("/org", quotaFn("org"), sc.defaultHandler) - sc.fakeReq("GET", "/org").exec() - assert.Equal(t, 200, sc.resp.Code) - }) - - t.Run("user Org quota reached", func(t *testing.T) { - setting.Quota.User.Org = 4 - sc.m.Get("/org", quotaFn("org"), sc.defaultHandler) - sc.fakeReq("GET", "/org").exec() - assert.Equal(t, 403, sc.resp.Code) - }) - - t.Run("org dashboard quota not reached", func(t *testing.T) { - setting.Quota.Org.Dashboard = 10 - sc.m.Get("/dashboard", quotaFn("dashboard"), sc.defaultHandler) - sc.fakeReq("GET", "/dashboard").exec() - assert.Equal(t, 200, sc.resp.Code) - }) - - t.Run("org dashboard quota reached", func(t *testing.T) { - setting.Quota.Org.Dashboard = 4 - sc.m.Get("/dashboard", quotaFn("dashboard"), sc.defaultHandler) - sc.fakeReq("GET", "/dashboard").exec() - assert.Equal(t, 403, sc.resp.Code) - }) - - t.Run("org dashboard quota reached but quotas disabled", func(t *testing.T) { - setting.Quota.Org.Dashboard = 4 - setting.Quota.Enabled = false - sc.m.Get("/dashboard", quotaFn("dashboard"), sc.defaultHandler) - sc.fakeReq("GET", "/dashboard").exec() - assert.Equal(t, 200, sc.resp.Code) - }) - }) } diff --git a/pkg/middleware/recovery_test.go b/pkg/middleware/recovery_test.go index 2e56f07ed14..0bc002b8b5d 100644 --- a/pkg/middleware/recovery_test.go +++ b/pkg/middleware/recovery_test.go @@ -18,7 +18,7 @@ import ( func TestRecoveryMiddleware(t *testing.T) { 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) { + recoveryScenario(t, "recovery middleware should return JSON", apiURL, func(t *testing.T, sc *scenarioContext) { sc.handlerFunc = panicHandler sc.fakeReq("GET", apiURL).exec() sc.req.Header.Set("content-type", "application/json") @@ -37,7 +37,7 @@ func TestRecoveryMiddleware(t *testing.T) { assert.Equal(t, 500, sc.resp.Code) assert.Equal(t, "text/html; charset=UTF-8", sc.resp.Header().Get("content-type")) - assert.True(t, strings.Contains(sc.resp.Body.String(), "Grafana - Error")) + assert.Contains(t, sc.resp.Body.String(), "Grafana - Error") }) }) } @@ -76,7 +76,7 @@ func recoveryScenario(t *testing.T, desc string, url string, fn scenarioFunc) { contextHandler := getContextHandler(t, nil) sc.m.Use(contextHandler.Middleware) // mock out gc goroutine - sc.m.Use(OrgRedirect()) + sc.m.Use(OrgRedirect(cfg)) sc.defaultHandler = func(c *models.ReqContext) { sc.context = c diff --git a/pkg/middleware/testing.go b/pkg/middleware/testing.go index 6f387b12a5b..5732eba3302 100644 --- a/pkg/middleware/testing.go +++ b/pkg/middleware/testing.go @@ -67,6 +67,8 @@ func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map sc.resp = httptest.NewRecorder() req, err := http.NewRequest(method, url, nil) + require.NoError(sc.t, err) + q := req.URL.Query() for k, v := range queryParams { q.Add(k, v) @@ -78,11 +80,6 @@ func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map return sc } -func (sc *scenarioContext) handler(fn handlerFunc) *scenarioContext { - sc.handlerFunc = fn - return sc -} - func (sc *scenarioContext) exec() { sc.t.Helper() @@ -109,6 +106,9 @@ func (sc *scenarioContext) exec() { if sc.resp.Header().Get("Content-Type") == "application/json; charset=UTF-8" { err := json.NewDecoder(sc.resp.Body).Decode(&sc.respJson) require.NoError(sc.t, err) + sc.t.Log("Decoded JSON", "json", sc.respJson) + } else { + sc.t.Log("Not decoding JSON") } } diff --git a/pkg/middleware/validate_host.go b/pkg/middleware/validate_host.go index ec2ac8253eb..008aff348a1 100644 --- a/pkg/middleware/validate_host.go +++ b/pkg/middleware/validate_host.go @@ -8,7 +8,7 @@ import ( "gopkg.in/macaron.v1" ) -func ValidateHostHeader(domain string) macaron.Handler { +func ValidateHostHeader(cfg *setting.Cfg) macaron.Handler { return func(c *models.ReqContext) { // ignore local render calls if c.IsRenderCall { @@ -20,8 +20,8 @@ func ValidateHostHeader(domain string) macaron.Handler { h = h[:i] } - if !strings.EqualFold(h, domain) { - c.Redirect(strings.TrimSuffix(setting.AppUrl, "/")+c.Req.RequestURI, 301) + if !strings.EqualFold(h, cfg.Domain) { + c.Redirect(strings.TrimSuffix(cfg.AppURL, "/")+c.Req.RequestURI, 301) return } } diff --git a/pkg/models/context.go b/pkg/models/context.go index 2a5e9a660c1..9feea510365 100644 --- a/pkg/models/context.go +++ b/pkg/models/context.go @@ -22,7 +22,7 @@ type ReqContext struct { } // Handle handles and logs error by given status. -func (ctx *ReqContext) Handle(status int, title string, err error) { +func (ctx *ReqContext) Handle(cfg *setting.Cfg, status int, title string, err error) { if err != nil { ctx.Logger.Error(title, "error", err) if setting.Env != setting.Prod { @@ -31,10 +31,10 @@ func (ctx *ReqContext) Handle(status int, title string, err error) { } ctx.Data["Title"] = title - ctx.Data["AppSubUrl"] = setting.AppSubUrl + ctx.Data["AppSubUrl"] = cfg.AppSubURL ctx.Data["Theme"] = "dark" - ctx.HTML(status, setting.ErrTemplateName) + ctx.HTML(status, cfg.ErrTemplateName) } func (ctx *ReqContext) IsApiRequest() bool { diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index e63a1fb21b5..222a7aa2625 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -307,17 +307,17 @@ func GetDashboardFolderUrl(isFolder bool, uid string, slug string) string { return GetDashboardUrl(uid, slug) } -// GetDashboardUrl return the html url for a dashboard +// GetDashboardUrl returns the HTML url for a dashboard. func GetDashboardUrl(uid string, slug string) string { return fmt.Sprintf("%s/d/%s/%s", setting.AppSubUrl, uid, slug) } -// GetFullDashboardUrl return the full url for a dashboard +// GetFullDashboardUrl returns the full URL for a dashboard. func GetFullDashboardUrl(uid string, slug string) string { return fmt.Sprintf("%sd/%s/%s", setting.AppUrl, uid, slug) } -// GetFolderUrl return the html url for a folder +// GetFolderUrl returns the HTML url for a folder. func GetFolderUrl(folderUid string, slug string) string { return fmt.Sprintf("%s/dashboards/f/%s/%s", setting.AppSubUrl, folderUid, slug) } diff --git a/pkg/models/quotas.go b/pkg/models/quotas.go index a2296b12d51..43b28b9c3e6 100644 --- a/pkg/models/quotas.go +++ b/pkg/models/quotas.go @@ -3,8 +3,6 @@ package models import ( "errors" "time" - - "github.com/grafana/grafana/pkg/setting" ) var ErrInvalidQuotaTarget = errors.New("invalid quota target") @@ -86,46 +84,3 @@ type UpdateUserQuotaCmd struct { Limit int64 `json:"limit"` UserId int64 `json:"-"` } - -func GetQuotaScopes(target string) ([]QuotaScope, error) { - scopes := make([]QuotaScope, 0) - switch target { - case "user": - scopes = append(scopes, - QuotaScope{Name: "global", Target: target, DefaultLimit: setting.Quota.Global.User}, - QuotaScope{Name: "org", Target: "org_user", DefaultLimit: setting.Quota.Org.User}, - ) - return scopes, nil - case "org": - scopes = append(scopes, - QuotaScope{Name: "global", Target: target, DefaultLimit: setting.Quota.Global.Org}, - QuotaScope{Name: "user", Target: "org_user", DefaultLimit: setting.Quota.User.Org}, - ) - return scopes, nil - case "dashboard": - scopes = append(scopes, - QuotaScope{Name: "global", Target: target, DefaultLimit: setting.Quota.Global.Dashboard}, - QuotaScope{Name: "org", Target: target, DefaultLimit: setting.Quota.Org.Dashboard}, - ) - return scopes, nil - case "data_source": - scopes = append(scopes, - QuotaScope{Name: "global", Target: target, DefaultLimit: setting.Quota.Global.DataSource}, - QuotaScope{Name: "org", Target: target, DefaultLimit: setting.Quota.Org.DataSource}, - ) - return scopes, nil - case "api_key": - scopes = append(scopes, - QuotaScope{Name: "global", Target: target, DefaultLimit: setting.Quota.Global.ApiKey}, - QuotaScope{Name: "org", Target: target, DefaultLimit: setting.Quota.Org.ApiKey}, - ) - return scopes, nil - case "session": - scopes = append(scopes, - QuotaScope{Name: "global", Target: target, DefaultLimit: setting.Quota.Global.Session}, - ) - return scopes, nil - default: - return scopes, ErrInvalidQuotaTarget - } -} diff --git a/pkg/services/contexthandler/authproxy/authproxy.go b/pkg/services/contexthandler/authproxy/authproxy.go index e77f2de78ff..80e5a5b9e0c 100644 --- a/pkg/services/contexthandler/authproxy/authproxy.go +++ b/pkg/services/contexthandler/authproxy/authproxy.go @@ -81,7 +81,7 @@ type Options struct { OrgID int64 } -// New instance of the AuthProxy +// New instance of the AuthProxy. func New(cfg *setting.Cfg, options *Options) *AuthProxy { header := options.Ctx.Req.Header.Get(cfg.AuthProxyHeaderName) return &AuthProxy{ @@ -93,7 +93,7 @@ func New(cfg *setting.Cfg, options *Options) *AuthProxy { } } -// IsEnabled checks if the proxy auth is enabled +// IsEnabled checks if the auth proxy is enabled. func (auth *AuthProxy) IsEnabled() bool { // Bail if the setting is not enabled return auth.cfg.AuthProxyEnabled diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index 1194db9f507..c6c596cab06 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -350,13 +350,13 @@ func logUserIn(auth *authproxy.AuthProxy, username string, logger log.Logger, ig return id, nil } -func handleError(ctx *models.ReqContext, err error, statusCode int, cb func(error)) { +func (h *ContextHandler) 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) + ctx.Handle(h.Cfg, statusCode, err.Error(), details) if cb != nil { cb(details) @@ -385,7 +385,7 @@ func (h *ContextHandler) initContextWithAuthProxy(ctx *models.ReqContext, orgID // Check if allowed to continue with this IP if err := auth.IsAllowedIP(); err != nil { - handleError(ctx, err, 407, func(details error) { + h.handleError(ctx, err, 407, func(details error) { logger.Error("Failed to check whitelisted IP addresses", "message", err.Error(), "error", details) }) return true @@ -393,7 +393,7 @@ func (h *ContextHandler) initContextWithAuthProxy(ctx *models.ReqContext, orgID id, err := logUserIn(auth, username, logger, false) if err != nil { - handleError(ctx, err, 407, nil) + h.handleError(ctx, err, 407, nil) return true } @@ -414,13 +414,13 @@ func (h *ContextHandler) initContextWithAuthProxy(ctx *models.ReqContext, orgID } id, err = logUserIn(auth, username, logger, true) if err != nil { - handleError(ctx, err, 407, nil) + h.handleError(ctx, err, 407, nil) return true } user, err = auth.GetSignedInUser(id) if err != nil { - handleError(ctx, err, 407, nil) + h.handleError(ctx, err, 407, nil) return true } } @@ -433,7 +433,7 @@ func (h *ContextHandler) initContextWithAuthProxy(ctx *models.ReqContext, orgID // Remember user data in cache if err := auth.Remember(id); err != nil { - handleError(ctx, err, 500, func(details error) { + h.handleError(ctx, err, 500, func(details error) { logger.Error( "Failed to store user in cache", "username", username, diff --git a/pkg/services/contexthandler/contexthandler_test.go b/pkg/services/contexthandler/contexthandler_test.go index a87235cbae6..bfbab42fc12 100644 --- a/pkg/services/contexthandler/contexthandler_test.go +++ b/pkg/services/contexthandler/contexthandler_test.go @@ -11,7 +11,6 @@ import ( "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" @@ -22,7 +21,7 @@ func TestDontRotateTokensOnCancelledRequests(t *testing.T) { ctxHdlr := getContextHandler(t) ctx, cancel := context.WithCancel(context.Background()) - reqContext, _, err := initTokenRotationScenario(ctx, t) + reqContext, _, err := initTokenRotationScenario(ctx, t, ctxHdlr) require.NoError(t, err) tryRotateCallCount := 0 @@ -46,7 +45,7 @@ func TestDontRotateTokensOnCancelledRequests(t *testing.T) { func TestTokenRotationAtEndOfRequest(t *testing.T) { ctxHdlr := getContextHandler(t) - reqContext, rr, err := initTokenRotationScenario(context.Background(), t) + reqContext, rr, err := initTokenRotationScenario(context.Background(), t, ctxHdlr) require.NoError(t, err) uts := &auth.FakeUserAuthTokenService{ @@ -80,18 +79,13 @@ func TestTokenRotationAtEndOfRequest(t *testing.T) { assert.True(t, foundLoginCookie, "Could not find cookie") } -func initTokenRotationScenario(ctx context.Context, t *testing.T) (*models.ReqContext, *httptest.ResponseRecorder, error) { +func initTokenRotationScenario(ctx context.Context, t *testing.T, ctxHdlr *ContextHandler) ( + *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" + ctxHdlr.Cfg.LoginCookieName = "login_token" var err error - setting.LoginMaxLifetime, err = gtime.ParseDuration("7d") + ctxHdlr.Cfg.LoginMaxLifetime, err = gtime.ParseDuration("7d") if err != nil { return nil, nil, err } diff --git a/pkg/services/quota/quota.go b/pkg/services/quota/quota.go index 452b55029f8..7b9b49c074b 100644 --- a/pkg/services/quota/quota.go +++ b/pkg/services/quota/quota.go @@ -1,18 +1,23 @@ package quota import ( + "errors" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/setting" ) +var ErrInvalidQuotaTarget = errors.New("invalid quota target") + func init() { registry.RegisterService(&QuotaService{}) } type QuotaService struct { AuthTokenService models.UserTokenService `inject:""` + Cfg *setting.Cfg `inject:""` } func (qs *QuotaService) Init() error { @@ -20,7 +25,7 @@ func (qs *QuotaService) Init() error { } func (qs *QuotaService) QuotaReached(c *models.ReqContext, target string) (bool, error) { - if !setting.Quota.Enabled { + if !qs.Cfg.Quota.Enabled { return false, nil } // No request context means this is a background service, like LDAP Background Sync. @@ -29,8 +34,9 @@ func (qs *QuotaService) QuotaReached(c *models.ReqContext, target string) (bool, if c == nil { return false, nil } + // get the list of scopes that this target is valid for. Org, User, Global - scopes, err := models.GetQuotaScopes(target) + scopes, err := qs.getQuotaScopes(target) if err != nil { return false, err } @@ -106,3 +112,46 @@ func (qs *QuotaService) QuotaReached(c *models.ReqContext, target string) (bool, return false, nil } + +func (qs *QuotaService) getQuotaScopes(target string) ([]models.QuotaScope, error) { + scopes := make([]models.QuotaScope, 0) + switch target { + case "user": + scopes = append(scopes, + models.QuotaScope{Name: "global", Target: target, DefaultLimit: qs.Cfg.Quota.Global.User}, + models.QuotaScope{Name: "org", Target: "org_user", DefaultLimit: qs.Cfg.Quota.Org.User}, + ) + return scopes, nil + case "org": + scopes = append(scopes, + models.QuotaScope{Name: "global", Target: target, DefaultLimit: qs.Cfg.Quota.Global.Org}, + models.QuotaScope{Name: "user", Target: "org_user", DefaultLimit: qs.Cfg.Quota.User.Org}, + ) + return scopes, nil + case "dashboard": + scopes = append(scopes, + models.QuotaScope{Name: "global", Target: target, DefaultLimit: qs.Cfg.Quota.Global.Dashboard}, + models.QuotaScope{Name: "org", Target: target, DefaultLimit: qs.Cfg.Quota.Org.Dashboard}, + ) + return scopes, nil + case "data_source": + scopes = append(scopes, + models.QuotaScope{Name: "global", Target: target, DefaultLimit: qs.Cfg.Quota.Global.DataSource}, + models.QuotaScope{Name: "org", Target: target, DefaultLimit: qs.Cfg.Quota.Org.DataSource}, + ) + return scopes, nil + case "api_key": + scopes = append(scopes, + models.QuotaScope{Name: "global", Target: target, DefaultLimit: qs.Cfg.Quota.Global.ApiKey}, + models.QuotaScope{Name: "org", Target: target, DefaultLimit: qs.Cfg.Quota.Org.ApiKey}, + ) + return scopes, nil + case "session": + scopes = append(scopes, + models.QuotaScope{Name: "global", Target: target, DefaultLimit: qs.Cfg.Quota.Global.Session}, + ) + return scopes, nil + default: + return scopes, ErrInvalidQuotaTarget + } +} diff --git a/pkg/services/sqlstore/org.go b/pkg/services/sqlstore/org.go index f924de731f3..f836991c4a9 100644 --- a/pkg/services/sqlstore/org.go +++ b/pkg/services/sqlstore/org.go @@ -1,7 +1,6 @@ package sqlstore import ( - "context" "fmt" "time" @@ -275,14 +274,3 @@ func getOrCreateOrg(sess *DBSession, orgName string) (int64, error) { return org.Id, nil } - -func createDefaultOrg(ctx context.Context) error { - return inTransactionCtx(ctx, func(sess *DBSession) error { - _, err := getOrCreateOrg(sess, mainOrgName) - if err != nil { - return err - } - - return nil - }) -} diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index 56997bae68b..90d45d54ab8 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -118,10 +118,13 @@ func (ss *SQLStore) Init() error { func (ss *SQLStore) ensureMainOrgAndAdminUser() error { err := ss.InTransaction(context.Background(), func(ctx context.Context) error { + ss.log.Debug("Ensuring main org and admin user exist") 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 { + // TODO: Should be able to rename "Count" to "count", for more standard SQL style + // Just have to make sure it gets deserialized properly into models.SystemUserCountStats + 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) } @@ -137,23 +140,27 @@ func (ss *SQLStore) ensureMainOrgAndAdminUser() error { // ensure admin user if !ss.Cfg.DisableInitAdminCreation { - cmd := models.CreateUserCommand{} - cmd.Login = setting.AdminUser - cmd.Email = setting.AdminUser + "@localhost" - cmd.Password = setting.AdminPassword - cmd.IsAdmin = true - + ss.log.Debug("Creating default admin user") + cmd := models.CreateUserCommand{ + Login: ss.Cfg.AdminUser, + Email: ss.Cfg.AdminUser + "@localhost", + Password: ss.Cfg.AdminPassword, + IsAdmin: true, + } if err := bus.DispatchCtx(ctx, &cmd); err != nil { return fmt.Errorf("failed to create admin user: %s", err) } - ss.log.Info("Created default admin", "user", setting.AdminUser) + ss.log.Info("Created default admin", "user", ss.Cfg.AdminUser) return nil } - // ensure default org if default admin user is disabled - if err := createDefaultOrg(ctx); err != nil { - return errutil.Wrap("Failed to create default organization", err) + // ensure default org even if default admin user is disabled + if err := inTransactionCtx(ctx, func(sess *DBSession) error { + _, err := getOrCreateOrg(sess, mainOrgName) + return err + }); err != nil { + return fmt.Errorf("failed to create default organization: %w", err) } ss.log.Info("Created default organization") diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index b6a61db825b..c0d584051bb 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -40,10 +40,6 @@ const ( Test = "test" ) -var ( - ErrTemplateName = "error" -) - // 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 ( @@ -126,10 +122,6 @@ var ( ViewersCanEdit bool // HTTP auth - AdminUser string - AdminPassword string - LoginCookieName string - LoginMaxLifetime time.Duration SigV4AuthEnabled bool AnonymousEnabled bool @@ -271,6 +263,8 @@ type Cfg struct { TokenRotationIntervalMinutes int SigV4AuthEnabled bool BasicAuthEnabled bool + AdminUser string + AdminPassword string // Auth proxy settings AuthProxyEnabled bool @@ -724,7 +718,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { cfg.IsEnterprise = IsEnterprise cfg.Packaging = Packaging - cfg.ErrTemplateName = ErrTemplateName + cfg.ErrTemplateName = "error" ApplicationName = "Grafana" @@ -1013,8 +1007,8 @@ func readSecuritySettings(iniFile *ini.File, cfg *Cfg) error { // admin cfg.DisableInitAdminCreation = security.Key("disable_initial_admin_creation").MustBool(false) - AdminUser = valueAsString(security, "admin_user", "") - AdminPassword = valueAsString(security, "admin_password", "") + cfg.AdminUser = valueAsString(security, "admin_user", "") + cfg.AdminPassword = valueAsString(security, "admin_password", "") return nil } @@ -1022,8 +1016,7 @@ func readSecuritySettings(iniFile *ini.File, cfg *Cfg) error { func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) { auth := iniFile.Section("auth") - LoginCookieName = valueAsString(auth, "login_cookie_name", "grafana_session") - cfg.LoginCookieName = LoginCookieName + cfg.LoginCookieName = valueAsString(auth, "login_cookie_name", "grafana_session") maxInactiveDaysVal := auth.Key("login_maximum_inactive_lifetime_days").MustString("") if maxInactiveDaysVal != "" { maxInactiveDaysVal = fmt.Sprintf("%sd", maxInactiveDaysVal) @@ -1049,7 +1042,6 @@ func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) { if err != nil { return err } - LoginMaxLifetime = cfg.LoginMaxLifetime cfg.ApiKeyMaxSecondsToLive = auth.Key("api_key_max_seconds_to_live").MustInt64(-1) diff --git a/pkg/setting/setting_test.go b/pkg/setting/setting_test.go index f975a87211b..21d1caf7acc 100644 --- a/pkg/setting/setting_test.go +++ b/pkg/setting/setting_test.go @@ -30,7 +30,7 @@ func TestLoadingSettings(t *testing.T) { err := cfg.Load(&CommandLineArgs{HomePath: "../../"}) So(err, ShouldBeNil) - So(AdminUser, ShouldEqual, "admin") + So(cfg.AdminUser, ShouldEqual, "admin") So(cfg.RendererCallbackUrl, ShouldEqual, "http://localhost:3000/") }) @@ -61,7 +61,7 @@ func TestLoadingSettings(t *testing.T) { err = cfg.Load(&CommandLineArgs{HomePath: "../../"}) So(err, ShouldBeNil) - So(AdminUser, ShouldEqual, "superduper") + So(cfg.AdminUser, ShouldEqual, "superduper") So(cfg.DataPath, ShouldEqual, filepath.Join(HomePath, "data")) So(cfg.LogsPath, ShouldEqual, filepath.Join(cfg.DataPath, "log")) }) diff --git a/pkg/tsdb/cloudwatch/cloudwatch.go b/pkg/tsdb/cloudwatch/cloudwatch.go index acefa9deb70..fba4b6ac49c 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch.go +++ b/pkg/tsdb/cloudwatch/cloudwatch.go @@ -452,7 +452,7 @@ func isTerminated(queryStatus string) bool { return queryStatus == "Complete" || queryStatus == "Cancelled" || queryStatus == "Failed" || queryStatus == "Timeout" } -// CloudWatch client factory. +// newCWClient is a CloudWatch client factory. // // Stubbable by tests. var newCWClient = func(sess *session.Session) cloudwatchiface.CloudWatchAPI { @@ -464,7 +464,7 @@ var newCWClient = func(sess *session.Session) cloudwatchiface.CloudWatchAPI { return client } -// CloudWatch logs client factory. +// newCWLogsClient is a CloudWatch logs client factory. // // Stubbable by tests. var newCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI {