You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							545 lines
						
					
					
						
							16 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							545 lines
						
					
					
						
							16 KiB
						
					
					
				| package s3api | |
| 
 | |
| import ( | |
| 	"os" | |
| 	"reflect" | |
| 	"testing" | |
| 
 | |
| 	"github.com/seaweedfs/seaweedfs/weed/credential" | |
| 	. "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" | |
| 	"github.com/stretchr/testify/assert" | |
| 
 | |
| 	"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" | |
| 	jsonpb "google.golang.org/protobuf/encoding/protojson" | |
| ) | |
| 
 | |
| func TestIdentityListFileFormat(t *testing.T) { | |
| 
 | |
| 	s3ApiConfiguration := &iam_pb.S3ApiConfiguration{} | |
| 
 | |
| 	identity1 := &iam_pb.Identity{ | |
| 		Name: "some_name", | |
| 		Credentials: []*iam_pb.Credential{ | |
| 			{ | |
| 				AccessKey: "some_access_key1", | |
| 				SecretKey: "some_secret_key2", | |
| 			}, | |
| 		}, | |
| 		Actions: []string{ | |
| 			ACTION_ADMIN, | |
| 			ACTION_READ, | |
| 			ACTION_WRITE, | |
| 		}, | |
| 	} | |
| 	identity2 := &iam_pb.Identity{ | |
| 		Name: "some_read_only_user", | |
| 		Credentials: []*iam_pb.Credential{ | |
| 			{ | |
| 				AccessKey: "some_access_key1", | |
| 				SecretKey: "some_secret_key1", | |
| 			}, | |
| 		}, | |
| 		Actions: []string{ | |
| 			ACTION_READ, | |
| 		}, | |
| 	} | |
| 	identity3 := &iam_pb.Identity{ | |
| 		Name: "some_normal_user", | |
| 		Credentials: []*iam_pb.Credential{ | |
| 			{ | |
| 				AccessKey: "some_access_key2", | |
| 				SecretKey: "some_secret_key2", | |
| 			}, | |
| 		}, | |
| 		Actions: []string{ | |
| 			ACTION_READ, | |
| 			ACTION_WRITE, | |
| 		}, | |
| 	} | |
| 
 | |
| 	s3ApiConfiguration.Identities = append(s3ApiConfiguration.Identities, identity1) | |
| 	s3ApiConfiguration.Identities = append(s3ApiConfiguration.Identities, identity2) | |
| 	s3ApiConfiguration.Identities = append(s3ApiConfiguration.Identities, identity3) | |
| 
 | |
| 	m := jsonpb.MarshalOptions{ | |
| 		EmitUnpopulated: true, | |
| 		Indent:          "  ", | |
| 	} | |
| 
 | |
| 	text, _ := m.Marshal(s3ApiConfiguration) | |
| 
 | |
| 	println(string(text)) | |
| 
 | |
| } | |
| 
 | |
