From f734b2d4bf154b372d382283a8ef09fe1c808154 Mon Sep 17 00:00:00 2001 From: chrislu Date: Sun, 14 Dec 2025 16:08:56 -0800 Subject: [PATCH] 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/constants.go | 32 +++ weed/iam/helpers.go | 126 +++++++++++ weed/iam/helpers_test.go | 135 ++++++++++++ weed/iam/responses.go | 140 ++++++++++++ weed/iamapi/iamapi_management_handlers.go | 125 ++--------- weed/iamapi/iamapi_response.go | 129 ++--------- weed/s3api/s3api_embedded_iam.go | 252 +++------------------- 7 files changed, 509 insertions(+), 430 deletions(-) create mode 100644 weed/iam/constants.go create mode 100644 weed/iam/helpers.go create mode 100644 weed/iam/helpers_test.go create mode 100644 weed/iam/responses.go diff --git a/weed/iam/constants.go b/weed/iam/constants.go new file mode 100644 index 000000000..0b857a896 --- /dev/null +++ b/weed/iam/constants.go @@ -0,0 +1,32 @@ +package iam + +// Character sets for credential generation +const ( + CharsetUpper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + Charset = CharsetUpper + "abcdefghijklmnopqrstuvwxyz/" +) + +// Policy document version +const PolicyDocumentVersion = "2012-10-17" + +// Error message templates +const UserDoesNotExist = "the user with name %s cannot be found." + +// Statement action constants - these map to IAM policy actions +const ( + StatementActionAdmin = "*" + StatementActionWrite = "Put*" + StatementActionWriteAcp = "PutBucketAcl" + StatementActionRead = "Get*" + StatementActionReadAcp = "GetBucketAcl" + StatementActionList = "List*" + StatementActionTagging = "Tagging*" + StatementActionDelete = "DeleteBucket*" +) + +// Access key lengths +const ( + AccessKeyIdLength = 21 + SecretAccessKeyLength = 42 +) + diff --git a/weed/iam/helpers.go b/weed/iam/helpers.go new file mode 100644 index 000000000..02b5fe5b4 --- /dev/null +++ b/weed/iam/helpers.go @@ -0,0 +1,126 @@ +package iam + +import ( + "crypto/rand" + "crypto/sha1" + "fmt" + "math/big" + "sort" + + "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" +) + +// Hash computes a SHA1 hash of the input string. +func Hash(s *string) string { + h := sha1.New() + h.Write([]byte(*s)) + return fmt.Sprintf("%x", h.Sum(nil)) +} + +// GenerateRandomString generates a cryptographically secure random string. +// Uses crypto/rand for security-sensitive credential generation. +func GenerateRandomString(length int, charset string) (string, error) { + if length <= 0 { + return "", fmt.Errorf("length must be positive, got %d", length) + } + if charset == "" { + return "", fmt.Errorf("charset must not be empty") + } + b := make([]byte, length) + for i := range b { + n, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) + if err != nil { + return "", fmt.Errorf("failed to generate random index: %w", err) + } + b[i] = charset[n.Int64()] + } + return string(b), nil +} + +// GenerateAccessKeyId generates a new access key ID. +func GenerateAccessKeyId() (string, error) { + return GenerateRandomString(AccessKeyIdLength, CharsetUpper) +} + +// GenerateSecretAccessKey generates a new secret access key. +func GenerateSecretAccessKey() (string, error) { + return GenerateRandomString(SecretAccessKeyLength, Charset) +} + +// StringSlicesEqual compares two string slices for equality, ignoring order. +// This is used instead of reflect.DeepEqual to avoid order-dependent comparisons. +func StringSlicesEqual(a, b []string) bool { + if len(a) != len(b) { + return false + } + // Make copies to avoid modifying the originals + aCopy := make([]string, len(a)) + bCopy := make([]string, len(b)) + copy(aCopy, a) + copy(bCopy, b) + sort.Strings(aCopy) + sort.Strings(bCopy) + for i := range aCopy { + if aCopy[i] != bCopy[i] { + return false + } + } + return true +} + +// MapToStatementAction converts a policy statement action to an S3 action constant. +func MapToStatementAction(action string) string { + switch action { + case StatementActionAdmin: + return s3_constants.ACTION_ADMIN + case StatementActionWrite: + return s3_constants.ACTION_WRITE + case StatementActionWriteAcp: + return s3_constants.ACTION_WRITE_ACP + case StatementActionRead: + return s3_constants.ACTION_READ + case StatementActionReadAcp: + return s3_constants.ACTION_READ_ACP + case StatementActionList: + return s3_constants.ACTION_LIST + case StatementActionTagging: + return s3_constants.ACTION_TAGGING + case StatementActionDelete: + return s3_constants.ACTION_DELETE_BUCKET + default: + return "" + } +} + +// MapToIdentitiesAction converts an S3 action constant to a policy statement action. +func MapToIdentitiesAction(action string) string { + switch action { + case s3_constants.ACTION_ADMIN: + return StatementActionAdmin + case s3_constants.ACTION_WRITE: + return StatementActionWrite + case s3_constants.ACTION_WRITE_ACP: + return StatementActionWriteAcp + case s3_constants.ACTION_READ: + return StatementActionRead + case s3_constants.ACTION_READ_ACP: + return StatementActionReadAcp + case s3_constants.ACTION_LIST: + return StatementActionList + case s3_constants.ACTION_TAGGING: + return StatementActionTagging + case s3_constants.ACTION_DELETE_BUCKET: + return StatementActionDelete + default: + return "" + } +} + +// MaskAccessKey masks an access key for logging, showing only the first 4 characters. +func MaskAccessKey(accessKeyId string) string { + if len(accessKeyId) > 4 { + return accessKeyId[:4] + "***" + } + return accessKeyId +} + 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) + } +} + diff --git a/weed/iam/responses.go b/weed/iam/responses.go new file mode 100644 index 000000000..a45c9fd16 --- /dev/null +++ b/weed/iam/responses.go @@ -0,0 +1,140 @@ +package iam + +import ( + "encoding/xml" + "fmt" + "time" + + "github.com/aws/aws-sdk-go/service/iam" +) + +// CommonResponse is embedded in all IAM response types to provide RequestId. +type CommonResponse struct { + ResponseMetadata struct { + RequestId string `xml:"RequestId"` + } `xml:"ResponseMetadata"` +} + +// SetRequestId sets a unique request ID based on current timestamp. +func (r *CommonResponse) SetRequestId() { + r.ResponseMetadata.RequestId = fmt.Sprintf("%d", time.Now().UnixNano()) +} + +// ListUsersResponse is the response for ListUsers action. +type ListUsersResponse struct { + CommonResponse + XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ ListUsersResponse"` + ListUsersResult struct { + Users []*iam.User `xml:"Users>member"` + IsTruncated bool `xml:"IsTruncated"` + } `xml:"ListUsersResult"` +} + +// ListAccessKeysResponse is the response for ListAccessKeys action. +type ListAccessKeysResponse struct { + CommonResponse + XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ ListAccessKeysResponse"` + ListAccessKeysResult struct { + AccessKeyMetadata []*iam.AccessKeyMetadata `xml:"AccessKeyMetadata>member"` + IsTruncated bool `xml:"IsTruncated"` + } `xml:"ListAccessKeysResult"` +} + +// DeleteAccessKeyResponse is the response for DeleteAccessKey action. +type DeleteAccessKeyResponse struct { + CommonResponse + XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ DeleteAccessKeyResponse"` +} + +// CreatePolicyResponse is the response for CreatePolicy action. +type CreatePolicyResponse struct { + CommonResponse + XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ CreatePolicyResponse"` + CreatePolicyResult struct { + Policy iam.Policy `xml:"Policy"` + } `xml:"CreatePolicyResult"` +} + +// CreateUserResponse is the response for CreateUser action. +type CreateUserResponse struct { + CommonResponse + XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ CreateUserResponse"` + CreateUserResult struct { + User iam.User `xml:"User"` + } `xml:"CreateUserResult"` +} + +// DeleteUserResponse is the response for DeleteUser action. +type DeleteUserResponse struct { + CommonResponse + XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ DeleteUserResponse"` +} + +// GetUserResponse is the response for GetUser action. +type GetUserResponse struct { + CommonResponse + XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ GetUserResponse"` + GetUserResult struct { + User iam.User `xml:"User"` + } `xml:"GetUserResult"` +} + +// UpdateUserResponse is the response for UpdateUser action. +type UpdateUserResponse struct { + CommonResponse + XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ UpdateUserResponse"` +} + +// CreateAccessKeyResponse is the response for CreateAccessKey action. +type CreateAccessKeyResponse struct { + CommonResponse + XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ CreateAccessKeyResponse"` + CreateAccessKeyResult struct { + AccessKey iam.AccessKey `xml:"AccessKey"` + } `xml:"CreateAccessKeyResult"` +} + +// PutUserPolicyResponse is the response for PutUserPolicy action. +type PutUserPolicyResponse struct { + CommonResponse + XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ PutUserPolicyResponse"` +} + +// DeleteUserPolicyResponse is the response for DeleteUserPolicy action. +type DeleteUserPolicyResponse struct { + CommonResponse + XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ DeleteUserPolicyResponse"` +} + +// GetUserPolicyResponse is the response for GetUserPolicy action. +type GetUserPolicyResponse struct { + CommonResponse + XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ GetUserPolicyResponse"` + GetUserPolicyResult struct { + UserName string `xml:"UserName"` + PolicyName string `xml:"PolicyName"` + PolicyDocument string `xml:"PolicyDocument"` + } `xml:"GetUserPolicyResult"` +} + +// ErrorResponse is the IAM error response format. +type ErrorResponse struct { + CommonResponse + XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ ErrorResponse"` + Error struct { + iam.ErrorDetails + Type string `xml:"Type"` + } `xml:"Error"` +} + +// Error represents an IAM API error with code and underlying error. +type Error struct { + Code string + Error error +} + +// Policies stores IAM policies (used for managed policy storage). +type Policies struct { + Policies map[string]interface{} `json:"policies"` +} + diff --git a/weed/iamapi/iamapi_management_handlers.go b/weed/iamapi/iamapi_management_handlers.go index 1985b042f..899db7ff3 100644 --- a/weed/iamapi/iamapi_management_handlers.go +++ b/weed/iamapi/iamapi_management_handlers.go @@ -1,45 +1,40 @@ package iamapi // This file provides IAM API handlers for the standalone IAM server. -// NOTE: There is code duplication with weed/s3api/s3api_embedded_iam.go. -// See GitHub issue #7747 for the planned refactoring to extract common IAM logic -// into a shared package. +// Common IAM types and helpers are imported from the shared weed/iam package. import ( - "crypto/rand" - "crypto/sha1" "encoding/json" "errors" "fmt" - "math/big" "net/http" "net/url" - "sort" "strings" "sync" + "github.com/aws/aws-sdk-go/service/iam" "github.com/seaweedfs/seaweedfs/weed/glog" + iamlib "github.com/seaweedfs/seaweedfs/weed/iam" "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" "github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" "github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine" - "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" "github.com/seaweedfs/seaweedfs/weed/s3api/s3err" - - "github.com/aws/aws-sdk-go/service/iam" ) +// Constants from shared package const ( - charsetUpper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - charset = charsetUpper + "abcdefghijklmnopqrstuvwxyz/" - policyDocumentVersion = "2012-10-17" - StatementActionAdmin = "*" - StatementActionWrite = "Put*" - StatementActionWriteAcp = "PutBucketAcl" - StatementActionRead = "Get*" - StatementActionReadAcp = "GetBucketAcl" - StatementActionList = "List*" - StatementActionTagging = "Tagging*" - StatementActionDelete = "DeleteBucket*" + charsetUpper = iamlib.CharsetUpper + charset = iamlib.Charset + policyDocumentVersion = iamlib.PolicyDocumentVersion + StatementActionAdmin = iamlib.StatementActionAdmin + StatementActionWrite = iamlib.StatementActionWrite + StatementActionWriteAcp = iamlib.StatementActionWriteAcp + StatementActionRead = iamlib.StatementActionRead + StatementActionReadAcp = iamlib.StatementActionReadAcp + StatementActionList = iamlib.StatementActionList + StatementActionTagging = iamlib.StatementActionTagging + StatementActionDelete = iamlib.StatementActionDelete + USER_DOES_NOT_EXIST = iamlib.UserDoesNotExist ) var ( @@ -47,105 +42,29 @@ var ( policyLock = sync.RWMutex{} ) +// Helper function wrappers using shared package func MapToStatementAction(action string) string { - switch action { - case StatementActionAdmin: - return s3_constants.ACTION_ADMIN - case StatementActionWrite: - return s3_constants.ACTION_WRITE - case StatementActionWriteAcp: - return s3_constants.ACTION_WRITE_ACP - case StatementActionRead: - return s3_constants.ACTION_READ - case StatementActionReadAcp: - return s3_constants.ACTION_READ_ACP - case StatementActionList: - return s3_constants.ACTION_LIST - case StatementActionTagging: - return s3_constants.ACTION_TAGGING - case StatementActionDelete: - return s3_constants.ACTION_DELETE_BUCKET - default: - return "" - } + return iamlib.MapToStatementAction(action) } func MapToIdentitiesAction(action string) string { - switch action { - case s3_constants.ACTION_ADMIN: - return StatementActionAdmin - case s3_constants.ACTION_WRITE: - return StatementActionWrite - case s3_constants.ACTION_WRITE_ACP: - return StatementActionWriteAcp - case s3_constants.ACTION_READ: - return StatementActionRead - case s3_constants.ACTION_READ_ACP: - return StatementActionReadAcp - case s3_constants.ACTION_LIST: - return StatementActionList - case s3_constants.ACTION_TAGGING: - return StatementActionTagging - case s3_constants.ACTION_DELETE_BUCKET: - return StatementActionDelete - default: - return "" - } + return iamlib.MapToIdentitiesAction(action) } -const ( - USER_DOES_NOT_EXIST = "the user with name %s cannot be found." -) - type Policies struct { Policies map[string]policy_engine.PolicyDocument `json:"policies"` } func Hash(s *string) string { - h := sha1.New() - h.Write([]byte(*s)) - return fmt.Sprintf("%x", h.Sum(nil)) + return iamlib.Hash(s) } -// StringWithCharset generates a cryptographically secure random string. -// Uses crypto/rand for security-sensitive credential generation. func StringWithCharset(length int, charset string) (string, error) { - if length <= 0 { - return "", fmt.Errorf("length must be positive, got %d", length) - } - if charset == "" { - return "", fmt.Errorf("charset must not be empty") - } - b := make([]byte, length) - for i := range b { - n, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) - if err != nil { - return "", fmt.Errorf("failed to generate random index: %w", err) - } - b[i] = charset[n.Int64()] - } - return string(b), nil + return iamlib.GenerateRandomString(length, charset) } -// stringSlicesEqual compares two string slices for equality, ignoring order. -// This is used instead of reflect.DeepEqual to avoid order-dependent comparisons. func stringSlicesEqual(a, b []string) bool { - if len(a) != len(b) { - return false - } - // Make copies to avoid modifying the originals - aCopy := make([]string, len(a)) - bCopy := make([]string, len(b)) - copy(aCopy, a) - copy(bCopy, b) - sort.Strings(aCopy) - sort.Strings(bCopy) - for i := range aCopy { - if aCopy[i] != bCopy[i] { - return false - } - } - return true + return iamlib.StringSlicesEqual(a, b) } func (iama *IamApiServer) ListUsers(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp ListUsersResponse) { diff --git a/weed/iamapi/iamapi_response.go b/weed/iamapi/iamapi_response.go index fc68ce5a5..c16b1f79b 100644 --- a/weed/iamapi/iamapi_response.go +++ b/weed/iamapi/iamapi_response.go @@ -1,113 +1,26 @@ package iamapi -import ( - "encoding/xml" - "fmt" - "time" +// This file re-exports IAM response types from the shared weed/iam package +// for backwards compatibility with existing code. - "github.com/aws/aws-sdk-go/service/iam" +import ( + iamlib "github.com/seaweedfs/seaweedfs/weed/iam" ) -type CommonResponse struct { - ResponseMetadata struct { - RequestId string `xml:"RequestId"` - } `xml:"ResponseMetadata"` -} - -type ListUsersResponse struct { - CommonResponse - XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ ListUsersResponse"` - ListUsersResult struct { - Users []*iam.User `xml:"Users>member"` - IsTruncated bool `xml:"IsTruncated"` - } `xml:"ListUsersResult"` -} - -type ListAccessKeysResponse struct { - CommonResponse - XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ ListAccessKeysResponse"` - ListAccessKeysResult struct { - AccessKeyMetadata []*iam.AccessKeyMetadata `xml:"AccessKeyMetadata>member"` - IsTruncated bool `xml:"IsTruncated"` - } `xml:"ListAccessKeysResult"` -} - -type DeleteAccessKeyResponse struct { - CommonResponse - XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ DeleteAccessKeyResponse"` -} - -type CreatePolicyResponse struct { - CommonResponse - XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ CreatePolicyResponse"` - CreatePolicyResult struct { - Policy iam.Policy `xml:"Policy"` - } `xml:"CreatePolicyResult"` -} - -type CreateUserResponse struct { - CommonResponse - XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ CreateUserResponse"` - CreateUserResult struct { - User iam.User `xml:"User"` - } `xml:"CreateUserResult"` -} - -type DeleteUserResponse struct { - CommonResponse - XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ DeleteUserResponse"` -} - -type GetUserResponse struct { - CommonResponse - XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ GetUserResponse"` - GetUserResult struct { - User iam.User `xml:"User"` - } `xml:"GetUserResult"` -} - -type UpdateUserResponse struct { - CommonResponse - XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ UpdateUserResponse"` -} - -type CreateAccessKeyResponse struct { - CommonResponse - XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ CreateAccessKeyResponse"` - CreateAccessKeyResult struct { - AccessKey iam.AccessKey `xml:"AccessKey"` - } `xml:"CreateAccessKeyResult"` -} - -type PutUserPolicyResponse struct { - CommonResponse - XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ PutUserPolicyResponse"` -} - -type DeleteUserPolicyResponse struct { - CommonResponse - XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ DeleteUserPolicyResponse"` -} - -type GetUserPolicyResponse struct { - CommonResponse - XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ GetUserPolicyResponse"` - GetUserPolicyResult struct { - UserName string `xml:"UserName"` - PolicyName string `xml:"PolicyName"` - PolicyDocument string `xml:"PolicyDocument"` - } `xml:"GetUserPolicyResult"` -} - -type ErrorResponse struct { - CommonResponse - XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ ErrorResponse"` - Error struct { - iam.ErrorDetails - Type string `xml:"Type"` - } `xml:"Error"` -} - -func (r *CommonResponse) SetRequestId() { - r.ResponseMetadata.RequestId = fmt.Sprintf("%d", time.Now().UnixNano()) -} +// Type aliases for IAM response types from shared package +type ( + CommonResponse = iamlib.CommonResponse + ListUsersResponse = iamlib.ListUsersResponse + ListAccessKeysResponse = iamlib.ListAccessKeysResponse + DeleteAccessKeyResponse = iamlib.DeleteAccessKeyResponse + CreatePolicyResponse = iamlib.CreatePolicyResponse + CreateUserResponse = iamlib.CreateUserResponse + DeleteUserResponse = iamlib.DeleteUserResponse + GetUserResponse = iamlib.GetUserResponse + UpdateUserResponse = iamlib.UpdateUserResponse + CreateAccessKeyResponse = iamlib.CreateAccessKeyResponse + PutUserPolicyResponse = iamlib.PutUserPolicyResponse + DeleteUserPolicyResponse = iamlib.DeleteUserPolicyResponse + GetUserPolicyResponse = iamlib.GetUserPolicyResponse + ErrorResponse = iamlib.ErrorResponse +) diff --git a/weed/s3api/s3api_embedded_iam.go b/weed/s3api/s3api_embedded_iam.go index e7a30b4c1..5dce8a3e0 100644 --- a/weed/s3api/s3api_embedded_iam.go +++ b/weed/s3api/s3api_embedded_iam.go @@ -1,29 +1,22 @@ package s3api // This file provides IAM API functionality embedded in the S3 server. -// NOTE: There is code duplication with weed/iamapi/iamapi_management_handlers.go. -// See GitHub issue #7747 for the planned refactoring to extract common IAM logic -// into a shared package. +// Common IAM types and helpers are imported from the shared weed/iam package. import ( "context" - "crypto/rand" - "crypto/sha1" "encoding/json" - "encoding/xml" "errors" "fmt" - "math/big" "net/http" "net/url" - "sort" "strings" "sync" - "time" "github.com/aws/aws-sdk-go/service/iam" "github.com/seaweedfs/seaweedfs/weed/credential" "github.com/seaweedfs/seaweedfs/weed/glog" + iamlib "github.com/seaweedfs/seaweedfs/weed/iam" "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" "github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" "github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine" @@ -48,233 +41,54 @@ func NewEmbeddedIamApi(credentialManager *credential.CredentialManager, iam *Ide } } -// IAM response types -type iamCommonResponse struct { - ResponseMetadata struct { - RequestId string `xml:"RequestId"` - } `xml:"ResponseMetadata"` -} - -func (r *iamCommonResponse) SetRequestId() { - r.ResponseMetadata.RequestId = fmt.Sprintf("%d", time.Now().UnixNano()) -} - -type iamListUsersResponse struct { - iamCommonResponse - XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ ListUsersResponse"` - ListUsersResult struct { - Users []*iam.User `xml:"Users>member"` - IsTruncated bool `xml:"IsTruncated"` - } `xml:"ListUsersResult"` -} - -type iamListAccessKeysResponse struct { - iamCommonResponse - XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ ListAccessKeysResponse"` - ListAccessKeysResult struct { - AccessKeyMetadata []*iam.AccessKeyMetadata `xml:"AccessKeyMetadata>member"` - IsTruncated bool `xml:"IsTruncated"` - } `xml:"ListAccessKeysResult"` -} - -type iamDeleteAccessKeyResponse struct { - iamCommonResponse - XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ DeleteAccessKeyResponse"` -} - -type iamCreatePolicyResponse struct { - iamCommonResponse - XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ CreatePolicyResponse"` - CreatePolicyResult struct { - Policy iam.Policy `xml:"Policy"` - } `xml:"CreatePolicyResult"` -} - -type iamCreateUserResponse struct { - iamCommonResponse - XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ CreateUserResponse"` - CreateUserResult struct { - User iam.User `xml:"User"` - } `xml:"CreateUserResult"` -} - -type iamDeleteUserResponse struct { - iamCommonResponse - XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ DeleteUserResponse"` -} - -type iamGetUserResponse struct { - iamCommonResponse - XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ GetUserResponse"` - GetUserResult struct { - User iam.User `xml:"User"` - } `xml:"GetUserResult"` -} - -type iamUpdateUserResponse struct { - iamCommonResponse - XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ UpdateUserResponse"` -} - -type iamCreateAccessKeyResponse struct { - iamCommonResponse - XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ CreateAccessKeyResponse"` - CreateAccessKeyResult struct { - AccessKey iam.AccessKey `xml:"AccessKey"` - } `xml:"CreateAccessKeyResult"` -} - -type iamPutUserPolicyResponse struct { - iamCommonResponse - XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ PutUserPolicyResponse"` -} - -type iamDeleteUserPolicyResponse struct { - iamCommonResponse - XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ DeleteUserPolicyResponse"` -} - -type iamGetUserPolicyResponse struct { - iamCommonResponse - XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ GetUserPolicyResponse"` - GetUserPolicyResult struct { - UserName string `xml:"UserName"` - PolicyName string `xml:"PolicyName"` - PolicyDocument string `xml:"PolicyDocument"` - } `xml:"GetUserPolicyResult"` -} - -type iamErrorResponse struct { - iamCommonResponse - XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ ErrorResponse"` - Error struct { - iam.ErrorDetails - Type string `xml:"Type"` - } `xml:"Error"` -} - -type iamError struct { - Code string - Error error -} - -// Policies stores IAM policies -type iamPolicies struct { - Policies map[string]policy_engine.PolicyDocument `json:"policies"` -} - -const ( - iamCharsetUpper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - iamCharset = iamCharsetUpper + "abcdefghijklmnopqrstuvwxyz/" - iamPolicyDocumentVersion = "2012-10-17" - iamUserDoesNotExist = "the user with name %s cannot be found." -) - -// Statement action constants -const ( - iamStatementActionAdmin = "*" - iamStatementActionWrite = "Put*" - iamStatementActionWriteAcp = "PutBucketAcl" - iamStatementActionRead = "Get*" - iamStatementActionReadAcp = "GetBucketAcl" - iamStatementActionList = "List*" - iamStatementActionTagging = "Tagging*" - iamStatementActionDelete = "DeleteBucket*" +// Type aliases for IAM response types from shared package +type ( + iamCommonResponse = iamlib.CommonResponse + iamListUsersResponse = iamlib.ListUsersResponse + iamListAccessKeysResponse = iamlib.ListAccessKeysResponse + iamDeleteAccessKeyResponse = iamlib.DeleteAccessKeyResponse + iamCreatePolicyResponse = iamlib.CreatePolicyResponse + iamCreateUserResponse = iamlib.CreateUserResponse + iamDeleteUserResponse = iamlib.DeleteUserResponse + iamGetUserResponse = iamlib.GetUserResponse + iamUpdateUserResponse = iamlib.UpdateUserResponse + iamCreateAccessKeyResponse = iamlib.CreateAccessKeyResponse + iamPutUserPolicyResponse = iamlib.PutUserPolicyResponse + iamDeleteUserPolicyResponse = iamlib.DeleteUserPolicyResponse + iamGetUserPolicyResponse = iamlib.GetUserPolicyResponse + iamErrorResponse = iamlib.ErrorResponse + iamError = iamlib.Error ) +// Helper function wrappers using shared package func iamHash(s *string) string { - h := sha1.New() - h.Write([]byte(*s)) - return fmt.Sprintf("%x", h.Sum(nil)) + return iamlib.Hash(s) } -// iamStringWithCharset generates a cryptographically secure random string. -// Uses crypto/rand for security-sensitive credential generation. func iamStringWithCharset(length int, charset string) (string, error) { - if length <= 0 { - return "", fmt.Errorf("length must be positive, got %d", length) - } - if charset == "" { - return "", fmt.Errorf("charset must not be empty") - } - b := make([]byte, length) - for i := range b { - n, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) - if err != nil { - return "", fmt.Errorf("failed to generate random index: %w", err) - } - b[i] = charset[n.Int64()] - } - return string(b), nil + return iamlib.GenerateRandomString(length, charset) } -// iamStringSlicesEqual compares two string slices for equality, ignoring order. -// This is used instead of reflect.DeepEqual to avoid order-dependent comparisons. func iamStringSlicesEqual(a, b []string) bool { - if len(a) != len(b) { - return false - } - // Make copies to avoid modifying the originals - aCopy := make([]string, len(a)) - bCopy := make([]string, len(b)) - copy(aCopy, a) - copy(bCopy, b) - sort.Strings(aCopy) - sort.Strings(bCopy) - for i := range aCopy { - if aCopy[i] != bCopy[i] { - return false - } - } - return true + return iamlib.StringSlicesEqual(a, b) } func iamMapToStatementAction(action string) string { - switch action { - case iamStatementActionAdmin: - return ACTION_ADMIN - case iamStatementActionWrite: - return ACTION_WRITE - case iamStatementActionWriteAcp: - return ACTION_WRITE_ACP - case iamStatementActionRead: - return ACTION_READ - case iamStatementActionReadAcp: - return ACTION_READ_ACP - case iamStatementActionList: - return ACTION_LIST - case iamStatementActionTagging: - return ACTION_TAGGING - case iamStatementActionDelete: - return ACTION_DELETE_BUCKET - default: - return "" - } + return iamlib.MapToStatementAction(action) } func iamMapToIdentitiesAction(action string) string { - switch action { - case ACTION_ADMIN: - return iamStatementActionAdmin - case ACTION_WRITE: - return iamStatementActionWrite - case ACTION_WRITE_ACP: - return iamStatementActionWriteAcp - case ACTION_READ: - return iamStatementActionRead - case ACTION_READ_ACP: - return iamStatementActionReadAcp - case ACTION_LIST: - return iamStatementActionList - case ACTION_TAGGING: - return iamStatementActionTagging - case ACTION_DELETE_BUCKET: - return iamStatementActionDelete - default: - return "" - } + return iamlib.MapToIdentitiesAction(action) } +// Constants from shared package +const ( + iamCharsetUpper = iamlib.CharsetUpper + iamCharset = iamlib.Charset + iamPolicyDocumentVersion = iamlib.PolicyDocumentVersion + iamUserDoesNotExist = iamlib.UserDoesNotExist +) + func newIamErrorResponse(errCode string, errMsg string) iamErrorResponse { errorResp := iamErrorResponse{} errorResp.Error.Type = "Sender"