aboutsummaryrefslogtreecommitdiff
path: root/weed/s3api
diff options
context:
space:
mode:
Diffstat (limited to 'weed/s3api')
-rw-r--r--weed/s3api/auth_credentials.go54
-rw-r--r--weed/s3api/s3_constants/header.go18
-rw-r--r--weed/s3api/s3api_bucket_handlers.go31
-rw-r--r--weed/s3api/s3api_bucket_handlers_test.go113
-rw-r--r--weed/s3api/s3api_server.go2
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)