| func TestCanDo(t *testing.T) { | |
| 	ident1 := &Identity{ | |
| 		Name: "anything", | |
| 		Actions: []Action{ | |
| 			"Write:bucket1/a/b/c/*", | |
| 			"Write:bucket1/a/b/other", | |
| 		}, | |
| 	} | |
| 	// object specific | |
| 	assert.Equal(t, true, ident1.canDo(ACTION_WRITE, "bucket1", "/a/b/c/d.txt")) | |
| 	assert.Equal(t, true, ident1.canDo(ACTION_WRITE, "bucket1", "/a/b/c/d/e.txt")) | |
| 	assert.Equal(t, false, ident1.canDo(ACTION_DELETE_BUCKET, "bucket1", "")) | |
| 	assert.Equal(t, false, ident1.canDo(ACTION_WRITE, "bucket1", "/a/b/other/some"), "action without *") | |
| 	assert.Equal(t, false, ident1.canDo(ACTION_WRITE, "bucket1", "/a/b/*"), "action on parent directory") | |
| 
 | |
| 	// bucket specific | |
| 	ident2 := &Identity{ | |
| 		Name: "anything", | |
| 		Actions: []Action{ | |
| 			"Read:bucket1", | |
| 			"Write:bucket1/*", | |
| 			"WriteAcp:bucket1", | |
| 		}, | |
| 	} | |
| 	assert.Equal(t, true, ident2.canDo(ACTION_READ, "bucket1", "/a/b/c/d.txt")) | |
| 	assert.Equal(t, true, ident2.canDo(ACTION_WRITE, "bucket1", "/a/b/c/d.txt")) | |
| 	assert.Equal(t, true, ident2.canDo(ACTION_WRITE_ACP, "bucket1", "")) | |
| 	assert.Equal(t, false, ident2.canDo(ACTION_READ_ACP, "bucket1", "")) | |
| 	assert.Equal(t, false, ident2.canDo(ACTION_LIST, "bucket1", "/a/b/c/d.txt")) | |
| 
 | |
| 	// across buckets | |
| 	ident3 := &Identity{ | |
| 		Name: "anything", | |
| 		Actions: []Action{ | |
| 			"Read", | |
| 			"Write", | |
| 		}, | |
| 	} | |
| 	assert.Equal(t, true, ident3.canDo(ACTION_READ, "bucket1", "/a/b/c/d.txt")) | |
| 	assert.Equal(t, true, ident3.canDo(ACTION_WRITE, "bucket1", "/a/b/c/d.txt")) | |
| 	assert.Equal(t, false, ident3.canDo(ACTION_LIST, "bucket1", "/a/b/other/some")) | |
| 	assert.Equal(t, false, ident3.canDo(ACTION_WRITE_ACP, "bucket1", "")) | |
| 
 | |
| 	// partial buckets | |
| 	ident4 := &Identity{ | |
| 		Name: "anything", | |
| 		Actions: []Action{ | |
| 			"Read:special_*", | |
| 			"ReadAcp:special_*", | |
| 		}, | |
| 	} | |
| 	assert.Equal(t, true, ident4.canDo(ACTION_READ, "special_bucket", "/a/b/c/d.txt")) | |
| 	assert.Equal(t, true, ident4.canDo(ACTION_READ_ACP, "special_bucket", "")) | |
| 	assert.Equal(t, false, ident4.canDo(ACTION_READ, "bucket1", "/a/b/c/d.txt")) | |
| 
 | |
| 	// admin buckets | |
| 	ident5 := &Identity{ | |
| 		Name: "anything", | |
| 		Actions: []Action{ | |
| 			"Admin:special_*", | |
| 		}, | |
| 	} | |
| 	assert.Equal(t, true, ident5.canDo(ACTION_READ, "special_bucket", "/a/b/c/d.txt")) | |
| 	assert.Equal(t, true, ident5.canDo(ACTION_READ_ACP, "special_bucket", "")) | |
| 	assert.Equal(t, true, ident5.canDo(ACTION_WRITE, "special_bucket", "/a/b/c/d.txt")) | |
| 	assert.Equal(t, true, ident5.canDo(ACTION_WRITE_ACP, "special_bucket", "")) | |
| 
 | |
| 	// anonymous buckets | |
| 	ident6 := &Identity{ | |
| 		Name: "anonymous", | |
| 		Actions: []Action{ | |
| 			"Read", | |
| 		}, | |
| 	} | |
| 	assert.Equal(t, true, ident6.canDo(ACTION_READ, "anything_bucket", "/a/b/c/d.txt")) | |
| 
 | |
| 	//test deleteBucket operation | |
| 	ident7 := &Identity{ | |
| 		Name: "anything", | |
| 		Actions: []Action{ | |
| 			"DeleteBucket:bucket1", | |
| 		}, | |
| 	} | |
| 	assert.Equal(t, true, ident7.canDo(ACTION_DELETE_BUCKET, "bucket1", "")) | |
| } | |
| 
 | |
| type LoadS3ApiConfigurationTestCase struct { | |
| 	pbAccount   *iam_pb.Account | |
| 	pbIdent     *iam_pb.Identity | |
| 	expectIdent *Identity | |
| } | |
| 
 | |
