Contexthandler: Remove code that is no longer used (#73101)

* Contexthandler: remove dead code

* Contexthandler: Add tests

* Update pkg/tests/api/alerting/api_alertmanager_test.go

Co-authored-by: Jo <joao.guerreiro@grafana.com>

---------

Co-authored-by: Jo <joao.guerreiro@grafana.com>
This commit is contained in:
Karl Persson 2023-08-09 15:17:59 +02:00 committed by GitHub
parent 5d8e6aa162
commit e53e22ef2a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 208 additions and 1757 deletions

View File

@ -19,33 +19,25 @@ import (
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/db/dbtest"
"github.com/grafana/grafana/pkg/infra/fs"
"github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/models/usertoken"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/annotations/annotationstest"
"github.com/grafana/grafana/pkg/services/anonymous/anontest"
"github.com/grafana/grafana/pkg/services/auth/authtest"
"github.com/grafana/grafana/pkg/services/auth/jwt"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/authn/authntest"
"github.com/grafana/grafana/pkg/services/contexthandler"
"github.com/grafana/grafana/pkg/services/contexthandler/authproxy"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboards"
dashver "github.com/grafana/grafana/pkg/services/dashboardversion"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/ldap/service"
"github.com/grafana/grafana/pkg/services/licensing"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/login/loginservice"
"github.com/grafana/grafana/pkg/services/login/logintest"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/org/orgtest"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/services/search"
"github.com/grafana/grafana/pkg/services/search/model"
"github.com/grafana/grafana/pkg/services/searchusers"
@ -197,25 +189,12 @@ func getContextHandler(t *testing.T, cfg *setting.Cfg) *contexthandler.ContextHa
cfg = setting.NewCfg()
}
sqlStore := db.InitTestDB(t)
remoteCacheSvc := &remotecache.RemoteCache{}
cfg.RemoteCacheOptions = &setting.RemoteCacheOptions{
Name: "database",
}
userAuthTokenSvc := authtest.NewFakeUserAuthTokenService()
renderSvc := &fakeRenderService{}
authJWTSvc := jwt.NewFakeJWTService()
tracer := tracing.InitializeTracerForTest()
authProxy := authproxy.ProvideAuthProxy(cfg, remoteCacheSvc, loginservice.LoginServiceMock{}, &usertest.FakeUserService{}, sqlStore, service.NewLDAPFakeService())
loginService := &logintest.LoginServiceFake{}
authenticator := &logintest.AuthenticatorFake{}
ctxHdlr := contexthandler.ProvideService(cfg, userAuthTokenSvc, authJWTSvc,
remoteCacheSvc, renderSvc, sqlStore, tracer, authProxy, loginService, nil,
authenticator, usertest.NewUserServiceFake(), orgtest.NewOrgServiceFake(),
nil, featuremgmt.WithFeatures(), &authntest.FakeService{
ExpectedIdentity: &authn.Identity{IsAnonymous: true, SessionToken: &usertoken.UserToken{}}}, &anontest.FakeAnonymousSessionService{})
return ctxHdlr
return contexthandler.ProvideService(
cfg,
tracing.NewFakeTracer(),
featuremgmt.WithFeatures(),
&authntest.FakeService{ExpectedIdentity: &authn.Identity{IsAnonymous: true, SessionToken: &usertoken.UserToken{}}},
)
}
func setupScenarioContext(t *testing.T, url string) *scenarioContext {
@ -240,14 +219,6 @@ func setupScenarioContext(t *testing.T, url string) *scenarioContext {
return sc
}
type fakeRenderService struct {
rendering.Service
}
func (s *fakeRenderService) Init() error {
return nil
}
// FIXME: This user should not be anonymous
func userWithPermissions(orgID int64, permissions []accesscontrol.Permission) *user.SignedInUser {
return &user.SignedInUser{IsAnonymous: true, OrgID: orgID, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByAction(permissions)}}

View File

@ -66,10 +66,10 @@ func TestOrgUsersAPIEndpoint_userLoggedIn(t *testing.T) {
orgService := orgtest.NewOrgServiceFake()
orgService.ExpectedSearchOrgUsersResult = &org.SearchOrgUsersQueryResult{}
hs.orgService = orgService
setUpGetOrgUsersDB(t, sqlStore)
mock := dbtest.NewFakeDB()
loggedInUserScenario(t, "When calling GET on", "api/org/users", "api/org/users", func(sc *scenarioContext) {
setUpGetOrgUsersDB(t, sqlStore)
orgService.ExpectedSearchOrgUsersResult = &org.SearchOrgUsersQueryResult{
OrgUsers: []*org.OrgUserDTO{
{Login: testUserLogin, Email: "testUser@grafana.com"},
@ -89,8 +89,6 @@ func TestOrgUsersAPIEndpoint_userLoggedIn(t *testing.T) {
}, mock)
loggedInUserScenario(t, "When calling GET on", "api/org/users/search", "api/org/users/search", func(sc *scenarioContext) {
setUpGetOrgUsersDB(t, sqlStore)
orgService.ExpectedSearchOrgUsersResult = &org.SearchOrgUsersQueryResult{
OrgUsers: []*org.OrgUserDTO{
{
@ -123,8 +121,6 @@ func TestOrgUsersAPIEndpoint_userLoggedIn(t *testing.T) {
}, mock)
loggedInUserScenario(t, "When calling GET with page and limit query parameters on", "api/org/users/search", "api/org/users/search", func(sc *scenarioContext) {
setUpGetOrgUsersDB(t, sqlStore)
orgService.ExpectedSearchOrgUsersResult = &org.SearchOrgUsersQueryResult{
OrgUsers: []*org.OrgUserDTO{
{
@ -159,7 +155,6 @@ func TestOrgUsersAPIEndpoint_userLoggedIn(t *testing.T) {
t.Cleanup(func() { settings.HiddenUsers = make(map[string]struct{}) })
loggedInUserScenario(t, "When calling GET on", "api/org/users", "api/org/users", func(sc *scenarioContext) {
setUpGetOrgUsersDB(t, sqlStore)
orgService.ExpectedSearchOrgUsersResult = &org.SearchOrgUsersQueryResult{
OrgUsers: []*org.OrgUserDTO{
{Login: testUserLogin, Email: "testUser@grafana.com"},
@ -183,8 +178,6 @@ func TestOrgUsersAPIEndpoint_userLoggedIn(t *testing.T) {
loggedInUserScenarioWithRole(t, "When calling GET as an admin on", "GET", "api/org/users/lookup",
"api/org/users/lookup", org.RoleAdmin, func(sc *scenarioContext) {
setUpGetOrgUsersDB(t, sqlStore)
sc.handlerFunc = hs.GetOrgUsersForCurrentOrgLookup
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()

View File

@ -15,15 +15,16 @@ import (
"github.com/grafana/grafana/pkg/services/authn/authntest"
"github.com/grafana/grafana/pkg/services/contexthandler"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
)
func setupAuthMiddlewareTest(t *testing.T, identity *authn.Identity, authErr error) *contexthandler.ContextHandler {
return contexthandler.ProvideService(setting.NewCfg(), nil, nil, nil, nil, nil, tracing.NewFakeTracer(), nil, nil, nil, nil, nil, nil, nil, nil, &authntest.FakeService{
return contexthandler.ProvideService(setting.NewCfg(), tracing.NewFakeTracer(), featuremgmt.WithFeatures(), &authntest.FakeService{
ExpectedErr: authErr,
ExpectedIdentity: identity,
}, nil)
})
}
func TestAuth_Middleware(t *testing.T) {

View File

@ -15,8 +15,6 @@ import (
"github.com/grafana/grafana/pkg/infra/fs"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/anonymous/anontest"
"github.com/grafana/grafana/pkg/services/auth/authtest"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/authn/authntest"
"github.com/grafana/grafana/pkg/services/contexthandler"
@ -254,10 +252,5 @@ func getContextHandler(t *testing.T, cfg *setting.Cfg, authnService authn.Servic
t.Helper()
tracer := tracing.NewFakeTracer()
return contexthandler.ProvideService(cfg, authtest.NewFakeUserAuthTokenService(), nil,
nil, nil, nil, tracer, nil,
nil, nil, nil, nil, nil,
nil, featuremgmt.WithFeatures(featuremgmt.FlagAccessTokenExpirationCheck),
authnService, &anontest.FakeAnonymousSessionService{},
)
return contexthandler.ProvideService(cfg, tracer, featuremgmt.WithFeatures(), authnService)
}

View File

@ -45,7 +45,6 @@ import (
"github.com/grafana/grafana/pkg/services/certgenerator"
"github.com/grafana/grafana/pkg/services/cleanup"
"github.com/grafana/grafana/pkg/services/contexthandler"
"github.com/grafana/grafana/pkg/services/contexthandler/authproxy"
"github.com/grafana/grafana/pkg/services/correlations"
"github.com/grafana/grafana/pkg/services/dashboardimport"
dashboardimportservice "github.com/grafana/grafana/pkg/services/dashboardimport/service"
@ -302,7 +301,6 @@ var wireBasicSet = wire.NewSet(
sanitizer.ProvideService,
secretsStore.ProvideService,
avatar.ProvideAvatarCacheServer,
authproxy.ProvideAuthProxy,
statscollector.ProvideService,
corekind.KindSet,
cuectx.GrafanaCUEContext,

View File

@ -12,9 +12,9 @@ import (
)
var (
errEmptyPassword = errutil.NewBase(errutil.StatusBadRequest, "password-auth.empty", errutil.WithPublicMessage("Invalid username or password"))
errPasswordAuthFailed = errutil.NewBase(errutil.StatusBadRequest, "password-auth.failed", errutil.WithPublicMessage("Invalid username or password"))
errInvalidPassword = errutil.NewBase(errutil.StatusBadRequest, "password-auth.invalid", errutil.WithPublicMessage("Invalid password or username"))
errEmptyPassword = errutil.NewBase(errutil.StatusUnauthorized, "password-auth.empty", errutil.WithPublicMessage("Invalid username or password"))
errPasswordAuthFailed = errutil.NewBase(errutil.StatusUnauthorized, "password-auth.failed", errutil.WithPublicMessage("Invalid username or password"))
errInvalidPassword = errutil.NewBase(errutil.StatusUnauthorized, "password-auth.invalid", errutil.WithPublicMessage("Invalid password or username"))
errLoginAttemptBlocked = errutil.NewBase(errutil.StatusUnauthorized, "login-attempt.blocked", errutil.WithPublicMessage("Invalid username or password"))
)

View File

@ -1,222 +0,0 @@
package contexthandler
import (
"errors"
"fmt"
"net/http"
"strings"
"github.com/jmespath/go-jmespath"
"github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/models/roletype"
authJWT "github.com/grafana/grafana/pkg/services/auth/jwt"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
loginsvc "github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
)
const (
InvalidJWT = "Invalid JWT"
InvalidRole = "Invalid Role"
UserNotFound = "User not found"
authQueryParamName = "auth_token"
)
func (h *ContextHandler) initContextWithJWT(ctx *contextmodel.ReqContext, orgId int64) bool {
if !h.Cfg.JWTAuthEnabled || h.Cfg.JWTAuthHeaderName == "" {
return false
}
jwtToken := ctx.Req.Header.Get(h.Cfg.JWTAuthHeaderName)
if jwtToken == "" && h.Cfg.JWTAuthURLLogin {
jwtToken = ctx.Req.URL.Query().Get(authQueryParamName)
}
if jwtToken == "" {
return false
}
stripSensitiveParam(h.Cfg, ctx.Req)
// Strip the 'Bearer' prefix if it exists.
jwtToken = strings.TrimPrefix(jwtToken, "Bearer ")
// If the "sub" claim is missing or empty then pass the control to the next handler
if !authJWT.HasSubClaim(jwtToken) {
return false
}
claims, err := h.JWTAuthService.Verify(ctx.Req.Context(), jwtToken)
if err != nil {
ctx.Logger.Debug("Failed to verify JWT", "error", err)
ctx.JsonApiErr(http.StatusUnauthorized, InvalidJWT, err)
return true
}
query := user.GetSignedInUserQuery{OrgID: orgId}
sub, _ := claims["sub"].(string)
if sub == "" {
ctx.Logger.Warn("Got a JWT without the mandatory 'sub' claim", "error", err)
ctx.JsonApiErr(http.StatusUnauthorized, InvalidJWT, err)
return true
}
extUser := &loginsvc.ExternalUserInfo{
AuthModule: "jwt",
AuthId: sub,
OrgRoles: map[int64]org.RoleType{},
// we do not want to sync team memberships from JWT authentication see - https://github.com/grafana/grafana/issues/62175
SkipTeamSync: true,
}
if key := h.Cfg.JWTAuthUsernameClaim; key != "" {
query.Login, _ = claims[key].(string)
extUser.Login, _ = claims[key].(string)
}
if key := h.Cfg.JWTAuthEmailClaim; key != "" {
query.Email, _ = claims[key].(string)
extUser.Email, _ = claims[key].(string)
}
if name, _ := claims["name"].(string); name != "" {
extUser.Name = name
}
var role roletype.RoleType
var grafanaAdmin bool
if !h.Cfg.JWTAuthSkipOrgRoleSync {
role, grafanaAdmin = h.extractJWTRoleAndAdmin(claims)
if h.Cfg.JWTAuthRoleAttributeStrict && !role.IsValid() {
ctx.Logger.Debug("Extracted Role is invalid")
ctx.JsonApiErr(http.StatusForbidden, InvalidRole, nil)
return true
}
if role.IsValid() {
var orgID int64
if h.Cfg.AutoAssignOrg && h.Cfg.AutoAssignOrgId > 0 {
orgID = int64(h.Cfg.AutoAssignOrgId)
ctx.Logger.Debug("The user has a role assignment and organization membership is auto-assigned",
"role", role, "orgId", orgID)
} else {
orgID = int64(1)
ctx.Logger.Debug("The user has a role assignment and organization membership is not auto-assigned",
"role", role, "orgId", orgID)
}
extUser.OrgRoles[orgID] = role
if h.Cfg.JWTAuthAllowAssignGrafanaAdmin {
extUser.IsGrafanaAdmin = &grafanaAdmin
}
}
}
if query.Login == "" && query.Email == "" {
ctx.Logger.Debug("Failed to get an authentication claim from JWT")
ctx.JsonApiErr(http.StatusUnauthorized, InvalidJWT, err)
return true
}
if h.Cfg.JWTAuthAutoSignUp {
upsert := &loginsvc.UpsertUserCommand{
ReqContext: ctx,
SignupAllowed: h.Cfg.JWTAuthAutoSignUp,
ExternalUser: extUser,
UserLookupParams: loginsvc.UserLookupParams{
UserID: nil,
Login: &query.Login,
Email: &query.Email,
},
}
if _, err := h.loginService.UpsertUser(ctx.Req.Context(), upsert); err != nil {
ctx.Logger.Error("Failed to upsert JWT user", "error", err)
return false
}
}
queryResult, err := h.userService.GetSignedInUserWithCacheCtx(ctx.Req.Context(), &query)
if err != nil {
if errors.Is(err, user.ErrUserNotFound) {
ctx.Logger.Debug(
"Failed to find user using JWT claims",
"email_claim", query.Email,
"username_claim", query.Login,
)
err = login.ErrInvalidCredentials
ctx.JsonApiErr(http.StatusUnauthorized, UserNotFound, err)
} else {
ctx.Logger.Error("Failed to get signed in user", "error", err)
ctx.JsonApiErr(http.StatusUnauthorized, InvalidJWT, err)
}
return true
}
ctx.SignedInUser = queryResult
ctx.IsSignedIn = true
return true
}
const roleGrafanaAdmin = "GrafanaAdmin"
func (h *ContextHandler) extractJWTRoleAndAdmin(claims map[string]interface{}) (org.RoleType, bool) {
if h.Cfg.JWTAuthRoleAttributePath == "" {
return "", false
}
role, err := searchClaimsForStringAttr(h.Cfg.JWTAuthRoleAttributePath, claims)
if err != nil || role == "" {
return "", false
}
if role == roleGrafanaAdmin {
return org.RoleAdmin, true
}
return org.RoleType(role), false
}
func searchClaimsForAttr(attributePath string, claims map[string]interface{}) (interface{}, error) {
if attributePath == "" {
return "", errors.New("no attribute path specified")
}
if len(claims) == 0 {
return "", errors.New("empty claims provided")
}
val, err := jmespath.Search(attributePath, claims)
if err != nil {
return "", fmt.Errorf("failed to search claims with provided path: %q: %w", attributePath, err)
}
return val, nil
}
func searchClaimsForStringAttr(attributePath string, claims map[string]interface{}) (string, error) {
val, err := searchClaimsForAttr(attributePath, claims)
if err != nil {
return "", err
}
strVal, ok := val.(string)
if ok {
return strVal, nil
}
return "", nil
}
// remove sensitive query params
// avoid JWT URL login passing auth_token in URL
func stripSensitiveParam(cfg *setting.Cfg, httpRequest *http.Request) {
if cfg.JWTAuthURLLogin {
params := httpRequest.URL.Query()
if params.Has(authQueryParamName) {
params.Del(authQueryParamName)
httpRequest.URL.RawQuery = params.Encode()
}
}
}

View File

@ -1,129 +0,0 @@
package contexthandler
import (
"context"
"fmt"
"net/http"
"strconv"
"testing"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/services/anonymous/anontest"
"github.com/grafana/grafana/pkg/services/auth/authtest"
"github.com/grafana/grafana/pkg/services/auth/jwt"
"github.com/grafana/grafana/pkg/services/authn/authntest"
"github.com/grafana/grafana/pkg/services/contexthandler/authproxy"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/ldap/service"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/login/loginservice"
"github.com/grafana/grafana/pkg/services/org/orgtest"
"github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/services/secrets/fakes"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/services/user/usertest"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
)
const userID = int64(1)
const orgID = int64(4)
// Test initContextWithAuthProxy with a cached user ID that is no longer valid.
//
// In this case, the cache entry should be ignored/cleared and another attempt should be done to sign the user
// in without cache.
func TestInitContextWithAuthProxy_CachedInvalidUserID(t *testing.T) {
const name = "markelog"
svc := getContextHandler(t)
req, err := http.NewRequest("POST", "http://example.com", nil)
require.NoError(t, err)
ctx := &contextmodel.ReqContext{
Context: &web.Context{Req: req},
Logger: log.New("Test"),
}
req.Header.Set(svc.Cfg.AuthProxyHeaderName, name)
h, err := authproxy.HashCacheKey(name)
require.NoError(t, err)
key := fmt.Sprintf(authproxy.CachePrefix, h)
t.Logf("Injecting stale user ID in cache with key %q", key)
userIdPayload := []byte(strconv.FormatInt(int64(33), 10))
err = svc.RemoteCache.Set(context.Background(), key, userIdPayload, 0)
require.NoError(t, err)
authEnabled := svc.initContextWithAuthProxy(ctx, orgID)
require.True(t, authEnabled)
require.Equal(t, userID, ctx.SignedInUser.UserID)
require.True(t, ctx.IsSignedIn)
cachedByteArray, err := svc.RemoteCache.Get(context.Background(), key)
require.NoError(t, err)
cacheUserId, err := strconv.ParseInt(string(cachedByteArray), 10, 64)
require.NoError(t, err)
require.Equal(t, userID, cacheUserId)
}
type fakeRenderService struct {
rendering.Service
}
func getContextHandler(t *testing.T) *ContextHandler {
t.Helper()
sqlStore := db.InitTestDB(t)
cfg := setting.NewCfg()
cfg.RemoteCacheOptions = &setting.RemoteCacheOptions{
Name: "database",
}
cfg.AuthProxyHeaderName = "X-Killa"
cfg.AuthProxyEnabled = true
cfg.AuthProxyHeaderProperty = "username"
remoteCacheSvc, err := remotecache.ProvideService(cfg, sqlStore, &usagestats.UsageStatsMock{}, fakes.NewFakeSecretsService())
require.NoError(t, err)
userAuthTokenSvc := authtest.NewFakeUserAuthTokenService()
renderSvc := &fakeRenderService{}
authJWTSvc := jwt.NewFakeJWTService()
tracer := tracing.InitializeTracerForTest()
loginService := loginservice.LoginServiceMock{ExpectedUser: &user.User{ID: userID}}
userService := usertest.FakeUserService{
GetSignedInUserFn: func(ctx context.Context, query *user.GetSignedInUserQuery) (*user.SignedInUser, error) {
if query.UserID != userID {
return &user.SignedInUser{}, user.ErrUserNotFound
}
return &user.SignedInUser{
UserID: userID,
OrgID: orgID,
}, nil
},
}
orgService := orgtest.NewOrgServiceFake()
authProxy := authproxy.ProvideAuthProxy(cfg, remoteCacheSvc, loginService, &userService, nil, service.NewLDAPFakeService())
authenticator := &fakeAuthenticator{}
return ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc,
renderSvc, sqlStore, tracer, authProxy, loginService, nil, authenticator,
&userService, orgService, nil, featuremgmt.WithFeatures(),
&authntest.FakeService{}, &anontest.FakeAnonymousSessionService{})
}
type fakeAuthenticator struct{}
func (fa *fakeAuthenticator) AuthenticateUser(c context.Context, query *login.LoginUserQuery) error {
return nil
}

View File

@ -1,384 +0,0 @@
package authproxy
import (
"context"
"encoding/hex"
"errors"
"fmt"
"hash/fnv"
"net"
"net/mail"
"path"
"reflect"
"strconv"
"strings"
"time"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/remotecache"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/ldap"
"github.com/grafana/grafana/pkg/services/ldap/service"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
const (
// CachePrefix is a prefix for the cache key
CachePrefix = "auth-proxy-sync-ttl:%s"
)
// supportedHeaders states the supported headers configuration fields
var supportedHeaderFields = []string{"Name", "Email", "Login", "Groups", "Role"}
// AuthProxy struct
type AuthProxy struct {
cfg *setting.Cfg
remoteCache *remotecache.RemoteCache
loginService login.Service
sqlStore db.DB
userService user.Service
ldapService service.LDAP
logger log.Logger
}
func ProvideAuthProxy(cfg *setting.Cfg, remoteCache *remotecache.RemoteCache,
loginService login.Service, userService user.Service,
sqlStore db.DB, ldapService service.LDAP) *AuthProxy {
return &AuthProxy{
cfg: cfg,
remoteCache: remoteCache,
loginService: loginService,
sqlStore: sqlStore,
userService: userService,
logger: log.New("auth.proxy"),
ldapService: ldapService,
}
}
// Error auth proxy specific error
type Error struct {
Message string
DetailsError error
}
// newError returns an Error.
func newError(message string, err error) Error {
return Error{
Message: message,
DetailsError: err,
}
}
// Error returns the error message.
func (err Error) Error() string {
return err.Message
}
// IsEnabled checks if the auth proxy is enabled.
func (auth *AuthProxy) IsEnabled() bool {
// Bail if the setting is not enabled
return auth.cfg.AuthProxyEnabled
}
// HasHeader checks if we have specified header
func (auth *AuthProxy) HasHeader(reqCtx *contextmodel.ReqContext) bool {
header := auth.getDecodedHeader(reqCtx, auth.cfg.AuthProxyHeaderName)
return len(header) != 0
}
// IsAllowedIP returns whether provided IP is allowed.
func (auth *AuthProxy) IsAllowedIP(ip string) error {
if len(strings.TrimSpace(auth.cfg.AuthProxyWhitelist)) == 0 {
return nil
}
proxies := strings.Split(auth.cfg.AuthProxyWhitelist, ",")
proxyObjs := make([]*net.IPNet, 0, len(proxies))
for _, proxy := range proxies {
result, err := coerceProxyAddress(proxy)
if err != nil {
return newError("could not get the network", err)
}
proxyObjs = append(proxyObjs, result)
}
sourceIP, _, err := net.SplitHostPort(ip)
if err != nil {
return newError("could not parse address", err)
}
sourceObj := net.ParseIP(sourceIP)
for _, proxyObj := range proxyObjs {
if proxyObj.Contains(sourceObj) {
return nil
}
}
return newError("proxy authentication required", fmt.Errorf(
"request for user from %s is not from the authentication proxy",
sourceIP,
))
}
func HashCacheKey(key string) (string, error) {
hasher := fnv.New128a()
if _, err := hasher.Write([]byte(key)); err != nil {
return "", err
}
return hex.EncodeToString(hasher.Sum(nil)), nil
}
// getKey forms a key for the cache based on the headers received as part of the authentication flow.
// Our configuration supports multiple headers. The main header contains the email or username.
// And the additional ones that allow us to specify extra attributes: Name, Email, Role, or Groups.
func (auth *AuthProxy) getKey(reqCtx *contextmodel.ReqContext) (string, error) {
header := auth.getDecodedHeader(reqCtx, auth.cfg.AuthProxyHeaderName)
key := strings.TrimSpace(header) // start the key with the main header
auth.headersIterator(reqCtx, func(_, header string) {
key = strings.Join([]string{key, header}, "-") // compose the key with any additional headers
})
hashedKey, err := HashCacheKey(key)
if err != nil {
return "", err
}
return fmt.Sprintf(CachePrefix, hashedKey), nil
}
// Login logs in user ID by whatever means possible.
func (auth *AuthProxy) Login(reqCtx *contextmodel.ReqContext, ignoreCache bool) (int64, error) {
if !ignoreCache {
// Error here means absent cache - we don't need to handle that
id, err := auth.getUserViaCache(reqCtx)
if err == nil && id != 0 {
return id, nil
}
}
if auth.cfg.LDAPAuthEnabled {
id, err := auth.LoginViaLDAP(reqCtx)
if err != nil {
if errors.Is(err, ldap.ErrInvalidCredentials) {
return 0, newError("proxy authentication required", ldap.ErrInvalidCredentials)
}
return 0, newError("failed to get the user", err)
}
return id, nil
}
id, err := auth.loginViaHeader(reqCtx)
if err != nil {
return 0, newError("failed to log in as user, specified in auth proxy header", err)
}
return id, nil
}
// getUserViaCache gets user ID from cache.
func (auth *AuthProxy) getUserViaCache(reqCtx *contextmodel.ReqContext) (int64, error) {
cacheKey, err := auth.getKey(reqCtx)
if err != nil {
return 0, err
}
auth.logger.Debug("Getting user ID via auth cache", "cacheKey", cacheKey)
cachedValue, err := auth.remoteCache.Get(reqCtx.Req.Context(), cacheKey)
if err != nil {
return 0, err
}
userId, err := strconv.ParseInt(string(cachedValue), 10, 64)
if err != nil {
auth.logger.Debug("Failed getting user ID via auth cache", "error", err)
return 0, err
}
auth.logger.Debug("Successfully got user ID via auth cache", "id", cachedValue)
return userId, nil
}
// RemoveUserFromCache removes user from cache.
func (auth *AuthProxy) RemoveUserFromCache(reqCtx *contextmodel.ReqContext) error {
cacheKey, err := auth.getKey(reqCtx)
if err != nil {
return err
}
auth.logger.Debug("Removing user from auth cache", "cacheKey", cacheKey)
if err := auth.remoteCache.Delete(reqCtx.Req.Context(), cacheKey); err != nil {
return err
}
auth.logger.Debug("Successfully removed user from auth cache", "cacheKey", cacheKey)
return nil
}
// LoginViaLDAP logs in user via LDAP request
func (auth *AuthProxy) LoginViaLDAP(reqCtx *contextmodel.ReqContext) (int64, error) {
header := auth.getDecodedHeader(reqCtx, auth.cfg.AuthProxyHeaderName)
extUser, err := auth.ldapService.User(header)
if err != nil {
return 0, err
}
// Have to sync grafana and LDAP user during log in
upsert := &login.UpsertUserCommand{
ReqContext: reqCtx,
SignupAllowed: auth.cfg.LDAPAllowSignup,
ExternalUser: extUser,
UserLookupParams: login.UserLookupParams{
Login: &extUser.Login,
Email: &extUser.Email,
UserID: nil,
},
}
u, err := auth.loginService.UpsertUser(reqCtx.Req.Context(), upsert)
if err != nil {
return 0, err
}
return u.ID, nil
}
// loginViaHeader logs in user from the header only
func (auth *AuthProxy) loginViaHeader(reqCtx *contextmodel.ReqContext) (int64, error) {
header := auth.getDecodedHeader(reqCtx, auth.cfg.AuthProxyHeaderName)
extUser := &login.ExternalUserInfo{
AuthModule: login.AuthProxyAuthModule,
AuthId: header,
}
switch auth.cfg.AuthProxyHeaderProperty {
case "username":
extUser.Login = header
emailAddr, emailErr := mail.ParseAddress(header) // only set Email if it can be parsed as an email address
if emailErr == nil {
extUser.Email = emailAddr.Address
}
case "email":
extUser.Email = header
extUser.Login = header
default:
return 0, fmt.Errorf("auth proxy header property invalid")
}
auth.headersIterator(reqCtx, func(field string, header string) {
switch field {
case "Groups":
extUser.Groups = util.SplitString(header)
case "Role":
// If Role header is specified, we update the user role of the default org
if header != "" {
rt := org.RoleType(header)
if rt.IsValid() {
extUser.OrgRoles = map[int64]org.RoleType{}
orgID := int64(1)
if auth.cfg.AutoAssignOrg && auth.cfg.AutoAssignOrgId > 0 {
orgID = int64(auth.cfg.AutoAssignOrgId)
}
extUser.OrgRoles[orgID] = rt
}
}
default:
reflect.ValueOf(extUser).Elem().FieldByName(field).SetString(header)
}
})
upsert := &login.UpsertUserCommand{
ReqContext: reqCtx,
SignupAllowed: auth.cfg.AuthProxyAutoSignUp,
ExternalUser: extUser,
UserLookupParams: login.UserLookupParams{
UserID: nil,
Login: &extUser.Login,
Email: &extUser.Email,
},
}
result, err := auth.loginService.UpsertUser(reqCtx.Req.Context(), upsert)
if err != nil {
return 0, err
}
return result.ID, nil
}
// getDecodedHeader gets decoded value of a header with given headerName
func (auth *AuthProxy) getDecodedHeader(reqCtx *contextmodel.ReqContext, headerName string) string {
headerValue := reqCtx.Req.Header.Get(headerName)
if auth.cfg.AuthProxyHeadersEncoded {
headerValue = util.DecodeQuotedPrintable(headerValue)
}
return headerValue
}
// headersIterator iterates over all non-empty supported additional headers
func (auth *AuthProxy) headersIterator(reqCtx *contextmodel.ReqContext, fn func(field string, header string)) {
for _, field := range supportedHeaderFields {
h := auth.cfg.AuthProxyHeaders[field]
if h == "" {
continue
}
if value := auth.getDecodedHeader(reqCtx, h); value != "" {
fn(field, strings.TrimSpace(value))
}
}
}
// GetSignedInUser gets full signed in user info.
func (auth *AuthProxy) GetSignedInUser(userID int64, orgID int64) (*user.SignedInUser, error) {
return auth.userService.GetSignedInUser(context.Background(), &user.GetSignedInUserQuery{
OrgID: orgID,
UserID: userID,
})
}
// Remember user in cache
func (auth *AuthProxy) Remember(reqCtx *contextmodel.ReqContext, id int64) error {
key, err := auth.getKey(reqCtx)
if err != nil {
return err
}
// Check if user already in cache
cachedValue, err := auth.remoteCache.Get(reqCtx.Req.Context(), key)
if err == nil && len(cachedValue) != 0 {
return nil
}
expiration := time.Duration(auth.cfg.AuthProxySyncTTL) * time.Minute
userIdPayload := []byte(strconv.FormatInt(id, 10))
if err := auth.remoteCache.Set(reqCtx.Req.Context(), key, userIdPayload, expiration); err != nil {
return err
}
return nil
}
// coerceProxyAddress gets network of the presented CIDR notation
func coerceProxyAddress(proxyAddr string) (*net.IPNet, error) {
proxyAddr = strings.TrimSpace(proxyAddr)
if !strings.Contains(proxyAddr, "/") {
proxyAddr = path.Join(proxyAddr, "32")
}
_, network, err := net.ParseCIDR(proxyAddr)
if err != nil {
return nil, fmt.Errorf("could not parse the network: %w", err)
}
return network, nil
}

View File

@ -1,168 +0,0 @@
package authproxy
import (
"context"
"fmt"
"net/http"
"strconv"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/remotecache"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/ldap/service"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/login/loginservice"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
)
const hdrName = "markelog"
const id int64 = 42
func prepareMiddleware(t *testing.T, remoteCache *remotecache.RemoteCache, configureReq func(*http.Request, *setting.Cfg)) (*AuthProxy, *contextmodel.ReqContext) {
t.Helper()
req, err := http.NewRequest("POST", "http://example.com", nil)
require.NoError(t, err)
cfg := setting.NewCfg()
if configureReq != nil {
configureReq(req, cfg)
} else {
cfg.AuthProxyHeaderName = "X-Killa"
req.Header.Set(cfg.AuthProxyHeaderName, hdrName)
}
ctx := &contextmodel.ReqContext{
Context: &web.Context{Req: req},
}
loginService := loginservice.LoginServiceMock{
ExpectedUser: &user.User{
ID: id,
},
}
return ProvideAuthProxy(cfg, remoteCache, loginService, nil, nil, service.NewLDAPFakeService()), ctx
}
func TestMiddlewareContext(t *testing.T) {
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
h, err := HashCacheKey(hdrName)
require.NoError(t, err)
key := fmt.Sprintf(CachePrefix, h)
userIdPayload := []byte(strconv.FormatInt(id, 10))
err = cache.Set(context.Background(), key, userIdPayload, 0)
require.NoError(t, err)
// Set up the middleware
auth, reqCtx := prepareMiddleware(t, cache, nil)
gotKey, err := auth.getKey(reqCtx)
require.NoError(t, err)
assert.Equal(t, key, gotKey)
gotID, err := auth.Login(reqCtx, false)
require.NoError(t, err)
assert.Equal(t, id, gotID)
})
t.Run("When the cache key contains additional headers", func(t *testing.T) {
const id int64 = 33
const group = "grafana-core-team"
const role = "Admin"
h, err := HashCacheKey(hdrName + "-" + group + "-" + role)
require.NoError(t, err)
key := fmt.Sprintf(CachePrefix, h)
userIdPayload := []byte(strconv.FormatInt(id, 10))
err = cache.Set(context.Background(), key, userIdPayload, 0)
require.NoError(t, err)
auth, reqCtx := prepareMiddleware(t, cache, func(req *http.Request, cfg *setting.Cfg) {
cfg.AuthProxyHeaderName = "X-Killa"
cfg.AuthProxyHeaders = map[string]string{"Groups": "X-WEBAUTH-GROUPS", "Role": "X-WEBAUTH-ROLE"}
req.Header.Set(cfg.AuthProxyHeaderName, hdrName)
req.Header.Set("X-WEBAUTH-GROUPS", group)
req.Header.Set("X-WEBAUTH-ROLE", role)
})
assert.Equal(t, "auth-proxy-sync-ttl:f5acfffd56daac98d502ef8c8b8c5d56", key)
gotID, err := auth.Login(reqCtx, false)
require.NoError(t, err)
assert.Equal(t, id, gotID)
})
}
func TestMiddlewareContext_ldap(t *testing.T) {
t.Run("Logs in via LDAP", func(t *testing.T) {
cache := remotecache.NewFakeStore(t)
auth, reqCtx := prepareMiddleware(t, cache, nil)
auth.cfg.LDAPAuthEnabled = true
ldapFake := &service.LDAPFakeService{
ExpectedUser: &login.ExternalUserInfo{UserId: id},
}
auth.ldapService = ldapFake
gotID, err := auth.Login(reqCtx, false)
require.NoError(t, err)
assert.Equal(t, id, gotID)
assert.True(t, ldapFake.UserCalled)
})
t.Run("Gets nice error if LDAP is enabled, but not configured", func(t *testing.T) {
const id int64 = 42
cache := remotecache.NewFakeStore(t)
auth, reqCtx := prepareMiddleware(t, cache, nil)
auth.cfg.LDAPAuthEnabled = true
ldapFake := &service.LDAPFakeService{
ExpectedUser: nil,
ExpectedError: service.ErrUnableToCreateLDAPClient,
}
auth.ldapService = ldapFake
gotID, err := auth.Login(reqCtx, false)
require.EqualError(t, err, "failed to get the user")
assert.NotEqual(t, id, gotID)
assert.True(t, ldapFake.UserCalled)
})
}
func TestDecodeHeader(t *testing.T) {
cache := remotecache.NewFakeStore(t)
t.Run("should not decode header if not enabled in settings", func(t *testing.T) {
auth, reqCtx := prepareMiddleware(t, cache, func(req *http.Request, cfg *setting.Cfg) {
cfg.AuthProxyHeaderName = "X-WEBAUTH-USER"
cfg.AuthProxyHeadersEncoded = false
req.Header.Set(cfg.AuthProxyHeaderName, "M=C3=BCnchen")
})
header := auth.getDecodedHeader(reqCtx, auth.cfg.AuthProxyHeaderName)
assert.Equal(t, "M=C3=BCnchen", header)
})
t.Run("should decode header if enabled in settings", func(t *testing.T) {
auth, reqCtx := prepareMiddleware(t, cache, func(req *http.Request, cfg *setting.Cfg) {
cfg.AuthProxyHeaderName = "X-WEBAUTH-USER"
cfg.AuthProxyHeadersEncoded = true
req.Header.Set(cfg.AuthProxyHeaderName, "M=C3=BCnchen")
})
header := auth.getDecodedHeader(reqCtx, auth.cfg.AuthProxyHeaderName)
assert.Equal(t, "München", header)
})
}

View File

@ -4,100 +4,38 @@ package contexthandler
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"golang.org/x/sync/singleflight"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/components/apikeygen"
"github.com/grafana/grafana/pkg/components/satokengen"
"github.com/grafana/grafana/pkg/infra/db"
"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/infra/tracing"
loginpkg "github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/services/anonymous"
"github.com/grafana/grafana/pkg/services/apikey"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/auth/jwt"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/contexthandler/authproxy"
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/oauthtoken"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
)
const (
InvalidUsernamePassword = "invalid username or password"
/* #nosec */
InvalidAPIKey = "invalid API key"
)
func ProvideService(cfg *setting.Cfg, tokenService auth.UserTokenService, jwtService jwt.JWTService,
remoteCache *remotecache.RemoteCache, renderService rendering.Service, sqlStore db.DB,
tracer tracing.Tracer, authProxy *authproxy.AuthProxy, loginService login.Service,
apiKeyService apikey.Service, authenticator loginpkg.Authenticator, userService user.Service,
orgService org.Service, oauthTokenService oauthtoken.OAuthTokenService, features *featuremgmt.FeatureManager,
authnService authn.Service, anonDeviceService anonymous.Service,
func ProvideService(cfg *setting.Cfg, tracer tracing.Tracer, features *featuremgmt.FeatureManager, authnService authn.Service,
) *ContextHandler {
return &ContextHandler{
Cfg: cfg,
AuthTokenService: tokenService,
JWTAuthService: jwtService,
RemoteCache: remoteCache,
RenderService: renderService, SQLStore: sqlStore,
tracer: tracer,
authProxy: authProxy,
authenticator: authenticator,
loginService: loginService,
apiKeyService: apiKeyService,
userService: userService,
orgService: orgService,
oauthTokenService: oauthTokenService,
features: features,
AuthnService: authnService,
anonDeviceService: anonDeviceService,
singleflight: new(singleflight.Group),
Cfg: cfg,
tracer: tracer,
features: features,
authnService: authnService,
}
}
// ContextHandler is a middleware.
type ContextHandler struct {
Cfg *setting.Cfg
AuthTokenService auth.UserTokenService
JWTAuthService auth.JWTVerifierService
RemoteCache *remotecache.RemoteCache
RenderService rendering.Service
SQLStore db.DB
tracer tracing.Tracer
authProxy *authproxy.AuthProxy
authenticator loginpkg.Authenticator
loginService login.Service
apiKeyService apikey.Service
userService user.Service
orgService org.Service
oauthTokenService oauthtoken.OAuthTokenService
features *featuremgmt.FeatureManager
AuthnService authn.Service
singleflight *singleflight.Group
anonDeviceService anonymous.Service
// GetTime returns the current time.
// Stubbable by tests.
GetTime func() time.Time
Cfg *setting.Cfg
tracer tracing.Tracer
features *featuremgmt.FeatureManager
authnService authn.Service
}
type reqContextKey = ctxkey.Key
@ -169,62 +107,21 @@ func (h *ContextHandler) Middleware(next http.Handler) http.Handler {
reqContext.Logger = reqContext.Logger.New("traceID", traceID)
}
if h.Cfg.AuthBrokerEnabled {
identity, err := h.AuthnService.Authenticate(ctx, &authn.Request{HTTPRequest: reqContext.Req, Resp: reqContext.Resp})
if err != nil {
if errors.Is(err, auth.ErrInvalidSessionToken) || errors.Is(err, authn.ErrExpiredAccessToken) {
// Burn the cookie in case of invalid, expired or missing token
reqContext.Resp.Before(h.deleteInvalidCookieEndOfRequestFunc(reqContext))
}
// Hack: set all errors on LookupTokenErr, so we can check it in auth middlewares
reqContext.LookupTokenErr = err
} else {
reqContext.SignedInUser = identity.SignedInUser()
reqContext.UserToken = identity.SessionToken
reqContext.IsSignedIn = !identity.IsAnonymous
reqContext.AllowAnonymous = identity.IsAnonymous
reqContext.IsRenderCall = identity.AuthenticatedBy == login.RenderModule
identity, err := h.authnService.Authenticate(ctx, &authn.Request{HTTPRequest: reqContext.Req, Resp: reqContext.Resp})
if err != nil {
if errors.Is(err, auth.ErrInvalidSessionToken) || errors.Is(err, authn.ErrExpiredAccessToken) {
// Burn the cookie in case of invalid, expired or missing token
reqContext.Resp.Before(h.deleteInvalidCookieEndOfRequestFunc(reqContext))
}
// Hack: set all errors on LookupTokenErr, so we can check it in auth middlewares
reqContext.LookupTokenErr = err
} else {
const headerName = "X-Grafana-Org-Id"
orgID := int64(0)
orgIDHeader := reqContext.Req.Header.Get(headerName)
if orgIDHeader != "" {
id, err := strconv.ParseInt(orgIDHeader, 10, 64)
if err == nil {
orgID = id
} else {
reqContext.Logger.Debug("Received invalid header", "header", headerName, "value", orgIDHeader)
}
}
queryParameters, err := url.ParseQuery(reqContext.Req.URL.RawQuery)
if err != nil {
reqContext.Logger.Error("Failed to parse query parameters", "error", err)
}
if queryParameters.Has("targetOrgId") {
targetOrg, err := strconv.ParseInt(queryParameters.Get("targetOrgId"), 10, 64)
if err == nil {
orgID = targetOrg
} else {
reqContext.Logger.Error("Invalid target organization ID", "error", err)
}
}
// 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(reqContext):
case h.initContextWithJWT(reqContext, orgID):
case h.initContextWithAPIKey(reqContext):
case h.initContextWithBasicAuth(reqContext, orgID):
case h.initContextWithAuthProxy(reqContext, orgID):
case h.initContextWithToken(reqContext, orgID):
case h.initContextWithAnonymousUser(reqContext):
}
reqContext.SignedInUser = identity.SignedInUser()
reqContext.UserToken = identity.SessionToken
reqContext.IsSignedIn = !identity.IsAnonymous
reqContext.AllowAnonymous = identity.IsAnonymous
reqContext.IsRenderCall = identity.AuthenticatedBy == login.RenderModule
}
reqContext.Logger = reqContext.Logger.New("userId", reqContext.UserID, "orgId", reqContext.OrgID, "uname", reqContext.Login)
@ -236,353 +133,10 @@ func (h *ContextHandler) Middleware(next http.Handler) http.Handler {
{Num: reqContext.UserID}},
)
// when using authn service this is implemented as a post auth hook
if !h.Cfg.AuthBrokerEnabled {
// update last seen every 5min
if reqContext.ShouldUpdateLastSeenAt() {
reqContext.Logger.Debug("Updating last user_seen_at", "user_id", reqContext.UserID)
err := h.userService.UpdateLastSeenAt(mContext.Req.Context(), &user.UpdateUserLastSeenAtCommand{UserID: reqContext.UserID, OrgID: reqContext.OrgID})
if err != nil && !errors.Is(err, user.ErrLastSeenUpToDate) {
reqContext.Logger.Error("Failed to update last_seen_at", "error", err)
}
}
}
next.ServeHTTP(w, r)
})
}
func (h *ContextHandler) initContextWithAnonymousUser(reqContext *contextmodel.ReqContext) bool {
_, span := h.tracer.Start(reqContext.Req.Context(), "initContextWithAnonymousUser")
defer span.End()
if !h.Cfg.AnonymousEnabled {
return false
}
getOrg := org.GetOrgByNameQuery{Name: h.Cfg.AnonymousOrgName}
orga, err := h.orgService.GetByName(reqContext.Req.Context(), &getOrg)
if err != nil {
reqContext.Logger.Error("Anonymous access organization error.", "org_name", h.Cfg.AnonymousOrgName, "error", err)
return false
}
httpReqCopy := &http.Request{}
if reqContext.Req != nil && reqContext.Req.Header != nil {
// avoid r.HTTPRequest.Clone(context.Background()) as we do not require a full clone
httpReqCopy.Header = reqContext.Req.Header.Clone()
httpReqCopy.RemoteAddr = reqContext.Req.RemoteAddr
}
go func() {
defer func() {
if err := recover(); err != nil {
reqContext.Logger.Warn("tag anon session panic", "err", err)
}
}()
if err := h.anonDeviceService.TagDevice(context.Background(), httpReqCopy, anonymous.AnonDevice); err != nil {
reqContext.Logger.Warn("Failed to tag anonymous session", "error", err)
}
}()
reqContext.IsSignedIn = false
reqContext.AllowAnonymous = true
reqContext.SignedInUser = &user.SignedInUser{IsAnonymous: true}
reqContext.OrgRole = org.RoleType(h.Cfg.AnonymousOrgRole)
reqContext.OrgID = orga.ID
reqContext.OrgName = orga.Name
return true
}
func (h *ContextHandler) getPrefixedAPIKey(ctx context.Context, keyString string) (*apikey.APIKey, error) {
// prefixed decode key
decoded, err := satokengen.Decode(keyString)
if err != nil {
return nil, err
}
hash, err := decoded.Hash()
if err != nil {
return nil, err
}
return h.apiKeyService.GetAPIKeyByHash(ctx, hash)
}
func (h *ContextHandler) getAPIKey(ctx context.Context, keyString string) (*apikey.APIKey, error) {
decoded, err := apikeygen.Decode(keyString)
if err != nil {
return nil, err
}
// fetch key
keyQuery := apikey.GetByNameQuery{KeyName: decoded.Name, OrgID: decoded.OrgId}
key, err := h.apiKeyService.GetApiKeyByName(ctx, &keyQuery)
if err != nil {
return nil, err
}
// validate api key
isValid, err := apikeygen.IsValid(decoded, key.Key)
if err != nil {
return nil, err
}
if !isValid {
return nil, apikeygen.ErrInvalidApiKey
}
return key, nil
}
func (h *ContextHandler) initContextWithAPIKey(reqContext *contextmodel.ReqContext) bool {
header := reqContext.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
}
_, span := h.tracer.Start(reqContext.Req.Context(), "initContextWithAPIKey")
defer span.End()
var (
apiKey *apikey.APIKey
errKey error
)
if strings.HasPrefix(keyString, satokengen.GrafanaPrefix) {
apiKey, errKey = h.getPrefixedAPIKey(reqContext.Req.Context(), keyString) // decode prefixed key
} else {
apiKey, errKey = h.getAPIKey(reqContext.Req.Context(), keyString) // decode legacy api key
}
if errKey != nil {
status := http.StatusInternalServerError
if errors.Is(errKey, apikeygen.ErrInvalidApiKey) {
status = http.StatusUnauthorized
}
// this is when the getPrefixAPIKey return error form the apikey package instead of the apikeygen
// when called in the sqlx store methods
if errors.Is(errKey, apikey.ErrInvalid) {
status = http.StatusUnauthorized
}
reqContext.JsonApiErr(status, InvalidAPIKey, errKey)
return true
}
// check for expiration
getTime := h.GetTime
if getTime == nil {
getTime = time.Now
}
if apiKey.Expires != nil && *apiKey.Expires <= getTime().Unix() {
reqContext.JsonApiErr(http.StatusUnauthorized, "Expired API key", nil)
return true
}
if apiKey.IsRevoked != nil && *apiKey.IsRevoked {
reqContext.JsonApiErr(http.StatusUnauthorized, "Revoked token", nil)
return true
}
// non-blocking update api_key last used date
go func(id int64) {
defer func() {
if err := recover(); err != nil {
reqContext.Logger.Error("api key authentication panic", "err", err)
}
}()
if err := h.apiKeyService.UpdateAPIKeyLastUsedDate(context.Background(), id); err != nil {
reqContext.Logger.Warn("failed to update last use date for api key", "id", id)
}
}(apiKey.ID)
if apiKey.ServiceAccountId == nil || *apiKey.ServiceAccountId < 1 { //There is no service account attached to the apikey
// Use the old APIkey method. This provides backwards compatibility.
// will probably have to be supported for a long time.
reqContext.SignedInUser = &user.SignedInUser{}
reqContext.OrgRole = apiKey.Role
reqContext.ApiKeyID = apiKey.ID
reqContext.OrgID = apiKey.OrgID
reqContext.IsSignedIn = true
return true
}
//There is a service account attached to the API key
//Use service account linked to API key as the signed in user
querySignedInUser := user.GetSignedInUserQuery{UserID: *apiKey.ServiceAccountId, OrgID: apiKey.OrgID}
querySignedInUserResult, err := h.userService.GetSignedInUserWithCacheCtx(reqContext.Req.Context(), &querySignedInUser)
if err != nil {
reqContext.Logger.Error(
"Failed to link API key to service account in",
"id", querySignedInUser.UserID,
"org", querySignedInUser.OrgID,
"err", err,
)
reqContext.JsonApiErr(http.StatusInternalServerError, "Unable to link API key to service account", err)
return true
}
// disabled service accounts are not allowed to access the API
if querySignedInUserResult.IsDisabled {
reqContext.JsonApiErr(http.StatusUnauthorized, "Service account is disabled", nil)
return true
}
reqContext.IsSignedIn = true
reqContext.SignedInUser = querySignedInUserResult
return true
}
func (h *ContextHandler) initContextWithBasicAuth(reqContext *contextmodel.ReqContext, orgID int64) bool {
if !h.Cfg.BasicAuthEnabled {
return false
}
header := reqContext.Req.Header.Get("Authorization")
if header == "" {
return false
}
_, span := h.tracer.Start(reqContext.Req.Context(), "initContextWithBasicAuth")
defer span.End()
username, password, err := util.DecodeBasicAuthHeader(header)
if err != nil {
reqContext.JsonApiErr(401, "Invalid Basic Auth Header", err)
return true
}
authQuery := login.LoginUserQuery{
Username: username,
Password: password,
Cfg: h.Cfg,
}
if err := h.authenticator.AuthenticateUser(reqContext.Req.Context(), &authQuery); err != nil {
reqContext.Logger.Debug(
"Failed to authorize the user",
"username", username,
"err", err,
)
if errors.Is(err, user.ErrUserNotFound) {
err = login.ErrInvalidCredentials
}
reqContext.JsonApiErr(401, InvalidUsernamePassword, err)
return true
}
usr := authQuery.User
query := user.GetSignedInUserQuery{UserID: usr.ID, OrgID: orgID}
queryResult, err := h.userService.GetSignedInUserWithCacheCtx(reqContext.Req.Context(), &query)
if err != nil {
reqContext.Logger.Error(
"Failed at user signed in",
"id", usr.ID,
"org", orgID,
)
reqContext.JsonApiErr(401, InvalidUsernamePassword, err)
return true
}
reqContext.SignedInUser = queryResult
reqContext.IsSignedIn = true
return true
}
func (h *ContextHandler) initContextWithToken(reqContext *contextmodel.ReqContext, orgID int64) bool {
if h.Cfg.LoginCookieName == "" {
return false
}
rawToken := reqContext.GetCookie(h.Cfg.LoginCookieName)
if rawToken == "" {
return false
}
ctx, span := h.tracer.Start(reqContext.Req.Context(), "initContextWithToken")
defer span.End()
token, err := h.AuthTokenService.LookupToken(ctx, rawToken)
if err != nil {
reqContext.Logger.Warn("failed to look up session from cookie", "error", err)
if errors.Is(err, auth.ErrInvalidSessionToken) {
// Burn the cookie in case of invalid or revoked token
reqContext.Resp.Before(h.deleteInvalidCookieEndOfRequestFunc(reqContext))
}
reqContext.LookupTokenErr = err
return false
}
if h.features.IsEnabled(featuremgmt.FlagClientTokenRotation) {
if token.NeedsRotation(time.Duration(h.Cfg.TokenRotationIntervalMinutes) * time.Minute) {
reqContext.LookupTokenErr = authn.ErrTokenNeedsRotation.Errorf("token needs rotation")
return true
}
}
query := user.GetSignedInUserQuery{UserID: token.UserId, OrgID: orgID}
queryResult, err := h.userService.GetSignedInUserWithCacheCtx(ctx, &query)
if err != nil {
reqContext.Logger.Error("Failed to get user with id", "userId", token.UserId, "error", err)
return false
}
if h.features.IsEnabled(featuremgmt.FlagAccessTokenExpirationCheck) {
// Check whether the logged in User has a token (whether the User used an OAuth provider to login)
oauthToken, exists, _ := h.oauthTokenService.HasOAuthEntry(ctx, queryResult)
if exists {
if h.hasAccessTokenExpired(oauthToken) {
reqContext.Logger.Info("access token expired", "userId", query.UserID, "expiry", fmt.Sprintf("%v", oauthToken.OAuthExpiry))
// If the User doesn't have a refresh_token or refreshing the token was unsuccessful then log out the User and invalidate the OAuth tokens
if err = h.oauthTokenService.TryTokenRefresh(ctx, oauthToken); err != nil {
if !errors.Is(err, oauthtoken.ErrNoRefreshTokenFound) {
reqContext.Logger.Error("could not fetch a new access token", "userId", oauthToken.UserId, "error", err)
}
reqContext.Resp.Before(h.deleteInvalidCookieEndOfRequestFunc(reqContext))
if err = h.oauthTokenService.InvalidateOAuthTokens(ctx, oauthToken); err != nil {
reqContext.Logger.Error("could not invalidate OAuth tokens", "userId", oauthToken.UserId, "error", err)
}
err = h.AuthTokenService.RevokeToken(ctx, token, false)
if err != nil && !errors.Is(err, auth.ErrUserTokenNotFound) {
reqContext.Logger.Error("failed to revoke auth token", "error", err)
}
return false
}
}
}
}
reqContext.SignedInUser = queryResult
reqContext.IsSignedIn = true
reqContext.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.
reqContext.Resp.Before(h.rotateEndOfRequestFunc(reqContext))
return true
}
func (h *ContextHandler) deleteInvalidCookieEndOfRequestFunc(reqContext *contextmodel.ReqContext) web.BeforeFunc {
return func(w web.ResponseWriter) {
if h.features.IsEnabled(featuremgmt.FlagClientTokenRotation) {
@ -599,198 +153,6 @@ func (h *ContextHandler) deleteInvalidCookieEndOfRequestFunc(reqContext *context
}
}
func (h *ContextHandler) rotateEndOfRequestFunc(reqContext *contextmodel.ReqContext) web.BeforeFunc {
return func(w web.ResponseWriter) {
if h.features.IsEnabled(featuremgmt.FlagClientTokenRotation) {
return
}
// 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(reqContext.Context.Req.Context().Err(), context.Canceled) {
return
}
// if there is no user token attached to reqContext, skip.
if reqContext.UserToken == nil {
return
}
ctx, span := h.tracer.Start(reqContext.Req.Context(), "rotateEndOfRequestFunc")
defer span.End()
addr := reqContext.RemoteAddr()
ip, err := network.GetIPFromAddress(addr)
if err != nil {
reqContext.Logger.Debug("Failed to get client IP address", "addr", addr, "err", err)
ip = nil
}
rotated, newToken, err := h.AuthTokenService.TryRotateToken(ctx, reqContext.UserToken, ip, reqContext.Req.UserAgent())
if err != nil {
reqContext.Logger.Error("Failed to rotate token", "error", err)
return
}
if rotated {
reqContext.UserToken = newToken
authn.WriteSessionCookie(reqContext.Resp, h.Cfg, newToken)
}
}
}
func (h *ContextHandler) initContextWithRenderAuth(reqContext *contextmodel.ReqContext) bool {
key := reqContext.GetCookie("renderKey")
if key == "" {
return false
}
ctx, span := h.tracer.Start(reqContext.Req.Context(), "initContextWithRenderAuth")
defer span.End()
renderUser, exists := h.RenderService.GetRenderUser(reqContext.Req.Context(), key)
if !exists {
reqContext.JsonApiErr(401, "Invalid Render Key", nil)
return true
}
reqContext.SignedInUser = &user.SignedInUser{
OrgID: renderUser.OrgID,
UserID: renderUser.UserID,
OrgRole: org.RoleType(renderUser.OrgRole),
}
// UserID can be 0 for background tasks and, in this case, there is no user info to retrieve
if renderUser.UserID != 0 {
query := user.GetSignedInUserQuery{UserID: renderUser.UserID, OrgID: renderUser.OrgID}
queryResult, err := h.userService.GetSignedInUserWithCacheCtx(ctx, &query)
if err == nil {
reqContext.SignedInUser = queryResult
}
}
reqContext.IsSignedIn = true
reqContext.IsRenderCall = true
reqContext.LastSeenAt = time.Now()
return true
}
func logUserIn(reqContext *contextmodel.ReqContext, 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(reqContext, 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 (h *ContextHandler) handleError(ctx *contextmodel.ReqContext, err error, statusCode int, cb func(error)) {
details := err
var e authproxy.Error
if errors.As(err, &e) {
details = e.DetailsError
}
ctx.Handle(h.Cfg, statusCode, err.Error(), details)
if cb != nil {
cb(details)
}
}
func (h *ContextHandler) initContextWithAuthProxy(reqContext *contextmodel.ReqContext, orgID int64) bool {
username := reqContext.Req.Header.Get(h.Cfg.AuthProxyHeaderName)
logger := log.New("auth.proxy")
// Bail if auth proxy is not enabled
if !h.authProxy.IsEnabled() {
return false
}
// If there is no header - we can't move forward
if !h.authProxy.HasHeader(reqContext) {
return false
}
_, span := h.tracer.Start(reqContext.Req.Context(), "initContextWithAuthProxy")
defer span.End()
// Check if allowed continuing with this IP
if err := h.authProxy.IsAllowedIP(reqContext.Req.RemoteAddr); err != nil {
h.handleError(reqContext, err, 407, func(details error) {
logger.Error("Failed to check whitelisted IP addresses", "message", err.Error(), "error", details)
})
return true
}
id, err := logUserIn(reqContext, h.authProxy, username, logger, false)
if err != nil {
h.handleError(reqContext, err, 407, nil)
return true
}
logger.Debug("Got user ID, getting full user info", "userID", id)
user, err := h.authProxy.GetSignedInUser(id, orgID)
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 := h.authProxy.RemoveUserFromCache(reqContext); err != nil {
if !errors.Is(err, remotecache.ErrCacheItemNotFound) {
logger.Error("Got unexpected error when removing user from auth cache", "error", err)
}
}
id, err = logUserIn(reqContext, h.authProxy, username, logger, true)
if err != nil {
h.handleError(reqContext, err, 407, nil)
return true
}
user, err = h.authProxy.GetSignedInUser(id, orgID)
if err != nil {
h.handleError(reqContext, err, 407, nil)
return true
}
}
logger.Debug("Successfully got user info", "userID", user.UserID, "username", user.Login)
// Add user info to context
reqContext.SignedInUser = user
reqContext.IsSignedIn = true
// Remember user data in cache
if err := h.authProxy.Remember(reqContext, id); err != nil {
h.handleError(reqContext, err, 500, func(details error) {
logger.Error(
"Failed to store user in cache",
"username", username,
"message", err.Error(),
"error", details,
)
})
return true
}
return true
}
type authHTTPHeaderListContextKey struct{}
var authHTTPHeaderListKey = authHTTPHeaderListContextKey{}
@ -840,16 +202,3 @@ func AuthHTTPHeaderListFromContext(c context.Context) *AuthHTTPHeaderList {
}
return nil
}
func (h *ContextHandler) hasAccessTokenExpired(token *login.UserAuth) bool {
if token.OAuthExpiry.IsZero() {
return false
}
getTime := h.GetTime
if getTime == nil {
getTime = time.Now
}
return token.OAuthExpiry.Round(0).Add(-oauthtoken.ExpiryDelta).Before(getTime())
}

View File

@ -1,123 +1,181 @@
package contexthandler
package contexthandler_test
import (
"context"
"net"
"net/http"
"net/http/httptest"
"errors"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend/gtime"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/auth/authtest"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/authn/authntest"
"github.com/grafana/grafana/pkg/services/contexthandler"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web/webtest"
)
func TestDontRotateTokensOnCancelledRequests(t *testing.T) {
ctxHdlr := getContextHandler(t)
tryRotateCallCount := 0
ctxHdlr.AuthTokenService = &authtest.FakeUserAuthTokenService{
TryRotateTokenProvider: func(ctx context.Context, token *auth.UserToken, clientIP net.IP,
userAgent string) (bool, *auth.UserToken, error) {
tryRotateCallCount++
return false, nil, nil
},
}
func TestContextHandler(t *testing.T) {
t.Run("should set auth error if authentication was unsuccessful", func(t *testing.T) {
handler := contexthandler.ProvideService(
setting.NewCfg(),
tracing.NewFakeTracer(),
featuremgmt.WithFeatures(),
&authntest.FakeService{ExpectedErr: errors.New("some error")},
)
ctx, cancel := context.WithCancel(context.Background())
reqContext, _, err := initTokenRotationScenario(ctx, t, ctxHdlr)
require.NoError(t, err)
reqContext.UserToken = &auth.UserToken{AuthToken: "oldtoken"}
server := webtest.NewServer(t, routing.NewRouteRegister())
server.Mux.Use(handler.Middleware)
server.Mux.Get("/api/handler", func(c *contextmodel.ReqContext) {
require.False(t, c.IsSignedIn)
require.EqualValues(t, &user.SignedInUser{Permissions: map[int64]map[string][]string{}}, c.SignedInUser)
require.Error(t, c.LookupTokenErr)
})
fn := ctxHdlr.rotateEndOfRequestFunc(reqContext)
cancel()
fn(reqContext.Resp)
assert.Equal(t, 0, tryRotateCallCount, "Token rotation was attempted")
}
func TestTokenRotationAtEndOfRequest(t *testing.T) {
ctxHdlr := getContextHandler(t)
ctxHdlr.AuthTokenService = &authtest.FakeUserAuthTokenService{
TryRotateTokenProvider: func(ctx context.Context, token *auth.UserToken, clientIP net.IP,
userAgent string) (bool, *auth.UserToken, error) {
newToken, err := util.RandomHex(16)
require.NoError(t, err)
token.AuthToken = newToken
return true, token, nil
},
}
reqContext, rr, err := initTokenRotationScenario(context.Background(), t, ctxHdlr)
require.NoError(t, err)
reqContext.UserToken = &auth.UserToken{AuthToken: "oldtoken"}
ctxHdlr.rotateEndOfRequestFunc(reqContext)(reqContext.Resp)
foundLoginCookie := false
// nolint:bodyclose
resp := rr.Result()
t.Cleanup(func() {
err := resp.Body.Close()
assert.NoError(t, err)
_, err := server.Send(server.NewGetRequest("/api/handler"))
require.NoError(t, err)
})
for _, c := range resp.Cookies() {
if c.Name == "login_token" {
foundLoginCookie = true
require.NotEqual(t, reqContext.UserToken.AuthToken, c.Value, "Auth token is still the same")
t.Run("should set identity on successful authentication", func(t *testing.T) {
identity := &authn.Identity{ID: authn.NamespacedID(authn.NamespaceUser, 1), OrgID: 1}
handler := contexthandler.ProvideService(
setting.NewCfg(),
tracing.NewFakeTracer(),
featuremgmt.WithFeatures(),
&authntest.FakeService{ExpectedIdentity: identity},
)
server := webtest.NewServer(t, routing.NewRouteRegister())
server.Mux.Use(handler.Middleware)
server.Mux.Get("/api/handler", func(c *contextmodel.ReqContext) {
require.True(t, c.IsSignedIn)
require.EqualValues(t, identity.SignedInUser(), c.SignedInUser)
require.NoError(t, c.LookupTokenErr)
})
_, err := server.Send(server.NewGetRequest("/api/handler"))
require.NoError(t, err)
})
t.Run("should not set IsSignedIn on anonymous identity", func(t *testing.T) {
identity := &authn.Identity{IsAnonymous: true, OrgID: 1}
handler := contexthandler.ProvideService(
setting.NewCfg(),
tracing.NewFakeTracer(),
featuremgmt.WithFeatures(),
&authntest.FakeService{ExpectedIdentity: identity},
)
server := webtest.NewServer(t, routing.NewRouteRegister())
server.Mux.Use(handler.Middleware)
server.Mux.Get("/api/handler", func(c *contextmodel.ReqContext) {
require.False(t, c.IsSignedIn)
require.EqualValues(t, identity.SignedInUser(), c.SignedInUser)
require.NoError(t, c.LookupTokenErr)
})
_, err := server.Send(server.NewGetRequest("/api/handler"))
require.NoError(t, err)
})
t.Run("should set IsRenderCall when authenticated by render client", func(t *testing.T) {
identity := &authn.Identity{OrgID: 1, AuthenticatedBy: login.RenderModule}
handler := contexthandler.ProvideService(
setting.NewCfg(),
tracing.NewFakeTracer(),
featuremgmt.WithFeatures(),
&authntest.FakeService{ExpectedIdentity: identity},
)
server := webtest.NewServer(t, routing.NewRouteRegister())
server.Mux.Use(handler.Middleware)
server.Mux.Get("/api/handler", func(c *contextmodel.ReqContext) {
require.True(t, c.IsSignedIn)
require.True(t, c.IsRenderCall)
require.EqualValues(t, identity.SignedInUser(), c.SignedInUser)
require.NoError(t, c.LookupTokenErr)
})
_, err := server.Send(server.NewGetRequest("/api/handler"))
require.NoError(t, err)
})
t.Run("should delete session cookie on invalid session", func(t *testing.T) {
handler := contexthandler.ProvideService(
setting.NewCfg(),
tracing.NewFakeTracer(),
featuremgmt.WithFeatures(),
&authntest.FakeService{ExpectedErr: auth.ErrInvalidSessionToken},
)
server := webtest.NewServer(t, routing.NewRouteRegister())
server.Mux.Use(handler.Middleware)
server.Mux.Get("/api/handler", func(c *contextmodel.ReqContext) {})
res, err := server.Send(server.NewGetRequest("/api/handler"))
require.NoError(t, err)
cookies := res.Cookies()
require.Len(t, cookies, 1)
require.Equal(t, cookies[0].String(), "grafana_session_expiry=; Path=/; Max-Age=0")
require.NoError(t, res.Body.Close())
})
t.Run("should delete session cookie when oauth token refresh failed", func(t *testing.T) {
handler := contexthandler.ProvideService(
setting.NewCfg(),
tracing.NewFakeTracer(),
featuremgmt.WithFeatures(),
&authntest.FakeService{ExpectedErr: authn.ErrExpiredAccessToken.Errorf("")},
)
server := webtest.NewServer(t, routing.NewRouteRegister())
server.Mux.Use(handler.Middleware)
server.Mux.Get("/api/handler", func(c *contextmodel.ReqContext) {})
res, err := server.Send(server.NewGetRequest("/api/handler"))
require.NoError(t, err)
cookies := res.Cookies()
require.Len(t, cookies, 1)
require.Equal(t, cookies[0].String(), "grafana_session_expiry=; Path=/; Max-Age=0")
require.NoError(t, res.Body.Close())
})
t.Run("should store auth header in context", func(t *testing.T) {
cfg := setting.NewCfg()
cfg.JWTAuthEnabled = true
cfg.JWTAuthHeaderName = "jwt-header"
cfg.AuthProxyEnabled = true
cfg.AuthProxyHeaderName = "proxy-header"
cfg.AuthProxyHeaders = map[string]string{
"name": "proxy-header-name",
}
}
assert.True(t, foundLoginCookie, "Could not find cookie")
}
func initTokenRotationScenario(ctx context.Context, t *testing.T, ctxHdlr *ContextHandler) (
*contextmodel.ReqContext, *httptest.ResponseRecorder, error) {
t.Helper()
ctxHdlr.Cfg.LoginCookieName = "login_token"
var err error
ctxHdlr.Cfg.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 := &contextmodel.ReqContext{
Context: &web.Context{Req: 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(web.BeforeFunc) {}
func (mw mockWriter) Push(target string, opts *http.PushOptions) error {
return nil
}
func (mw mockWriter) CloseNotify() <-chan bool {
return make(<-chan bool)
}
func (mw mockWriter) Unwrap() http.ResponseWriter {
return mw
handler := contexthandler.ProvideService(
cfg,
tracing.NewFakeTracer(),
featuremgmt.WithFeatures(),
&authntest.FakeService{ExpectedIdentity: &authn.Identity{}},
)
server := webtest.NewServer(t, routing.NewRouteRegister())
server.Mux.Use(handler.Middleware)
server.Mux.Get("/api/handler", func(c *contextmodel.ReqContext) {
list := contexthandler.AuthHTTPHeaderListFromContext(c.Req.Context())
require.NotNil(t, list)
assert.Contains(t, list.Items, "jwt-header")
assert.Contains(t, list.Items, "proxy-header")
assert.Contains(t, list.Items, "proxy-header-name")
assert.Contains(t, list.Items, "Authorization")
})
_, err := server.Send(server.NewGetRequest("/api/handler"))
require.NoError(t, err)
})
}

View File

@ -101,7 +101,7 @@ func TestIntegrationAMConfigAccess(t *testing.T) {
desc: "un-authenticated request should fail",
url: "http://%s/api/alertmanager/grafana/config/api/v1/alerts",
expStatus: http.StatusUnauthorized,
expBody: `{"message":"Unauthorized"}`,
expBody: `"message":"Unauthorized"`,
},
{
desc: "viewer request should fail",
@ -171,7 +171,7 @@ func TestIntegrationAMConfigAccess(t *testing.T) {
desc: "un-authenticated request should fail",
url: "http://%s/api/alertmanager/grafana/config/api/v1/alerts",
expStatus: http.StatusUnauthorized,
expBody: `{"message": "Unauthorized"}`,
expBody: `{"extra":null,"message":"Unauthorized","messageId":"auth.unauthorized","statusCode":401,"traceID":""}`,
},
{
desc: "viewer request should succeed",
@ -235,7 +235,7 @@ func TestIntegrationAMConfigAccess(t *testing.T) {
desc: "un-authenticated request should fail",
url: "http://%s/api/alertmanager/grafana/config/api/v2/silences",
expStatus: http.StatusUnauthorized,
expBody: `{"message":"Unauthorized"}`,
expBody: `"message":"Unauthorized"`,
},
{
desc: "viewer request should fail",
@ -286,7 +286,7 @@ func TestIntegrationAMConfigAccess(t *testing.T) {
desc: "un-authenticated request should fail",
url: "http://%s/api/alertmanager/grafana/api/v2/silences",
expStatus: http.StatusUnauthorized,
expBody: `{"message": "Unauthorized"}`,
expBody: `"message": "Unauthorized"`,
},
{
desc: "viewer request should succeed",
@ -341,7 +341,7 @@ func TestIntegrationAMConfigAccess(t *testing.T) {
desc: "un-authenticated request should fail",
url: "http://%s/api/alertmanager/grafana/api/v2/silence/%s",
expStatus: http.StatusUnauthorized,
expBody: `{"message":"Unauthorized"}`,
expBody: `"message":"Unauthorized"`,
},
{
desc: "viewer request should fail",
@ -423,7 +423,7 @@ func TestIntegrationAlertAndGroupsQuery(t *testing.T) {
b, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
require.JSONEq(t, `{"message": "Unauthorized"}`, string(b))
require.Contains(t, string(b), `"message":"Unauthorized"`)
}
// Create a user to make authenticated requests
@ -447,11 +447,11 @@ func TestIntegrationAlertAndGroupsQuery(t *testing.T) {
})
b, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
var res map[string]interface{}
require.NoError(t, json.Unmarshal(b, &res))
require.Equal(t, "invalid username or password", res["message"])
assert.Equal(t, "Invalid username or password", res["message"])
}
// When there are no alerts available, it returns an empty list.

View File

@ -199,15 +199,6 @@ func CreateGrafDir(t *testing.T, opts ...GrafanaOpts) (string, string) {
_, err = serverSect.NewKey("static_root_path", publicDir)
require.NoError(t, err)
authSect, err := cfg.NewSection("auth")
require.NoError(t, err)
authBrokerState := "false"
if len(opts) > 0 && opts[0].AuthBrokerEnabled {
authBrokerState = "true"
}
_, err = authSect.NewKey("broker", authBrokerState)
require.NoError(t, err)
anonSect, err := cfg.NewSection("auth.anonymous")
require.NoError(t, err)
_, err = anonSect.NewKey("enabled", "true")