66 lines
2.6 KiB
Go
66 lines
2.6 KiB
Go
// Package audit records system-affecting actions (signal writes by users or the
|
|
// control-logic engine, interface and control-logic mutations) to an append-only
|
|
// SQLite log that audit staff can query later. It is enabled via [audit] in the
|
|
// config; when disabled a no-op Recorder is used so call sites need no guards.
|
|
package audit
|
|
|
|
import "time"
|
|
|
|
// Actor types distinguish human-initiated actions from automated ones.
|
|
const (
|
|
ActorUser = "user" // action attributed to an authenticated end-user
|
|
ActorSystem = "system" // action performed by the control-logic engine
|
|
)
|
|
|
|
// Outcomes record whether the action succeeded.
|
|
const (
|
|
OutcomeOK = "ok"
|
|
OutcomeError = "error"
|
|
)
|
|
|
|
// Event is a single audit record. Optional fields are omitted from the database
|
|
// when empty. Time defaults to the current time when zero.
|
|
type Event struct {
|
|
Time time.Time `json:"time"`
|
|
Actor string `json:"actor"` // username, or graph name for system actions
|
|
ActorType string `json:"actorType"` // ActorUser | ActorSystem
|
|
Action string `json:"action"` // e.g. "signal.write", "interface.update"
|
|
DS string `json:"ds,omitempty"` // data source (signal writes)
|
|
Signal string `json:"signal,omitempty"` // signal / target name
|
|
Value string `json:"value,omitempty"` // serialised written value
|
|
Detail string `json:"detail,omitempty"` // free-form context (id, graph, trigger)
|
|
IP string `json:"ip,omitempty"` // client address (user actions)
|
|
Outcome string `json:"outcome"` // OutcomeOK | OutcomeError
|
|
Error string `json:"error,omitempty"` // message when Outcome==OutcomeError
|
|
}
|
|
|
|
// Filter selects a subset of events for Query. Zero-valued fields are ignored.
|
|
type Filter struct {
|
|
Start time.Time
|
|
End time.Time
|
|
Actor string
|
|
Action string
|
|
DS string
|
|
Signal string
|
|
Limit int // <=0 means a default cap is applied
|
|
}
|
|
|
|
// Recorder appends events and answers queries. Implementations must be safe for
|
|
// concurrent use and Record must never block the caller for long.
|
|
type Recorder interface {
|
|
Record(Event)
|
|
Query(Filter) ([]Event, error)
|
|
Close() error
|
|
}
|
|
|
|
// nopRecorder is used when auditing is disabled. It discards every event.
|
|
type nopRecorder struct{}
|
|
|
|
func (nopRecorder) Record(Event) {}
|
|
func (nopRecorder) Query(Filter) ([]Event, error) { return []Event{}, nil }
|
|
func (nopRecorder) Close() error { return nil }
|
|
|
|
// Nop returns a Recorder that discards everything. Call sites can hold a Recorder
|
|
// unconditionally and call Record without checking whether auditing is enabled.
|
|
func Nop() Recorder { return nopRecorder{} }
|