RBAC: Redirect to /login when forceLogin is set (#56469)

This commit is contained in:
Emil Tullstedt 2022-10-07 08:18:56 +02:00 committed by GitHub
parent b622a87aee
commit bb479e030a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 115 additions and 1 deletions

View File

@ -3,12 +3,18 @@ package accesscontrol
import ( import (
"bytes" "bytes"
"context" "context"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"regexp"
"strconv" "strconv"
"strings"
"text/template" "text/template"
"time" "time"
"github.com/grafana/grafana/pkg/middleware/cookies"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting" "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) { 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) 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 { func newID() string {
// Less ambiguity than alphanumerical. // Less ambiguity than alphanumerical.
numerical := []byte("0123456789") numerical := []byte("0123456789")

View File

@ -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) { return func(c *web.Context) {
reqCtx := &models.ReqContext{ reqCtx := &models.ReqContext{
Context: c, Context: c,
@ -92,6 +138,9 @@ func contextProvider() web.Handler {
IsSignedIn: true, IsSignedIn: true,
SkipCache: true, SkipCache: true,
} }
for _, modifier := range modifiers {
modifier(reqCtx)
}
c.Req = c.Req.WithContext(ctxkey.Set(c.Req.Context(), reqCtx)) c.Req = c.Req.WithContext(ctxkey.Set(c.Req.Context(), reqCtx))
} }
} }