From f734b2d4bf154b372d382283a8ef09fe1c808154 Mon Sep 17 00:00:00 2001 From: chrislu Date: Sun, 14 Dec 2025 16:08:56 -0800 Subject: Refactor: Extract common IAM logic into shared weed/iam package (#7747) This resolves GitHub issue #7747 by extracting duplicated IAM code into a shared package that both the embedded S3 IAM and standalone IAM use. New shared package (weed/iam/): - constants.go: Common constants (charsets, action strings, error messages) - helpers.go: Shared helper functions (Hash, GenerateRandomString, GenerateAccessKeyId, GenerateSecretAccessKey, StringSlicesEqual, MapToStatementAction, MapToIdentitiesAction, MaskAccessKey) - responses.go: Common IAM response structs (CommonResponse, ListUsersResponse, CreateUserResponse, etc.) - helpers_test.go: Unit tests for shared helpers Updated files: - weed/s3api/s3api_embedded_iam.go: Use type aliases and function wrappers to the shared package, removing ~200 lines of duplicated code - weed/iamapi/iamapi_management_handlers.go: Use shared package for constants and helper functions, removing ~100 lines of duplicated code - weed/iamapi/iamapi_response.go: Re-export types from shared package for backwards compatibility Benefits: - Single source of truth for IAM constants and helpers - Easier maintenance - changes only need to be made in one place - Reduced risk of inconsistencies between embedded and standalone IAM - Better test coverage through shared test suite --- weed/iam/helpers_test.go | 135 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 weed/iam/helpers_test.go (limited to 'weed/iam/helpers_test.go') diff --git a/weed/iam/helpers_test.go b/weed/iam/helpers_test.go new file mode 100644 index 000000000..c9913d28a --- /dev/null +++ b/weed/iam/helpers_test.go @@ -0,0 +1,135 @@ +package iam + +import ( + "testing" + + "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" + "github.com/stretchr/testify/assert" +) + +func TestHash(t *testing.T) { + input := "test" + result := Hash(&input) + assert.NotEmpty(t, result) + assert.Len(t, result, 40) // SHA1 hex is 40 chars + + // Same input should produce same hash + result2 := Hash(&input) + assert.Equal(t, result, result2) + + // Different input should produce different hash + different := "different" + result3 := Hash(&different) + assert.NotEqual(t, result, result3) +} + +func TestGenerateRandomString(t *testing.T) { + // Valid generation + result, err := GenerateRandomString(10, CharsetUpper) + assert.NoError(t, err) + assert.Len(t, result, 10) + + // Different calls should produce different results (with high probability) + result2, err := GenerateRandomString(10, CharsetUpper) + assert.NoError(t, err) + assert.NotEqual(t, result, result2) + + // Invalid length + _, err = GenerateRandomString(0, CharsetUpper) + assert.Error(t, err) + + _, err = GenerateRandomString(-1, CharsetUpper) + assert.Error(t, err) + + // Empty charset + _, err = GenerateRandomString(10, "") + assert.Error(t, err) +} + +func TestGenerateAccessKeyId(t *testing.T) { + keyId, err := GenerateAccessKeyId() + assert.NoError(t, err) + assert.Len(t, keyId, AccessKeyIdLength) +} + +func TestGenerateSecretAccessKey(t *testing.T) { + secretKey, err := GenerateSecretAccessKey() + assert.NoError(t, err) + assert.Len(t, secretKey, SecretAccessKeyLength) +} + +func TestStringSlicesEqual(t *testing.T) { + tests := []struct { + a []string + b []string + expected bool + }{ + {[]string{"a", "b", "c"}, []string{"a", "b", "c"}, true}, + {[]string{"c", "b", "a"}, []string{"a", "b", "c"}, true}, // Order independent + {[]string{"a", "b"}, []string{"a", "b", "c"}, false}, + {[]string{}, []string{}, true}, + {nil, nil, true}, + {[]string{"a"}, []string{"b"}, false}, + } + + for _, test := range tests { + result := StringSlicesEqual(test.a, test.b) + assert.Equal(t, test.expected, result) + } +} + +func TestMapToStatementAction(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {StatementActionAdmin, s3_constants.ACTION_ADMIN}, + {StatementActionWrite, s3_constants.ACTION_WRITE}, + {StatementActionRead, s3_constants.ACTION_READ}, + {StatementActionList, s3_constants.ACTION_LIST}, + {StatementActionDelete, s3_constants.ACTION_DELETE_BUCKET}, + {"unknown", ""}, + } + + for _, test := range tests { + result := MapToStatementAction(test.input) + assert.Equal(t, test.expected, result) + } +} + +func TestMapToIdentitiesAction(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {s3_constants.ACTION_ADMIN, StatementActionAdmin}, + {s3_constants.ACTION_WRITE, StatementActionWrite}, + {s3_constants.ACTION_READ, StatementActionRead}, + {s3_constants.ACTION_LIST, StatementActionList}, + {s3_constants.ACTION_DELETE_BUCKET, StatementActionDelete}, + {"unknown", ""}, + } + + for _, test := range tests { + result := MapToIdentitiesAction(test.input) + assert.Equal(t, test.expected, result) + } +} + +func TestMaskAccessKey(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"AKIAIOSFODNN7EXAMPLE", "AKIA***"}, + {"AKIA", "AKIA"}, + {"AKI", "AKI"}, + {"", ""}, + } + + for _, test := range tests { + result := MaskAccessKey(test.input) + assert.Equal(t, test.expected, result) + } +} + -- cgit v1.2.3