aboutsummaryrefslogtreecommitdiff
path: root/weed/s3api/policy_conversion_test.go
diff options
context:
space:
mode:
authorChris Lu <chrislusf@users.noreply.github.com>2025-11-12 23:46:52 -0800
committerGitHub <noreply@github.com>2025-11-12 23:46:52 -0800
commit2a9d4d1e23a99ddbdd4b99d3ddc3ff78cdfdf7ae (patch)
treefecb24a5439ab69f9b82f79c311305a082457e5e /weed/s3api/policy_conversion_test.go
parent508d06d9a5c763668ba149a8f1182e8552505c2b (diff)
downloadseaweedfs-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.go614
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))
+ }
+}
+