| func TestLoadS3ApiConfiguration(t *testing.T) { | |
| 	specifiedAccount := Account{ | |
| 		Id:           "specifiedAccountID", | |
| 		DisplayName:  "specifiedAccountName", | |
| 		EmailAddress: "specifiedAccounEmail@example.com", | |
| 	} | |
| 	pbSpecifiedAccount := iam_pb.Account{ | |
| 		Id:           "specifiedAccountID", | |
| 		DisplayName:  "specifiedAccountName", | |
| 		EmailAddress: "specifiedAccounEmail@example.com", | |
| 	} | |
| 	testCases := map[string]*LoadS3ApiConfigurationTestCase{ | |
| 		"notSpecifyAccountId": { | |
| 			pbIdent: &iam_pb.Identity{ | |
| 				Name: "notSpecifyAccountId", | |
| 				Actions: []string{ | |
| 					"Read", | |
| 					"Write", | |
| 				}, | |
| 				Credentials: []*iam_pb.Credential{ | |
| 					{ | |
| 						AccessKey: "some_access_key1", | |
| 						SecretKey: "some_secret_key2", | |
| 					}, | |
| 				}, | |
| 			}, | |
| 			expectIdent: &Identity{ | |
| 				Name:         "notSpecifyAccountId", | |
| 				Account:      &AccountAdmin, | |
| 				PrincipalArn: "arn:seaweed:iam::user/notSpecifyAccountId", | |
| 				Actions: []Action{ | |
| 					"Read", | |
| 					"Write", | |
| 				}, | |
| 				Credentials: []*Credential{ | |
| 					{ | |
| 						AccessKey: "some_access_key1", | |
| 						SecretKey: "some_secret_key2", | |
| 					}, | |
| 				}, | |
| 			}, | |
| 		}, | |
| 		"specifiedAccountID": { | |
| 			pbAccount: &pbSpecifiedAccount, | |
| 			pbIdent: &iam_pb.Identity{ | |
| 				Name:    "specifiedAccountID", | |
| 				Account: &pbSpecifiedAccount, | |
| 				Actions: []string{ | |
| 					"Read", | |
| 					"Write", | |
| 				}, | |
| 			}, | |
| 			expectIdent: &Identity{ | |
| 				Name:         "specifiedAccountID", | |
| 				Account:      &specifiedAccount, | |
| 				PrincipalArn: "arn:seaweed:iam::user/specifiedAccountID", | |
| 				Actions: []Action{ | |
| 					"Read", | |
| 					"Write", | |
| 				}, | |
| 			}, | |
| 		}, | |
| 		"anonymous": { | |
| 			pbIdent: &iam_pb.Identity{ | |
| 				Name: "anonymous", | |
| 				Actions: []string{ | |
| 					"Read", | |
| 					"Write", | |
| 				}, | |
| 			}, | |
| 			expectIdent: &Identity{ | |
| 				Name:         "anonymous", | |
| 				Account:      &AccountAnonymous, | |
| 				PrincipalArn: "arn:seaweed:iam::user/anonymous", | |
| 				Actions: []Action{ | |
| 					"Read", | |
| 					"Write", | |
| 				}, | |
| 			}, | |
| 		}, | |
| 	} | |
| 
 | |
| 	config := &iam_pb.S3ApiConfiguration{ | |
| 		Identities: make([]*iam_pb.Identity, 0), | |
| 	} | |
| 	for _, v := range testCases { | |
| 		config.Identities = append(config.Identities, v.pbIdent) | |
| 		if v.pbAccount != nil { | |
| 			config.Accounts = append(config.Accounts, v.pbAccount) | |
| 		} | |
| 	} | |
| 
 | |
| 	iam := IdentityAccessManagement{} | |
| 	err := iam.loadS3ApiConfiguration(config) | |
| 	if err != nil { | |
| 		return | |
| 	} | |
| 
 | |
| 	for _, ident := range iam.identities { | |
| 		tc := testCases[ident.Name] | |
| 		if !reflect.DeepEqual(ident, tc.expectIdent) { | |
| 			t.Errorf("not expect for ident name %s", ident.Name) | |
| 		} | |
| 	} | |
| } | |
| 
 | |
