diff --git a/models/auth/user_session.go b/models/auth/user_session.go index 475c812322..8179e8e58e 100644 --- a/models/auth/user_session.go +++ b/models/auth/user_session.go @@ -76,6 +76,21 @@ func GetUserSessionsByUserID(ctx context.Context, userID int64) ([]*UserSession, Desc("created_unix").Find(&sessions) } +// CountUserSessionsByUserID returns (total, active) session counts for a user +// without materializing the rows. +func CountUserSessionsByUserID(ctx context.Context, userID int64) (total, active int64, err error) { + e := db.GetEngine(ctx) + total, err = e.Where("user_id = ?", userID).Count(new(UserSession)) + if err != nil { + return 0, 0, err + } + active, err = e.Where("user_id = ? AND logout_unix = 0", userID).Count(new(UserSession)) + if err != nil { + return 0, 0, err + } + return total, active, nil +} + // InvalidateUserSession marks a session as logged out func InvalidateUserSession(ctx context.Context, sessionID string) error { _, err := db.GetEngine(ctx).Where("id = ? AND logout_unix = 0", sessionID). diff --git a/modules/session/virtual.go b/modules/session/virtual.go index 90434e18e2..642382466f 100644 --- a/modules/session/virtual.go +++ b/modules/session/virtual.go @@ -17,6 +17,11 @@ import ( postgres "gitea.com/go-chi/session/postgres" ) +// tombstoneTTL is how long a destroyed session ID is remembered so that +// concurrent requests releasing after destruction cannot recreate the session +// or be re-authenticated. +const tombstoneTTL = 10 * time.Minute + // VirtualSessionProvider represents a shadowed session provider implementation. // It wraps a real session provider and adds "tombstone" tracking for destroyed // sessions so that concurrent requests (e.g. EventSource) cannot accidentally @@ -30,6 +35,8 @@ type VirtualSessionProvider struct { // a FileStore reference may call Release() and recreate the file. // By tracking destroyed IDs, Read() returns an inert VirtualStore // that prevents re-authentication and avoids recreating the file. + // Entries self-expire after tombstoneTTL via time.AfterFunc so the map + // stays bounded regardless of session GC interval. destroyedSIDs sync.Map // sid -> time.Time } @@ -100,6 +107,10 @@ func (o *VirtualSessionProvider) Destroy(sid string) error { o.lock.Lock() defer o.lock.Unlock() o.destroyedSIDs.Store(sid, time.Now()) + // Self-expire the tombstone so the map stays bounded between session GCs. + time.AfterFunc(tombstoneTTL, func() { + o.destroyedSIDs.Delete(sid) + }) return o.provider.Destroy(sid) } @@ -124,28 +135,13 @@ func (o *VirtualSessionProvider) GC() { o.provider.GC() - // Clean up tombstone entries and re-destroy any files that may have - // been recreated by concurrent requests releasing after destruction. - cutoff := time.Now().Add(-10 * time.Minute) - var stale []string - var active []string - o.destroyedSIDs.Range(func(key, value any) bool { - sid := key.(string) - if value.(time.Time).Before(cutoff) { - stale = append(stale, sid) - } else { - active = append(active, sid) - } + // Re-destroy any sessions that may have been recreated by concurrent + // requests releasing after destruction. Tombstones themselves expire + // via time.AfterFunc in Destroy() so no manual cleanup is needed here. + o.destroyedSIDs.Range(func(key, _ any) bool { + _ = o.provider.Destroy(key.(string)) return true }) - for _, sid := range stale { - o.destroyedSIDs.Delete(sid) - } - if len(active) > 0 { - for _, sid := range active { - _ = o.provider.Destroy(sid) - } - } } func init() { diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go index 2e18f72f89..76cc78e67e 100644 --- a/routers/web/admin/users.go +++ b/routers/web/admin/users.go @@ -305,19 +305,13 @@ func ViewUser(ctx *context.Context) { ctx.Data["Users"] = orgs // needed to be able to use explore/user_list template ctx.Data["OrgsTotal"] = len(orgs) - userSessions, err := auth.GetUserSessionsByUserID(ctx, u.ID) + sessionsTotal, sessionsActive, err := auth.CountUserSessionsByUserID(ctx, u.ID) if err != nil { - ctx.ServerError("GetUserSessionsByUserID", err) + ctx.ServerError("CountUserSessionsByUserID", err) return } - ctx.Data["SessionsTotal"] = len(userSessions) - activeCount := 0 - for _, s := range userSessions { - if s.LogoutUnix == 0 { - activeCount++ - } - } - ctx.Data["SessionsActive"] = activeCount + ctx.Data["SessionsTotal"] = sessionsTotal + ctx.Data["SessionsActive"] = sessionsActive ctx.HTML(http.StatusOK, tplUserView) } diff --git a/templates/admin/user/sessions.tmpl b/templates/admin/user/sessions.tmpl index 75d3e197c3..adc4e37fd5 100644 --- a/templates/admin/user/sessions.tmpl +++ b/templates/admin/user/sessions.tmpl @@ -14,14 +14,14 @@