diff --git a/weed/s3api/auth_credentials.go b/weed/s3api/auth_credentials.go index 289fbd556..0d99e43eb 100644 --- a/weed/s3api/auth_credentials.go +++ b/weed/s3api/auth_credentials.go @@ -421,8 +421,10 @@ func (iam *IdentityAccessManagement) Auth(f http.HandlerFunc, action Action) htt glog.V(3).Infof("auth error: %v", errCode) if errCode == s3err.ErrNone { + // Store the authenticated identity in request context (secure, cannot be spoofed) if identity != nil && identity.Name != "" { - r.Header.Set(s3_constants.AmzIdentityId, identity.Name) + ctx := s3_constants.SetIdentityNameInContext(r.Context(), identity.Name) + r = r.WithContext(ctx) } f(w, r) return diff --git a/weed/s3api/s3_constants/header.go b/weed/s3api/s3_constants/header.go index 1ef6f62c5..bcd3e5e05 100644 --- a/weed/s3api/s3_constants/header.go +++ b/weed/s3api/s3_constants/header.go @@ -17,6 +17,7 @@ package s3_constants import ( + "context" "net/http" "strings" @@ -174,3 +175,29 @@ var PassThroughHeaders = map[string]string{ func IsSeaweedFSInternalHeader(headerKey string) bool { return strings.HasPrefix(strings.ToLower(headerKey), SeaweedFSInternalPrefix) } + +// Context keys for storing authenticated identity information +type contextKey string + +const ( + contextKeyIdentityName contextKey = "s3-identity-name" +) + +// SetIdentityNameInContext stores the authenticated identity name in the request context +// This is the secure way to propagate identity - headers can be spoofed, context cannot +func SetIdentityNameInContext(ctx context.Context, identityName string) context.Context { + if identityName != "" { + return context.WithValue(ctx, contextKeyIdentityName, identityName) + } + return ctx +} + +// GetIdentityNameFromContext retrieves the authenticated identity name from the request context +// Returns empty string if no identity is set (unauthenticated request) +// This is the secure way to retrieve identity - never read from headers directly +func GetIdentityNameFromContext(r *http.Request) string { + if name, ok := r.Context().Value(contextKeyIdentityName).(string); ok { + return name + } + return "" +} diff --git a/weed/s3api/s3api_bucket_handlers.go b/weed/s3api/s3api_bucket_handlers.go index 4222c911e..d838ebeb0 100644 --- a/weed/s3api/s3api_bucket_handlers.go +++ b/weed/s3api/s3api_bucket_handlers.go @@ -59,12 +59,9 @@ func (s3a *S3ApiServer) ListBucketsHandler(w http.ResponseWriter, r *http.Reques return } - identityId := "" - if identity != nil { - identityId = identity.Name - } - // Note: For unauthenticated requests, identityId remains empty. - // We never read from request headers to prevent reflecting unvalidated user input. + // Get authenticated identity from context (secure, cannot be spoofed) + // For unauthenticated requests, this returns empty string + identityId := s3_constants.GetIdentityNameFromContext(r) var listBuckets ListAllMyBucketsList for _, entry := range entries { @@ -164,7 +161,8 @@ func (s3a *S3ApiServer) PutBucketHandler(w http.ResponseWriter, r *http.Request) } // Check if bucket already exists and handle ownership/settings - currentIdentityId := r.Header.Get(s3_constants.AmzIdentityId) + // Get authenticated identity from context (secure, cannot be spoofed) + currentIdentityId := s3_constants.GetIdentityNameFromContext(r) // Check collection existence first collectionExists := false @@ -247,7 +245,8 @@ func (s3a *S3ApiServer) PutBucketHandler(w http.ResponseWriter, r *http.Request) } fn := func(entry *filer_pb.Entry) { - if identityId := r.Header.Get(s3_constants.AmzIdentityId); identityId != "" { + // Get authenticated identity from context (secure, cannot be spoofed) + if identityId := s3_constants.GetIdentityNameFromContext(r); identityId != "" { if entry.Extended == nil { entry.Extended = make(map[string][]byte) } @@ -576,7 +575,8 @@ func (s3a *S3ApiServer) hasAccess(r *http.Request, entry *filer_pb.Entry) bool { return true } - identityId := r.Header.Get(s3_constants.AmzIdentityId) + // Get authenticated identity from context (secure, cannot be spoofed) + identityId := s3_constants.GetIdentityNameFromContext(r) if id, ok := entry.Extended[s3_constants.AmzIdentityId]; ok { if identityId != string(id) { glog.V(3).Infof("hasAccess: %s != %s (entry.Extended = %v)", identityId, id, entry.Extended) diff --git a/weed/s3api/s3err/audit_fluent.go b/weed/s3api/s3err/audit_fluent.go index ef2459eac..5d617ce1c 100644 --- a/weed/s3api/s3err/audit_fluent.go +++ b/weed/s3api/s3err/audit_fluent.go @@ -152,7 +152,7 @@ func GetAccessLog(r *http.Request, HTTPStatusCode int, s3errCode ErrorCode) *Acc HostHeader: hostHeader, RequestID: r.Header.Get("X-Request-ID"), RemoteIP: remoteIP, - Requester: r.Header.Get(s3_constants.AmzIdentityId), + Requester: s3_constants.GetIdentityNameFromContext(r), // Get from context, not header (secure) SignatureVersion: r.Header.Get(s3_constants.AmzAuthType), UserAgent: r.Header.Get("user-agent"), HostId: hostname, diff --git a/weed/server/filer_server_handlers_read.go b/weed/server/filer_server_handlers_read.go index 1a66dd045..052256ed2 100644 --- a/weed/server/filer_server_handlers_read.go +++ b/weed/server/filer_server_handlers_read.go @@ -122,18 +122,21 @@ func (fs *FilerServer) GetOrHeadHandler(w http.ResponseWriter, r *http.Request) writeJsonQuiet(w, r, http.StatusOK, entry) return } - if entry.Attr.Mime == "" || (entry.Attr.Mime == s3_constants.FolderMimeType && r.Header.Get(s3_constants.AmzIdentityId) == "") { - // Don't return directory meta if config value is set to true - if fs.option.ExposeDirectoryData == false { + // S3 API requests never reach filer HTTP handlers (they use gRPC + direct volume server access). + // For S3-created directories (FolderMimeType), return the directory object metadata itself + // rather than listing contents. This allows S3 clients to GET directory objects they created. + if entry.Attr.Mime == s3_constants.FolderMimeType { + // S3-created directory object - return metadata + w.Header().Set(s3_constants.SeaweedFSIsDirectoryKey, "true") + } else if entry.Attr.Mime == "" { + // Regular filer directory - show listing if enabled + if !fs.option.ExposeDirectoryData { writeJsonError(w, r, http.StatusForbidden, errors.New("directory listing is disabled")) return } - // return index of directory for non s3 gateway fs.listDirectoryHandler(w, r) return } - // inform S3 API this is a user created directory key object - w.Header().Set(s3_constants.SeaweedFSIsDirectoryKey, "true") } if isForDirectory && entry.Attr.Mime != s3_constants.FolderMimeType {