mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
5d8e6aa162
commit
e53e22ef2a
@ -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)}}
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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"))
|
||||
)
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
})
|
||||
}
|
@ -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())
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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")
|
||||
|
Loading…
Reference in New Issue
Block a user