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:
+85
-43
@@ -8,10 +8,12 @@ import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
"gitea.dev/models/db"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/audit"
|
||||
)
|
||||
|
||||
// ReplacePrimaryEmailAddress replaces the user's primary email address with the given email address.
|
||||
@@ -30,7 +32,8 @@ func ReplacePrimaryEmailAddress(ctx context.Context, u *user_model.User, emailSt
|
||||
return err
|
||||
}
|
||||
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
var newEmail *user_model.EmailAddress
|
||||
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||
// Check if address exists already
|
||||
email, err := user_model.GetEmailAddressByEmail(ctx, emailStr)
|
||||
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
||||
@@ -53,65 +56,104 @@ func ReplacePrimaryEmailAddress(ctx context.Context, u *user_model.User, emailSt
|
||||
}
|
||||
|
||||
// Insert new primary address
|
||||
if _, err := user_model.InsertEmailAddress(ctx, &user_model.EmailAddress{
|
||||
newEmail = &user_model.EmailAddress{
|
||||
UID: u.ID,
|
||||
Email: emailStr,
|
||||
IsActivated: true,
|
||||
IsPrimary: true,
|
||||
}); err != nil {
|
||||
}
|
||||
if _, err := user_model.InsertEmailAddress(ctx, newEmail); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u.Email = emailStr
|
||||
return user_model.UpdateUserCols(ctx, u, "email")
|
||||
})
|
||||
}
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
func AddEmailAddresses(ctx context.Context, u *user_model.User, emails []string) error {
|
||||
for _, emailStr := range emails {
|
||||
if err := user_model.ValidateEmail(emailStr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if address exists already
|
||||
email, err := user_model.GetEmailAddressByEmail(ctx, emailStr)
|
||||
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
if email != nil {
|
||||
return user_model.ErrEmailAlreadyUsed{Email: emailStr}
|
||||
}
|
||||
|
||||
// Insert new address
|
||||
email = &user_model.EmailAddress{
|
||||
UID: u.ID,
|
||||
Email: emailStr,
|
||||
IsActivated: !setting.Service.RegisterEmailConfirm,
|
||||
IsPrimary: false,
|
||||
}
|
||||
if _, err := user_model.InsertEmailAddress(ctx, email); err != nil {
|
||||
return err
|
||||
}
|
||||
if newEmail != nil {
|
||||
audit.Record(ctx, audit_model.UserEmailPrimaryChange, u, "email", newEmail.Email)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteEmailAddresses(ctx context.Context, u *user_model.User, emails []string) error {
|
||||
for _, emailStr := range emails {
|
||||
// Check if address exists
|
||||
email, err := user_model.GetEmailAddressOfUser(ctx, emailStr, u.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if email.IsPrimary {
|
||||
return user_model.ErrPrimaryEmailCannotDelete{Email: emailStr}
|
||||
func AddEmailAddresses(ctx context.Context, u *user_model.User, emailsToAdd []string) error {
|
||||
emails := make([]*user_model.EmailAddress, 0, len(emailsToAdd))
|
||||
|
||||
// Audit only after the transaction committed, so a partial batch neither persists nor records.
|
||||
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||
for _, emailStr := range emailsToAdd {
|
||||
if err := user_model.ValidateEmail(emailStr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if address exists already
|
||||
email, err := user_model.GetEmailAddressByEmail(ctx, emailStr)
|
||||
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
if email != nil {
|
||||
return user_model.ErrEmailAlreadyUsed{Email: emailStr}
|
||||
}
|
||||
|
||||
// Insert new address
|
||||
email = &user_model.EmailAddress{
|
||||
UID: u.ID,
|
||||
Email: emailStr,
|
||||
IsActivated: !setting.Service.RegisterEmailConfirm,
|
||||
IsPrimary: false,
|
||||
}
|
||||
if _, err := user_model.InsertEmailAddress(ctx, email); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
emails = append(emails, email)
|
||||
}
|
||||
|
||||
// Remove address
|
||||
if _, err := db.DeleteByID[user_model.EmailAddress](ctx, email.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, email := range emails {
|
||||
audit.Record(ctx, audit_model.UserEmailAdd, u, "email", email.Email)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteEmailAddresses(ctx context.Context, u *user_model.User, emailsToRemove []string) error {
|
||||
emails := make([]*user_model.EmailAddress, 0, len(emailsToRemove))
|
||||
|
||||
// Audit only after the transaction committed, so a partial batch neither persists nor records.
|
||||
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||
for _, emailStr := range emailsToRemove {
|
||||
// Check if address exists
|
||||
email, err := user_model.GetEmailAddressOfUser(ctx, emailStr, u.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if email.IsPrimary {
|
||||
return user_model.ErrPrimaryEmailCannotDelete{Email: emailStr}
|
||||
}
|
||||
|
||||
// Remove address
|
||||
if _, err := db.DeleteByID[user_model.EmailAddress](ctx, email.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
emails = append(emails, email)
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, email := range emails {
|
||||
audit.Record(ctx, audit_model.UserEmailRemove, u, "email", email.Email)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
+36
-2
@@ -7,12 +7,14 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
user_model "gitea.dev/models/user"
|
||||
password_module "gitea.dev/modules/auth/password"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/structs"
|
||||
"gitea.dev/services/audit"
|
||||
)
|
||||
|
||||
type UpdateOptionField[T any] struct {
|
||||
@@ -61,6 +63,8 @@ type UpdateOptions struct {
|
||||
func UpdateUser(ctx context.Context, u *user_model.User, opts *UpdateOptions) error {
|
||||
cols := make([]string, 0, 20)
|
||||
|
||||
oldIsActive, oldIsRestricted, oldIsAdmin, oldVisibility := u.IsActive, u.IsRestricted, u.IsAdmin, u.Visibility
|
||||
|
||||
if opts.KeepEmailPrivate.Has() {
|
||||
u.KeepEmailPrivate = opts.KeepEmailPrivate.Value()
|
||||
|
||||
@@ -183,7 +187,24 @@ func UpdateUser(ctx context.Context, u *user_model.User, opts *UpdateOptions) er
|
||||
cols = append(cols, "last_login_unix")
|
||||
}
|
||||
|
||||
return user_model.UpdateUserCols(ctx, u, cols...)
|
||||
if err := user_model.UpdateUserCols(ctx, u, cols...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if u.IsActive != oldIsActive {
|
||||
audit.Record(ctx, audit_model.UserActive, u, "active", u.IsActive)
|
||||
}
|
||||
if u.IsAdmin != oldIsAdmin {
|
||||
audit.Record(ctx, audit_model.UserAdmin, u, "admin", u.IsAdmin)
|
||||
}
|
||||
if u.IsRestricted != oldIsRestricted {
|
||||
audit.Record(ctx, audit_model.UserRestricted, u, "restricted", u.IsRestricted)
|
||||
}
|
||||
if u.Visibility != oldVisibility {
|
||||
audit.Record(ctx, audit_model.UserVisibility, u, "old_visibility", oldVisibility.String(), "new_visibility", u.Visibility.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type UpdateAuthOptions struct {
|
||||
@@ -195,12 +216,17 @@ type UpdateAuthOptions struct {
|
||||
}
|
||||
|
||||
func UpdateAuth(ctx context.Context, u *user_model.User, opts *UpdateAuthOptions) error {
|
||||
loginSourceChanged := false
|
||||
authSourceName := ""
|
||||
if opts.LoginSource.Has() {
|
||||
source, err := auth_model.GetSourceByID(ctx, opts.LoginSource.Value())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
loginSourceChanged = u.LoginSource != source.ID
|
||||
authSourceName = source.Name
|
||||
|
||||
u.LoginType = source.Type
|
||||
u.LoginSource = source.ID
|
||||
}
|
||||
@@ -241,7 +267,15 @@ func UpdateAuth(ctx context.Context, u *user_model.User, opts *UpdateAuthOptions
|
||||
}
|
||||
|
||||
if deleteAuthTokens {
|
||||
return auth_model.DeleteAuthTokensByUserID(ctx, u.ID)
|
||||
if err := auth_model.DeleteAuthTokensByUserID(ctx, u.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
audit.Record(ctx, audit_model.UserPassword, u)
|
||||
}
|
||||
if loginSourceChanged {
|
||||
audit.Record(ctx, audit_model.UserAuthenticationSource, u, "auth_source", authSourceName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+22
-1
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/organization"
|
||||
packages_model "gitea.dev/models/packages"
|
||||
@@ -24,6 +25,7 @@ import (
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/agit"
|
||||
asymkey_service "gitea.dev/services/asymkey"
|
||||
"gitea.dev/services/audit"
|
||||
org_service "gitea.dev/services/org"
|
||||
"gitea.dev/services/packages"
|
||||
container_service "gitea.dev/services/packages/container"
|
||||
@@ -56,7 +58,13 @@ func RenameUser(ctx context.Context, u *user_model.User, newUserName string, doe
|
||||
u.Name = oldUserName
|
||||
return err
|
||||
}
|
||||
return repo_model.UpdateRepositoryOwnerNames(ctx, u.ID, newUserName)
|
||||
if err := repo_model.UpdateRepositoryOwnerNames(ctx, u.ID, newUserName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
recordNameChange(ctx, u, oldUserName)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
@@ -114,9 +122,20 @@ func RenameUser(ctx context.Context, u *user_model.User, newUserName string, doe
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
recordNameChange(ctx, u, oldUserName)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func recordNameChange(ctx context.Context, u *user_model.User, oldUserName string) {
|
||||
if u.IsOrganization() {
|
||||
audit.Record(ctx, audit_model.OrganizationName, u, "previous_name", oldUserName)
|
||||
} else {
|
||||
audit.Record(ctx, audit_model.UserName, u, "previous_name", oldUserName)
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteUser completely and permanently deletes everything of a user,
|
||||
// but issues/comments/pulls will be kept and shown as someone has been deleted,
|
||||
// unless the user is younger than USER_DELETE_WITH_COMMENTS_MAX_DAYS.
|
||||
@@ -270,6 +289,8 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error {
|
||||
}
|
||||
}
|
||||
|
||||
audit.Record(ctx, audit_model.UserDelete, u)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
activities_model "gitea.dev/models/activities"
|
||||
audit_model "gitea.dev/models/audit"
|
||||
"gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
@@ -18,6 +19,7 @@ import (
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/test"
|
||||
"gitea.dev/modules/timeutil"
|
||||
org_service "gitea.dev/services/org"
|
||||
|
||||
@@ -177,6 +179,8 @@ func TestRenameUser(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Only capitalization", func(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Audit.RecordOutput, setting.AuditRecordOutputDatabase)()
|
||||
|
||||
caps := strings.ToUpper(user.Name)
|
||||
unittest.AssertNotExistsBean(t, &user_model.User{ID: user.ID, Name: caps})
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: user.ID, OwnerName: user.Name})
|
||||
@@ -185,6 +189,11 @@ func TestRenameUser(t *testing.T) {
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: user.ID, Name: caps})
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: user.ID, OwnerName: caps})
|
||||
unittest.AssertExistsAndLoadBean(t, &audit_model.Event{
|
||||
Action: audit_model.UserName,
|
||||
ScopeType: audit_model.ScopeUser,
|
||||
ScopeID: user.ID,
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Already exists", func(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user