diff options
Diffstat (limited to 'weed/s3api')
| -rw-r--r-- | weed/s3api/auth_credentials.go | 54 | ||||
| -rw-r--r-- | weed/s3api/s3_constants/header.go | 18 | ||||
| -rw-r--r-- | weed/s3api/s3api_bucket_handlers.go | 31 | ||||
| -rw-r--r-- | weed/s3api/s3api_bucket_handlers_test.go | 113 | ||||
| -rw-r--r-- | weed/s3api/s3api_server.go | 2 |
5 files changed, 192 insertions, 26 deletions
diff --git a/weed/s3api/auth_credentials.go b/weed/s3api/auth_credentials.go index 148839d3e..b5824f2d1 100644 --- a/weed/s3api/auth_credentials.go +++ b/weed/s3api/auth_credentials.go @@ -40,6 +40,7 @@ type IdentityAccessManagement struct { identities []*Identity accessKeyIdent map[string]*Identity + nameToIdentity map[string]*Identity // O(1) lookup by identity name accounts map[string]*Account emailAccount map[string]*Account hashes map[string]*sync.Pool @@ -219,6 +220,7 @@ func NewIdentityAccessManagementWithStore(option *S3ApiServerOption, explicitSto if len(iam.identities) == 0 { iam.identities = []*Identity{envIdentity} iam.accessKeyIdent = map[string]*Identity{accessKeyId: envIdentity} + iam.nameToIdentity = map[string]*Identity{envIdentity.Name: envIdentity} iam.isAuthEnabled = true } iam.m.Unlock() @@ -270,6 +272,7 @@ func (iam *IdentityAccessManagement) loadS3ApiConfiguration(config *iam_pb.S3Api var identities []*Identity var identityAnonymous *Identity accessKeyIdent := make(map[string]*Identity) + nameToIdentity := make(map[string]*Identity) accounts := make(map[string]*Account) emailAccount := make(map[string]*Account) foundAccountAdmin := false @@ -348,6 +351,7 @@ func (iam *IdentityAccessManagement) loadS3ApiConfiguration(config *iam_pb.S3Api accessKeyIdent[cred.AccessKey] = t } identities = append(identities, t) + nameToIdentity[t.Name] = t } iam.m.Lock() @@ -357,6 +361,7 @@ func (iam *IdentityAccessManagement) loadS3ApiConfiguration(config *iam_pb.S3Api iam.accounts = accounts iam.emailAccount = emailAccount iam.accessKeyIdent = accessKeyIdent + iam.nameToIdentity = nameToIdentity if !iam.isAuthEnabled { // one-directional, no toggling iam.isAuthEnabled = len(identities) > 0 } @@ -365,7 +370,7 @@ func (iam *IdentityAccessManagement) loadS3ApiConfiguration(config *iam_pb.S3Api // Log configuration summary glog.V(1).Infof("Loaded %d identities, %d accounts, %d access keys. Auth enabled: %v", len(identities), len(accounts), len(accessKeyIdent), iam.isAuthEnabled) - + if glog.V(2) { glog.V(2).Infof("Access key to identity mapping:") for accessKey, identity := range accessKeyIdent { @@ -383,29 +388,42 @@ func (iam *IdentityAccessManagement) isEnabled() bool { func (iam *IdentityAccessManagement) lookupByAccessKey(accessKey string) (identity *Identity, cred *Credential, found bool) { iam.m.RLock() defer iam.m.RUnlock() - - glog.V(3).Infof("Looking up access key: %s (total keys registered: %d)", accessKey, len(iam.accessKeyIdent)) - + + // Helper function to truncate access key for logging to avoid credential exposure + truncate := func(key string) string { + const mask = "***" + if len(key) > 4 { + return key[:4] + mask + } + // For very short keys, never log the full key + return mask + } + + truncatedKey := truncate(accessKey) + + glog.V(3).Infof("Looking up access key: %s (len=%d, total keys registered: %d)", + truncatedKey, len(accessKey), len(iam.accessKeyIdent)) + if ident, ok := iam.accessKeyIdent[accessKey]; ok { for _, credential := range ident.Credentials { if credential.AccessKey == accessKey { - glog.V(2).Infof("Found access key %s for identity %s", accessKey, ident.Name) + glog.V(2).Infof("Found access key %s for identity %s", truncatedKey, ident.Name) return ident, credential, true } } } - - glog.V(1).Infof("Could not find access key %s. Available keys: %d, Auth enabled: %v", - accessKey, len(iam.accessKeyIdent), iam.isAuthEnabled) - + + glog.V(2).Infof("Could not find access key %s (len=%d). Available keys: %d, Auth enabled: %v", + truncatedKey, len(accessKey), len(iam.accessKeyIdent), iam.isAuthEnabled) + // Log all registered access keys at higher verbosity for debugging if glog.V(3) { glog.V(3).Infof("Registered access keys:") for key := range iam.accessKeyIdent { - glog.V(3).Infof(" - %s", key) + glog.V(3).Infof(" - %s (len=%d)", truncate(key), len(key)) } } - + return nil, nil, false } @@ -418,6 +436,13 @@ func (iam *IdentityAccessManagement) lookupAnonymous() (identity *Identity, foun return nil, false } +func (iam *IdentityAccessManagement) lookupByIdentityName(name string) *Identity { + iam.m.RLock() + defer iam.m.RUnlock() + + return iam.nameToIdentity[name] +} + // generatePrincipalArn generates an ARN for a user identity func generatePrincipalArn(identityName string) string { // Handle special cases @@ -463,6 +488,9 @@ func (iam *IdentityAccessManagement) Auth(f http.HandlerFunc, action Action) htt // Store the authenticated identity in request context (secure, cannot be spoofed) if identity != nil && identity.Name != "" { ctx := s3_constants.SetIdentityNameInContext(r.Context(), identity.Name) + // Also store the full identity object for handlers that need it (e.g., ListBuckets) + // This is especially important for JWT users whose identity is not in the identities list + ctx = s3_constants.SetIdentityInContext(ctx, identity) r = r.WithContext(ctx) } f(w, r) @@ -689,14 +717,14 @@ func (iam *IdentityAccessManagement) GetCredentialManager() *credential.Credenti // LoadS3ApiConfigurationFromCredentialManager loads configuration using the credential manager func (iam *IdentityAccessManagement) LoadS3ApiConfigurationFromCredentialManager() error { glog.V(1).Infof("Loading S3 API configuration from credential manager") - + s3ApiConfiguration, err := iam.credentialManager.LoadConfiguration(context.Background()) if err != nil { glog.Errorf("Failed to load configuration from credential manager: %v", err) return fmt.Errorf("failed to load configuration from credential manager: %w", err) } - glog.V(2).Infof("Credential manager returned %d identities and %d accounts", + glog.V(2).Infof("Credential manager returned %d identities and %d accounts", len(s3ApiConfiguration.Identities), len(s3ApiConfiguration.Accounts)) if err := iam.loadS3ApiConfiguration(s3ApiConfiguration); err != nil { diff --git a/weed/s3api/s3_constants/header.go b/weed/s3api/s3_constants/header.go index a232eb189..377f355f6 100644 --- a/weed/s3api/s3_constants/header.go +++ b/weed/s3api/s3_constants/header.go @@ -178,7 +178,8 @@ func IsSeaweedFSInternalHeader(headerKey string) bool { type contextKey string const ( - contextKeyIdentityName contextKey = "s3-identity-name" + contextKeyIdentityName contextKey = "s3-identity-name" + contextKeyIdentityObject contextKey = "s3-identity-object" ) // SetIdentityNameInContext stores the authenticated identity name in the request context @@ -199,3 +200,18 @@ func GetIdentityNameFromContext(r *http.Request) string { } return "" } + +// SetIdentityInContext stores the full authenticated identity object in the request context +// This is used to pass the full identity (including for JWT users) to handlers +func SetIdentityInContext(ctx context.Context, identity interface{}) context.Context { + if identity != nil { + return context.WithValue(ctx, contextKeyIdentityObject, identity) + } + return ctx +} + +// GetIdentityFromContext retrieves the full identity object from the request context +// Returns nil if no identity is set (unauthenticated request) +func GetIdentityFromContext(r *http.Request) interface{} { + return r.Context().Value(contextKeyIdentityObject) +} diff --git a/weed/s3api/s3api_bucket_handlers.go b/weed/s3api/s3api_bucket_handlers.go index a810dfd37..09bea9aa8 100644 --- a/weed/s3api/s3api_bucket_handlers.go +++ b/weed/s3api/s3api_bucket_handlers.go @@ -38,15 +38,28 @@ func (s3a *S3ApiServer) ListBucketsHandler(w http.ResponseWriter, r *http.Reques glog.V(3).Infof("ListBucketsHandler") + // Get authenticated identity from context (set by Auth middleware) + // For unauthenticated requests, this returns empty string + identityId := s3_constants.GetIdentityNameFromContext(r) + + // Get the full identity object for permission and ownership checks + // This is especially important for JWT users whose identity is not in the identities list + // Note: We store the full Identity object in context for simplicity. Future optimization + // could use a lightweight, credential-free view (name, account, actions, principal ARN) + // for better data minimization. var identity *Identity - var s3Err s3err.ErrorCode if s3a.iam.isEnabled() { - // Use authRequest instead of authUser for consistency with other endpoints - // This ensures the same authentication flow and any fixes (like prefix handling) are applied - identity, s3Err = s3a.iam.authRequest(r, s3_constants.ACTION_LIST) - if s3Err != s3err.ErrNone { - s3err.WriteErrorResponse(w, r, s3Err) - return + // Try to get the full identity from context first (works for all auth types including JWT) + if identityObj := s3_constants.GetIdentityFromContext(r); identityObj != nil { + if id, ok := identityObj.(*Identity); ok { + identity = id + } else { + glog.Warningf("ListBucketsHandler: identity object in context has unexpected type: %T", identityObj) + } + } + // Fallback to looking up by name if not in context (backward compatibility) + if identity == nil && identityId != "" { + identity = s3a.iam.lookupByIdentityName(identityId) } } @@ -59,10 +72,6 @@ func (s3a *S3ApiServer) ListBucketsHandler(w http.ResponseWriter, r *http.Reques return } - // 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 { if entry.IsDirectory { diff --git a/weed/s3api/s3api_bucket_handlers_test.go b/weed/s3api/s3api_bucket_handlers_test.go index 40137412d..40357a2b7 100644 --- a/weed/s3api/s3api_bucket_handlers_test.go +++ b/weed/s3api/s3api_bucket_handlers_test.go @@ -663,3 +663,116 @@ func TestListBucketsOwnershipCaseSensitivity(t *testing.T) { }) } } + +// TestListBucketsIssue7647 reproduces and verifies the fix for issue #7647 +// where an admin user with proper permissions could create buckets but couldn't list them +func TestListBucketsIssue7647(t *testing.T) { + t.Run("admin user can see their created buckets", func(t *testing.T) { + // Simulate the exact scenario from issue #7647: + // User "root" with ["Admin", "Read", "Write", "Tagging", "List"] permissions + + // Create identity for root user with Admin action + rootIdentity := &Identity{ + Name: "root", + Credentials: []*Credential{ + { + AccessKey: "ROOTID", + SecretKey: "ROOTSECRET", + }, + }, + Actions: []Action{ + s3_constants.ACTION_ADMIN, + s3_constants.ACTION_READ, + s3_constants.ACTION_WRITE, + s3_constants.ACTION_TAGGING, + s3_constants.ACTION_LIST, + }, + } + + // Create a bucket entry as if it was created by the root user + bucketEntry := &filer_pb.Entry{ + Name: "test", + IsDirectory: true, + Extended: map[string][]byte{ + s3_constants.AmzIdentityId: []byte("root"), + }, + Attributes: &filer_pb.FuseAttributes{ + Crtime: time.Now().Unix(), + Mtime: time.Now().Unix(), + }, + } + + // Test bucket visibility - should be visible to root (owner) + isVisible := isBucketVisibleToIdentity(bucketEntry, rootIdentity) + assert.True(t, isVisible, "Root user should see their own bucket") + + // Test that admin can also see buckets they don't own + otherUserBucket := &filer_pb.Entry{ + Name: "other-bucket", + IsDirectory: true, + Extended: map[string][]byte{ + s3_constants.AmzIdentityId: []byte("otheruser"), + }, + Attributes: &filer_pb.FuseAttributes{ + Crtime: time.Now().Unix(), + Mtime: time.Now().Unix(), + }, + } + + isVisible = isBucketVisibleToIdentity(otherUserBucket, rootIdentity) + assert.True(t, isVisible, "Admin user should see all buckets, even ones they don't own") + + // Test permission check for List action + canList := rootIdentity.canDo(s3_constants.ACTION_LIST, "test", "") + assert.True(t, canList, "Root user with List action should be able to list buckets") + }) + + t.Run("admin user sees buckets without owner metadata", func(t *testing.T) { + // Admin users should see buckets even if they don't have owner metadata + // (this can happen with legacy buckets or manual creation) + + rootIdentity := &Identity{ + Name: "root", + Actions: []Action{ + s3_constants.ACTION_ADMIN, + s3_constants.ACTION_LIST, + }, + } + + bucketWithoutOwner := &filer_pb.Entry{ + Name: "legacy-bucket", + IsDirectory: true, + Extended: map[string][]byte{}, // No owner metadata + Attributes: &filer_pb.FuseAttributes{ + Crtime: time.Now().Unix(), + }, + } + + isVisible := isBucketVisibleToIdentity(bucketWithoutOwner, rootIdentity) + assert.True(t, isVisible, "Admin should see buckets without owner metadata") + }) + + t.Run("non-admin user cannot see buckets without owner", func(t *testing.T) { + // Non-admin users should not see buckets without owner metadata + + regularUser := &Identity{ + Name: "user1", + Actions: []Action{ + s3_constants.ACTION_READ, + s3_constants.ACTION_LIST, + }, + } + + bucketWithoutOwner := &filer_pb.Entry{ + Name: "legacy-bucket", + IsDirectory: true, + Extended: map[string][]byte{}, // No owner metadata + Attributes: &filer_pb.FuseAttributes{ + Crtime: time.Now().Unix(), + }, + } + + isVisible := isBucketVisibleToIdentity(bucketWithoutOwner, regularUser) + assert.False(t, isVisible, "Non-admin should not see buckets without owner metadata") + }) +} diff --git a/weed/s3api/s3api_server.go b/weed/s3api/s3api_server.go index d75f53dd4..939c891c3 100644 --- a/weed/s3api/s3api_server.go +++ b/weed/s3api/s3api_server.go @@ -537,7 +537,7 @@ func (s3a *S3ApiServer) registerRouter(router *mux.Router) { }) // ListBuckets - apiRouter.Methods(http.MethodGet).Path("/").HandlerFunc(track(s3a.ListBucketsHandler, "LIST")) + apiRouter.Methods(http.MethodGet).Path("/").HandlerFunc(track(s3a.iam.Auth(s3a.ListBucketsHandler, ACTION_LIST), "LIST")) // NotFound apiRouter.NotFoundHandler = http.HandlerFunc(s3err.NotFoundHandler) |
