initial commit
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
// 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] + "..."
|
||||
}
|
||||
Reference in New Issue
Block a user