| func TestNewIdentityAccessManagementWithStoreEnvVars(t *testing.T) { | |
| 	// Save original environment | |
| 	originalAccessKeyId := os.Getenv("AWS_ACCESS_KEY_ID") | |
| 	originalSecretAccessKey := os.Getenv("AWS_SECRET_ACCESS_KEY") | |
| 
 | |
| 	// Clean up after test | |
| 	defer func() { | |
| 		if originalAccessKeyId != "" { | |
| 			os.Setenv("AWS_ACCESS_KEY_ID", originalAccessKeyId) | |
| 		} else { | |
| 			os.Unsetenv("AWS_ACCESS_KEY_ID") | |
| 		} | |
| 		if originalSecretAccessKey != "" { | |
| 			os.Setenv("AWS_SECRET_ACCESS_KEY", originalSecretAccessKey) | |
| 		} else { | |
| 			os.Unsetenv("AWS_SECRET_ACCESS_KEY") | |
| 		} | |
| 	}() | |
| 
 | |
| 	tests := []struct { | |
| 		name              string | |
| 		accessKeyId       string | |
| 		secretAccessKey   string | |
| 		expectEnvIdentity bool | |
| 		expectedName      string | |
| 		description       string | |
| 	}{ | |
| 		{ | |
| 			name:              "Environment variables used as fallback", | |
| 			accessKeyId:       "AKIA1234567890ABCDEF", | |
| 			secretAccessKey:   "secret123456789012345678901234567890abcdef12", | |
| 			expectEnvIdentity: true, | |
| 			expectedName:      "admin-AKIA1234", | |
| 			description:       "When no config file and no filer config, environment variables should be used", | |
| 		}, | |
| 		{ | |
| 			name:              "Short access key fallback", | |
| 			accessKeyId:       "SHORT", | |
| 			secretAccessKey:   "secret123456789012345678901234567890abcdef12", | |
| 			expectEnvIdentity: true, | |
| 			expectedName:      "admin-SHORT", | |
| 			description:       "Short access keys should work correctly as fallback", | |
| 		}, | |
| 		{ | |
| 			name:              "No env vars means no identities", | |
| 			accessKeyId:       "", | |
| 			secretAccessKey:   "", | |
| 			expectEnvIdentity: false, | |
| 			expectedName:      "", | |
| 			description:       "When no env vars and no config, should have no identities", | |
| 		}, | |
| 	} | |
| 
 | |
| 	for _, tt := range tests { | |
| 		t.Run(tt.name, func(t *testing.T) { | |
| 			// Set up environment variables | |
| 			if tt.accessKeyId != "" { | |
| 				os.Setenv("AWS_ACCESS_KEY_ID", tt.accessKeyId) | |
| 			} else { | |
| 				os.Unsetenv("AWS_ACCESS_KEY_ID") | |
| 			} | |
| 			if tt.secretAccessKey != "" { | |
| 				os.Setenv("AWS_SECRET_ACCESS_KEY", tt.secretAccessKey) | |
| 			} else { | |
| 				os.Unsetenv("AWS_SECRET_ACCESS_KEY") | |
| 			} | |
| 
 | |
| 			// Create IAM instance with memory store for testing (no config file) | |
| 			option := &S3ApiServerOption{ | |
| 				Config: "", // No config file - this should trigger environment variable fallback | |
| 			} | |
| 			iam := NewIdentityAccessManagementWithStore(option, string(credential.StoreTypeMemory)) | |
| 
 | |
| 			if tt.expectEnvIdentity { | |
| 				// Should have exactly one identity from environment variables | |
| 				assert.Len(t, iam.identities, 1, "Should have exactly one identity from environment variables") | |
| 
 | |
| 				identity := iam.identities[0] | |
| 				assert.Equal(t, tt.expectedName, identity.Name, "Identity name should match expected") | |
| 				assert.Len(t, identity.Credentials, 1, "Should have one credential") | |
| 				assert.Equal(t, tt.accessKeyId, identity.Credentials[0].AccessKey, "Access key should match environment variable") | |
| 				assert.Equal(t, tt.secretAccessKey, identity.Credentials[0].SecretKey, "Secret key should match environment variable") | |
| 				assert.Contains(t, identity.Actions, Action(ACTION_ADMIN), "Should have admin action") | |
| 			} else { | |
| 				// When no env vars, should have no identities (since no config file) | |
| 				assert.Len(t, iam.identities, 0, "Should have no identities when no env vars and no config file") | |
| 			} | |
| 		}) | |
| 	} | |
| } | |
| 
 | |
