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:
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package anonymous
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
TagSession(context.Context, *http.Request) error
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user