mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-12 17:10:55 +00:00
Co-authored-by: bircni <bircni@users.noreply.github.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
166 lines
5.6 KiB
Go
166 lines
5.6 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package audit
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
audit_model "gitea.dev/models/audit"
|
|
user_model "gitea.dev/models/user"
|
|
"gitea.dev/modules/log"
|
|
"gitea.dev/modules/reqctx"
|
|
"gitea.dev/modules/setting"
|
|
"gitea.dev/modules/timeutil"
|
|
)
|
|
|
|
// RecordParams describes an audit event. Callers (or domain-specific helpers)
|
|
// supply metadata; the message is rendered from the action's template.
|
|
type RecordParams struct {
|
|
Action audit_model.Action
|
|
Actor audit_model.EntityRef
|
|
ActorCredential string
|
|
Impersonator *audit_model.EntityRef
|
|
Scope audit_model.EntityRef
|
|
Metadata map[string]any
|
|
}
|
|
|
|
type originContextKeyType struct{}
|
|
|
|
var originContextKey originContextKeyType
|
|
|
|
type requestInfoContextKeyType struct{}
|
|
|
|
var requestInfoContextKey requestInfoContextKeyType
|
|
|
|
// requestInfo is what an audit event needs to know about the request it was
|
|
// recorded for. The routers publish it, so services don't have to reach for the
|
|
// request themselves.
|
|
type requestInfo struct {
|
|
origin audit_model.Origin
|
|
ipAddress string
|
|
}
|
|
|
|
// WithOrigin returns a context that records audit events with the given origin.
|
|
// It is for entry points that serve no request, eg: the CLI or a cron task.
|
|
func WithOrigin(ctx context.Context, origin audit_model.Origin) context.Context {
|
|
return context.WithValue(ctx, originContextKey, origin)
|
|
}
|
|
|
|
// SetRequestInfo attributes the audit events recorded while serving a request
|
|
// to the given origin and client address.
|
|
func SetRequestInfo(store reqctx.RequestDataStore, origin audit_model.Origin, ipAddress string) {
|
|
store.SetContextValue(requestInfoContextKey, &requestInfo{origin: origin, ipAddress: ipAddress})
|
|
}
|
|
|
|
func requestInfoFromContext(ctx context.Context) *requestInfo {
|
|
info, _ := ctx.Value(requestInfoContextKey).(*requestInfo)
|
|
return info
|
|
}
|
|
|
|
func buildEvent(ctx context.Context, params RecordParams) *audit_model.Event {
|
|
e := &audit_model.Event{
|
|
Action: params.Action,
|
|
ActorID: params.Actor.ID,
|
|
ActorName: params.Actor.DisplayName(),
|
|
ActorCredential: params.ActorCredential,
|
|
ScopeType: params.Scope.Type,
|
|
ScopeID: params.Scope.ID,
|
|
ScopeName: params.Scope.DisplayName(),
|
|
Message: renderMessage(params.Action, params.Actor, params.Scope, params.Metadata),
|
|
Metadata: audit_model.EncodeMetadata(params.Metadata),
|
|
IPAddress: getIPAddress(ctx),
|
|
Origin: getOrigin(ctx),
|
|
TimestampUnix: timeutil.TimeStamp(time.Now().Unix()),
|
|
}
|
|
if params.Impersonator != nil {
|
|
e.ImpersonatorID = params.Impersonator.ID
|
|
e.ImpersonatorName = params.Impersonator.DisplayName()
|
|
}
|
|
return e
|
|
}
|
|
|
|
func getIPAddress(ctx context.Context) string {
|
|
if info := requestInfoFromContext(ctx); info != nil {
|
|
return info.ipAddress
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func getOrigin(ctx context.Context) audit_model.Origin {
|
|
if origin, ok := ctx.Value(originContextKey).(audit_model.Origin); ok && origin != "" {
|
|
return origin
|
|
}
|
|
if info := requestInfoFromContext(ctx); info != nil && info.origin != "" {
|
|
return info.origin
|
|
}
|
|
return audit_model.OriginSystem
|
|
}
|
|
|
|
// Record writes an audit event for an action against a scope entity. The actor
|
|
// is the signed-in user of the surrounding request, or whoever audit.WithDoer
|
|
// named for a background context.
|
|
//
|
|
// The scope is the affected entity and may be a *user.User,
|
|
// *organization.Organization, *repo.Repository, an EntityRef, or nil for an
|
|
// instance-wide/system event. Metadata is supplied as alternating
|
|
// string-key/value pairs and fills the placeholders of the action's message
|
|
// template, so every key a template names must be passed here.
|
|
//
|
|
// audit.Record(ctx, audit_model.RepositoryArchive, repo)
|
|
// audit.Record(ctx, audit_model.RepositoryDeployKeyAdd, repo, "deploy_key", key.Name)
|
|
func Record(ctx context.Context, action audit_model.Action, scope any, metadata ...any) {
|
|
RecordAs(ctx, doerFromContext(ctx), action, scope, metadata...)
|
|
}
|
|
|
|
// RecordAs is Record with an explicit actor, for the few call sites where the
|
|
// acting user is not the one the context resolves to.
|
|
func RecordAs(ctx context.Context, doer *user_model.User, action audit_model.Action, scope any, metadata ...any) {
|
|
writeEvent(ctx, RecordParams{
|
|
Action: action,
|
|
Actor: actorRef(doer),
|
|
ActorCredential: actorCredential(ctx, doer),
|
|
Impersonator: impersonatorRef(ImpersonatorFromContext(ctx), doer),
|
|
Scope: scopeRef(scope),
|
|
Metadata: metaPairs(metadata...),
|
|
})
|
|
}
|
|
|
|
// writeEvent persists an audit event when audit logging is enabled.
|
|
func writeEvent(ctx context.Context, params RecordParams) {
|
|
if !setting.AuditRecordEnabled() {
|
|
return
|
|
}
|
|
|
|
e := buildEvent(ctx, params)
|
|
|
|
if err := audit_model.InsertEvent(ctx, e); err != nil {
|
|
log.Error("Error writing audit event action=%s actor=%s scope=%s/%d to database: %v", e.Action, e.ActorName, e.ScopeType, e.ScopeID, err)
|
|
}
|
|
}
|
|
|
|
func FindEvents(ctx context.Context, opts *audit_model.EventSearchOptions) ([]*audit_model.Event, int64, error) {
|
|
return audit_model.FindEvents(ctx, opts)
|
|
}
|
|
|
|
// metaPairs builds caller-defined metadata from alternating string-key/value
|
|
// pairs. Keys should be stable for log parsers. A non-string key is skipped and
|
|
// logged rather than panicking: audit recording must never crash the request
|
|
// that triggered it.
|
|
func metaPairs(pairs ...any) map[string]any {
|
|
if len(pairs) == 0 {
|
|
return nil
|
|
}
|
|
m := make(map[string]any, len(pairs)/2)
|
|
for i := 0; i+1 < len(pairs); i += 2 {
|
|
key, ok := pairs[i].(string)
|
|
if !ok {
|
|
log.Error("audit: metadata key must be string, got %T; skipping pair", pairs[i])
|
|
continue
|
|
}
|
|
m[key] = pairs[i+1]
|
|
}
|
|
return m
|
|
}
|