mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-19 03:03:39 +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:
co-authored by
bircni
wxiaoguang
parent
4d43445532
commit
da37b7916b
@@ -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))
|
||||
|
||||
@@ -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" {
|
||||
|
||||
@@ -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},
|
||||
})
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user