aboutsummaryrefslogtreecommitdiff
path: root/weed/s3api/s3_action_resolver.go
blob: 83431424c456ea53d25f64bf4c5368909da43cce (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
package s3api

import (
	"net/http"
	"net/url"
	"strings"

	"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
)

// ResolveS3Action determines the specific S3 action from HTTP request context.
// This is the unified implementation used by both the bucket policy engine
// and the IAM integration for consistent action resolution.
//
// It examines the HTTP method, path, query parameters, and headers to determine
// the most specific S3 action string (e.g., "s3:DeleteObject", "s3:PutObjectTagging").
//
// Parameters:
//   - r: HTTP request containing method, URL, query params, and headers
//   - baseAction: Coarse-grained action constant (e.g., ACTION_WRITE, ACTION_READ)
//   - bucket: Bucket name from the request path
//   - object: Object key from the request path (may be empty for bucket operations)
//
// Returns:
//   - Specific S3 action string (e.g., "s3:DeleteObject")
//   - Falls back to base action mapping if no specific resolution is possible
//   - Always returns a valid S3 action string (never empty)
func ResolveS3Action(r *http.Request, baseAction string, bucket string, object string) string {
	if r == nil || r.URL == nil {
		// No HTTP context available: fall back to coarse-grained mapping
		// This ensures consistent behavior and avoids returning empty strings
		return mapBaseActionToS3Format(baseAction)
	}

	method := r.Method
	query := r.URL.Query()

	// Determine if this is an object or bucket operation
	// Note: "/" is treated as bucket-level, not object-level
	hasObject := object != "" && object != "/"

	// Priority 1: Check for specific query parameters that indicate specific actions
	// These override everything else because they explicitly indicate the operation type
	if action := resolveFromQueryParameters(query, method, hasObject); action != "" {
		return action
	}

	// Priority 2: Handle basic operations based on method and resource type
	// Only use the result if a specific action was resolved; otherwise fall through to Priority 3
	if hasObject {
		if action := resolveObjectLevelAction(method, baseAction); action != "" {
			return action
		}
	} else if bucket != "" {
		if action := resolveBucketLevelAction(method, baseAction); action != "" {
			return action
		}
	}

	// Priority 3: Fallback to legacy action mapping
	return mapBaseActionToS3Format(baseAction)
}

// bucketQueryActions maps bucket-level query parameters to their corresponding S3 actions by HTTP method
var bucketQueryActions = map[string]map[string]string{
	"policy": {
		http.MethodGet:    s3_constants.S3_ACTION_GET_BUCKET_POLICY,
		http.MethodPut:    s3_constants.S3_ACTION_PUT_BUCKET_POLICY,
		http.MethodDelete: s3_constants.S3_ACTION_DELETE_BUCKET_POLICY,
	},
	"cors": {
		http.MethodGet:    s3_constants.S3_ACTION_GET_BUCKET_CORS,
		http.MethodPut:    s3_constants.S3_ACTION_PUT_BUCKET_CORS,
		http.MethodDelete: s3_constants.S3_ACTION_DELETE_BUCKET_CORS,
	},
	"lifecycle": {
		http.MethodGet:    s3_constants.S3_ACTION_GET_BUCKET_LIFECYCLE,
		http.MethodPut:    s3_constants.S3_ACTION_PUT_BUCKET_LIFECYCLE,
		http.MethodDelete: s3_constants.S3_ACTION_PUT_BUCKET_LIFECYCLE, // DELETE uses same permission as PUT
	},
	"versioning": {
		http.MethodGet: s3_constants.S3_ACTION_GET_BUCKET_VERSIONING,
		http.MethodPut: s3_constants.S3_ACTION_PUT_BUCKET_VERSIONING,
	},
	"notification": {
		http.MethodGet: s3_constants.S3_ACTION_GET_BUCKET_NOTIFICATION,
		http.MethodPut: s3_constants.S3_ACTION_PUT_BUCKET_NOTIFICATION,
	},
	"object-lock": {
		http.MethodGet: s3_constants.S3_ACTION_GET_BUCKET_OBJECT_LOCK,
		http.MethodPut: s3_constants.S3_ACTION_PUT_BUCKET_OBJECT_LOCK,
	},
}

// resolveFromQueryParameters checks query parameters to determine specific S3 actions
func resolveFromQueryParameters(query url.Values, method string, hasObject bool) string {
	// Multipart upload operations with uploadId parameter (object-level only)
	// All multipart operations require an object in the path
	if hasObject && query.Has("uploadId") {
		switch method {
		case http.MethodPut:
			if query.Has("partNumber") {
				return s3_constants.S3_ACTION_UPLOAD_PART
			}
		case http.MethodPost:
			return s3_constants.S3_ACTION_COMPLETE_MULTIPART
		case http.MethodDelete:
			return s3_constants.S3_ACTION_ABORT_MULTIPART
		case http.MethodGet:
			return s3_constants.S3_ACTION_LIST_PARTS
		}
	}

	// Multipart upload operations
	// CreateMultipartUpload: POST /bucket/object?uploads (object-level)
	// ListMultipartUploads: GET /bucket?uploads (bucket-level)
	if query.Has("uploads") {
		if method == http.MethodPost && hasObject {
			return s3_constants.S3_ACTION_CREATE_MULTIPART
		} else if method == http.MethodGet && !hasObject {
			return s3_constants.S3_ACTION_LIST_MULTIPART_UPLOADS
		}
	}

	// ACL operations
	if query.Has("acl") {
		switch method {
		case http.MethodGet, http.MethodHead:
			if hasObject {
				return s3_constants.S3_ACTION_GET_OBJECT_ACL
			}
			return s3_constants.S3_ACTION_GET_BUCKET_ACL
		case http.MethodPut:
			if hasObject {
				return s3_constants.S3_ACTION_PUT_OBJECT_ACL
			}
			return s3_constants.S3_ACTION_PUT_BUCKET_ACL
		}
	}

	// Tagging operations
	if query.Has("tagging") {
		switch method {
		case http.MethodGet:
			if hasObject {
				return s3_constants.S3_ACTION_GET_OBJECT_TAGGING
			}
			return s3_constants.S3_ACTION_GET_BUCKET_TAGGING
		case http.MethodPut:
			if hasObject {
				return s3_constants.S3_ACTION_PUT_OBJECT_TAGGING
			}
			return s3_constants.S3_ACTION_PUT_BUCKET_TAGGING
		case http.MethodDelete:
			if hasObject {
				return s3_constants.S3_ACTION_DELETE_OBJECT_TAGGING
			}
			return s3_constants.S3_ACTION_DELETE_BUCKET_TAGGING
		}
	}

	// Versioning operations - distinguish between versionId (specific version) and versions (list versions)
	// versionId: Used to access/delete a specific version of an object (e.g., GET /bucket/key?versionId=xyz)
	if query.Has("versionId") {
		if hasObject {
			switch method {
			case http.MethodGet, http.MethodHead:
				return s3_constants.S3_ACTION_GET_OBJECT_VERSION
			case http.MethodDelete:
				return s3_constants.S3_ACTION_DELETE_OBJECT_VERSION
			}
		}
	}

	// versions: Used to list all versions of objects in a bucket (e.g., GET /bucket?versions)
	if query.Has("versions") {
		if method == http.MethodGet && !hasObject {
			return s3_constants.S3_ACTION_LIST_BUCKET_VERSIONS
		}
	}

	// Check bucket-level query parameters using data-driven approach
	// These are strictly bucket-level operations, so only apply when !hasObject
	if !hasObject {
		for param, actions := range bucketQueryActions {
			if query.Has(param) {
				if action, ok := actions[method]; ok {
					return action
				}
			}
		}
	}

	// Location (GET only, bucket-level)
	if query.Has("location") && method == http.MethodGet && !hasObject {
		return s3_constants.S3_ACTION_GET_BUCKET_LOCATION
	}

	// Object retention and legal hold operations (object-level only)
	if hasObject {
		if query.Has("retention") {
			switch method {
			case http.MethodGet:
				return s3_constants.S3_ACTION_GET_OBJECT_RETENTION
			case http.MethodPut:
				return s3_constants.S3_ACTION_PUT_OBJECT_RETENTION
			}
		}

		if query.Has("legal-hold") {
			switch method {
			case http.MethodGet:
				return s3_constants.S3_ACTION_GET_OBJECT_LEGAL_HOLD
			case http.MethodPut:
				return s3_constants.S3_ACTION_PUT_OBJECT_LEGAL_HOLD
			}
		}
	}

	// Batch delete - POST request with delete query parameter (bucket-level operation)
	// Example: POST /bucket?delete (not POST /bucket/object?delete)
	if query.Has("delete") && method == http.MethodPost && !hasObject {
		return s3_constants.S3_ACTION_DELETE_OBJECT
	}

	return ""
}

// resolveObjectLevelAction determines the S3 action for object-level operations
func resolveObjectLevelAction(method string, baseAction string) string {
	switch method {
	case http.MethodGet, http.MethodHead:
		if baseAction == s3_constants.ACTION_READ {
			return s3_constants.S3_ACTION_GET_OBJECT
		}

	case http.MethodPut:
		if baseAction == s3_constants.ACTION_WRITE {
			// Note: CopyObject operations also use s3:PutObject permission (same as MinIO/AWS)
			// Copy requires s3:PutObject on destination and s3:GetObject on source
			return s3_constants.S3_ACTION_PUT_OBJECT
		}

	case http.MethodDelete:
		// CRITICAL: Map DELETE method to s3:DeleteObject
		// This fixes the architectural limitation where ACTION_WRITE was mapped to s3:PutObject
		if baseAction == s3_constants.ACTION_WRITE {
			return s3_constants.S3_ACTION_DELETE_OBJECT
		}

	case http.MethodPost:
		// POST without query params is typically multipart or form upload
		if baseAction == s3_constants.ACTION_WRITE {
			return s3_constants.S3_ACTION_PUT_OBJECT
		}
	}

	return ""
}

// resolveBucketLevelAction determines the S3 action for bucket-level operations
func resolveBucketLevelAction(method string, baseAction string) string {
	switch method {
	case http.MethodGet, http.MethodHead:
		if baseAction == s3_constants.ACTION_LIST || baseAction == s3_constants.ACTION_READ {
			return s3_constants.S3_ACTION_LIST_BUCKET
		}

	case http.MethodPut:
		if baseAction == s3_constants.ACTION_WRITE {
			return s3_constants.S3_ACTION_CREATE_BUCKET
		}

	case http.MethodDelete:
		if baseAction == s3_constants.ACTION_DELETE_BUCKET {
			return s3_constants.S3_ACTION_DELETE_BUCKET
		}

	case http.MethodPost:
		// POST to bucket is typically form upload
		if baseAction == s3_constants.ACTION_WRITE {
			return s3_constants.S3_ACTION_PUT_OBJECT
		}
	}

	return ""
}

// mapBaseActionToS3Format converts coarse-grained base actions to S3 format
// This is the fallback when no specific resolution is found
func mapBaseActionToS3Format(baseAction string) string {
	// Handle actions that already have s3: prefix
	if strings.HasPrefix(baseAction, "s3:") {
		return baseAction
	}

	// Map coarse-grained actions to their most common S3 equivalent
	// Note: The s3_constants values ARE the string values (e.g., ACTION_READ = "Read")
	switch baseAction {
	case s3_constants.ACTION_READ: // "Read"
		return s3_constants.S3_ACTION_GET_OBJECT
	case s3_constants.ACTION_WRITE: // "Write"
		return s3_constants.S3_ACTION_PUT_OBJECT
	case s3_constants.ACTION_LIST: // "List"
		return s3_constants.S3_ACTION_LIST_BUCKET
	case s3_constants.ACTION_TAGGING: // "Tagging"
		return s3_constants.S3_ACTION_PUT_OBJECT_TAGGING
	case s3_constants.ACTION_ADMIN: // "Admin"
		return s3_constants.S3_ACTION_ALL
	case s3_constants.ACTION_READ_ACP: // "ReadAcp"
		return s3_constants.S3_ACTION_GET_OBJECT_ACL
	case s3_constants.ACTION_WRITE_ACP: // "WriteAcp"
		return s3_constants.S3_ACTION_PUT_OBJECT_ACL
	case s3_constants.ACTION_DELETE_BUCKET: // "DeleteBucket"
		return s3_constants.S3_ACTION_DELETE_BUCKET
	case s3_constants.ACTION_BYPASS_GOVERNANCE_RETENTION:
		return s3_constants.S3_ACTION_BYPASS_GOVERNANCE
	case s3_constants.ACTION_GET_OBJECT_RETENTION:
		return s3_constants.S3_ACTION_GET_OBJECT_RETENTION
	case s3_constants.ACTION_PUT_OBJECT_RETENTION:
		return s3_constants.S3_ACTION_PUT_OBJECT_RETENTION
	case s3_constants.ACTION_GET_OBJECT_LEGAL_HOLD:
		return s3_constants.S3_ACTION_GET_OBJECT_LEGAL_HOLD
	case s3_constants.ACTION_PUT_OBJECT_LEGAL_HOLD:
		return s3_constants.S3_ACTION_PUT_OBJECT_LEGAL_HOLD
	case s3_constants.ACTION_GET_BUCKET_OBJECT_LOCK_CONFIG:
		return s3_constants.S3_ACTION_GET_BUCKET_OBJECT_LOCK
	case s3_constants.ACTION_PUT_BUCKET_OBJECT_LOCK_CONFIG:
		return s3_constants.S3_ACTION_PUT_BUCKET_OBJECT_LOCK
	default:
		// For unknown actions, prefix with s3: to maintain format consistency
		return "s3:" + baseAction
	}
}