rdma: close publication gaps for RC pre-session failures

Review of the operational publication found four gaps where the
records disagreed with the S3 surface or were missing entirely.

Authorization-failure records lost the requester because the
pre-session publisher did not carry the authenticated account; the
account now flows into the record, so the audit trail names who was
denied.

Signature failures and malformed PREPARE headers ended the request
before any publication point. The auth adapter now publishes
authentication failures through the route handler, and header
validation failures publish with whatever object identity the
headers still carried, matching the S3 surface where every denied
request still logs.

RC requests carried no region: the custom routes run before the
middleware that stores the region local, so event records lacked
awsRegion and audit host headers read s3..amazonaws.com. The auth
adapter sets the region for every verified RC request.

Operational records reported generic 500 statuses for protocol
errors that the wire answers with a specific status (a resource
limit rejection logged 500 while the client saw 429). Status
mapping now reuses the route error mapping, so the recorded status
always equals the wire status.
This commit is contained in:
Jihyeon Gim
2026-09-09 13:16:52 +09:00
parent 49b1c6f6bc
commit 39aae5435c
4 changed files with 50 additions and 26 deletions
+7 -1
View File
@@ -1287,13 +1287,19 @@ func runGateway(ctx context.Context, be backend.Backend) error {
// nil on success without doing so. Wrap it so a verified
// request reaches the route handler, while errors end the
// chain as usual.
rcH := rcroutes.New(rcSvc, be, iamSvc, readonly, disableACLs)
rcAuth := func(ctx fiber.Ctx) error {
// The RC routes run before the default-values
// middleware sets the request locals; the access
// logger and event schema read the region from
// there, so set it for every verified request.
utils.ContextKeyRegion.Set(ctx, region)
if err := rcVerify(ctx); err != nil {
rcH.PublishAuthFailure(ctx, err)
return rcroutes.WriteRouteError(ctx, err)
}
return ctx.Next()
}
rcH := rcroutes.New(rcSvc, be, iamSvc, readonly, disableACLs)
// The gateway builds the access logger, metrics manager,
// and event sender inside RunVersityGW; hand them to the
// RC routes as soon as they exist so finished transfers
+7 -10
View File
@@ -18,7 +18,6 @@
package rcroutes
import (
"errors"
"fmt"
"sync"
"time"
@@ -30,7 +29,6 @@ import (
"github.com/versity/versitygw/metrics"
"github.com/versity/versitygw/rdma/rcserver"
"github.com/versity/versitygw/s3api/utils"
"github.com/versity/versitygw/s3err"
"github.com/versity/versitygw/s3event"
"github.com/versity/versitygw/s3log"
)
@@ -118,16 +116,14 @@ func (e *opsEmitter) publish(err error, bytes int64) {
}
// httpStatusFromError maps an operation error to the HTTP status
// the S3 surface would have answered with.
// the S3 surface would have answered with, using the same route
// error mapping as the wire response so operational records never
// disagree with what the client saw.
func httpStatusFromError(err error) int {
if err == nil {
return 200
}
var serr s3err.S3Error
if errors.As(err, &serr) {
return serr.StatusCode()
}
return 500
return routeError(err).HTTPStatusCode
}
// sessionRecord is one tracked session with its captured context.
@@ -248,14 +244,15 @@ func (t *opsTracker) publishClaimed(sessionID string, err error, bytes ...int64)
// publishRequest emits an operation record for a request that ended
// before any session existed (authentication, authorization, or
// header failures): no tracking table entry, single emission.
func (t *opsTracker) publishRequest(ctx fiber.Ctx, err error,
bucket, key string, isPut bool) {
func (t *opsTracker) publishRequest(ctx fiber.Ctx, acct auth.Account,
err error, bucket, key string, isPut bool) {
if t == nil {
return
}
emit := &opsEmitter{
ops: t.ops,
app: t.app,
acct: acct,
region: regionFromCtx(ctx),
bucket: bucket,
key: key,
+33 -15
View File
@@ -83,6 +83,14 @@ type Handler struct {
ops *opsTracker
}
// PublishAuthFailure emits an operation record for a request whose
// authentication failed before any route logic ran. The gateway
// auth adapter calls it so signature failures appear in the access
// log like they do on the S3 surface.
func (h *Handler) PublishAuthFailure(ctx fiber.Ctx, err error) {
h.ops.publishRequest(ctx, auth.Account{}, err, "", "", false)
}
// SetOpsServices injects the operational service instances once the
// gateway has created them, and wires the native teardown callback
// that publishes sessions no request path ever completed.
@@ -142,43 +150,53 @@ func (h *Handler) prepareCore(ctx fiber.Ctx) error {
}
defer h.svc.Leave()
if proto := ctx.Get(hdrProtocol); proto != protocolValue {
return invalidHeader(hdrProtocol, proto)
}
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
// Header parse failures end the request before authorization;
// publish them as request records too, with whatever object
// identity the malformed headers still carried.
publishHeaderErr := func(err error) error {
target := ctx.Get(hdrTarget)
bucket, key, _ := splitTarget(target)
h.ops.publishRequest(ctx, acct, err, bucket, key, false)
return err
}
if proto := ctx.Get(hdrProtocol); proto != protocolValue {
return publishHeaderErr(invalidHeader(hdrProtocol, proto))
}
op := strings.ToUpper(ctx.Get(hdrOp))
if op != "GET" && op != "PUT" {
return invalidHeader(hdrOp, ctx.Get(hdrOp))
return publishHeaderErr(invalidHeader(hdrOp, ctx.Get(hdrOp)))
}
isPut := op == "PUT"
target := ctx.Get(hdrTarget)
bucket, key, ok := splitTarget(target)
if !ok {
return invalidHeader(hdrTarget, target)
return publishHeaderErr(invalidHeader(hdrTarget, target))
}
size, err := parseUint(ctx.Get(hdrSize), 10, 64)
if err != nil || size == 0 {
return invalidHeader(hdrSize, ctx.Get(hdrSize))
return publishHeaderErr(invalidHeader(hdrSize, ctx.Get(hdrSize)))
}
offset, err := parseUint(ctx.Get(hdrOffset), 10, 64)
if err != nil {
return invalidHeader(hdrOffset, ctx.Get(hdrOffset))
return publishHeaderErr(invalidHeader(hdrOffset, ctx.Get(hdrOffset)))
}
psn, err := parseUint(ctx.Get(hdrPsn), 16, 32)
if err != nil || psn == 0 || psn > 0xffffff {
return invalidHeader(hdrPsn, ctx.Get(hdrPsn))
return publishHeaderErr(invalidHeader(hdrPsn, ctx.Get(hdrPsn)))
}
cookie, err := parseUint(ctx.Get(hdrCookie), 16, 32)
if err != nil || cookie == 0 {
return invalidHeader(hdrCookie, ctx.Get(hdrCookie))
return publishHeaderErr(invalidHeader(hdrCookie, ctx.Get(hdrCookie)))
}
isPut := op == "PUT"
// Authorize through the regular object-access chain.
if err := h.authorize(ctx, acct, isRoot, bucket, key, isPut); err != nil {
h.ops.publishRequest(ctx, err, bucket, key, isPut)
h.ops.publishRequest(ctx, acct, err, bucket, key, isPut)
return err
}
@@ -193,7 +211,7 @@ func (h *Handler) prepareCore(ctx fiber.Ctx) error {
ClientToken: ctx.Get(hdrToken),
})
if err != nil {
h.ops.publishRequest(ctx, mapRcError(err), bucket, key, isPut)
h.ops.publishRequest(ctx, acct, mapRcError(err), bucket, key, isPut)
return mapRcError(err)
}
@@ -202,12 +220,12 @@ func (h *Handler) prepareCore(ctx fiber.Ctx) error {
if !isPut {
if err := h.stageGet(ctx, resp.SessionID, bucket, key, offset, size); err != nil {
_ = h.svc.FinishPrepare(resp.SessionID, false)
h.ops.publishRequest(ctx, err, bucket, key, isPut)
h.ops.publishRequest(ctx, acct, err, bucket, key, isPut)
return err
}
}
if err := h.svc.FinishPrepare(resp.SessionID, true); err != nil {
h.ops.publishRequest(ctx, mapRcError(err), bucket, key, isPut)
h.ops.publishRequest(ctx, acct, mapRcError(err), bucket, key, isPut)
return mapRcError(err)
}
+3
View File
@@ -61,3 +61,6 @@ type OpsServices struct {
// SetOpsServices is a stub: without RDMA support there is nothing
// to publish into.
func (h *Handler) SetOpsServices(ops OpsServices) {}
// PublishAuthFailure is a stub mirror of the linux handler.
func (h *Handler) PublishAuthFailure(ctx fiber.Ctx, err error) {}