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:
@@ -0,0 +1,105 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package audit
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"net/http"
|
||||
"slices"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
"gitea.dev/models/db"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
// ViewOptions configures a scoped audit log listing. The admin view leaves
|
||||
// ScopeType empty to list every event; the user/org/repo views constrain the
|
||||
// query to their own scope.
|
||||
type ViewOptions struct {
|
||||
Template templates.TplName
|
||||
ScopeType audit_model.ScopeType
|
||||
ScopeID int64
|
||||
// PageData holds ctx.Data flags to enable for the active navigation tab.
|
||||
PageData map[string]any
|
||||
}
|
||||
|
||||
var filterableOrigins = []audit_model.Origin{audit_model.OriginUI, audit_model.OriginAPI, audit_model.OriginCLI, audit_model.OriginSystem}
|
||||
|
||||
// SearchOptionsFromRequest builds the event query from the request filters and
|
||||
// publishes the applied values to ctx.Data so the filter form can render its
|
||||
// current state. The listing and the export share it to stay in sync.
|
||||
func SearchOptionsFromRequest(ctx *context.Context, scopeType audit_model.ScopeType, scopeID int64) *audit_model.EventSearchOptions {
|
||||
// only the two known sort values are accepted, anything else falls back to the default
|
||||
sort := util.Iif(audit_model.EventSort(ctx.FormString("sort")) == audit_model.SortTimestampAsc, audit_model.SortTimestampAsc, audit_model.SortTimestampDesc)
|
||||
|
||||
opts := &audit_model.EventSearchOptions{
|
||||
Sort: sort,
|
||||
ScopeType: scopeType,
|
||||
ScopeID: scopeID,
|
||||
}
|
||||
|
||||
if action := audit_model.Action(ctx.FormString("action")); action != "" {
|
||||
if _, ok := audit_model.MessageTemplate(action); ok {
|
||||
opts.Action = action
|
||||
} else if audit_model.IsActionFilter(action) {
|
||||
opts.ActionPrefix = action
|
||||
}
|
||||
}
|
||||
if origin := audit_model.Origin(ctx.FormString("origin")); slices.Contains(filterableOrigins, origin) {
|
||||
opts.Origin = origin
|
||||
}
|
||||
if actor := ctx.FormTrim("actor"); actor != "" {
|
||||
u, err := user_model.GetUserByName(ctx, actor)
|
||||
if err != nil {
|
||||
opts.ActorID = -1 // an unknown actor matches nothing rather than everything
|
||||
} else {
|
||||
opts.ActorID = u.ID
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Data["AuditSort"] = string(opts.Sort)
|
||||
ctx.Data["AuditFilterAction"] = string(opts.Action)
|
||||
if opts.ActionPrefix != "" {
|
||||
ctx.Data["AuditFilterAction"] = string(opts.ActionPrefix)
|
||||
}
|
||||
ctx.Data["AuditFilterOrigin"] = string(opts.Origin)
|
||||
ctx.Data["AuditFilterActor"] = ctx.FormTrim("actor")
|
||||
ctx.Data["AuditActions"] = audit_model.ActionFilters()
|
||||
ctx.Data["AuditOrigins"] = filterableOrigins
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
// View renders a paginated audit log listing shared by the admin, user, org and
|
||||
// repo settings pages. Only the scope filter, template and page flags differ.
|
||||
func View(ctx *context.Context, opts ViewOptions) {
|
||||
ctx.Data["Title"] = ctx.Tr("audit.title")
|
||||
ctx.Data["AuditRecordEnabled"] = setting.AuditRecordEnabled()
|
||||
maps.Copy(ctx.Data, opts.PageData)
|
||||
|
||||
page := max(ctx.FormInt("page"), 1)
|
||||
|
||||
searchOpts := SearchOptionsFromRequest(ctx, opts.ScopeType, opts.ScopeID)
|
||||
searchOpts.ListOptions = db.ListOptions{
|
||||
Page: page,
|
||||
PageSize: setting.UI.Admin.NoticePagingNum,
|
||||
}
|
||||
|
||||
evs, total, err := audit.FindEvents(ctx, searchOpts)
|
||||
if err != nil {
|
||||
ctx.ServerError("FindEvents", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["AuditEvents"] = evs
|
||||
|
||||
ctx.Data["Page"] = context.NewPagerBuilder(ctx).TotalCount(total).PerPageLimit(setting.UI.Admin.NoticePagingNum).CurPage(page).Build()
|
||||
|
||||
ctx.HTML(http.StatusOK, opts.Template)
|
||||
}
|
||||
@@ -5,10 +5,13 @@ package secrets
|
||||
|
||||
import (
|
||||
"gitea.dev/models/db"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
secret_model "gitea.dev/models/secret"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
secret_service "gitea.dev/services/secrets"
|
||||
@@ -26,29 +29,49 @@ func SetSecretsContext(ctx *context.Context, ownerID, repoID int64) {
|
||||
ctx.Data["DescriptionMaxLength"] = secret_model.SecretDescriptionMaxLength
|
||||
}
|
||||
|
||||
func PerformSecretsPost(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
|
||||
form := web.GetForm[*forms.AddSecretForm](ctx)
|
||||
func secretOwnerRepoIDs(owner *user_model.User, repo *repo_model.Repository) (ownerID, repoID int64) {
|
||||
if owner != nil {
|
||||
ownerID = owner.ID
|
||||
}
|
||||
if repo != nil {
|
||||
repoID = repo.ID
|
||||
}
|
||||
return ownerID, repoID
|
||||
}
|
||||
|
||||
s, _, err := secret_service.CreateOrUpdateSecret(ctx, ownerID, repoID, form.Name, util.NormalizeStringEOL(form.Data), form.Description)
|
||||
func PerformSecretsPost(ctx *context.Context, owner *user_model.User, repo *repo_model.Repository, redirectURL string) {
|
||||
form := web.GetForm[*forms.AddSecretForm](ctx)
|
||||
ownerID, repoID := secretOwnerRepoIDs(owner, repo)
|
||||
|
||||
s, created, err := secret_service.CreateOrUpdateSecret(ctx, ownerID, repoID, form.Name, util.NormalizeStringEOL(form.Data), form.Description)
|
||||
if err != nil {
|
||||
ctx.JSONErrorAuto(err)
|
||||
return
|
||||
}
|
||||
|
||||
actions := audit.SecretUpdate
|
||||
if created {
|
||||
actions = audit.SecretAdd
|
||||
}
|
||||
audit.RecordScoped(ctx, owner, repo, actions, "secret", s.Name)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("secrets.save_success", s.Name))
|
||||
ctx.JSONRedirect(redirectURL)
|
||||
}
|
||||
|
||||
func PerformSecretsDelete(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
|
||||
func PerformSecretsDelete(ctx *context.Context, owner *user_model.User, repo *repo_model.Repository, redirectURL string) {
|
||||
id := ctx.FormInt64("id")
|
||||
ownerID, repoID := secretOwnerRepoIDs(owner, repo)
|
||||
|
||||
err := secret_service.DeleteSecretByID(ctx, ownerID, repoID, id)
|
||||
s, err := secret_service.DeleteSecretByID(ctx, ownerID, repoID, id)
|
||||
if err != nil {
|
||||
log.Error("DeleteSecretByID(%d) failed: %v", id, err)
|
||||
ctx.JSONError(ctx.Tr("secrets.deletion.failed"))
|
||||
return
|
||||
}
|
||||
|
||||
audit.RecordScoped(ctx, owner, repo, audit.SecretRemove, "secret", s.Name)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("secrets.deletion.success"))
|
||||
ctx.JSONRedirect(redirectURL)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user