| // TestBucketLevelListPermissions tests that bucket-level List permissions work correctly | |
| // This test validates the fix for issue #7066 | |
| func TestBucketLevelListPermissions(t *testing.T) { | |
| 	// Test the functionality that was broken in issue #7066 | |
|  | |
| 	t.Run("Bucket Wildcard Permissions", func(t *testing.T) { | |
| 		// Create identity with bucket-level List permission using wildcards | |
| 		identity := &Identity{ | |
| 			Name: "bucket-user", | |
| 			Actions: []Action{ | |
| 				"List:mybucket*", | |
| 				"Read:mybucket*", | |
| 				"ReadAcp:mybucket*", | |
| 				"Write:mybucket*", | |
| 				"WriteAcp:mybucket*", | |
| 				"Tagging:mybucket*", | |
| 			}, | |
| 		} | |
| 
 | |
| 		// Test cases for bucket-level wildcard permissions | |
| 		testCases := []struct { | |
| 			name        string | |
| 			action      Action | |
| 			bucket      string | |
| 			object      string | |
| 			shouldAllow bool | |
| 			description string | |
| 		}{ | |
| 			{ | |
| 				name:        "exact bucket match", | |
| 				action:      "List", | |
| 				bucket:      "mybucket", | |
| 				object:      "", | |
| 				shouldAllow: true, | |
| 				description: "Should allow access to exact bucket name", | |
| 			}, | |
| 			{ | |
| 				name:        "bucket with suffix", | |
| 				action:      "List", | |
| 				bucket:      "mybucket-prod", | |
| 				object:      "", | |
| 				shouldAllow: true, | |
| 				description: "Should allow access to bucket with matching prefix", | |
| 			}, | |
| 			{ | |
| 				name:        "bucket with numbers", | |
| 				action:      "List", | |
| 				bucket:      "mybucket123", | |
| 				object:      "", | |
| 				shouldAllow: true, | |
| 				description: "Should allow access to bucket with numbers", | |
| 			}, | |
| 			{ | |
| 				name:        "different bucket", | |
| 				action:      "List", | |
| 				bucket:      "otherbucket", | |
| 				object:      "", | |
| 				shouldAllow: false, | |
| 				description: "Should deny access to bucket with different prefix", | |
| 			}, | |
| 			{ | |
| 				name:        "partial match", | |
| 				action:      "List", | |
| 				bucket:      "notmybucket", | |
| 				object:      "", | |
| 				shouldAllow: false, | |
| 				description: "Should deny access to bucket that contains but doesn't start with the prefix", | |
| 			}, | |
| 		} | |
| 
 | |
| 		for _, tc := range testCases { | |
| 			t.Run(tc.name, func(t *testing.T) { | |
| 				result := identity.canDo(tc.action, tc.bucket, tc.object) | |
| 				assert.Equal(t, tc.shouldAllow, result, tc.description) | |
| 			}) | |
| 		} | |
| 	}) | |
| 
 | |
| 	t.Run("Global List Permission", func(t *testing.T) { | |
| 		// Create identity with global List permission | |
| 		identity := &Identity{ | |
| 			Name: "global-user", | |
| 			Actions: []Action{ | |
| 				"List", | |
| 			}, | |
| 		} | |
| 
 | |
| 		// Should allow access to any bucket | |
| 		testCases := []string{"anybucket", "mybucket", "test-bucket", "prod-data"} | |
| 
 | |
| 		for _, bucket := range testCases { | |
| 			result := identity.canDo("List", bucket, "") | |
| 			assert.True(t, result, "Global List permission should allow access to bucket %s", bucket) | |
| 		} | |
| 	}) | |
| 
 | |
| 	t.Run("No Wildcard Exact Match", func(t *testing.T) { | |
| 		// Create identity with exact bucket permission (no wildcard) | |
| 		identity := &Identity{ | |
| 			Name: "exact-user", | |
| 			Actions: []Action{ | |
| 				"List:specificbucket", | |
| 			}, | |
| 		} | |
| 
 | |
| 		// Should only allow access to the exact bucket | |
| 		assert.True(t, identity.canDo("List", "specificbucket", ""), "Should allow access to exact bucket") | |
| 		assert.False(t, identity.canDo("List", "specificbucket-test", ""), "Should deny access to bucket with suffix") | |
| 		assert.False(t, identity.canDo("List", "otherbucket", ""), "Should deny access to different bucket") | |
| 	}) | |
| 
 | |
| 	t.Log("This test validates the fix for issue #7066") | |
| 	t.Log("Bucket-level List permissions like 'List:bucket*' work correctly") | |
| 	t.Log("ListBucketsHandler now uses consistent authentication flow") | |
| } | |
| 
 | |
