Files
seaweedfs/weed/s3api/s3err/audit_fluent_test.go
T
Chris Lu 3f1eaf9724 fix(s3/audit): emit audit log for successful GET/HEAD (#9467)
* fix(s3/audit): emit audit log for successful GET/HEAD

Successful GET/HEAD object requests never produced a fluent audit entry
because those handlers write the response directly (streaming for GET,
WriteHeader for HEAD) and never reach a PostLog call site. The wiki
advertises GET as an audited verb, so the asymmetry surprises operators
who rely on the log for read-access auditing.

Move the safety net into the track() middleware: tag each request with
an audit-tracking flag, let PostLog/PostAccessLog (delete path) mark it,
and emit a single fallback entry after the handler returns when nothing
fired. The recorder's status flows into the fallback so the audit row
still reflects 200/206 vs 404 etc. No double logging for handlers that
already emit (write helpers, error paths, bulk delete).

Refs #9463

* fix(s3/audit): defensive nil checks on audit-tracking helpers

Address PR review: guard against nil request and nil *atomic.Bool stored
under the audit-tracking key. The conditions are unreachable today (the
key is private and we only ever store new(atomic.Bool)), but the checks
are free and keep the helpers safe if a future caller misbehaves.

* test(s3/audit): track() audit fallback coverage + stale comment cleanup (#9469)

test(s3/audit): cover track() fallback wiring + cleanup

Adds two unit tests in weed/s3api/stats_test.go that exercise the
audit-tracking flag set up by track(): one verifies the fallback path
fires when a handler writes the response directly (the GET/HEAD object
regression in #9463), the other verifies the flag is set when a handler
emits PostLog itself so the fallback is skipped.

To make the wiring observable without standing up fluent, PostLog now
marks the audit flag before short-circuiting on a nil Logger; production
behavior is unchanged (no logger, no posting) but the flag stays
consistent.

Also drops two stale comments in s3api_object_handlers.go that still
referenced proxyToFiler — that helper was removed when GET/HEAD started
streaming from volume servers directly.

Stacks on #9467.
2026-05-13 09:24:59 -07:00

103 lines
2.9 KiB
Go

package s3err
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/seaweedfs/seaweedfs/weed/util/request_id"
"github.com/stretchr/testify/assert"
)
func TestGetAccessLogUsesAmzRequestID(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/bucket/object", nil)
req = req.WithContext(request_id.Set(req.Context(), "req-123"))
log := GetAccessLog(req, http.StatusOK, ErrNone)
assert.Equal(t, "req-123", log.RequestID)
}
func TestGetAccessLogRemoteIP(t *testing.T) {
tests := []struct {
name string
remoteAddr string
xRealIP string
xForwardedFor string
expectedRemote string
}{
{
name: "falls back to RemoteAddr (port stripped) when no headers set",
remoteAddr: "10.89.0.1:35832",
expectedRemote: "10.89.0.1",
},
{
name: "preserves IPv6 host from RemoteAddr",
remoteAddr: "[2001:db8::1]:35832",
expectedRemote: "2001:db8::1",
},
{
name: "returns RemoteAddr unchanged when no port present",
remoteAddr: "@",
expectedRemote: "@",
},
{
name: "uses X-Real-IP when X-Forwarded-For is absent",
remoteAddr: "10.89.0.1:35832",
xRealIP: "203.0.113.7",
expectedRemote: "203.0.113.7",
},
{
name: "prefers X-Forwarded-For over X-Real-IP",
remoteAddr: "10.89.0.1:35832",
xRealIP: "203.0.113.7",
xForwardedFor: "198.51.100.42",
expectedRemote: "198.51.100.42",
},
{
name: "uses first hop in X-Forwarded-For chain",
remoteAddr: "10.89.0.1:35832",
xForwardedFor: "198.51.100.42, 10.0.0.5, 10.89.0.1",
expectedRemote: "198.51.100.42",
},
{
name: "skips empty leading entries in X-Forwarded-For",
remoteAddr: "10.89.0.1:35832",
xForwardedFor: ", 198.51.100.42",
expectedRemote: "198.51.100.42",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/bucket/object", nil)
req.RemoteAddr = tc.remoteAddr
if tc.xRealIP != "" {
req.Header.Set("X-Real-IP", tc.xRealIP)
}
if tc.xForwardedFor != "" {
req.Header.Set("X-Forwarded-For", tc.xForwardedFor)
}
log := GetAccessLog(req, http.StatusOK, ErrNone)
assert.Equal(t, tc.expectedRemote, log.RemoteIP)
})
}
}
func TestAuditTrackingFlag(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/bucket/object", nil)
assert.False(t, AuditAlreadyLogged(req), "untracked request reports not logged")
tracked := EnsureAuditTracking(req)
assert.NotSame(t, req, tracked, "EnsureAuditTracking returns a new request when no flag is present")
assert.False(t, AuditAlreadyLogged(tracked), "tracked request starts unlogged")
again := EnsureAuditTracking(tracked)
assert.Same(t, tracked, again, "EnsureAuditTracking is idempotent when flag already present")
MarkAuditLogged(tracked)
assert.True(t, AuditAlreadyLogged(tracked), "flag flips after MarkAuditLogged")
}