Authn: Anon session service (#63052)

* add anon sessions package

* add usage stat fn

* implement count for cache

* add anonservice to authn broker

* lint

* add tests for remote cache count

* move anon service to services

* wrap tagging in goroutine

* make func used
This commit is contained in:
Jo
2023-02-21 16:21:18 +01:00
committed by GitHub
parent 56c8661929
commit ff78103a24
21 changed files with 331 additions and 56 deletions
+91
View File
@@ -0,0 +1,91 @@
package anonimpl
import (
"context"
"encoding/hex"
"fmt"
"hash/fnv"
"net/http"
"strings"
"time"
"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/usagestats"
"github.com/grafana/grafana/pkg/web"
)
const thirtyDays = 30 * 24 * time.Hour
const anonCachePrefix = "anon-session"
type AnonSession struct {
ip string
userAgent string
}
func (a *AnonSession) Key() (string, error) {
key := strings.Builder{}
key.WriteString(a.ip)
key.WriteString(a.userAgent)
hash := fnv.New128a()
if _, err := hash.Write([]byte(key.String())); err != nil {
return "", fmt.Errorf("failed to write to hash: %w", err)
}
return strings.Join([]string{anonCachePrefix, hex.EncodeToString(hash.Sum(nil))}, ":"), nil
}
type AnonSessionService struct {
remoteCache remotecache.CacheStorage
log log.Logger
}
func ProvideAnonymousSessionService(remoteCache remotecache.CacheStorage, usageStats usagestats.Service) *AnonSessionService {
a := &AnonSessionService{
remoteCache: remoteCache,
log: log.New("anonymous-session-service"),
}
usageStats.RegisterMetricsFunc(a.UsageStatFn)
return a
}
func (a *AnonSessionService) UsageStatFn(ctx context.Context) (map[string]interface{}, error) {
sessionCount, err := a.remoteCache.Count(ctx, anonCachePrefix)
if err != nil {
return nil, nil
}
return map[string]interface{}{
"stats.anonymous.session.count": sessionCount,
}, nil
}
func (a *AnonSessionService) TagSession(ctx context.Context, httpReq *http.Request) error {
addr := web.RemoteAddr(httpReq)
ip, err := network.GetIPFromAddress(addr)
if err != nil {
a.log.Debug("failed to parse ip from address", "addr", addr)
return nil
}
clientIPStr := ip.String()
if len(ip) == 0 {
clientIPStr = ""
}
anonSession := &AnonSession{
ip: clientIPStr,
userAgent: httpReq.UserAgent(),
}
key, err := anonSession.Key()
if err != nil {
return err
}
return a.remoteCache.Set(ctx, key, key, thirtyDays)
}
+13
View File
@@ -0,0 +1,13 @@
package anontest
import (
"context"
"net/http"
)
type FakeAnonymousSessionService struct {
}
func (f *FakeAnonymousSessionService) TagSession(ctx context.Context, httpReq *http.Request) error {
return nil
}
+10
View File
@@ -0,0 +1,10 @@
package anonymous
import (
"context"
"net/http"
)
type Service interface {
TagSession(context.Context, *http.Request) error
}
+3 -3
View File
@@ -8,7 +8,6 @@ import (
"github.com/hashicorp/go-multierror"
"go.opentelemetry.io/otel/attribute"
"github.com/grafana/grafana/pkg/infra/kvstore"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/network"
"github.com/grafana/grafana/pkg/infra/remotecache"
@@ -16,6 +15,7 @@ import (
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"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/authn"
@@ -54,7 +54,7 @@ func ProvideService(
apikeyService apikey.Service, userService user.Service,
jwtService auth.JWTVerifierService,
usageStats usagestats.Service,
kvstore kvstore.KVStore,
anonSessionService anonymous.Service,
userProtectionService login.UserProtectionService,
loginAttempts loginattempt.Service, quotaService quota.Service,
authInfoService login.AuthInfoService, renderService rendering.Service,
@@ -83,7 +83,7 @@ func ProvideService(
}
if s.cfg.AnonymousEnabled {
s.RegisterClient(clients.ProvideAnonymous(cfg, orgService, kvstore))
s.RegisterClient(clients.ProvideAnonymous(cfg, orgService, anonSessionService))
}
var proxyClients []authn.ProxyClient
+21 -8
View File
@@ -4,8 +4,8 @@ import (
"context"
"strings"
"github.com/grafana/grafana/pkg/infra/kvstore"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/anonymous"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/setting"
@@ -13,18 +13,20 @@ import (
var _ authn.ContextAwareClient = new(Anonymous)
func ProvideAnonymous(cfg *setting.Cfg, orgService org.Service, _ kvstore.KVStore) *Anonymous {
func ProvideAnonymous(cfg *setting.Cfg, orgService org.Service, anonSessionService anonymous.Service) *Anonymous {
return &Anonymous{
cfg: cfg,
log: log.New("authn.anonymous"),
orgService: orgService,
cfg: cfg,
log: log.New("authn.anonymous"),
orgService: orgService,
anonSessionService: anonSessionService,
}
}
type Anonymous struct {
cfg *setting.Cfg
log log.Logger
orgService org.Service
cfg *setting.Cfg
log log.Logger
orgService org.Service
anonSessionService anonymous.Service
}
func (a *Anonymous) Name() string {
@@ -38,6 +40,17 @@ func (a *Anonymous) Authenticate(ctx context.Context, r *authn.Request) (*authn.
return nil, err
}
go func() {
defer func() {
if err := recover(); err != nil {
a.log.Warn("tag anon session panic", "err", err)
}
}()
if err := a.anonSessionService.TagSession(ctx, r.HTTPRequest); err != nil {
a.log.Warn("Failed to tag anonymous session", "error", err)
}
}()
return &authn.Identity{
IsAnonymous: true,
OrgID: o.ID,
+5 -3
View File
@@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/anonymous/anontest"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/org/orgtest"
@@ -45,9 +46,10 @@ func TestAnonymous_Authenticate(t *testing.T) {
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
c := Anonymous{
cfg: tt.cfg,
log: log.NewNopLogger(),
orgService: &orgtest.FakeOrgService{ExpectedOrg: tt.org, ExpectedError: tt.err},
cfg: tt.cfg,
log: log.NewNopLogger(),
orgService: &orgtest.FakeOrgService{ExpectedOrg: tt.org, ExpectedError: tt.err},
anonSessionService: &anontest.FakeAnonymousSessionService{},
}
identity, err := c.Authenticate(context.Background(), &authn.Request{})
@@ -13,6 +13,7 @@ import (
"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/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"
@@ -116,7 +117,8 @@ func getContextHandler(t *testing.T) *ContextHandler {
return ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc,
renderSvc, sqlStore, tracer, authProxy, loginService, nil, authenticator,
&userService, orgService, nil, featuremgmt.WithFeatures(), &authntest.FakeService{})
&userService, orgService, nil, featuremgmt.WithFeatures(),
&authntest.FakeService{}, &anontest.FakeAnonymousSessionService{})
}
type fakeAuthenticator struct{}
+49 -35
View File
@@ -22,6 +22,7 @@ import (
"github.com/grafana/grafana/pkg/infra/tracing"
loginpkg "github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/middleware/cookies"
"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"
@@ -53,48 +54,50 @@ func ProvideService(cfg *setting.Cfg, tokenService auth.UserTokenService, jwtSer
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,
authnService authn.Service, anonSessionService anonymous.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,
singleflight: new(singleflight.Group),
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,
anonSessionService: anonSessionService,
singleflight: new(singleflight.Group),
}
}
// 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
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
anonSessionService anonymous.Service
// GetTime returns the current time.
// Stubbable by tests.
GetTime func() time.Time
@@ -234,6 +237,17 @@ func (h *ContextHandler) initContextWithAnonymousUser(reqContext *contextmodel.R
return false
}
go func() {
defer func() {
if err := recover(); err != nil {
reqContext.Logger.Warn("tag anon session panic", "err", err)
}
}()
if err := h.anonSessionService.TagSession(reqContext.Req.Context(), reqContext.Req); err != nil {
reqContext.Logger.Warn("Failed to tag anonymous session", "error", err)
}
}()
reqContext.IsSignedIn = false
reqContext.AllowAnonymous = true
reqContext.SignedInUser = &user.SignedInUser{IsAnonymous: true}