Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ require (
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/go-webauthn/x v0.2.6 // indirect
github.com/goccy/go-json v0.10.6 // indirect
github.com/gofrs/flock v0.13.0 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
Expand Down Expand Up @@ -232,6 +233,7 @@ require (
github.com/olekukonko/ll v0.1.8 // indirect
github.com/olekukonko/tablewriter v1.1.4 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/orcaman/concurrent-map/v2 v2.0.1 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/pierrec/lz4/v4 v4.1.27 // indirect
github.com/pjbgf/sha1cd v0.6.0 // indirect
Expand Down Expand Up @@ -291,3 +293,5 @@ replace github.com/Azure/azure-sdk-for-go/sdk/azcore => github.com/Azure/azure-s
replace github.com/Azure/azure-sdk-for-go/sdk/storage/azblob => github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.2 // v1.6.4+ uses API version unsupported by Azurite in CI

replace github.com/microsoft/go-mssqldb => github.com/microsoft/go-mssqldb v1.9.7 // downgraded with Azure SDK

replace gitea.com/go-chi/session => github.com/a1012112796/gitea-session-dev v0.0.0-20260813032837-8b29a15bbd0e // tmp develop status
8 changes: 6 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ gitea.com/go-chi/cache v0.2.1 h1:bfAPkvXlbcZxPCpcmDVCWoHgiBSBmZN/QosnZvEC0+g=
gitea.com/go-chi/cache v0.2.1/go.mod h1:Qic0HZ8hOHW62ETGbonpwz8WYypj9NieU9659wFUJ8Q=
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098 h1:p2ki+WK0cIeNQuqjR98IP2KZQKRzJJiV7aTeMAFwaWo=
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098/go.mod h1:LjzIOHlRemuUyO7WR12fmm18VZIlCAaOt9L3yKw40pk=
gitea.com/go-chi/session v0.0.0-20260708011333-ebced8a7a2d6 h1:YWzVGeC/8SZThrJS48ZmQYLkzssdeABxHPhbdnxPDIU=
gitea.com/go-chi/session v0.0.0-20260708011333-ebced8a7a2d6/go.mod h1:KDvcfMUoXfATPHs2mbMoXFTXT45/FAFAS39waz9tPk0=
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96 h1:+wWBi6Qfruqu7xJgjOIrKVQGiLUZdpKYCZewJ4clqhw=
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96/go.mod h1:VyMQP6ue6MKHM8UsOXfNfuMKD0oSAWZdXVcpHIN2yaY=
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 h1:IFT+hup2xejHqdhS7keYWioqfmxdnfblFDTGoOwcZ+o=
Expand Down Expand Up @@ -69,6 +67,8 @@ github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=
github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk=
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.10.0 h1:LvK7+C6qgz8BPnmn7xGekf8vTkcqTvyBBHaLYIMxx0g=
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.10.0/go.mod h1:I28hc9eaiqKCoOB+9Wh/P1IOScfm0xCgyWC6DAo0lmo=
github.com/a1012112796/gitea-session-dev v0.0.0-20260813032837-8b29a15bbd0e h1:29lstdCfGx3bk4zUoJld8PikcXjMQUd65BxGdqa/40U=
github.com/a1012112796/gitea-session-dev v0.0.0-20260813032837-8b29a15bbd0e/go.mod h1:EkYs+DF9g7qU6C9jCRroixbvr34mEAeNMkjzvpzdrVI=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
Expand Down Expand Up @@ -335,6 +335,8 @@ github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6Wezm
github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs=
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 h1:UjoPNDAQ5JPCjlxoJd6K8ALZqSDDhk2ymieAZOVaDg0=
Expand Down Expand Up @@ -580,6 +582,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/orcaman/concurrent-map/v2 v2.0.1 h1:jOJ5Pg2w1oeB6PeDurIYf6k9PQ+aTITr/6lP/L/zp6c=
github.com/orcaman/concurrent-map/v2 v2.0.1/go.mod h1:9Eq3TG2oBe5FirmYWQfYO5iH1q0Jv47PLaNK++uCdOM=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
Expand Down
19 changes: 19 additions & 0 deletions modules/session/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,23 @@ const (
KeySignInMethod = "signInMethod"

SignInMethodOAuth2 = "oauth2"

// KeySignInIP stores the client IP at sign-in time.
KeySignInIP = "signInIP"

// KeySignInTime stores the Unix timestamp at sign-in time.
KeySignInTime = "signInTime"

// KeySignInUserAgent stores the raw User-Agent header at sign-in time.
KeySignInUserAgent = "signInUserAgent"

// KeyRememberTokenID stores the remember-me auth token ID so it can be
// revoked together with the session.
KeyRememberTokenID = "rememberTokenID"
)

// Sign-in method constants for KeySignInMethod.
const (
SignInMethodPassword = "password"
SignInMethodRemember = "remember"
)
12 changes: 12 additions & 0 deletions modules/session/mem.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ package session
import (
"bytes"
"encoding/gob"
"errors"
"net/http"

"gitea.com/go-chi/session"
"gitea.com/go-chi/session/core"
)

type mockMemRawStore struct {
Expand Down Expand Up @@ -63,6 +65,16 @@ func (m mockMemStore) Destroy(writer http.ResponseWriter, request *http.Request)
return nil
}

// ListStoreByIndexer is not supported by the mock store; it exists to satisfy the Store interface.
func (m mockMemStore) ListStoreByIndexer(_ any) ([]core.RawStoreReadOnly, error) {
return nil, errors.New("mock store does not support session index")
}

// DestroySessionByID is not supported by the mock store; it exists to satisfy the Store interface.
func (m mockMemStore) DestroySessionByID(_ string, _ any) error {
return errors.New("mock store does not support session index")
}

func NewMockMemStore(sid string) Store {
return &mockMemStore{&mockMemRawStore{session.NewMemStore(sid)}}
}
5 changes: 5 additions & 0 deletions modules/session/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@ import (
"gitea.dev/modules/setting"

"gitea.com/go-chi/session"
"gitea.com/go-chi/session/core"
)

type RawStore = session.RawStore

type Store interface {
RawStore
Destroy(http.ResponseWriter, *http.Request) error
// ListStoreByIndexer returns read-only session stores indexed by indexValue.
ListStoreByIndexer(indexValue any) ([]core.RawStoreReadOnly, error)
// DestroySessionByID destroys the session identified by sid and removes its index entry.
DestroySessionByID(sid string, indexValue any) error
}

type mockStoreContextKeyStruct struct{}
Expand Down
56 changes: 56 additions & 0 deletions modules/session/virtual.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
"gitea.dev/modules/json"

"gitea.com/go-chi/session"
"gitea.com/go-chi/session/core"
couchbase "gitea.com/go-chi/session/couchbase"
"gitea.com/go-chi/session/index"
memcache "gitea.com/go-chi/session/memcache"
mysql "gitea.com/go-chi/session/mysql"
postgres "gitea.com/go-chi/session/postgres"
Expand All @@ -22,6 +24,8 @@
provider session.Provider
}

var _ index.ProviderSupportIndex = (*VirtualSessionProvider)(nil)

// Init initializes the cookie session provider with the given config.
func (o *VirtualSessionProvider) Init(gcLifetime int64, config string) error {
var opts session.Options
Expand Down Expand Up @@ -99,6 +103,58 @@
o.provider.GC()
}

func (o *VirtualSessionProvider) loadProviderSupportIndex() (index.ProviderSupportIndex, error) {
if _, ok := o.provider.(index.ProviderSupportIndex); !ok {
return nil, fmt.Errorf("VirtualSessionProvider: provider %T does not support index", o.provider)
}
return o.provider.(index.ProviderSupportIndex), nil

Check failure on line 110 in modules/session/virtual.go

View workflow job for this annotation

GitHub Actions / lint-backend

type assertion must be checked (forcetypeassert)

Check failure on line 110 in modules/session/virtual.go

View workflow job for this annotation

GitHub Actions / lint-backend

type assertion must be checked (forcetypeassert)
}

func (o *VirtualSessionProvider) ReadIndex(key index.IndexKey) (index.SessionIndex, error) {
p, err := o.loadProviderSupportIndex()
if err != nil {
return nil, err
}

return p.ReadIndex(key)
}

func (o *VirtualSessionProvider) PeekIndex(key index.IndexKey) (index.SessionIndexReadOnly, error) {
p, err := o.loadProviderSupportIndex()
if err != nil {
return nil, err
}

return p.PeekIndex(key)
}

func (o *VirtualSessionProvider) Peek(sid string, withData bool) (store core.RawStoreReadOnly, alive bool, err error) {
p, err := o.loadProviderSupportIndex()
if err != nil {
return nil, false, err
}

return p.Peek(sid, withData)
}

func (o *VirtualSessionProvider) CreateAt(sid string) (int64, error) {
p, err := o.loadProviderSupportIndex()
if err != nil {
return 0, err
}

return p.CreateAt(sid)
}

func (o *VirtualSessionProvider) ScanIndexes(fn func(key index.IndexKey) bool) error {
p, err := o.loadProviderSupportIndex()
if err != nil {
return err
}

return p.ScanIndexes(fn)
}

func init() {
session.Register("VirtualSession", &VirtualSessionProvider{})
}
Expand Down
3 changes: 3 additions & 0 deletions modules/setting/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ var SessionConfig = struct {
Domain string
// SameSite declares if your cookie should be restricted to a first-party or same-site context. Valid strings are "none", "lax", "strict". Default is "lax"
SameSite http.SameSite
// EnableSessionManager controls whether the session indexer and /user/settings/active_sessions page are enabled.
EnableSessionManager bool
}{
CookieName: "i_like_gitea",
Gclifetime: 86400,
Expand Down Expand Up @@ -66,6 +68,7 @@ func loadSessionFrom(rootCfg ConfigProvider) {
SessionConfig.Gclifetime = sec.Key("GC_INTERVAL_TIME").MustInt64(86400)
SessionConfig.Maxlifetime = sec.Key("SESSION_LIFE_TIME").MustInt64(86400)
SessionConfig.Domain = sec.Key("DOMAIN").String()
SessionConfig.EnableSessionManager = sec.Key("ENABLE_SESSION_MANAGER").MustBool(false)
samesiteString := sec.Key("SAME_SITE").In("lax", []string{"none", "lax", "strict"})
switch strings.ToLower(samesiteString) {
case "none":
Expand Down
15 changes: 15 additions & 0 deletions options/locale/locale_en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,21 @@
"settings.appearance": "Appearance",
"settings.password": "Password",
"settings.security": "Security",
"settings.active_sessions": "Active Sessions",
"settings.active_sessions.current": "Current Session",
"settings.active_sessions.revoke": "Revoke",
"settings.active_sessions.revoke_confirm": "Are you sure you want to revoke this session?",
"settings.active_sessions.revoke_success": "Session revoked successfully.",
"settings.active_sessions.cannot_revoke_current": "Cannot revoke the current session.",
"settings.active_sessions.none": "No other active sessions.",
"settings.active_sessions.count": "%d active sessions",
"settings.active_sessions.ip": "IP Address",
"settings.active_sessions.signin_time": "Sign-in Time",
"settings.active_sessions.signin_method": "Sign-in Method",
"settings.active_sessions.signin_method.password": "Password",
"settings.active_sessions.signin_method.oauth2": "OAuth2",
"settings.active_sessions.signin_method.remember": "Remember Me",
"settings.active_sessions.user_agent": "User Agent",
"settings.avatar": "Avatar",
"settings.ssh_gpg_keys": "SSH / GPG Keys",
"settings.social": "Social Accounts",
Expand Down
7 changes: 7 additions & 0 deletions routers/common/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ import (
"gitea.dev/modules/log"
"gitea.dev/modules/public"
"gitea.dev/modules/reqctx"
gitea_session "gitea.dev/modules/session"
"gitea.dev/modules/setting"
"gitea.dev/modules/web/routing"
"gitea.dev/services/context"

"gitea.com/go-chi/session"
"gitea.com/go-chi/session/index"
"github.com/chi-middleware/proxy"
"github.com/go-chi/chi/v5"
)
Expand Down Expand Up @@ -150,6 +152,11 @@ func MustInitSessioner() func(next http.Handler) http.Handler {

// in the future, if websocket is used, the websocket handler should manage its own session sync (release)
IgnoreReleaseForWebSocket: true,

IndexerOptions: index.SessionIndexerOptions{
EnableIndex: setting.SessionConfig.EnableSessionManager,
Indexer: gitea_session.KeyUID,
},
})
if err != nil {
log.Fatal("common.Sessioner failed: %v", err)
Expand Down
4 changes: 4 additions & 0 deletions routers/install/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,10 @@ func SubmitInstall(ctx *context.Context) {
ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
return
}
_ = ctx.Session.Set(session.KeySignInIP, ctx.RemoteAddr())
_ = ctx.Session.Set(session.KeySignInTime, time.Now().Unix())
_ = ctx.Session.Set(session.KeySignInUserAgent, ctx.Req.UserAgent())
_ = ctx.Session.Set(session.KeySignInMethod, session.SignInMethodPassword)
if err = ctx.Session.Release(); err != nil {
ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
return
Expand Down
34 changes: 25 additions & 9 deletions routers/web/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"net/http"
"net/url"
"strings"
"time"

"gitea.dev/models/auth"
"gitea.dev/models/db"
Expand Down Expand Up @@ -119,10 +120,12 @@ func autoSignIn(ctx *context.Context) (bool, error) {

ctx.SetSiteCookie(setting.CookieRememberName, nt.ID+":"+token, setting.LogInRememberDays*timeutil.Day)

if err := regenerateSession(ctx, map[string]any{
session.KeyUID: u.ID,
session.KeyUserHasTwoFactorAuth: userHasTwoFactorAuth,
}); err != nil {
updates := signInSessionMeta(ctx)
updates[session.KeyUID] = u.ID
updates[session.KeyUserHasTwoFactorAuth] = userHasTwoFactorAuth
updates[session.KeySignInMethod] = session.SignInMethodRemember
updates[session.KeyRememberTokenID] = t.ID
if err := regenerateSession(ctx, updates); err != nil {
return false, fmt.Errorf("unable to updateSession: %w", err)
}

Expand Down Expand Up @@ -388,10 +391,11 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember bool) {
}

auth_service.ClearSessionKeysForSignIn(ctx.Session)
if err := regenerateSession(ctx, map[string]any{
session.KeyUID: u.ID,
session.KeyUserHasTwoFactorAuth: userHasTwoFactorAuth,
}); err != nil {
updates := signInSessionMeta(ctx)
updates[session.KeyUID] = u.ID
updates[session.KeyUserHasTwoFactorAuth] = userHasTwoFactorAuth
updates[session.KeySignInMethod] = session.SignInMethodPassword
if err := regenerateSession(ctx, updates); err != nil {
ctx.ServerError("RegenerateSession", err)
return
}
Expand Down Expand Up @@ -879,7 +883,10 @@ func handleAccountActivation(ctx *context.Context, user *user_model.User) {

log.Trace("User activated: %s", user.Name)

if err := regenerateSession(ctx, map[string]any{session.KeyUID: user.ID}); err != nil {
updates := signInSessionMeta(ctx)
updates[session.KeyUID] = user.ID
updates[session.KeySignInMethod] = session.SignInMethodPassword
if err := regenerateSession(ctx, updates); err != nil {
log.Error("Unable to regenerate session for user: %-v with email: %s: %v", user, user.Email, err)
ctx.ServerError("ActivateUserEmail", err)
return
Expand Down Expand Up @@ -944,3 +951,12 @@ func regenerateSession(ctx *context.Context, updates map[string]any) error {
}
return nil
}

// signInSessionMeta returns the common session metadata to record on every sign-in.
func signInSessionMeta(ctx *context.Context) map[string]any {
return map[string]any{
session.KeySignInIP: ctx.RemoteAddr(),
session.KeySignInTime: time.Now().Unix(),
session.KeySignInUserAgent: ctx.Req.UserAgent(),
}
}
3 changes: 3 additions & 0 deletions routers/web/auth/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,9 @@ func handleOAuth2SignIn(ctx *context.Context, authSource *auth.Source, u *user_m
if err := regenerateSession(ctx, map[string]any{
session.KeyUID: u.ID,
session.KeyUserHasTwoFactorAuth: userHasTwoFactorAuth,
session.KeySignInIP: ctx.RemoteAddr(),
session.KeySignInTime: time.Now().Unix(),
session.KeySignInUserAgent: ctx.Req.UserAgent(),
session.KeySignInMethod: session.SignInMethodOAuth2,
}); err != nil {
ctx.ServerError("updateSession", err)
Expand Down
Loading
Loading