mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-14 19:22:09 +09:00
feat: Add audit logging (#38189)
Co-authored-by: bircni <bircni@users.noreply.github.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
actions_model "gitea.dev/models/actions"
|
||||
audit_model "gitea.dev/models/audit"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/auth/httpauth"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/timeutil"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/audit"
|
||||
)
|
||||
|
||||
// Ensure the struct implements the interface.
|
||||
@@ -71,7 +73,7 @@ func parseAuthBasic(req *http.Request) (ret struct{ authToken, uname, passwd str
|
||||
// VerifyAuthToken only the access token provided as parameter, used by other auth methods that want to reuse access token verification logic
|
||||
func (b *Basic) VerifyAuthToken(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore, authToken string) (*user_model.User, error) {
|
||||
// get oauth2 token's user's ID
|
||||
accessTokenScope, uid := GetOAuthAccessTokenScopeAndUserID(req.Context(), authToken)
|
||||
accessTokenScope, uid, grantID := GetOAuthAccessTokenScopeAndUserID(req.Context(), authToken)
|
||||
if uid != 0 {
|
||||
log.Trace("Basic Authorization: Valid OAuthAccessToken for user[%d]", uid)
|
||||
|
||||
@@ -83,6 +85,7 @@ func (b *Basic) VerifyAuthToken(req *http.Request, w http.ResponseWriter, store
|
||||
|
||||
store.GetData()["LoginMethod"] = OAuth2TokenMethodName
|
||||
store.GetData()["ApiTokenScope"] = accessTokenScope
|
||||
setAuthCredential(store, credentialOAuth2Grant, grantID)
|
||||
return u, nil
|
||||
}
|
||||
|
||||
@@ -103,6 +106,7 @@ func (b *Basic) VerifyAuthToken(req *http.Request, w http.ResponseWriter, store
|
||||
|
||||
store.GetData()["LoginMethod"] = AccessTokenMethodName
|
||||
store.GetData()["ApiTokenScope"] = token.Scope
|
||||
setAuthCredential(store, credentialAccessToken, token.ID)
|
||||
return u, nil
|
||||
} else if !errors.Is(err, util.ErrNotExist) {
|
||||
log.Error("GetAccessTokenBySHA: %v", err)
|
||||
@@ -180,6 +184,8 @@ func validateTOTP(req *http.Request, u *user_model.User) error {
|
||||
if ok, err := twofa.ValidateAndConsumeTOTP(req.Context(), req.Header.Get("X-Gitea-OTP")); err != nil {
|
||||
return err
|
||||
} else if !ok {
|
||||
audit.RecordAs(req.Context(), u, audit_model.UserAuthenticationFailTwoFactor, u)
|
||||
|
||||
return util.NewInvalidArgumentErrorf("invalid provided OTP")
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"gitea.dev/modules/web/middleware"
|
||||
)
|
||||
|
||||
// Credential kinds naming how a request authenticated, recorded so an audit
|
||||
// event can point at the token rather than only at its owner.
|
||||
const (
|
||||
credentialAccessToken = "access-token"
|
||||
credentialOAuth2Grant = "oauth2-grant"
|
||||
)
|
||||
|
||||
func setAuthCredential(store DataStore, kind string, id int64) {
|
||||
store.GetData()[middleware.ContextDataKeyAuthCredential] = kind + ":" + strconv.FormatInt(id, 10)
|
||||
}
|
||||
@@ -42,6 +42,17 @@ func ImpersonateUser(sess SessionStore, u *user_model.User) error {
|
||||
return sess.Release()
|
||||
}
|
||||
|
||||
// ImpersonatorUserID returns the ID of the admin behind an impersonated
|
||||
// session, or zero when the session is not impersonating anyone.
|
||||
func ImpersonatorUserID(sess SessionStore) int64 {
|
||||
data, ok := sess.Get(session.KeyImpersonatorData).(map[string]any)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
uid, _ := data[session.KeyUID].(int64)
|
||||
return uid
|
||||
}
|
||||
|
||||
func ExitImpersonatedUser(sess SessionStore) (bool, error) {
|
||||
impersonatorData, ok := sess.Get(session.KeyImpersonatorData).(map[string]any)
|
||||
if !ok {
|
||||
|
||||
+13
-10
@@ -25,35 +25,36 @@ import (
|
||||
|
||||
var _ Method = &OAuth2{}
|
||||
|
||||
// GetOAuthAccessTokenScopeAndUserID returns access token scope and user id
|
||||
func GetOAuthAccessTokenScopeAndUserID(ctx context.Context, accessToken string) (auth_model.AccessTokenScope, int64) {
|
||||
// GetOAuthAccessTokenScopeAndUserID returns access token scope, user id and the
|
||||
// grant the token was issued for.
|
||||
func GetOAuthAccessTokenScopeAndUserID(ctx context.Context, accessToken string) (_ auth_model.AccessTokenScope, userID, grantID int64) {
|
||||
var accessTokenScope auth_model.AccessTokenScope
|
||||
if !setting.OAuth2.Enabled {
|
||||
return accessTokenScope, 0
|
||||
return accessTokenScope, 0, 0
|
||||
}
|
||||
|
||||
// JWT tokens require a ".", if the token isn't like that, return early
|
||||
if !strings.Contains(accessToken, ".") {
|
||||
return accessTokenScope, 0
|
||||
return accessTokenScope, 0, 0
|
||||
}
|
||||
|
||||
token, err := oauth2_provider.ParseToken(accessToken, oauth2_provider.DefaultSigningKey)
|
||||
if err != nil {
|
||||
log.Trace("oauth2.ParseToken: %v", err)
|
||||
return accessTokenScope, 0
|
||||
return accessTokenScope, 0, 0
|
||||
}
|
||||
var grant *auth_model.OAuth2Grant
|
||||
if grant, err = auth_model.GetOAuth2GrantByID(ctx, token.GrantID); err != nil || grant == nil {
|
||||
return accessTokenScope, 0
|
||||
return accessTokenScope, 0, 0
|
||||
}
|
||||
if token.Kind != oauth2_provider.KindAccessToken {
|
||||
return accessTokenScope, 0
|
||||
return accessTokenScope, 0, 0
|
||||
}
|
||||
if token.ExpiresAt.Before(time.Now()) || token.IssuedAt.After(time.Now()) {
|
||||
return accessTokenScope, 0
|
||||
return accessTokenScope, 0, 0
|
||||
}
|
||||
accessTokenScope = oauth2_provider.GrantAdditionalScopes(grant.Scope)
|
||||
return accessTokenScope, grant.UserID
|
||||
return accessTokenScope, grant.UserID, grant.ID
|
||||
}
|
||||
|
||||
// CheckTaskIsRunning verifies that the TaskID corresponds to a running task
|
||||
@@ -118,9 +119,10 @@ func (o *OAuth2) userFromToken(ctx context.Context, tokenSHA string, store DataS
|
||||
}
|
||||
|
||||
// Otherwise, check if this is an OAuth access token
|
||||
accessTokenScope, uid := GetOAuthAccessTokenScopeAndUserID(ctx, tokenSHA)
|
||||
accessTokenScope, uid, grantID := GetOAuthAccessTokenScopeAndUserID(ctx, tokenSHA)
|
||||
if uid != 0 {
|
||||
store.GetData()["ApiTokenScope"] = accessTokenScope
|
||||
setAuthCredential(store, credentialOAuth2Grant, grantID)
|
||||
}
|
||||
return user_model.GetUserByID(ctx, uid)
|
||||
}
|
||||
@@ -141,6 +143,7 @@ func (o *OAuth2) userFromToken(ctx context.Context, tokenSHA string, store DataS
|
||||
log.Error("UpdateAccessToken: %v", err)
|
||||
}
|
||||
store.GetData()["ApiTokenScope"] = t.Scope
|
||||
setAuthCredential(store, credentialAccessToken, t.ID)
|
||||
return user_model.GetUserByID(ctx, t.UID)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,11 +9,13 @@ import (
|
||||
"strings"
|
||||
"uuid"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/session"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/services/audit"
|
||||
)
|
||||
|
||||
// Ensure the struct implements the interface.
|
||||
@@ -171,5 +173,7 @@ func (r *ReverseProxy) newUser(req *http.Request) *user_model.User {
|
||||
return nil
|
||||
}
|
||||
|
||||
audit.RecordAs(req.Context(), user_model.NewAuthenticationSourceUser(), audit_model.UserCreate, user)
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
+33
-2
@@ -6,11 +6,37 @@ package auth
|
||||
import (
|
||||
"context"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
"gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/services/audit"
|
||||
)
|
||||
|
||||
// CreateSource creates a AuthSource record in DB.
|
||||
func CreateSource(ctx context.Context, source *auth.Source) error {
|
||||
if err := auth.CreateSource(ctx, source); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
audit.Record(ctx, audit_model.SystemAuthenticationSourceAdd, nil,
|
||||
"auth_source", source.Name, "auth_source_type", source.Type.String(), "is_active", source.IsActive)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateSource updates a AuthSource record in DB.
|
||||
func UpdateSource(ctx context.Context, source *auth.Source) error {
|
||||
if err := auth.UpdateSource(ctx, source); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
audit.Record(ctx, audit_model.SystemAuthenticationSourceUpdate, nil,
|
||||
"auth_source", source.Name, "auth_source_type", source.Type.String(), "is_active", source.IsActive)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteSource deletes a AuthSource record in DB.
|
||||
func DeleteSource(ctx context.Context, source *auth.Source) error {
|
||||
count, err := db.GetEngine(ctx).Count(&user_model.User{LoginSource: source.ID})
|
||||
@@ -37,6 +63,11 @@ func DeleteSource(ctx context.Context, source *auth.Source) error {
|
||||
}
|
||||
}
|
||||
|
||||
_, err = db.GetEngine(ctx).ID(source.ID).Delete(new(auth.Source))
|
||||
return err
|
||||
if _, err = db.GetEngine(ctx).ID(source.ID).Delete(new(auth.Source)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
audit.Record(ctx, audit_model.SystemAuthenticationSourceRemove, nil, "auth_source", source.Name)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -8,12 +8,14 @@ import (
|
||||
"strings"
|
||||
|
||||
asymkey_model "gitea.dev/models/asymkey"
|
||||
audit_model "gitea.dev/models/audit"
|
||||
"gitea.dev/models/auth"
|
||||
user_model "gitea.dev/models/user"
|
||||
auth_module "gitea.dev/modules/auth"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/optional"
|
||||
asymkey_service "gitea.dev/services/asymkey"
|
||||
"gitea.dev/services/audit"
|
||||
source_service "gitea.dev/services/auth/source"
|
||||
user_service "gitea.dev/services/user"
|
||||
)
|
||||
@@ -21,6 +23,8 @@ import (
|
||||
// Authenticate queries if login/password is valid against the LDAP directory pool,
|
||||
// and create a local user if success when enabled.
|
||||
func (source *Source) Authenticate(ctx context.Context, user *user_model.User, userName, password string) (*user_model.User, error) {
|
||||
ctx = audit.WithDoer(ctx, user_model.NewAuthenticationSourceUser())
|
||||
|
||||
loginName := userName
|
||||
if user != nil {
|
||||
loginName = user.LoginName
|
||||
@@ -99,6 +103,8 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
|
||||
return user, err
|
||||
}
|
||||
|
||||
audit.Record(ctx, audit_model.UserCreate, user)
|
||||
|
||||
if isAttributeSSHPublicKeySet && asymkey_model.AddPublicKeysBySource(ctx, user, source.AuthSource, sr.SSHPublicKey, source.SSHKeysAreVerified) {
|
||||
if err := asymkey_service.RewriteAllPublicKeys(ctx); err != nil {
|
||||
return user, err
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
|
||||
asymkey_model "gitea.dev/models/asymkey"
|
||||
audit_model "gitea.dev/models/audit"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/organization"
|
||||
user_model "gitea.dev/models/user"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/optional"
|
||||
asymkey_service "gitea.dev/services/asymkey"
|
||||
"gitea.dev/services/audit"
|
||||
source_service "gitea.dev/services/auth/source"
|
||||
user_service "gitea.dev/services/user"
|
||||
)
|
||||
@@ -24,6 +26,10 @@ import (
|
||||
func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
|
||||
log.Trace("Doing: SyncExternalUsers[%s]", source.AuthSource.Name)
|
||||
|
||||
// everything this sync changes is attributed to the authentication source,
|
||||
// not to a signed-in user
|
||||
ctx = audit.WithDoer(ctx, user_model.NewAuthenticationSourceUser())
|
||||
|
||||
isAttributeSSHPublicKeySet := strings.TrimSpace(source.AttributeSSHPublicKey) != ""
|
||||
var sshKeysNeedUpdate bool
|
||||
|
||||
@@ -131,6 +137,8 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
|
||||
err = user_model.CreateUser(ctx, usr, &user_model.Meta{}, overwriteDefault)
|
||||
if err != nil {
|
||||
log.Error("SyncExternalUsers[%s]: Error creating user %s: %v", source.AuthSource.Name, su.Username, err)
|
||||
} else {
|
||||
audit.Record(ctx, audit_model.UserCreate, usr)
|
||||
}
|
||||
|
||||
if err == nil && isAttributeSSHPublicKeySet {
|
||||
|
||||
@@ -9,11 +9,13 @@ import (
|
||||
"strings"
|
||||
"uuid"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
"gitea.dev/models/auth"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/auth/pam"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/services/audit"
|
||||
)
|
||||
|
||||
// Authenticate queries if login/password is valid against the PAM,
|
||||
@@ -66,5 +68,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
|
||||
return user, err
|
||||
}
|
||||
|
||||
audit.RecordAs(ctx, user_model.NewAuthenticationSourceUser(), audit_model.UserCreate, user)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
@@ -10,10 +10,12 @@ import (
|
||||
"net/textproto"
|
||||
"strings"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/audit"
|
||||
)
|
||||
|
||||
// Authenticate queries if the provided login/password is authenticates against the SMTP server
|
||||
@@ -83,5 +85,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
|
||||
return user, err
|
||||
}
|
||||
|
||||
audit.RecordAs(ctx, user_model.NewAuthenticationSourceUser(), audit_model.UserCreate, user)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/container"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/services/audit"
|
||||
org_service "gitea.dev/services/org"
|
||||
)
|
||||
|
||||
@@ -31,6 +32,9 @@ func SyncGroupsToTeams(ctx context.Context, user *user_model.User, sourceUserGro
|
||||
|
||||
// SyncGroupsToTeamsCached maps authentication source groups to organization and team memberships
|
||||
func SyncGroupsToTeamsCached(ctx context.Context, user *user_model.User, sourceUserGroups container.Set[string], sourceGroupTeamMapping map[string]map[string][]string, performRemoval bool, orgCache map[string]*organization.Organization, teamCache map[string]*organization.Team) error {
|
||||
// team membership changes here come from the authentication source mapping
|
||||
ctx = audit.WithDoer(ctx, user_model.NewAuthenticationSourceUser())
|
||||
|
||||
membershipsToAdd, membershipsToRemove := resolveMappedMemberships(sourceUserGroups, sourceGroupTeamMapping)
|
||||
|
||||
if performRemoval {
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"sync"
|
||||
"uuid"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
"gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
user_model "gitea.dev/models/user"
|
||||
@@ -18,6 +19,7 @@ import (
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/auth/source/sspi"
|
||||
gitea_context "gitea.dev/services/context"
|
||||
)
|
||||
@@ -171,6 +173,8 @@ func (s *SSPI) newUser(ctx context.Context, username string, cfg *sspi.Source) (
|
||||
return nil, err
|
||||
}
|
||||
|
||||
audit.RecordAs(ctx, user_model.NewAuthenticationSourceUser(), audit_model.UserCreate, user)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user