1f77473d8f
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>
83 lines
2.1 KiB
Go
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
|
|
}
|