s3api_bucket_handlers_test.go 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. package s3api
  2. import (
  3. "encoding/json"
  4. "encoding/xml"
  5. "net/http/httptest"
  6. "testing"
  7. "time"
  8. "github.com/aws/aws-sdk-go/service/s3"
  9. "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
  10. "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
  11. "github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
  12. "github.com/stretchr/testify/assert"
  13. "github.com/stretchr/testify/require"
  14. )
  15. func TestPutBucketAclCannedAclSupport(t *testing.T) {
  16. // Test that the ExtractAcl function can handle various canned ACLs
  17. // This tests the core functionality without requiring a fully initialized S3ApiServer
  18. testCases := []struct {
  19. name string
  20. cannedAcl string
  21. shouldWork bool
  22. description string
  23. }{
  24. {
  25. name: "private",
  26. cannedAcl: s3_constants.CannedAclPrivate,
  27. shouldWork: true,
  28. description: "private ACL should be accepted",
  29. },
  30. {
  31. name: "public-read",
  32. cannedAcl: s3_constants.CannedAclPublicRead,
  33. shouldWork: true,
  34. description: "public-read ACL should be accepted",
  35. },
  36. {
  37. name: "public-read-write",
  38. cannedAcl: s3_constants.CannedAclPublicReadWrite,
  39. shouldWork: true,
  40. description: "public-read-write ACL should be accepted",
  41. },
  42. {
  43. name: "authenticated-read",
  44. cannedAcl: s3_constants.CannedAclAuthenticatedRead,
  45. shouldWork: true,
  46. description: "authenticated-read ACL should be accepted",
  47. },
  48. {
  49. name: "bucket-owner-read",
  50. cannedAcl: s3_constants.CannedAclBucketOwnerRead,
  51. shouldWork: true,
  52. description: "bucket-owner-read ACL should be accepted",
  53. },
  54. {
  55. name: "bucket-owner-full-control",
  56. cannedAcl: s3_constants.CannedAclBucketOwnerFullControl,
  57. shouldWork: true,
  58. description: "bucket-owner-full-control ACL should be accepted",
  59. },
  60. {
  61. name: "invalid-acl",
  62. cannedAcl: "invalid-acl-value",
  63. shouldWork: false,
  64. description: "invalid ACL should be rejected",
  65. },
  66. }
  67. for _, tc := range testCases {
  68. t.Run(tc.name, func(t *testing.T) {
  69. // Create a request with the specified canned ACL
  70. req := httptest.NewRequest("PUT", "/bucket?acl", nil)
  71. req.Header.Set(s3_constants.AmzCannedAcl, tc.cannedAcl)
  72. req.Header.Set(s3_constants.AmzAccountId, "test-account-123")
  73. // Create a mock IAM for testing
  74. mockIam := &mockIamInterface{}
  75. // Test the ACL extraction directly
  76. grants, errCode := ExtractAcl(req, mockIam, "", "test-account-123", "test-account-123", "test-account-123")
  77. if tc.shouldWork {
  78. assert.Equal(t, s3err.ErrNone, errCode, "Expected ACL parsing to succeed for %s", tc.cannedAcl)
  79. assert.NotEmpty(t, grants, "Expected grants to be generated for valid ACL %s", tc.cannedAcl)
  80. t.Logf("✓ PASS: %s - %s", tc.name, tc.description)
  81. } else {
  82. assert.NotEqual(t, s3err.ErrNone, errCode, "Expected ACL parsing to fail for invalid ACL %s", tc.cannedAcl)
  83. t.Logf("✓ PASS: %s - %s", tc.name, tc.description)
  84. }
  85. })
  86. }
  87. }
  88. // TestBucketWithoutACLIsNotPublicRead tests that buckets without ACLs are not public-read
  89. func TestBucketWithoutACLIsNotPublicRead(t *testing.T) {
  90. // Create a bucket config without ACL (like a freshly created bucket)
  91. config := &BucketConfig{
  92. Name: "test-bucket",
  93. IsPublicRead: false, // Should be explicitly false
  94. }
  95. // Verify that buckets without ACL are not public-read
  96. assert.False(t, config.IsPublicRead, "Bucket without ACL should not be public-read")
  97. }
  98. func TestBucketConfigInitialization(t *testing.T) {
  99. // Test that BucketConfig properly initializes IsPublicRead field
  100. config := &BucketConfig{
  101. Name: "test-bucket",
  102. IsPublicRead: false, // Explicitly set to false for private buckets
  103. }
  104. // Verify proper initialization
  105. assert.False(t, config.IsPublicRead, "Newly created bucket should not be public-read by default")
  106. }
  107. // TestUpdateBucketConfigCacheConsistency tests that updateBucketConfigCacheFromEntry
  108. // properly handles the IsPublicRead flag consistently with getBucketConfig
  109. func TestUpdateBucketConfigCacheConsistency(t *testing.T) {
  110. t.Run("bucket without ACL should have IsPublicRead=false", func(t *testing.T) {
  111. // Simulate an entry without ACL (like a freshly created bucket)
  112. entry := &filer_pb.Entry{
  113. Name: "test-bucket",
  114. Attributes: &filer_pb.FuseAttributes{
  115. FileMode: 0755,
  116. },
  117. // Extended is nil or doesn't contain ACL
  118. }
  119. // Test what updateBucketConfigCacheFromEntry would create
  120. config := &BucketConfig{
  121. Name: entry.Name,
  122. Entry: entry,
  123. IsPublicRead: false, // Should be explicitly false
  124. }
  125. // When Extended is nil, IsPublicRead should be false
  126. assert.False(t, config.IsPublicRead, "Bucket without Extended metadata should not be public-read")
  127. // When Extended exists but has no ACL key, IsPublicRead should also be false
  128. entry.Extended = make(map[string][]byte)
  129. entry.Extended["some-other-key"] = []byte("some-value")
  130. config = &BucketConfig{
  131. Name: entry.Name,
  132. Entry: entry,
  133. IsPublicRead: false, // Should be explicitly false
  134. }
  135. // Simulate the else branch: no ACL means private bucket
  136. if _, exists := entry.Extended[s3_constants.ExtAmzAclKey]; !exists {
  137. config.IsPublicRead = false
  138. }
  139. assert.False(t, config.IsPublicRead, "Bucket with Extended but no ACL should not be public-read")
  140. })
  141. t.Run("bucket with public-read ACL should have IsPublicRead=true", func(t *testing.T) {
  142. // Create a mock public-read ACL using AWS S3 SDK types
  143. publicReadGrants := []*s3.Grant{
  144. {
  145. Grantee: &s3.Grantee{
  146. Type: &s3_constants.GrantTypeGroup,
  147. URI: &s3_constants.GranteeGroupAllUsers,
  148. },
  149. Permission: &s3_constants.PermissionRead,
  150. },
  151. }
  152. aclBytes, err := json.Marshal(publicReadGrants)
  153. require.NoError(t, err)
  154. entry := &filer_pb.Entry{
  155. Name: "public-bucket",
  156. Extended: map[string][]byte{
  157. s3_constants.ExtAmzAclKey: aclBytes,
  158. },
  159. }
  160. config := &BucketConfig{
  161. Name: entry.Name,
  162. Entry: entry,
  163. IsPublicRead: false, // Start with false
  164. }
  165. // Simulate what updateBucketConfigCacheFromEntry would do
  166. if acl, exists := entry.Extended[s3_constants.ExtAmzAclKey]; exists {
  167. config.ACL = acl
  168. config.IsPublicRead = parseAndCachePublicReadStatus(acl)
  169. }
  170. assert.True(t, config.IsPublicRead, "Bucket with public-read ACL should be public-read")
  171. })
  172. }
  173. // mockIamInterface is a simple mock for testing
  174. type mockIamInterface struct{}
  175. func (m *mockIamInterface) GetAccountNameById(canonicalId string) string {
  176. return "test-user-" + canonicalId
  177. }
  178. func (m *mockIamInterface) GetAccountIdByEmail(email string) string {
  179. return "account-for-" + email
  180. }
  181. // TestListAllMyBucketsResultNamespace verifies that the ListAllMyBucketsResult
  182. // XML response includes the proper S3 namespace URI
  183. func TestListAllMyBucketsResultNamespace(t *testing.T) {
  184. // Create a sample ListAllMyBucketsResult response
  185. response := ListAllMyBucketsResult{
  186. Owner: CanonicalUser{
  187. ID: "test-owner-id",
  188. DisplayName: "test-owner",
  189. },
  190. Buckets: ListAllMyBucketsList{
  191. Bucket: []ListAllMyBucketsEntry{
  192. {
  193. Name: "test-bucket",
  194. CreationDate: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
  195. },
  196. },
  197. },
  198. }
  199. // Marshal the response to XML
  200. xmlData, err := xml.Marshal(response)
  201. require.NoError(t, err, "Failed to marshal XML response")
  202. xmlString := string(xmlData)
  203. // Verify that the XML contains the proper namespace
  204. assert.Contains(t, xmlString, `xmlns="http://s3.amazonaws.com/doc/2006-03-01/"`,
  205. "XML response should contain the S3 namespace URI")
  206. // Verify the root element has the correct name
  207. assert.Contains(t, xmlString, "<ListAllMyBucketsResult",
  208. "XML response should have ListAllMyBucketsResult root element")
  209. // Verify structure contains expected elements
  210. assert.Contains(t, xmlString, "<Owner>", "XML should contain Owner element")
  211. assert.Contains(t, xmlString, "<Buckets>", "XML should contain Buckets element")
  212. assert.Contains(t, xmlString, "<Bucket>", "XML should contain Bucket element")
  213. assert.Contains(t, xmlString, "<Name>test-bucket</Name>", "XML should contain bucket name")
  214. t.Logf("Generated XML:\n%s", xmlString)
  215. }