diff options
| author | Chris Lu <chrislusf@users.noreply.github.com> | 2025-11-12 23:46:52 -0800 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-11-12 23:46:52 -0800 |
| commit | 2a9d4d1e23a99ddbdd4b99d3ddc3ff78cdfdf7ae (patch) | |
| tree | fecb24a5439ab69f9b82f79c311305a082457e5e /weed/s3api/policy_conversion_test.go | |
| parent | 508d06d9a5c763668ba149a8f1182e8552505c2b (diff) | |
| download | seaweedfs-2a9d4d1e23a99ddbdd4b99d3ddc3ff78cdfdf7ae.tar.xz seaweedfs-2a9d4d1e23a99ddbdd4b99d3ddc3ff78cdfdf7ae.zip | |
Refactor data structure (#7472)
* refactor to avoids circular dependency
* converts a policy.PolicyDocument to policy_engine.PolicyDocument
* convert numeric types to strings
* Update weed/s3api/policy_conversion.go
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
* refactoring
* not skipping numeric and boolean values in arrays
* avoid nil
* edge cases
* handling conversion failure
The handling of unsupported types in convertToString could lead to silent policy alterations.
The conversion of map-based principals in convertPrincipal is too generic and could misinterpret policies.
* concise
* fix doc
* adjust warning
* recursion
* return errors
* reject empty principals
* better error message
---------
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Diffstat (limited to 'weed/s3api/policy_conversion_test.go')
| -rw-r--r-- | weed/s3api/policy_conversion_test.go | 614 |
1 files changed, 614 insertions, 0 deletions
diff --git a/weed/s3api/policy_conversion_test.go b/weed/s3api/policy_conversion_test.go new file mode 100644 index 000000000..e7a77126f --- /dev/null +++ b/weed/s3api/policy_conversion_test.go @@ -0,0 +1,614 @@ +package s3api + +import ( + "strings" + "testing" + + "github.com/seaweedfs/seaweedfs/weed/iam/policy" +) + +func TestConvertPolicyDocumentWithMixedTypes(t *testing.T) { + // Test that numeric and boolean values in arrays are properly converted + src := &policy.PolicyDocument{ + Version: "2012-10-17", + Statement: []policy.Statement{ + { + Sid: "TestMixedTypes", + Effect: "Allow", + Action: []string{"s3:GetObject"}, + Resource: []string{"arn:aws:s3:::bucket/*"}, + Principal: []interface{}{"user1", 123, true}, // Mixed types + Condition: map[string]map[string]interface{}{ + "NumericEquals": { + "s3:max-keys": []interface{}{100, 200, "300"}, // Mixed types + }, + "StringEquals": { + "s3:prefix": []interface{}{"test", 123, false}, // Mixed types + }, + }, + }, + }, + } + + // Convert + dest, err := ConvertPolicyDocumentToPolicyEngine(src) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + // Verify document structure + if dest == nil { + t.Fatal("Expected non-nil result") + } + if dest.Version != "2012-10-17" { + t.Errorf("Expected version '2012-10-17', got '%s'", dest.Version) + } + if len(dest.Statement) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(dest.Statement)) + } + + stmt := dest.Statement[0] + + // Verify Principal conversion (should have 3 items converted to strings) + if stmt.Principal == nil { + t.Fatal("Expected non-nil Principal") + } + principals := stmt.Principal.Strings() + if len(principals) != 3 { + t.Errorf("Expected 3 principals, got %d", len(principals)) + } + // Check that numeric and boolean were converted + expectedPrincipals := []string{"user1", "123", "true"} + for i, expected := range expectedPrincipals { + if principals[i] != expected { + t.Errorf("Principal[%d]: expected '%s', got '%s'", i, expected, principals[i]) + } + } + + // Verify Condition conversion + if len(stmt.Condition) != 2 { + t.Errorf("Expected 2 condition blocks, got %d", len(stmt.Condition)) + } + + // Check NumericEquals condition + numericCond, ok := stmt.Condition["NumericEquals"] + if !ok { + t.Fatal("Expected NumericEquals condition") + } + maxKeys, ok := numericCond["s3:max-keys"] + if !ok { + t.Fatal("Expected s3:max-keys in NumericEquals") + } + maxKeysStrs := maxKeys.Strings() + expectedMaxKeys := []string{"100", "200", "300"} + if len(maxKeysStrs) != len(expectedMaxKeys) { + t.Errorf("Expected %d max-keys values, got %d", len(expectedMaxKeys), len(maxKeysStrs)) + } + for i, expected := range expectedMaxKeys { + if maxKeysStrs[i] != expected { + t.Errorf("max-keys[%d]: expected '%s', got '%s'", i, expected, maxKeysStrs[i]) + } + } + + // Check StringEquals condition + stringCond, ok := stmt.Condition["StringEquals"] + if !ok { + t.Fatal("Expected StringEquals condition") + } + prefix, ok := stringCond["s3:prefix"] + if !ok { + t.Fatal("Expected s3:prefix in StringEquals") + } + prefixStrs := prefix.Strings() + expectedPrefix := []string{"test", "123", "false"} + if len(prefixStrs) != len(expectedPrefix) { + t.Errorf("Expected %d prefix values, got %d", len(expectedPrefix), len(prefixStrs)) + } + for i, expected := range expectedPrefix { + if prefixStrs[i] != expected { + t.Errorf("prefix[%d]: expected '%s', got '%s'", i, expected, prefixStrs[i]) + } + } +} + +func TestConvertPrincipalWithMapAndMixedTypes(t *testing.T) { + // Test AWS-style principal map with mixed types + principalMap := map[string]interface{}{ + "AWS": []interface{}{ + "arn:aws:iam::123456789012:user/Alice", + 456, // User ID as number + true, // Some boolean value + }, + } + + result, err := convertPrincipal(principalMap) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if result == nil { + t.Fatal("Expected non-nil result") + } + + strs := result.Strings() + if len(strs) != 3 { + t.Errorf("Expected 3 values, got %d", len(strs)) + } + + expectedValues := []string{ + "arn:aws:iam::123456789012:user/Alice", + "456", + "true", + } + + for i, expected := range expectedValues { + if strs[i] != expected { + t.Errorf("Value[%d]: expected '%s', got '%s'", i, expected, strs[i]) + } + } +} + +func TestConvertConditionValueWithMixedTypes(t *testing.T) { + // Test []interface{} with mixed types + mixedValues := []interface{}{ + "string", + 123, + true, + 456.78, + } + + result, err := convertConditionValue(mixedValues) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + strs := result.Strings() + + expectedValues := []string{"string", "123", "true", "456.78"} + if len(strs) != len(expectedValues) { + t.Errorf("Expected %d values, got %d", len(expectedValues), len(strs)) + } + + for i, expected := range expectedValues { + if strs[i] != expected { + t.Errorf("Value[%d]: expected '%s', got '%s'", i, expected, strs[i]) + } + } +} + +func TestConvertPolicyDocumentNil(t *testing.T) { + result, err := ConvertPolicyDocumentToPolicyEngine(nil) + if err != nil { + t.Errorf("Unexpected error for nil input: %v", err) + } + if result != nil { + t.Error("Expected nil result for nil input") + } +} + +func TestConvertPrincipalNil(t *testing.T) { + result, err := convertPrincipal(nil) + if err != nil { + t.Errorf("Unexpected error for nil input: %v", err) + } + if result != nil { + t.Error("Expected nil result for nil input") + } +} + +func TestConvertPrincipalEmptyArray(t *testing.T) { + // Empty array should return nil + result, err := convertPrincipal([]interface{}{}) + if err != nil { + t.Errorf("Unexpected error for empty array: %v", err) + } + if result != nil { + t.Error("Expected nil result for empty array") + } +} + +func TestConvertPrincipalUnknownType(t *testing.T) { + // Unknown types should return an error + result, err := convertPrincipal(12345) // Just a number, not valid principal + if err == nil { + t.Error("Expected error for unknown type") + } + if result != nil { + t.Error("Expected nil result for unknown type") + } +} + +func TestConvertPrincipalWithNilValues(t *testing.T) { + // Test that nil values in arrays are skipped for security + principalArray := []interface{}{ + "arn:aws:iam::123456789012:user/Alice", + nil, // Should be skipped + "arn:aws:iam::123456789012:user/Bob", + nil, // Should be skipped + } + + result, err := convertPrincipal(principalArray) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if result == nil { + t.Fatal("Expected non-nil result") + } + + strs := result.Strings() + // Should only have 2 values (nil values skipped) + if len(strs) != 2 { + t.Errorf("Expected 2 values (nil values skipped), got %d", len(strs)) + } + + expectedValues := []string{ + "arn:aws:iam::123456789012:user/Alice", + "arn:aws:iam::123456789012:user/Bob", + } + + for i, expected := range expectedValues { + if strs[i] != expected { + t.Errorf("Value[%d]: expected '%s', got '%s'", i, expected, strs[i]) + } + } +} + +func TestConvertConditionValueWithNilValues(t *testing.T) { + // Test that nil values in condition arrays are skipped + mixedValues := []interface{}{ + "string", + nil, // Should be skipped + 123, + nil, // Should be skipped + true, + } + + result, err := convertConditionValue(mixedValues) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + strs := result.Strings() + + // Should only have 3 values (nil values skipped) + expectedValues := []string{"string", "123", "true"} + if len(strs) != len(expectedValues) { + t.Errorf("Expected %d values (nil values skipped), got %d", len(expectedValues), len(strs)) + } + + for i, expected := range expectedValues { + if strs[i] != expected { + t.Errorf("Value[%d]: expected '%s', got '%s'", i, expected, strs[i]) + } + } +} + +func TestConvertPrincipalMapWithNilValues(t *testing.T) { + // Test AWS-style principal map with nil values + principalMap := map[string]interface{}{ + "AWS": []interface{}{ + "arn:aws:iam::123456789012:user/Alice", + nil, // Should be skipped + "arn:aws:iam::123456789012:user/Bob", + }, + } + + result, err := convertPrincipal(principalMap) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if result == nil { + t.Fatal("Expected non-nil result") + } + + strs := result.Strings() + // Should only have 2 values (nil value skipped) + if len(strs) != 2 { + t.Errorf("Expected 2 values (nil value skipped), got %d", len(strs)) + } + + expectedValues := []string{ + "arn:aws:iam::123456789012:user/Alice", + "arn:aws:iam::123456789012:user/Bob", + } + + for i, expected := range expectedValues { + if strs[i] != expected { + t.Errorf("Value[%d]: expected '%s', got '%s'", i, expected, strs[i]) + } + } +} + +func TestConvertToStringUnsupportedType(t *testing.T) { + // Test that unsupported types (e.g., nested maps/slices) return empty string + // This should trigger a warning log and return an error + + type customStruct struct { + Field string + } + + testCases := []struct { + name string + input interface{} + expected string + }{ + { + name: "nested map", + input: map[string]interface{}{"key": "value"}, + expected: "", // Unsupported, returns empty string + }, + { + name: "nested slice", + input: []int{1, 2, 3}, + expected: "", // Unsupported, returns empty string + }, + { + name: "custom struct", + input: customStruct{Field: "test"}, + expected: "", // Unsupported, returns empty string + }, + { + name: "function", + input: func() {}, + expected: "", // Unsupported, returns empty string + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := convertToString(tc.input) + // For unsupported types, we expect an error + if err == nil { + t.Error("Expected error for unsupported type") + } + if result != tc.expected { + t.Errorf("Expected '%s', got '%s'", tc.expected, result) + } + }) + } +} + +func TestConvertToStringSupportedTypes(t *testing.T) { + // Test that all supported types convert correctly + testCases := []struct { + name string + input interface{} + expected string + }{ + {"string", "test", "test"}, + {"bool true", true, "true"}, + {"bool false", false, "false"}, + {"int", 123, "123"}, + {"int8", int8(127), "127"}, + {"int16", int16(32767), "32767"}, + {"int32", int32(2147483647), "2147483647"}, + {"int64", int64(9223372036854775807), "9223372036854775807"}, + {"uint", uint(123), "123"}, + {"uint8", uint8(255), "255"}, + {"uint16", uint16(65535), "65535"}, + {"uint32", uint32(4294967295), "4294967295"}, + {"uint64", uint64(18446744073709551615), "18446744073709551615"}, + {"float32", float32(3.14), "3.14"}, + {"float64", float64(3.14159265359), "3.14159265359"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := convertToString(tc.input) + if err != nil { + t.Errorf("Unexpected error for supported type %s: %v", tc.name, err) + } + if result != tc.expected { + t.Errorf("Expected '%s', got '%s'", tc.expected, result) + } + }) + } +} + +func TestConvertPrincipalUnsupportedTypes(t *testing.T) { + // Test that unsupported principal types return errors + testCases := []struct { + name string + principal interface{} + }{ + { + name: "Service principal", + principal: map[string]interface{}{"Service": "s3.amazonaws.com"}, + }, + { + name: "Federated principal", + principal: map[string]interface{}{"Federated": "arn:aws:iam::123456789012:saml-provider/ExampleProvider"}, + }, + { + name: "Multiple keys", + principal: map[string]interface{}{"AWS": "arn:...", "Service": "s3.amazonaws.com"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := convertPrincipal(tc.principal) + if err == nil { + t.Error("Expected error for unsupported principal type") + } + if result != nil { + t.Error("Expected nil result for unsupported principal type") + } + }) + } +} + +func TestConvertPrincipalEmptyStrings(t *testing.T) { + // Test that empty string principals are rejected for security + testCases := []struct { + name string + principal interface{} + wantError string + }{ + { + name: "Empty string principal", + principal: "", + wantError: "principal string cannot be empty", + }, + { + name: "Empty string in array", + principal: []string{"arn:aws:iam::123456789012:user/Alice", "", "arn:aws:iam::123456789012:user/Bob"}, + wantError: "principal string in slice cannot be empty", + }, + { + name: "Empty string in interface array", + principal: []interface{}{"arn:aws:iam::123456789012:user/Alice", ""}, + wantError: "principal string in slice cannot be empty", + }, + { + name: "Empty string in AWS map", + principal: map[string]interface{}{ + "AWS": "", + }, + wantError: "principal string cannot be empty", + }, + { + name: "Empty string in AWS map array", + principal: map[string]interface{}{ + "AWS": []string{"arn:aws:iam::123456789012:user/Alice", ""}, + }, + wantError: "principal string in slice cannot be empty", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := convertPrincipal(tc.principal) + if err == nil { + t.Error("Expected error for empty principal string") + } else if !strings.Contains(err.Error(), tc.wantError) { + t.Errorf("Expected error containing %q, got: %v", tc.wantError, err) + } + if result != nil { + t.Error("Expected nil result for empty principal string") + } + }) + } +} + +func TestConvertStatementWithUnsupportedFields(t *testing.T) { + // Test that errors are returned for unsupported fields + // These fields are critical for policy semantics and ignoring them would be a security risk + + testCases := []struct { + name string + statement *policy.Statement + wantError string + }{ + { + name: "NotAction field", + statement: &policy.Statement{ + Sid: "TestNotAction", + Effect: "Deny", + Action: []string{"s3:GetObject"}, + NotAction: []string{"s3:PutObject", "s3:DeleteObject"}, + Resource: []string{"arn:aws:s3:::bucket/*"}, + }, + wantError: "NotAction is not supported", + }, + { + name: "NotResource field", + statement: &policy.Statement{ + Sid: "TestNotResource", + Effect: "Allow", + Action: []string{"s3:*"}, + Resource: []string{"arn:aws:s3:::bucket/*"}, + NotResource: []string{"arn:aws:s3:::bucket/secret/*"}, + }, + wantError: "NotResource is not supported", + }, + { + name: "NotPrincipal field", + statement: &policy.Statement{ + Sid: "TestNotPrincipal", + Effect: "Deny", + Action: []string{"s3:*"}, + Resource: []string{"arn:aws:s3:::bucket/*"}, + NotPrincipal: map[string]interface{}{"AWS": "arn:aws:iam::123456789012:user/Admin"}, + }, + wantError: "NotPrincipal is not supported", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // The conversion should fail with an error for security reasons + result, err := convertStatement(tc.statement) + if err == nil { + t.Error("Expected error for unsupported field, got nil") + } else if !strings.Contains(err.Error(), tc.wantError) { + t.Errorf("Expected error containing %q, got: %v", tc.wantError, err) + } + + // Verify zero-value struct is returned on error + if result.Sid != "" || result.Effect != "" { + t.Error("Expected zero-value struct on error") + } + }) + } +} + +func TestConvertStatementSuccess(t *testing.T) { + // Test successful conversion without unsupported fields + statement := &policy.Statement{ + Sid: "AllowGetObject", + Effect: "Allow", + Action: []string{"s3:GetObject"}, + Resource: []string{"arn:aws:s3:::bucket/*"}, + Principal: map[string]interface{}{ + "AWS": "arn:aws:iam::123456789012:user/Alice", + }, + } + + result, err := convertStatement(statement) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if result.Sid != statement.Sid { + t.Errorf("Expected Sid %q, got %q", statement.Sid, result.Sid) + } + if string(result.Effect) != statement.Effect { + t.Errorf("Expected Effect %q, got %q", statement.Effect, result.Effect) + } +} + +func TestConvertPolicyDocumentWithId(t *testing.T) { + // Test that policy document Id field triggers a warning + src := &policy.PolicyDocument{ + Version: "2012-10-17", + Id: "MyPolicyId", + Statement: []policy.Statement{ + { + Sid: "AllowGetObject", + Effect: "Allow", + Action: []string{"s3:GetObject"}, + Resource: []string{"arn:aws:s3:::bucket/*"}, + }, + }, + } + + // The conversion should succeed but log a warning about Id + dest, err := ConvertPolicyDocumentToPolicyEngine(src) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if dest == nil { + t.Fatal("Expected non-nil result") + } + + // Verify basic conversion worked + if dest.Version != src.Version { + t.Errorf("Expected Version %q, got %q", src.Version, dest.Version) + } + if len(dest.Statement) != 1 { + t.Errorf("Expected 1 statement, got %d", len(dest.Statement)) + } +} + |
