// Package audit records explicitly selected business events without request bodies. package audit import ( "context" "encoding/json" "errors" "git.yuntuo.hk/k2safe/go-omni/logx" "io" "sync" "time" ) type Event struct { Time time.Time `json:"time"` ActorID string `json:"actorId"` TenantID string `json:"tenantId,omitempty"` Action string `json:"action"` Resource string `json:"resource"` Outcome string `json:"outcome"` RequestID string `json:"requestId,omitempty"` TraceID string `json:"traceId,omitempty"` Fields map[string]string `json:"fields,omitempty"` } type Sink interface { Write(context.Context, Event) error } type Recorder struct { sink Sink allowed map[string]bool } func New(sink Sink, allowedFields ...string) (*Recorder, error) { if sink == nil || len(allowedFields) > 32 { return nil, errors.New("audit: sink and at most 32 fields required") } r := &Recorder{sink: sink, allowed: map[string]bool{}} for _, name := range allowedFields { if name == "" || len(name) > 64 { return nil, errors.New("audit: invalid field") } r.allowed[name] = true } return r, nil } func (r *Recorder) Record(ctx context.Context, event Event) error { if ctx == nil { return errors.New("audit: nil context") } if err := ctx.Err(); err != nil { return err } if event.ActorID == "" || event.Action == "" || event.Resource == "" || (event.Outcome != "success" && event.Outcome != "failure" && event.Outcome != "denied") { return errors.New("audit: actor/action/resource and valid outcome required") } for _, value := range []string{event.ActorID, event.TenantID, event.Action, event.Resource} { if len(value) > 512 { return errors.New("audit: event field too large") } } event.Time = time.Now().UTC() event.RequestID = logx.RequestID(ctx) event.TraceID = logx.TraceID(ctx) filtered := map[string]string{} for name, value := range event.Fields { if r.allowed[name] { if len(value) > 1024 { return errors.New("audit: attribute too large") } filtered[name] = value } } event.Fields = filtered return r.sink.Write(ctx, event) } // JSONSink borrows its writer and serializes complete JSON lines. Write errors // are returned; callers choose whether a failed audit must reject an operation. type JSONSink struct { mu sync.Mutex writer io.Writer } func NewJSONSink(writer io.Writer) (*JSONSink, error) { if writer == nil { return nil, errors.New("audit: writer required") } return &JSONSink{writer: writer}, nil } func (s *JSONSink) Write(ctx context.Context, event Event) error { data, err := json.Marshal(event) if err != nil { return err } data = append(data, '\n') s.mu.Lock() defer s.mu.Unlock() if err := ctx.Err(); err != nil { return err } n, err := s.writer.Write(data) if err == nil && n != len(data) { return io.ErrShortWrite } return err }