From bb479e030ac5cd23b8d94454d3204df851256ab2 Mon Sep 17 00:00:00 2001 From: Emil Tullstedt Date: Fri, 7 Oct 2022 08:18:56 +0200 Subject: [PATCH] RBAC: Redirect to /login when forceLogin is set (#56469) --- pkg/services/accesscontrol/middleware.go | 65 +++++++++++++++++++ pkg/services/accesscontrol/middleware_test.go | 51 ++++++++++++++- 2 files changed, 115 insertions(+), 1 deletion(-) diff --git a/pkg/services/accesscontrol/middleware.go b/pkg/services/accesscontrol/middleware.go index 4b7c0b1fb50..db11d0c2038 100644 --- a/pkg/services/accesscontrol/middleware.go +++ b/pkg/services/accesscontrol/middleware.go @@ -3,12 +3,18 @@ package accesscontrol import ( "bytes" "context" + "errors" "fmt" "net/http" + "net/url" + "regexp" "strconv" + "strings" "text/template" "time" + "github.com/grafana/grafana/pkg/middleware/cookies" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -23,6 +29,24 @@ func Middleware(ac AccessControl) func(web.Handler, Evaluator) web.Handler { } return func(c *models.ReqContext) { + if c.AllowAnonymous { + forceLogin, _ := strconv.ParseBool(c.Req.URL.Query().Get("forceLogin")) // ignoring error, assuming false for non-true values is ok. + orgID, err := strconv.ParseInt(c.Req.URL.Query().Get("orgId"), 10, 64) + if err == nil && orgID > 0 && orgID != c.OrgID { + forceLogin = true + } + + if !c.IsSignedIn && forceLogin { + unauthorized(c, nil) + } + } + + var revokedErr *models.TokenRevokedError + if errors.As(c.LookupTokenErr, &revokedErr) { + unauthorized(c, revokedErr) + return + } + authorize(c, ac, c.SignedInUser, evaluator) } } @@ -80,6 +104,47 @@ func deny(c *models.ReqContext, evaluator Evaluator, err error) { }) } +func unauthorized(c *models.ReqContext, err error) { + if c.IsApiRequest() { + response := map[string]interface{}{ + "message": "Unauthorized", + } + + var revokedErr *models.TokenRevokedError + if errors.As(err, &revokedErr) { + response["message"] = "Token revoked" + response["error"] = map[string]interface{}{ + "id": "ERR_TOKEN_REVOKED", + "maxConcurrentSessions": revokedErr.MaxConcurrentSessions, + } + } + + c.JSON(http.StatusUnauthorized, response) + return + } + + writeRedirectCookie(c) + c.Redirect(setting.AppSubUrl + "/login") +} + +func writeRedirectCookie(c *models.ReqContext) { + redirectTo := c.Req.RequestURI + if setting.AppSubUrl != "" && !strings.HasPrefix(redirectTo, setting.AppSubUrl) { + redirectTo = setting.AppSubUrl + c.Req.RequestURI + } + + // remove any forceLogin=true params + redirectTo = removeForceLoginParams(redirectTo) + + cookies.WriteCookie(c.Resp, "redirect_to", url.QueryEscape(redirectTo), 0, nil) +} + +var forceLoginParamsRegexp = regexp.MustCompile(`&?forceLogin=true`) + +func removeForceLoginParams(str string) string { + return forceLoginParamsRegexp.ReplaceAllString(str, "") +} + func newID() string { // Less ambiguity than alphanumerical. numerical := []byte("0123456789") diff --git a/pkg/services/accesscontrol/middleware_test.go b/pkg/services/accesscontrol/middleware_test.go index 8f5596193de..7ba3e180126 100644 --- a/pkg/services/accesscontrol/middleware_test.go +++ b/pkg/services/accesscontrol/middleware_test.go @@ -83,7 +83,53 @@ func TestMiddleware(t *testing.T) { } } -func contextProvider() web.Handler { +func TestMiddleware_forceLogin(t *testing.T) { + tests := []struct { + url string + redirectToLogin bool + }{ + {url: "/endpoint?forceLogin=true", redirectToLogin: true}, + {url: "/endpoint?forceLogin=false"}, + {url: "/endpoint"}, + } + + for _, tc := range tests { + var endpointCalled bool + + server := web.New() + server.UseMiddleware(web.Renderer("../../public/views", "[[", "]]")) + + server.Get("/endpoint", func(c *models.ReqContext) { + endpointCalled = true + c.Resp.WriteHeader(http.StatusOK) + }) + + ac := mock.New().WithPermissions([]accesscontrol.Permission{{Action: "endpoint:read", Scope: "endpoint:1"}}) + server.Use(contextProvider(func(c *models.ReqContext) { + c.AllowAnonymous = true + c.SignedInUser.IsAnonymous = true + c.IsSignedIn = false + })) + server.Use( + accesscontrol.Middleware(ac)(nil, accesscontrol.EvalPermission("endpoint:read", "endpoint:1")), + ) + + request, err := http.NewRequest(http.MethodGet, tc.url, nil) + assert.NoError(t, err) + recorder := httptest.NewRecorder() + + server.ServeHTTP(recorder, request) + + expectedCode := http.StatusOK + if tc.redirectToLogin { + expectedCode = http.StatusFound + } + assert.Equal(t, expectedCode, recorder.Code) + assert.Equal(t, !tc.redirectToLogin, endpointCalled, "/endpoint should be called?") + } +} + +func contextProvider(modifiers ...func(c *models.ReqContext)) web.Handler { return func(c *web.Context) { reqCtx := &models.ReqContext{ Context: c, @@ -92,6 +138,9 @@ func contextProvider() web.Handler { IsSignedIn: true, SkipCache: true, } + for _, modifier := range modifiers { + modifier(reqCtx) + } c.Req = c.Req.WithContext(ctxkey.Set(c.Req.Context(), reqCtx)) } }