File size: 1,804 Bytes
857a91b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package api

import (
	"log"
	"net"
	"net/http"
	"strings"

	"github.com/agent-matrix/matrix-runtime/internal/store"
)

// clientIP best-effort extracts the caller IP, honoring common proxy headers
// (the API typically runs behind Cloudflare / an ingress).
func clientIP(r *http.Request) string {
	if v := r.Header.Get("CF-Connecting-IP"); v != "" {
		return v
	}
	if v := r.Header.Get("X-Forwarded-For"); v != "" {
		return strings.TrimSpace(strings.Split(v, ",")[0])
	}
	if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
		return host
	}
	return r.RemoteAddr
}

// audit records a sensitive action. It is best-effort: a nil store or a write
// error never affects the request outcome (only logged).
func (s *Server) audit(r *http.Request, workspaceID, actor, action, target, status string, meta map[string]any) {
	if s.store == nil {
		return
	}
	if err := s.store.RecordAudit(store.AuditEvent{
		WorkspaceID: workspaceID,
		Actor:       actor,
		Action:      action,
		Target:      target,
		IP:          clientIP(r),
		Status:      status,
		Meta:        meta,
	}); err != nil {
		log.Printf("audit: could not record %s: %v", action, err)
	}
}

// truncate shortens s to at most n characters for audit targets.
func truncate(s string, n int) string {
	if len(s) <= n {
		return s
	}
	return s[:n] + "…"
}

// handleCloudAudit returns the workspace's recent audit events.
func (s *Server) handleCloudAudit(w http.ResponseWriter, r *http.Request) {
	u, ok := s.currentUser(r)
	if !ok {
		writeError(w, http.StatusUnauthorized, "not authenticated")
		return
	}
	events, err := s.store.ListAudit(u.WorkspaceID, 200)
	if err != nil {
		writeError(w, http.StatusInternalServerError, "could not load audit log")
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"events": events})
}