Files
Alphaeus Mote 1f77473d8f feat(server): trust local proxies by default, seed built-in schedules
Proxy / base-URL:
- ORCHESTRAD_TRUSTED_PROXIES now defaults to "local", trusting reverse
  proxies in loopback + RFC1918 + link-local/ULA ranges out of the box, so
  X-Forwarded-* (client IP, scheme, host) is honored behind an edge proxy
  without extra config. New keywords: local/private, all/any, none.
- OIDC redirect URI derivation now uses the trust-gated request base URL
  instead of reading X-Forwarded-Proto directly, and audit client IP now
  trusts the middleware-rewritten RemoteAddr rather than the raw (spoofable)
  X-Forwarded-For header. Both honor forwarded values only from trusted
  peers.

Schedules:
- Seed eight built-in schedules on startup (every 5/15/30 min, hourly,
  every 6/12h, daily, weekly), idempotent by name, so operators have
  ready-made cadences in the Schedules page and the rule editor's schedule
  dropdown without hand-building one.

Test covers the trusted-proxy keyword expansion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 17:14:49 -04:00

83 lines
2.1 KiB
Go

// Package api - Audit helpers shared across handlers
package api
import (
"net"
"net/http"
"github.com/Grace-Solutions/OrchestrAD/internal/audit"
)
// emitAudit records an audit event tied to the current HTTP request. It pulls
// the authenticated user (if any) out of the request context and captures the
// client IP and User-Agent headers. The call is a no-op when svc is nil so
// callers can pass nil safely in tests.
func emitAudit(svc *audit.Service, r *http.Request, eventType audit.EventType, resourceType, resourceID, action string, success bool, details map[string]any, errMsg string) {
if svc == nil {
return
}
event := audit.Event{
EventType: eventType,
Component: resourceType,
Action: action,
Details: details,
Success: success,
}
if resourceType != "" {
event.ResourceType = &resourceType
}
if resourceID != "" {
event.ResourceID = &resourceID
}
if errMsg != "" {
event.ErrorMessage = &errMsg
}
if user := GetUserFromContext(r.Context()); user != nil {
id := user.ID
name := user.Username
event.UserID = &id
event.Username = &name
}
if ip := clientIP(r); ip != "" {
event.IPAddress = &ip
}
if ua := r.UserAgent(); ua != "" {
event.UserAgent = &ua
}
_ = svc.Log(event)
}
// clientIP returns the caller's address. The proxy middleware already rewrites
// r.RemoteAddr from X-Forwarded-For / X-Real-IP when the immediate peer is a
// trusted proxy, so we trust r.RemoteAddr rather than re-reading the (spoofable)
// forwarded headers here.
func clientIP(r *http.Request) string {
if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
return host
}
return r.RemoteAddr
}
// requestBaseURL returns the externally-visible scheme://host for the request.
// It reflects X-Forwarded-Proto/Host only when the proxy middleware applied
// them (trusted peer); otherwise it falls back to the actual scheme and Host.
func requestBaseURL(r *http.Request) string {
scheme := r.URL.Scheme
if scheme == "" {
if r.TLS != nil {
scheme = "https"
} else {
scheme = "http"
}
}
host := r.Host
if host == "" {
host = r.URL.Host
}
return scheme + "://" + host
}