aboutsummaryrefslogtreecommitdiff
path: root/weed/s3api/s3api_object_handlers_postpolicy_test.go
blob: 357fb9c7cf79f860b8a3f44f4331227f272c02b8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
package s3api

import (
	"bytes"
	"io"
	"mime/multipart"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/gorilla/mux"
	"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
	"github.com/stretchr/testify/assert"
)

// TestPostPolicyKeyNormalization tests that object keys from presigned POST
// are properly normalized with a leading slash and duplicate slashes removed.
// This addresses issue #7713 where keys without leading slashes caused
// bucket and key to be concatenated without a separator.
func TestPostPolicyKeyNormalization(t *testing.T) {
	tests := []struct {
		name           string
		key            string
		expectedPrefix string // Expected path prefix after bucket
	}{
		{
			name:           "key without leading slash",
			key:            "test_image.png",
			expectedPrefix: "/test_image.png",
		},
		{
			name:           "key with leading slash",
			key:            "/test_image.png",
			expectedPrefix: "/test_image.png",
		},
		{
			name:           "key with path without leading slash",
			key:            "folder/subfolder/test_image.png",
			expectedPrefix: "/folder/subfolder/test_image.png",
		},
		{
			name:           "key with path with leading slash",
			key:            "/folder/subfolder/test_image.png",
			expectedPrefix: "/folder/subfolder/test_image.png",
		},
		{
			name:           "simple filename",
			key:            "file.txt",
			expectedPrefix: "/file.txt",
		},
		{
			name:           "key with duplicate slashes",
			key:            "folder//subfolder///file.txt",
			expectedPrefix: "/folder/subfolder/file.txt",
		},
		{
			name:           "key with leading duplicate slashes",
			key:            "//folder/file.txt",
			expectedPrefix: "/folder/file.txt",
		},
		{
			name:           "key with trailing slash",
			key:            "folder/",
			expectedPrefix: "/folder/",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Use the actual NormalizeObjectKey function
			object := s3_constants.NormalizeObjectKey(tt.key)

			// Verify the normalized object has the expected prefix
			assert.Equal(t, tt.expectedPrefix, object,
				"Key should be normalized correctly")

			// Verify path construction would be correct
			bucket := "my-bucket"
			bucketsPath := "/buckets"
			expectedPath := bucketsPath + "/" + bucket + tt.expectedPrefix
			actualPath := bucketsPath + "/" + bucket + object

			assert.Equal(t, expectedPath, actualPath,
				"File path should be correctly constructed with slash between bucket and key")

			// Verify we don't have double slashes (except at the start which is fine)
			assert.NotContains(t, actualPath[1:], "//",
				"Path should not contain double slashes")
		})
	}
}

// TestNormalizeObjectKey tests the NormalizeObjectKey function directly
func TestNormalizeObjectKey(t *testing.T) {
	tests := []struct {
		name     string
		input    string
		expected string
	}{
		{"empty string", "", "/"},
		{"simple file", "file.txt", "/file.txt"},
		{"with leading slash", "/file.txt", "/file.txt"},
		{"path without slash", "a/b/c.txt", "/a/b/c.txt"},
		{"path with slash", "/a/b/c.txt", "/a/b/c.txt"},
		{"duplicate slashes", "a//b///c.txt", "/a/b/c.txt"},
		{"leading duplicates", "///a/b.txt", "/a/b.txt"},
		{"all duplicates", "//a//b//", "/a/b/"},
		{"just slashes", "///", "/"},
		{"trailing slash", "folder/", "/folder/"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := s3_constants.NormalizeObjectKey(tt.input)
			assert.Equal(t, tt.expected, result)
		})
	}
}

// TestPostPolicyFilenameSubstitution tests the ${filename} substitution in keys
func TestPostPolicyFilenameSubstitution(t *testing.T) {
	tests := []struct {
		name             string
		keyTemplate      string
		uploadedFilename string
		expectedKey      string
	}{
		{
			name:             "filename at end",
			keyTemplate:      "uploads/${filename}",
			uploadedFilename: "photo.jpg",
			expectedKey:      "/uploads/photo.jpg",
		},
		{
			name:             "filename in middle",
			keyTemplate:      "user/files/${filename}/original",
			uploadedFilename: "document.pdf",
			expectedKey:      "/user/files/document.pdf/original",
		},
		{
			name:             "no substitution needed",
			keyTemplate:      "static/file.txt",
			uploadedFilename: "ignored.txt",
			expectedKey:      "/static/file.txt",
		},
		{
			name:             "filename only",
			keyTemplate:      "${filename}",
			uploadedFilename: "myfile.png",
			expectedKey:      "/myfile.png",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Simulate the substitution logic from PostPolicyBucketHandler
			key := tt.keyTemplate
			if tt.uploadedFilename != "" && strings.Contains(key, "${filename}") {
				key = strings.Replace(key, "${filename}", tt.uploadedFilename, -1)
			}

			// Normalize using the actual function
			object := s3_constants.NormalizeObjectKey(key)

			assert.Equal(t, tt.expectedKey, object,
				"Key should be correctly substituted and normalized")
		})
	}
}

