diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index bd44bd27723..14a668c0378 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -91,6 +91,7 @@ Alpha features might be changed or removed without prior notice. | `elasticsearchBackendMigration` | Use Elasticsearch as backend data source | | `secureSocksDatasourceProxy` | Enable secure socks tunneling for supported core datasources | | `authnService` | Use new auth service to perform authentication | +| `sessionRemoteCache` | Enable using remote cache for user sessions | ## Development feature toggles diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 518bf5e1c87..d6bcdf70bb7 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -83,4 +83,5 @@ export interface FeatureToggles { elasticsearchBackendMigration?: boolean; secureSocksDatasourceProxy?: boolean; authnService?: boolean; + sessionRemoteCache?: boolean; } diff --git a/pkg/services/auth/authimpl/auth_token.go b/pkg/services/auth/authimpl/auth_token.go index 7bf1d8bf332..991143f538a 100644 --- a/pkg/services/auth/authimpl/auth_token.go +++ b/pkg/services/auth/authimpl/auth_token.go @@ -4,30 +4,43 @@ import ( "context" "crypto/sha256" "encoding/hex" + "errors" "net" "strings" "time" "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/serverlock" "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) -const urgentRotateTime = 1 * time.Minute +const ( + ttl = 15 * time.Second + urgentRotateTime = 1 * time.Minute +) var getTime = time.Now -func ProvideUserAuthTokenService(sqlStore db.DB, cfg *setting.Cfg, serverLockService *serverlock.ServerLockService, quotaService quota.Service) (*UserAuthTokenService, error) { +func ProvideUserAuthTokenService(sqlStore db.DB, + serverLockService *serverlock.ServerLockService, + remoteCache *remotecache.RemoteCache, + features *featuremgmt.FeatureManager, + quotaService quota.Service, + cfg *setting.Cfg) (*UserAuthTokenService, error) { s := &UserAuthTokenService{ sqlStore: sqlStore, serverLockService: serverLockService, cfg: cfg, log: log.New("auth"), + remoteCache: remoteCache, + features: features, } defaultLimits, err := readQuotaConfig(cfg) @@ -43,6 +56,8 @@ func ProvideUserAuthTokenService(sqlStore db.DB, cfg *setting.Cfg, serverLockSer return s, err } + remotecache.Register(auth.UserToken{}) + return s, nil } @@ -51,6 +66,8 @@ type UserAuthTokenService struct { serverLockService *serverlock.ServerLockService cfg *setting.Cfg log log.Logger + remoteCache *remotecache.RemoteCache + features *featuremgmt.FeatureManager } func (s *UserAuthTokenService) CreateToken(ctx context.Context, user *user.User, clientIP net.IP, userAgent string) (*auth.UserToken, error) { @@ -101,7 +118,52 @@ func (s *UserAuthTokenService) CreateToken(ctx context.Context, user *user.User, return &userToken, err } +func (s *UserAuthTokenService) lookupTokenWithCache(ctx context.Context, unhashedToken string) (*auth.UserToken, error) { + hashedToken := hashToken(unhashedToken) + cacheKey := "auth_token:" + hashedToken + + session, errCache := s.remoteCache.Get(ctx, cacheKey) + if errCache == nil { + token := session.(auth.UserToken) + return &token, nil + } else { + if errors.Is(errCache, remotecache.ErrCacheItemNotFound) { + s.log.Debug("user auth token not found in cache", + "cacheKey", cacheKey) + } else { + s.log.Warn("failed to get user auth token from cache", + "cacheKey", cacheKey, "error", errCache) + } + } + + token, err := s.lookupToken(ctx, unhashedToken) + if err != nil { + return nil, err + } + + // only cache tokens until their near rotation time + // Near rotation time = tokens last rotation plus the rotation interval minus 2 ttl (=30s by default) + nextRotation := time.Unix(token.RotatedAt, 0). + Add(-2 * ttl). // subtract 2 ttl to make sure we don't cache tokens that are about to expire + Add(time.Duration(s.cfg.TokenRotationIntervalMinutes) * time.Minute) + if now := getTime(); now.Before(nextRotation) { + if err := s.remoteCache.Set(ctx, cacheKey, *token, ttl); err != nil { + s.log.Warn("could not cache token", "error", err, "cacheKey", cacheKey, "userId", token.UserId) + } + } + + return token, nil +} + func (s *UserAuthTokenService) LookupToken(ctx context.Context, unhashedToken string) (*auth.UserToken, error) { + if s.features != nil && s.features.IsEnabled(featuremgmt.FlagSessionRemoteCache) { + return s.lookupTokenWithCache(ctx, unhashedToken) + } + + return s.lookupToken(ctx, unhashedToken) +} + +func (s *UserAuthTokenService) lookupToken(ctx context.Context, unhashedToken string) (*auth.UserToken, error) { hashedToken := hashToken(unhashedToken) var model userAuthToken var exists bool diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 10035d8db75..5f6d9ddbf76 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -378,5 +378,10 @@ var ( Description: "Use new auth service to perform authentication", State: FeatureStateAlpha, }, + { + Name: "sessionRemoteCache", + Description: "Enable using remote cache for user sessions", + State: FeatureStateAlpha, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 31c794f62a2..36e3e91e96d 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -274,4 +274,8 @@ const ( // FlagAuthnService // Use new auth service to perform authentication FlagAuthnService = "authnService" + + // FlagSessionRemoteCache + // Enable using remote cache for user sessions + FlagSessionRemoteCache = "sessionRemoteCache" ) diff --git a/pkg/services/quota/quotaimpl/quota_test.go b/pkg/services/quota/quotaimpl/quota_test.go index 54d5c127cd1..d8ffa2432ae 100644 --- a/pkg/services/quota/quotaimpl/quota_test.go +++ b/pkg/services/quota/quotaimpl/quota_test.go @@ -465,7 +465,7 @@ func getQuotaBySrvTargetScope(t *testing.T, quotaService quota.Service, srv quot func setupEnv(t *testing.T, sqlStore *sqlstore.SQLStore, b bus.Bus, quotaService quota.Service) { _, err := apikeyimpl.ProvideService(sqlStore, sqlStore.Cfg, quotaService) require.NoError(t, err) - _, err = authimpl.ProvideUserAuthTokenService(sqlStore, sqlStore.Cfg, nil, quotaService) + _, err = authimpl.ProvideUserAuthTokenService(sqlStore, nil, nil, featuremgmt.WithFeatures(), quotaService, sqlStore.Cfg) require.NoError(t, err) _, err = dashboardStore.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) require.NoError(t, err)