mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-16 01:43:24 +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
@@ -21,7 +21,7 @@ var (
|
||||
|
||||
func newOAuth2CommonHandlers() *user_setting.OAuth2CommonHandlers {
|
||||
return &user_setting.OAuth2CommonHandlers{
|
||||
OwnerID: 0,
|
||||
Owner: nil, // instance-wide
|
||||
BasePathList: setting.AppSubURL + "/-/admin/applications",
|
||||
BasePathEditPrefix: setting.AppSubURL + "/-/admin/applications/oauth2",
|
||||
TplAppEdit: tplSettingsOauth2ApplicationEdit,
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
"gitea.dev/modules/httplib"
|
||||
"gitea.dev/modules/log"
|
||||
shared_audit "gitea.dev/routers/web/shared/audit"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
const auditExportPageSize = 1000
|
||||
|
||||
func ViewAuditLogs(ctx *context.Context) {
|
||||
shared_audit.View(ctx, shared_audit.ViewOptions{
|
||||
Template: "admin/audit/list",
|
||||
PageData: map[string]any{"PageIsAdminMonitorAudit": true},
|
||||
})
|
||||
}
|
||||
|
||||
func ExportAuditLogs(ctx *context.Context) {
|
||||
// the export mirrors the filters of the listing it was started from
|
||||
searchOpts := shared_audit.SearchOptionsFromRequest(ctx, "", 0)
|
||||
searchOpts.Sort = audit_model.SortTimestampAsc
|
||||
searchOpts.PageSize = auditExportPageSize
|
||||
|
||||
page := 1
|
||||
findPage := func() ([]*audit_model.Event, int64, error) {
|
||||
searchOpts.Page = page
|
||||
return audit.FindEvents(ctx, searchOpts)
|
||||
}
|
||||
|
||||
// the first page is fetched before any header is written so a failing query still results in a proper error page
|
||||
events, total, err := findPage()
|
||||
if err != nil {
|
||||
ctx.ServerError("FindEvents", err)
|
||||
return
|
||||
}
|
||||
|
||||
httplib.ServeSetHeaders(ctx.Resp, httplib.ServeHeaderOptions{
|
||||
ContentType: "application/x-ndjson; charset=utf-8",
|
||||
Filename: fmt.Sprintf("gitea-audit-log-%s.jsonl", time.Now().UTC().Format("20060102-150405Z")),
|
||||
ContentDisposition: httplib.ContentDispositionAttachment,
|
||||
})
|
||||
ctx.SetTotalCountHeader(total) // lets a client detect an export truncated by a mid-stream failure
|
||||
ctx.Resp.WriteHeader(http.StatusOK)
|
||||
|
||||
for {
|
||||
if err := audit.WriteEventsAsJSON(ctx.Resp, events); err != nil {
|
||||
log.Debug("Unable to write audit log export: %v", err)
|
||||
return
|
||||
}
|
||||
if len(events) < auditExportPageSize {
|
||||
return
|
||||
}
|
||||
|
||||
page++
|
||||
events, _, err = findPage()
|
||||
if err != nil {
|
||||
log.Error("Unable to continue audit log export: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -301,7 +301,7 @@ func NewAuthSourcePost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := auth.CreateSource(ctx, &auth.Source{
|
||||
if err := auth_service.CreateSource(ctx, &auth.Source{
|
||||
Type: auth.Type(form.Type),
|
||||
Name: form.Name,
|
||||
IsActive: form.IsActive,
|
||||
@@ -419,7 +419,7 @@ func EditAuthSourcePost(ctx *context.Context) {
|
||||
source.IsSyncEnabled = form.IsSyncEnabled
|
||||
source.Cfg = config
|
||||
source.TwoFactorPolicy = form.TwoFactorPolicy
|
||||
if err := auth.UpdateSource(ctx, source); err != nil {
|
||||
if err := auth_service.UpdateSource(ctx, source); err != nil {
|
||||
if errExist, ok := errors.AsType[auth.ErrSourceAlreadyExist](err); ok {
|
||||
ctx.Data["Err_Name"] = true
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("admin.auths.login_source_exist", errExist.Name), tplAuthEdit, form)
|
||||
|
||||
@@ -8,12 +8,14 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
"gitea.dev/models/db"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/user"
|
||||
)
|
||||
@@ -129,6 +131,13 @@ func ActivateEmail(ctx *context.Context) {
|
||||
ctx.Flash.Error(ctx.Tr("admin.emails.not_updated", err))
|
||||
}
|
||||
} else {
|
||||
// the email already changed, so a failed lookup must not fail the request
|
||||
if u, err := user_model.GetUserByID(ctx, uid); err != nil {
|
||||
log.Error("GetUserByID(%d) for audit: %v", uid, err)
|
||||
} else {
|
||||
audit.Record(ctx, audit_model.UserEmailActivate, u, "email", email, "activated", activate)
|
||||
}
|
||||
|
||||
log.Info("Activation for User ID: %d, email: %s, primary: %v changed to %v", uid, email, primary, activate)
|
||||
ctx.Flash.Info(ctx.Tr("admin.emails.updated"))
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/services/audit"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
@@ -61,9 +62,14 @@ func DefaultOrSystemWebhooks(ctx *context.Context) {
|
||||
|
||||
// DeleteDefaultOrSystemWebhook handler to delete an admin-defined system or default webhook
|
||||
func DeleteDefaultOrSystemWebhook(ctx *context.Context) {
|
||||
if err := webhook.DeleteDefaultSystemWebhook(ctx, ctx.FormInt64("id")); err != nil {
|
||||
hook, err := webhook.GetWebhookByID(ctx, ctx.FormInt64("id"))
|
||||
if err != nil {
|
||||
ctx.Flash.Error("GetWebhookByID: " + err.Error())
|
||||
} else if err := webhook.DeleteDefaultSystemWebhook(ctx, hook.ID); err != nil {
|
||||
ctx.Flash.Error("DeleteDefaultWebhook: " + err.Error())
|
||||
} else {
|
||||
audit.RecordScoped(ctx, nil, nil, audit.WebhookRemove, "webhook", hook.URL)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.webhook_deletion_success"))
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
audit_model "gitea.dev/models/audit"
|
||||
"gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
org_model "gitea.dev/models/organization"
|
||||
@@ -25,6 +26,7 @@ import (
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/web/explore"
|
||||
user_setting "gitea.dev/routers/web/user/setting"
|
||||
"gitea.dev/services/audit"
|
||||
auth_service "gitea.dev/services/auth"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
@@ -203,6 +205,8 @@ func NewUserPost(ctx *context.Context) {
|
||||
ctx.Flash.Warning(ctx.Tr("form.email_domain_is_not_allowed", u.Email))
|
||||
}
|
||||
|
||||
audit.Record(ctx, audit_model.UserCreate, u)
|
||||
|
||||
log.Trace("Account created by admin (%s): %s", ctx.Doer.Name, u.Name)
|
||||
|
||||
// Send email notification.
|
||||
@@ -468,6 +472,7 @@ func ImpersonateUser(ctx *context.Context) {
|
||||
ctx.ServerError("unable to impersonate user", err)
|
||||
return
|
||||
}
|
||||
audit.Record(ctx, audit_model.UserImpersonation, u)
|
||||
ctx.JSONRedirect(setting.AppSubURL + "/user/settings")
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user