121 lines
3.1 KiB
Go
121 lines
3.1 KiB
Go
// Package auditlog records administrative changes made through the web UI, the
|
|
// REST API and the CLI.
|
|
//
|
|
// Audit writes are rare compared to DNS queries, so they are synchronous and
|
|
// best-effort: a failure to record an audit entry is logged but never fails the
|
|
// operation the administrator asked for.
|
|
package auditlog
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/owen/vibedns/internal/database"
|
|
"github.com/owen/vibedns/internal/models"
|
|
)
|
|
|
|
// Sources an audit entry can originate from.
|
|
const (
|
|
SourceWeb = "web"
|
|
SourceAPI = "api"
|
|
SourceCLI = "cli"
|
|
SourceSystem = "system"
|
|
)
|
|
|
|
// Object types recorded in the audit log.
|
|
const (
|
|
ObjectZone = "zone"
|
|
ObjectRecord = "record"
|
|
ObjectNetwork = "network"
|
|
ObjectPolicy = "policy"
|
|
ObjectList = "list"
|
|
ObjectDomain = "domain"
|
|
ObjectSettings = "settings"
|
|
ObjectCache = "cache"
|
|
ObjectAdmin = "admin"
|
|
ObjectToken = "api_token"
|
|
ObjectBackup = "backup"
|
|
ObjectConfig = "config"
|
|
ObjectQueryLog = "query_log"
|
|
)
|
|
|
|
// Logger writes audit entries.
|
|
type Logger struct {
|
|
db *database.DB
|
|
log *slog.Logger
|
|
}
|
|
|
|
// New creates an audit logger.
|
|
func New(db *database.DB, log *slog.Logger) *Logger {
|
|
return &Logger{db: db, log: log}
|
|
}
|
|
|
|
// Actor identifies who performed an action and from where.
|
|
type Actor struct {
|
|
Name string
|
|
Source string
|
|
ClientIP string
|
|
}
|
|
|
|
// SystemActor is used for changes the server makes on its own behalf.
|
|
func SystemActor() Actor { return Actor{Name: "system", Source: SourceSystem} }
|
|
|
|
// CLIActor is used for changes made through the command line.
|
|
func CLIActor() Actor { return Actor{Name: "cli", Source: SourceCLI} }
|
|
|
|
// Record writes an audit entry. Secrets must never be passed in details: this
|
|
// is enforced by convention at the call sites, which pass names and counts
|
|
// rather than values.
|
|
func (l *Logger) Record(ctx context.Context, a Actor, action, objectType, objectID, objectName, details string) {
|
|
if l == nil || l.db == nil {
|
|
return
|
|
}
|
|
e := models.AuditEntry{
|
|
Actor: a.Name,
|
|
Source: defaultString(a.Source, SourceWeb),
|
|
ClientIP: a.ClientIP,
|
|
Action: action,
|
|
ObjectType: objectType,
|
|
ObjectID: objectID,
|
|
ObjectName: objectName,
|
|
Details: truncate(details, 2000),
|
|
}
|
|
if err := l.db.InsertAudit(ctx, e); err != nil {
|
|
l.log.Error("could not write audit entry", "error", err, "action", action)
|
|
}
|
|
}
|
|
|
|
// RecordID is Record with an integer object ID.
|
|
func (l *Logger) RecordID(ctx context.Context, a Actor, action, objectType string, id int64, objectName, details string) {
|
|
l.Record(ctx, a, action, objectType, strconv.FormatInt(id, 10), objectName, details)
|
|
}
|
|
|
|
// Changes renders a set of field changes as an audit detail string.
|
|
func Changes(pairs ...string) string {
|
|
if len(pairs)%2 != 0 {
|
|
return strings.Join(pairs, " ")
|
|
}
|
|
var parts []string
|
|
for i := 0; i < len(pairs); i += 2 {
|
|
parts = append(parts, fmt.Sprintf("%s=%s", pairs[i], pairs[i+1]))
|
|
}
|
|
return strings.Join(parts, ", ")
|
|
}
|
|
|
|
func defaultString(v, def string) string {
|
|
if strings.TrimSpace(v) == "" {
|
|
return def
|
|
}
|
|
return v
|
|
}
|
|
|
|
func truncate(s string, n int) string {
|
|
if len(s) <= n {
|
|
return s
|
|
}
|
|
return s[:n] + "..."
|
|
}
|