// TestExtractPostPolicyFormValues tests the form value extraction
func TestExtractPostPolicyFormValues(t *testing.T) {
	tests := []struct {
		name          string
		key           string
		contentType   string
		fileContent   string
		fileName      string
		expectSuccess bool
	}{
		{
			name:          "basic upload",
			key:           "test.txt",
			contentType:   "text/plain",
			fileContent:   "hello world",
			fileName:      "upload.txt",
			expectSuccess: true,
		},
		{
			name:          "upload with path key",
			key:           "folder/subfolder/test.txt",
			contentType:   "application/octet-stream",
			fileContent:   "binary data",
			fileName:      "data.bin",
			expectSuccess: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Create multipart form
			var buf bytes.Buffer
			writer := multipart.NewWriter(&buf)

			// Add form fields
			writer.WriteField("key", tt.key)
			writer.WriteField("Content-Type", tt.contentType)

			// Add file
			part, err := writer.CreateFormFile("file", tt.fileName)
			assert.NoError(t, err)
			_, err = io.WriteString(part, tt.fileContent)
			assert.NoError(t, err)

			err = writer.Close()
			assert.NoError(t, err)

			// Parse the form
			reader := multipart.NewReader(&buf, writer.Boundary())
			form, err := reader.ReadForm(5 * 1024 * 1024)
			assert.NoError(t, err)
			defer form.RemoveAll()

			// Extract values using the actual function
			filePart, fileName, fileContentType, fileSize, formValues, err := extractPostPolicyFormValues(form)

			if tt.expectSuccess {
				assert.NoError(t, err)
				assert.NotNil(t, filePart)
				assert.Equal(t, tt.fileName, fileName)
				assert.NotEmpty(t, fileContentType)
				assert.Greater(t, fileSize, int64(0))
				assert.Equal(t, tt.key, formValues.Get("Key"))

				filePart.Close()
			}
		})
	}
}

// TestPostPolicyPathConstruction is an integration-style test that verifies
// the complete path construction logic
func TestPostPolicyPathConstruction(t *testing.T) {
	s3a := &S3ApiServer{
		option: &S3ApiServerOption{
			BucketsPath: "/buckets",
		},
	}

	tests := []struct {
		name         string
		bucket       string
		formKey      string // Key as it would come from form (may not have leading slash)
		expectedPath string
	}{
		{
			name:         "simple key without slash - the bug case",
			bucket:       "my-bucket",
			formKey:      "test_image.png",
			expectedPath: "/buckets/my-bucket/test_image.png",
		},
		{
			name:         "simple key with slash",
			bucket:       "my-bucket",
			formKey:      "/test_image.png",
			expectedPath: "/buckets/my-bucket/test_image.png",
		},
		{
			name:         "nested path without leading slash",
			bucket:       "uploads",
			formKey:      "2024/01/photo.jpg",
			expectedPath: "/buckets/uploads/2024/01/photo.jpg",
		},
		{
			name:         "nested path with leading slash",
			bucket:       "uploads",
			formKey:      "/2024/01/photo.jpg",
			expectedPath: "/buckets/uploads/2024/01/photo.jpg",
		},
		{
			name:         "key with duplicate slashes",
			bucket:       "my-bucket",
			formKey:      "folder//file.txt",
			expectedPath: "/buckets/my-bucket/folder/file.txt",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Use the actual NormalizeObjectKey function
			object := s3_constants.NormalizeObjectKey(tt.formKey)

			// Construct path as done in PostPolicyBucketHandler
			filePath := s3a.option.BucketsPath + "/" + tt.bucket + object

			assert.Equal(t, tt.expectedPath, filePath,
				"File path should be correctly constructed")

			// Verify bucket and key are properly separated
			assert.Contains(t, filePath, tt.bucket+"/",
				"Bucket should be followed by a slash")
		})
	}
}

// TestPostPolicyBucketHandlerKeyExtraction tests that the handler correctly
// extracts and normalizes the key from a POST request
func TestPostPolicyBucketHandlerKeyExtraction(t *testing.T) {
	// Create a minimal S3ApiServer for testing
	s3a := &S3ApiServer{
		option: &S3ApiServerOption{
			BucketsPath: "/buckets",
		},
		iam: &IdentityAccessManagement{},
	}

	tests := []struct {
		name        string
		bucket      string
		key         string
		wantPathHas string // substring that must be in the constructed path
	}{
		{
			name:        "key without leading slash",
			bucket:      "test-bucket",
			key:         "simple-file.txt",
			wantPathHas: "/test-bucket/simple-file.txt",
		},
		{
			name:        "key with leading slash",
			bucket:      "test-bucket",
			key:         "/prefixed-file.txt",
			wantPathHas: "/test-bucket/prefixed-file.txt",
		},
		{
			name:        "key with duplicate slashes",
			bucket:      "test-bucket",
			key:         "folder//nested///file.txt",
			wantPathHas: "/test-bucket/folder/nested/file.txt",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Create multipart form body
			var buf bytes.Buffer
			writer := multipart.NewWriter(&buf)

			// Add required fields
			writer.WriteField("key", tt.key)
			writer.WriteField("Policy", "") // Empty policy for this test

			// Add file
			part, _ := writer.CreateFormFile("file", "test.txt")
			part.Write([]byte("test content"))
			writer.Close()

			// Create request
			req := httptest.NewRequest(http.MethodPost, "/"+tt.bucket, &buf)
			req.Header.Set("Content-Type", writer.FormDataContentType())

			// Set up mux vars (simulating router)
			req = mux.SetURLVars(req, map[string]string{"bucket": tt.bucket})

			// Parse form to extract key
			reader, _ := req.MultipartReader()
			form, _ := reader.ReadForm(5 * 1024 * 1024)
			defer form.RemoveAll()

			_, _, _, _, formValues, _ := extractPostPolicyFormValues(form)

			// Apply the same normalization as PostPolicyBucketHandler
			object := s3_constants.NormalizeObjectKey(formValues.Get("Key"))

			// Construct path
			filePath := s3a.option.BucketsPath + "/" + tt.bucket + object

			assert.Contains(t, filePath, tt.wantPathHas,
				"Path should contain properly separated bucket and key")
		})
	}
}