| // TestListBucketsAuthRequest tests that authRequest works correctly for ListBuckets operations | |
| // This test validates that the fix for the regression identified in PR #7067 works correctly | |
| func TestListBucketsAuthRequest(t *testing.T) { | |
| 	t.Run("ListBuckets special case handling", func(t *testing.T) { | |
| 		// Create identity with bucket-specific permissions (no global List permission) | |
| 		identity := &Identity{ | |
| 			Name:    "bucket-user", | |
| 			Account: &AccountAdmin, | |
| 			Actions: []Action{ | |
| 				Action("List:mybucket*"), | |
| 				Action("Read:mybucket*"), | |
| 			}, | |
| 		} | |
| 
 | |
| 		// Test 1: ListBuckets operation should succeed (bucket = "") | |
| 		// This would have failed before the fix because canDo("List", "", "") would return false | |
| 		// After the fix, it bypasses the canDo check for ListBuckets operations | |
|  | |
| 		// Simulate what happens in authRequest for ListBuckets: | |
| 		// action = ACTION_LIST, bucket = "", object = "" | |
|  | |
| 		// Before fix: identity.canDo(ACTION_LIST, "", "") would fail | |
| 		// After fix: the canDo check should be bypassed | |
|  | |
| 		// Test the individual canDo method to show it would fail without the special case | |
| 		result := identity.canDo(Action(ACTION_LIST), "", "") | |
| 		assert.False(t, result, "canDo should return false for empty bucket with bucket-specific permissions") | |
| 
 | |
| 		// Test with a specific bucket that matches the permission | |
| 		result2 := identity.canDo(Action(ACTION_LIST), "mybucket", "") | |
| 		assert.True(t, result2, "canDo should return true for matching bucket") | |
| 
 | |
| 		// Test with a specific bucket that doesn't match | |
| 		result3 := identity.canDo(Action(ACTION_LIST), "otherbucket", "") | |
| 		assert.False(t, result3, "canDo should return false for non-matching bucket") | |
| 	}) | |
| 
 | |
| 	t.Run("Object listing maintains permission enforcement", func(t *testing.T) { | |
| 		// Create identity with bucket-specific permissions | |
| 		identity := &Identity{ | |
| 			Name:    "bucket-user", | |
| 			Account: &AccountAdmin, | |
| 			Actions: []Action{ | |
| 				Action("List:mybucket*"), | |
| 			}, | |
| 		} | |
| 
 | |
| 		// For object listing operations, the normal permission checks should still apply | |
| 		// These operations have a specific bucket in the URL | |
|  | |
| 		// Should succeed for allowed bucket | |
| 		result1 := identity.canDo(Action(ACTION_LIST), "mybucket", "prefix/") | |
| 		assert.True(t, result1, "Should allow listing objects in permitted bucket") | |
| 
 | |
| 		result2 := identity.canDo(Action(ACTION_LIST), "mybucket-prod", "") | |
| 		assert.True(t, result2, "Should allow listing objects in wildcard-matched bucket") | |
| 
 | |
| 		// Should fail for disallowed bucket | |
| 		result3 := identity.canDo(Action(ACTION_LIST), "otherbucket", "") | |
| 		assert.False(t, result3, "Should deny listing objects in non-permitted bucket") | |
| 	}) | |
| 
 | |
| 	t.Log("This test validates the fix for the regression identified in PR #7067") | |
| 	t.Log("ListBuckets operation bypasses global permission check when bucket is empty") | |
| 	t.Log("Object listing still properly enforces bucket-level permissions") | |
| }
 |