Add user session management and auditing

This commit is contained in:
Eric Lesiuta committed 2026-03-20 21:26:30 -04:00
1 parent 455dd20365
commit 0b354c8048
25 files changed
+1299 -11

No files matched your search

+21 -1
View File
@@ -7,13 +7,16 @@ package auth
import (
"errors"
"fmt"
"net"
"net/http"
auth_model "code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/auth/webauthn"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/web/middleware"
user_service "code.gitea.io/gitea/services/user"
)
@@ -39,7 +42,7 @@ func Init() {
}
// handleSignIn clears existing session variables and stores new ones for the specified user object
func handleSignIn(resp http.ResponseWriter, req *http.Request, sess SessionStore, user *user_model.User) {
func handleSignIn(resp http.ResponseWriter, req *http.Request, sess SessionStore, user *user_model.User, loginMethod string) {
// We need to regenerate the session...
newSess, err := session.RegenerateSession(resp, req)
if err != nil {
@@ -65,6 +68,23 @@ func handleSignIn(resp http.ResponseWriter, req *http.Request, sess SessionStore
log.Error(fmt.Sprintf("Error setting session: %v", err))
}
// Create tracked user session record
ip := req.RemoteAddr
if host, _, err := net.SplitHostPort(ip); err == nil {
ip = host
}
if err := auth_model.CreateUserSession(req.Context(), &auth_model.UserSession{
ID: sess.ID(),
UserID: user.ID,
LoginIP: ip,
LastIP: ip,
UserAgent: req.UserAgent(),
LoginMethod: loginMethod,
LastAccessUnix: timeutil.TimeStampNow(),
}); err != nil {
log.Error("Failed to create user session record: %v", err)
}
// Language setting of the user overwrites the one previously set
// If the user does not have a locale set, we save the current one.
if len(user.Language) == 0 {
+1 -1
View File
@@ -119,7 +119,7 @@ func (r *ReverseProxy) Verify(req *http.Request, w http.ResponseWriter, store Da
if r.CreateSession {
if sess != nil && (sess.Get("uid") == nil || sess.Get("uid").(int64) != user.ID) {
handleSignIn(w, req, sess, user)
handleSignIn(w, req, sess, user, "reverse_proxy")
}
}
store.GetData()["IsReverseProxy"] = true
+1 -1
View File
@@ -121,7 +121,7 @@ func (s *SSPI) Verify(req *http.Request, w http.ResponseWriter, store DataStore,
}
if s.CreateSession {
handleSignIn(w, req, sess, user)
handleSignIn(w, req, sess, user, "sspi")
}
log.Trace("SSPI Authorization: Logged in user %-v", user)
+18
View File
@@ -8,6 +8,7 @@ import (
"time"
activities_model "code.gitea.io/gitea/models/activities"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/system"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git/gitcmd"
@@ -171,6 +172,22 @@ func registerDeleteOldSystemNotices() {
})
}
func registerCleanupUserSessions() {
RegisterTaskFatal("cleanup_user_sessions", &OlderThanConfig{
BaseConfig: BaseConfig{
Enabled: false,
RunAtStart: false,
Schedule: "@every 24h",
},
OlderThan: time.Hour * 24 * 30, // 30 day retention
}, func(ctx context.Context, _ *user_model.User, config Config) error {
olderThanConfig := config.(*OlderThanConfig)
retentionSeconds := int64(olderThanConfig.OlderThan.Seconds())
maxLifetime := setting.SessionConfig.Maxlifetime
return auth_model.CleanupExpiredUserSessions(ctx, retentionSeconds, maxLifetime)
})
}
type GCLFSConfig struct {
BaseConfig
OlderThan time.Duration
@@ -239,4 +256,5 @@ func initExtendedTasks() {
registerDeleteOldSystemNotices()
registerGCLFS()
registerRebuildIssueIndexer()
registerCleanupUserSessions()
}
+11
View File
@@ -411,6 +411,17 @@ func (f *WebauthnDeleteForm) Validate(req *http.Request, errs binding.Errors) bi
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
// RevokeSessionForm for revoking a user session
type RevokeSessionForm struct {
SessionID string `binding:"Required"`
}
// Validate validates the fields
func (f *RevokeSessionForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
// PackageSettingForm form for package settings
type PackageSettingForm struct {
Action string
+4
View File
@@ -193,6 +193,10 @@ func deleteUser(ctx context.Context, u *user_model.User, purge bool) (err error)
return fmt.Errorf("DeleteAuthTokensByUserID: %w", err)
}
if err := auth_model.DeleteUserSessionsByUserID(ctx, u.ID); err != nil {
return fmt.Errorf("DeleteUserSessionsByUserID: %w", err)
}
if _, err = db.DeleteByID[user_model.User](ctx, u.ID); err != nil {
return fmt.Errorf("delete: %w", err)
}