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:
bircni
2026-09-12 10:15:23 +02:00
committed by GitHub
parent 4d43445532
commit da37b7916b
136 changed files with 3864 additions and 209 deletions
+14 -2
View File
@@ -9,6 +9,7 @@ import (
actions_model "gitea.dev/models/actions"
activities_model "gitea.dev/models/activities"
audit_model "gitea.dev/models/audit"
"gitea.dev/models/db"
org_model "gitea.dev/models/organization"
packages_model "gitea.dev/models/packages"
@@ -21,6 +22,7 @@ import (
"gitea.dev/modules/storage"
"gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/services/audit"
repo_service "gitea.dev/services/repository"
)
@@ -85,6 +87,8 @@ func DeleteOrganization(ctx context.Context, org *org_model.Organization, purge
return err
}
audit.Record(ctx, audit_model.OrganizationDelete, org.AsUser())
// FIXME: system notice
// Note: There are something just cannot be roll back,
// so just keep error logs of those operations.
@@ -158,9 +162,10 @@ func ChangeOrganizationVisibility(ctx context.Context, org *org_model.Organizati
return nil
}
oldVisibility := org.Visibility
org.Visibility = visibility
// FIXME: If it's a big forks network(forks and sub forks), the database transaction will be too long to fail.
return db.WithTx(ctx, func(ctx context.Context) error {
if err := db.WithTx(ctx, func(ctx context.Context) error {
if err := user_model.UpdateUserColsNoAutoTime(ctx, org.AsUser(), "visibility"); err != nil {
return err
}
@@ -177,7 +182,14 @@ func ChangeOrganizationVisibility(ctx context.Context, org *org_model.Organizati
}
}
return nil
})
}); err != nil {
return err
}
audit.Record(ctx, audit_model.OrganizationVisibility, org.AsUser(),
"old_visibility", oldVisibility.String(), "new_visibility", visibility.String())
return nil
}
// UpdateOrgEmailAddress validates and updates the organization's contact email.
+50 -8
View File
@@ -9,6 +9,7 @@ import (
"fmt"
"strings"
audit_model "gitea.dev/models/audit"
"gitea.dev/models/db"
git_model "gitea.dev/models/git"
issues_model "gitea.dev/models/issues"
@@ -20,11 +21,23 @@ import (
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
"gitea.dev/modules/util"
"gitea.dev/services/audit"
repo_service "gitea.dev/services/repository"
"xorm.io/builder"
)
// recordTeamAudit emits a team-related audit event scoped to the owning organization.
func recordTeamAudit(ctx context.Context, action audit_model.Action, team *organization.Team, metadata ...any) {
audit.Record(ctx, action, audit.ScopeFromUserID(ctx, team.OrgID), metadata...)
}
// recordTeamMemberAudit emits a team membership audit event scoped to the
// owning organization.
func recordTeamMemberAudit(ctx context.Context, action audit_model.Action, team *organization.Team, member *user_model.User) {
recordTeamAudit(ctx, action, team, "team", team.Name, "member", member.Name)
}
// NewTeam creates a record of new team.
// It's caller's responsibility to assign organization ID.
func NewTeam(ctx context.Context, t *organization.Team) (err error) {
@@ -56,7 +69,7 @@ func NewTeam(ctx context.Context, t *organization.Team) (err error) {
return organization.ErrTeamAlreadyExist{OrgID: t.OrgID, Name: t.LowerName}
}
return db.WithTx(ctx, func(ctx context.Context) error {
if err = db.WithTx(ctx, func(ctx context.Context) error {
if err = db.Insert(ctx, t); err != nil {
return err
}
@@ -82,7 +95,13 @@ func NewTeam(ctx context.Context, t *organization.Team) (err error) {
// Update organization number of teams.
_, err = db.Exec(ctx, "UPDATE `user` SET num_teams=num_teams+1 WHERE id = ?", t.OrgID)
return err
})
}); err != nil {
return err
}
recordTeamAudit(ctx, audit_model.OrganizationTeamAdd, t, "team", t.Name)
return nil
}
// UpdateTeam updates information of team.
@@ -95,7 +114,7 @@ func UpdateTeam(ctx context.Context, t *organization.Team, authChanged, includeA
t.Description = t.Description[:255]
}
return db.WithTx(ctx, func(ctx context.Context) error {
if err = db.WithTx(ctx, func(ctx context.Context) error {
t.LowerName = strings.ToLower(t.Name)
has, err := db.Exist[organization.Team](ctx, builder.Eq{
"org_id": t.OrgID,
@@ -155,13 +174,22 @@ func UpdateTeam(ctx context.Context, t *organization.Team, authChanged, includeA
}
return nil
})
}); err != nil {
return err
}
recordTeamAudit(ctx, audit_model.OrganizationTeamUpdate, t, "team", t.Name)
if authChanged {
recordTeamAudit(ctx, audit_model.OrganizationTeamPermission, t, "team", t.Name, "permission", t.AccessMode.ToString())
}
return nil
}
// DeleteTeam deletes given team.
// It's caller's responsibility to assign organization ID.
func DeleteTeam(ctx context.Context, t *organization.Team) error {
return db.WithTx(ctx, func(ctx context.Context) error {
if err := db.WithTx(ctx, func(ctx context.Context) error {
if err := t.LoadMembers(ctx); err != nil {
return err
}
@@ -205,7 +233,13 @@ func DeleteTeam(ctx context.Context, t *organization.Team) error {
// Update organization number of teams.
_, err := db.Exec(ctx, "UPDATE `user` SET num_teams=num_teams-1 WHERE id=?", t.OrgID)
return err
})
}); err != nil {
return err
}
recordTeamAudit(ctx, audit_model.OrganizationTeamRemove, t, "team", t.Name)
return nil
}
// AddTeamMember adds new membership of given team to given organization,
@@ -250,6 +284,8 @@ func AddTeamMember(ctx context.Context, team *organization.Team, user *user_mode
return err
}
recordTeamMemberAudit(ctx, audit_model.OrganizationTeamMemberAdd, team, user)
// this behaviour may spend much time so run it in a goroutine
// FIXME: Update watch repos batchly
if setting.Service.AutoWatchNewRepos {
@@ -347,7 +383,13 @@ func removeInvalidOrgUser(ctx context.Context, orgID int64, user *user_model.Use
// RemoveTeamMember removes member from given team of given organization.
func RemoveTeamMember(ctx context.Context, team *organization.Team, user *user_model.User) error {
return db.WithTx(ctx, func(ctx context.Context) error {
if err := db.WithTx(ctx, func(ctx context.Context) error {
return removeTeamMember(ctx, team, user)
})
}); err != nil {
return err
}
recordTeamMemberAudit(ctx, audit_model.OrganizationTeamMemberRemove, team, user)
return nil
}
+9 -2
View File
@@ -8,11 +8,13 @@ import (
"context"
"fmt"
audit_model "gitea.dev/models/audit"
"gitea.dev/models/db"
"gitea.dev/models/organization"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/services/audit"
)
// RemoveOrgUser removes user from given organization.
@@ -47,7 +49,7 @@ func RemoveOrgUser(ctx context.Context, org *organization.Organization, user *us
}
}
return db.WithTx(ctx, func(ctx context.Context) error {
if err := db.WithTx(ctx, func(ctx context.Context) error {
if _, err := db.DeleteByID[organization.OrgUser](ctx, ou.ID); err != nil {
return err
} else if _, err = db.Exec(ctx, "UPDATE `user` SET num_members=num_members-1 WHERE id=?", org.ID); err != nil {
@@ -94,5 +96,10 @@ func RemoveOrgUser(ctx context.Context, org *organization.Organization, user *us
}
}
return nil
})
}); err != nil {
return err
}
audit.Record(ctx, audit_model.OrganizationMemberRemove, audit.ScopeFromUserID(ctx, org.ID), "member", user.Name)
return nil
}
+20
View File
@@ -6,9 +6,12 @@ package org
import (
"testing"
audit_model "gitea.dev/models/audit"
"gitea.dev/models/organization"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"gitea.dev/modules/setting"
"gitea.dev/modules/test"
"github.com/stretchr/testify/assert"
)
@@ -71,3 +74,20 @@ func TestRemoveOrgUser(t *testing.T) {
unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{OrgID: org7.ID, UID: user5.ID})
unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{})
}
func TestRemoveOrgUserRecordsAudit(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
defer test.MockVariableValue(&setting.Audit.RecordOutput, setting.AuditRecordOutputDatabase)()
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{OrgID: org.ID, UID: user.ID})
assert.NoError(t, RemoveOrgUser(t.Context(), org, user))
unittest.AssertExistsAndLoadBean(t, &audit_model.Event{
Action: audit_model.OrganizationMemberRemove,
ScopeType: audit_model.ScopeOrganization,
ScopeID: org.ID,
})
}