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 08:15:23 +00:00
committed by GitHub
co-authored by bircni wxiaoguang
parent 4d43445532
commit da37b7916b
136 changed files with 3863 additions and 208 deletions
+1
View File
@@ -1385,6 +1385,7 @@ func disableOrEnableWorkflowFile(ctx *context_module.Context, isEnable bool) {
ctx.ServerError("UpdateRepoUnit", err)
return
}
actions_service.RecordWorkflowToggle(ctx, ctx.Repo.Repository, workflow, isEnable)
if isEnable {
ctx.Flash.Success(ctx.Tr("actions.workflow.enable_success", workflow))
+8 -2
View File
@@ -9,6 +9,7 @@ import (
"slices"
"strings"
audit_model "gitea.dev/models/audit"
"gitea.dev/models/db"
issues_model "gitea.dev/models/issues"
project_model "gitea.dev/models/project"
@@ -24,6 +25,7 @@ import (
"gitea.dev/modules/web"
"gitea.dev/routers/web/shared/issue"
shared_user "gitea.dev/routers/web/shared/user"
"gitea.dev/services/audit"
"gitea.dev/services/context"
"gitea.dev/services/forms"
project_service "gitea.dev/services/projects"
@@ -134,7 +136,7 @@ func NewProjectPost(ctx *context.Context) {
return
}
if err := project_model.NewProject(ctx, &project_model.Project{
project := &project_model.Project{
RepoID: ctx.Repo.Repository.ID,
Title: form.Title,
Description: form.Content,
@@ -142,10 +144,12 @@ func NewProjectPost(ctx *context.Context) {
TemplateType: form.TemplateType,
CardType: form.CardType,
Type: project_model.TypeRepository,
}); err != nil {
}
if err := project_model.NewProject(ctx, project); err != nil {
ctx.ServerError("NewProject", err)
return
}
audit.Record(ctx, audit_model.ProjectCreate, ctx.Repo.Repository, "project", project.Title, "project_id", project.ID)
ctx.Flash.Success(ctx.Tr("repo.projects.create_success", form.Title))
ctx.Redirect(ctx.Repo.RepoLink + "/projects")
@@ -191,6 +195,7 @@ func DeleteProject(ctx *context.Context) {
if err := project_model.DeleteProjectByID(ctx, p.ID); err != nil {
ctx.Flash.Error("DeleteProjectByID: " + err.Error())
} else {
audit.Record(ctx, audit_model.ProjectDelete, ctx.Repo.Repository, "project", p.Title, "project_id", p.ID)
ctx.Flash.Success(ctx.Tr("repo.projects.deletion_success"))
}
@@ -265,6 +270,7 @@ func EditProjectPost(ctx *context.Context) {
ctx.ServerError("UpdateProjects", err)
return
}
audit.Record(ctx, audit_model.ProjectUpdate, ctx.Repo.Repository, "project", p.Title, "project_id", p.ID)
ctx.Flash.Success(ctx.Tr("repo.projects.edit_success", p.Title))
if ctx.FormString("redirect") == "project" {
+19
View File
@@ -0,0 +1,19 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
audit_model "gitea.dev/models/audit"
shared_audit "gitea.dev/routers/web/shared/audit"
"gitea.dev/services/context"
)
func ViewAuditLogs(ctx *context.Context) {
shared_audit.View(ctx, shared_audit.ViewOptions{
Template: "repo/settings/audit_logs",
ScopeType: audit_model.ScopeRepository,
ScopeID: ctx.Repo.Repository.ID,
PageData: map[string]any{"PageIsSettingsAudit": true},
})
}
+3
View File
@@ -8,6 +8,7 @@ import (
"net/http"
asymkey_model "gitea.dev/models/asymkey"
audit_model "gitea.dev/models/audit"
"gitea.dev/models/db"
deploykey_model "gitea.dev/models/deploykey"
"gitea.dev/models/perm"
@@ -15,6 +16,7 @@ import (
"gitea.dev/modules/setting"
"gitea.dev/modules/util"
asymkey_service "gitea.dev/services/asymkey"
"gitea.dev/services/audit"
"gitea.dev/services/context"
"gitea.dev/services/forms"
)
@@ -68,6 +70,7 @@ func DeployKeysPost(ctx *context.Context) {
return
}
audit.Record(ctx, audit_model.RepositoryDeployKeyAdd, ctx.Repo.Repository, "deploy_key", key.Name)
ctx.Flash.Success(ctx.Tr("repo.settings.add_key_success", key.Name))
ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings/keys")
}
@@ -12,6 +12,7 @@ import (
"strings"
"time"
audit_model "gitea.dev/models/audit"
git_model "gitea.dev/models/git"
"gitea.dev/models/organization"
"gitea.dev/models/perm"
@@ -24,6 +25,7 @@ import (
"gitea.dev/modules/templates"
"gitea.dev/modules/web"
"gitea.dev/routers/web/repo"
"gitea.dev/services/audit"
"gitea.dev/services/context"
"gitea.dev/services/forms"
pull_service "gitea.dev/services/pull"
@@ -149,8 +151,10 @@ func SettingsProtectedBranchPost(ctx *context.Context) {
return
}
}
isNewProtectedBranch := false
if protectBranch == nil {
// No options found, create defaults.
isNewProtectedBranch = true
protectBranch = &git_model.ProtectedBranch{
RepoID: ctx.Repo.Repository.ID,
RuleName: f.RuleName,
@@ -291,6 +295,12 @@ func SettingsProtectedBranchPost(ctx *context.Context) {
return
}
if isNewProtectedBranch {
audit.Record(ctx, audit_model.RepositoryBranchProtectionAdd, ctx.Repo.Repository, "rule", protectBranch.RuleName)
} else {
audit.Record(ctx, audit_model.RepositoryBranchProtectionUpdate, ctx.Repo.Repository, "rule", protectBranch.RuleName)
}
ctx.Flash.Success(ctx.Tr("repo.settings.update_protect_branch_success", protectBranch.RuleName))
ctx.Redirect(fmt.Sprintf("%s/settings/branches?rule_name=%s", ctx.Repo.RepoLink, protectBranch.RuleName))
}
@@ -323,6 +333,8 @@ func DeleteProtectedBranchRulePost(ctx *context.Context) {
return
}
audit.Record(ctx, audit_model.RepositoryBranchProtectionRemove, ctx.Repo.Repository, "rule", rule.RuleName)
ctx.Flash.Success(ctx.Tr("repo.settings.remove_protected_branch_success", rule.RuleName))
ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings/branches")
}
@@ -8,6 +8,7 @@ import (
"net/http"
"strings"
audit_model "gitea.dev/models/audit"
git_model "gitea.dev/models/git"
"gitea.dev/models/organization"
"gitea.dev/models/perm"
@@ -17,6 +18,7 @@ import (
"gitea.dev/modules/setting"
"gitea.dev/modules/templates"
"gitea.dev/modules/web"
"gitea.dev/services/audit"
"gitea.dev/services/context"
"gitea.dev/services/forms"
)
@@ -65,6 +67,8 @@ func NewProtectedTagPost(ctx *context.Context) {
return
}
audit.Record(ctx, audit_model.RepositoryTagProtectionAdd, ctx.Repo.Repository, "pattern", pt.NamePattern)
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath())
}
@@ -118,6 +122,8 @@ func EditProtectedTagPost(ctx *context.Context) {
return
}
audit.Record(ctx, audit_model.RepositoryTagProtectionUpdate, ctx.Repo.Repository, "pattern", pt.NamePattern)
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/tags")
}
@@ -134,6 +140,8 @@ func DeleteProtectedTagPost(ctx *context.Context) {
return
}
audit.Record(ctx, audit_model.RepositoryTagProtectionRemove, ctx.Repo.Repository, "pattern", pt.NamePattern)
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/tags")
}
+11 -7
View File
@@ -7,6 +7,8 @@ import (
"errors"
"net/http"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/setting"
"gitea.dev/modules/templates"
shared "gitea.dev/routers/web/shared/secrets"
@@ -22,6 +24,8 @@ const (
)
type secretsCtx struct {
Owner *user_model.User
Repo *repo_model.Repository
OwnerID int64
RepoID int64
IsRepo bool
@@ -34,7 +38,7 @@ type secretsCtx struct {
func getSecretsCtx(ctx *context.Context) (*secretsCtx, error) {
if ctx.Data["PageIsRepoSettings"] == true {
return &secretsCtx{
OwnerID: 0,
Repo: ctx.Repo.Repository,
RepoID: ctx.Repo.Repository.ID,
IsRepo: true,
SecretsTemplate: tplRepoSecrets,
@@ -48,8 +52,8 @@ func getSecretsCtx(ctx *context.Context) (*secretsCtx, error) {
return nil, nil //nolint:nilnil // error is already handled by ctx.ServerError
}
return &secretsCtx{
Owner: ctx.ContextUser,
OwnerID: ctx.ContextUser.ID,
RepoID: 0,
IsOrg: true,
SecretsTemplate: tplOrgSecrets,
RedirectLink: ctx.Org.OrgLink + "/settings/actions/secrets",
@@ -58,8 +62,8 @@ func getSecretsCtx(ctx *context.Context) (*secretsCtx, error) {
if ctx.Data["PageIsUserSettings"] == true {
return &secretsCtx{
Owner: ctx.Doer,
OwnerID: ctx.Doer.ID,
RepoID: 0,
IsUser: true,
SecretsTemplate: tplUserSecrets,
RedirectLink: setting.AppSubURL + "/user/settings/actions/secrets",
@@ -105,8 +109,8 @@ func SecretsPost(ctx *context.Context) {
shared.PerformSecretsPost(
ctx,
sCtx.OwnerID,
sCtx.RepoID,
sCtx.Owner,
sCtx.Repo,
sCtx.RedirectLink,
)
}
@@ -119,8 +123,8 @@ func SecretsDelete(ctx *context.Context) {
}
shared.PerformSecretsDelete(
ctx,
sCtx.OwnerID,
sCtx.RepoID,
sCtx.Owner,
sCtx.Repo,
sCtx.RedirectLink,
)
}
+16
View File
@@ -11,6 +11,7 @@ import (
"strings"
"time"
audit_model "gitea.dev/models/audit"
"gitea.dev/models/db"
"gitea.dev/models/organization"
access_model "gitea.dev/models/perm/access"
@@ -31,6 +32,7 @@ import (
"gitea.dev/modules/web"
repo_router "gitea.dev/routers/web/repo"
actions_service "gitea.dev/services/actions"
"gitea.dev/services/audit"
"gitea.dev/services/context"
"gitea.dev/services/forms"
"gitea.dev/services/migrations"
@@ -478,6 +480,8 @@ func handleSettingsPostPushMirrorRemove(ctx *context.Context) {
return
}
audit.Record(ctx, audit_model.RepositoryMirrorPushRemove, repo, "mirror_id", m.ID, "remote_address", m.RemoteAddress)
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
ctx.Redirect(repo.Link() + "/settings")
}
@@ -542,6 +546,8 @@ func handleSettingsPostPushMirrorAdd(ctx *context.Context) {
return
}
audit.Record(ctx, audit_model.RepositoryMirrorPushAdd, repo, "mirror_id", m.ID, "remote_address", m.RemoteAddress)
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
ctx.Redirect(repo.Link() + "/settings")
}
@@ -724,6 +730,9 @@ func handleSettingsPostSigning(ctx *context.Context) {
ctx.ServerError("UpdateRepositoryColsNoAutoTime", err)
return
}
audit.Record(ctx, audit_model.RepositorySigningVerification, repo, "trust_model", repo.TrustModel.String())
log.Trace("Repository signing settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
}
@@ -808,6 +817,9 @@ func handleSettingsPostConvert(ctx *context.Context) {
ctx.ServerError("DeleteMirrorByRepoID", err)
return
}
audit.Record(ctx, audit_model.RepositoryConvertMirror, repo)
log.Trace("Repository converted from mirror to regular: %s", repo.FullName())
ctx.Flash.Success(ctx.Tr("repo.settings.convert_succeed"))
ctx.JSONRedirect(repo.Link())
@@ -1018,6 +1030,8 @@ func handleSettingsPostArchive(ctx *context.Context) {
// update issue indexer
issue_indexer.UpdateRepoIndexer(ctx, repo.ID)
audit.Record(ctx, audit_model.RepositoryArchive, repo)
ctx.Flash.Success(ctx.Tr("repo.settings.archive.success"))
log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
@@ -1046,6 +1060,8 @@ func handleSettingsPostUnarchive(ctx *context.Context) {
// update issue indexer
issue_indexer.UpdateRepoIndexer(ctx, repo.ID)
audit.Record(ctx, audit_model.RepositoryUnarchive, repo)
ctx.Flash.Success(ctx.Tr("repo.settings.unarchive.success"))
log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+25 -1
View File
@@ -15,6 +15,7 @@ import (
"gitea.dev/models/db"
"gitea.dev/models/perm"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/models/webhook"
"gitea.dev/modules/git"
@@ -25,6 +26,7 @@ import (
"gitea.dev/modules/util"
"gitea.dev/modules/web"
webhook_module "gitea.dev/modules/webhook"
"gitea.dev/services/audit"
"gitea.dev/services/context"
"gitea.dev/services/convert"
"gitea.dev/services/forms"
@@ -58,6 +60,8 @@ func Webhooks(ctx *context.Context) {
}
type ownerRepoCtx struct {
Owner *user_model.User
Repo *repo_model.Repository
OwnerID int64
RepoID int64
IsAdmin bool
@@ -71,6 +75,7 @@ type ownerRepoCtx struct {
func getOwnerRepoCtx(ctx *context.Context) (*ownerRepoCtx, error) {
if ctx.Data["PageIsRepoSettings"] == true {
return &ownerRepoCtx{
Repo: ctx.Repo.Repository,
RepoID: ctx.Repo.Repository.ID,
Link: path.Join(ctx.Repo.RepoLink, "settings/hooks"),
LinkNew: path.Join(ctx.Repo.RepoLink, "settings/hooks"),
@@ -80,6 +85,7 @@ func getOwnerRepoCtx(ctx *context.Context) (*ownerRepoCtx, error) {
if ctx.Data["PageIsOrgSettings"] == true {
return &ownerRepoCtx{
Owner: ctx.ContextUser,
OwnerID: ctx.ContextUser.ID,
Link: path.Join(ctx.Org.OrgLink, "settings/hooks"),
LinkNew: path.Join(ctx.Org.OrgLink, "settings/hooks"),
@@ -89,6 +95,7 @@ func getOwnerRepoCtx(ctx *context.Context) (*ownerRepoCtx, error) {
if ctx.Data["PageIsUserSettings"] == true {
return &ownerRepoCtx{
Owner: ctx.Doer,
OwnerID: ctx.Doer.ID,
Link: path.Join(setting.AppSubURL, "/user/settings/hooks"),
LinkNew: path.Join(setting.AppSubURL, "/user/settings/hooks"),
@@ -109,6 +116,14 @@ func getOwnerRepoCtx(ctx *context.Context) (*ownerRepoCtx, error) {
return nil, errors.New("unable to set OwnerRepo context")
}
// recordWebhookAudit emits a webhook audit event scoped to the repository,
// organization, user, or instance (admin/system) the webhook belongs to. The
// shared add/edit handlers run in any of these contexts, so the scope is derived
// from orCtx rather than assuming a repository.
func (orCtx *ownerRepoCtx) recordWebhookAudit(ctx *context.Context, actions audit.ScopedActions, url string) {
audit.RecordScoped(ctx, orCtx.Owner, orCtx.Repo, actions, "webhook", url)
}
func checkHookType(ctx *context.Context) string {
hookType := strings.ToLower(ctx.PathParam("type"))
if !util.SliceContainsString(setting.Webhook.Types, hookType, true) {
@@ -258,6 +273,8 @@ func createWebhook(ctx *context.Context, params webhookParams) {
return
}
orCtx.recordWebhookAudit(ctx, audit.WebhookAdd, w.URL)
ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
ctx.Redirect(orCtx.Link)
}
@@ -311,6 +328,8 @@ func editWebhook(ctx *context.Context, params webhookParams) {
return
}
orCtx.recordWebhookAudit(ctx, audit.WebhookUpdate, w.URL)
ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
}
@@ -736,9 +755,14 @@ func ReplayWebhook(ctx *context.Context) {
// DeleteWebhook delete a webhook
func DeleteWebhook(ctx *context.Context) {
if err := webhook.DeleteWebhookByRepoID(ctx, ctx.Repo.Repository.ID, ctx.FormInt64("id")); err != nil {
hook, err := webhook.GetWebhookByRepoID(ctx, ctx.Repo.Repository.ID, ctx.FormInt64("id"))
if err != nil {
ctx.Flash.Error("GetWebhookByRepoID: " + err.Error())
} else if err := webhook.DeleteWebhookByRepoID(ctx, ctx.Repo.Repository.ID, hook.ID); err != nil {
ctx.Flash.Error("DeleteWebhookByRepoID: " + err.Error())
} else {
audit.RecordScoped(ctx, nil, ctx.Repo.Repository, audit.WebhookRemove, "webhook", hook.URL)
ctx.Flash.Success(ctx.Tr("repo.settings.webhook_deletion_success"))
}