S3: add context aware action resolution (#7479)
* add context aware action resolution * isAnonymous * add s3 action resolver * refactor * correct action name * no need for action copy object * Simplify by removing the method-action mismatch path * use PUT instead of DELETE action * refactor * constants * versionId vs versions * address comments * comment * adjust messages * ResolveS3Action * address comments * refactor * simplify * more checks * not needed * simplify
This commit is contained in:
@@ -39,8 +39,8 @@ func TestListPartsActionMapping(t *testing.T) {
|
||||
objectKey: "test-object.txt",
|
||||
queryParams: map[string]string{"uploadId": "test-upload-id"},
|
||||
fallbackAction: s3_constants.ACTION_READ,
|
||||
expectedAction: "s3:ListParts",
|
||||
description: "GET request with uploadId should map to s3:ListParts (this was the missing mapping)",
|
||||
expectedAction: "s3:ListMultipartUploadParts",
|
||||
description: "GET request with uploadId should map to s3:ListMultipartUploadParts (this was the missing mapping)",
|
||||
},
|
||||
{
|
||||
name: "get_object_with_uploadId_and_other_params",
|
||||
@@ -53,18 +53,18 @@ func TestListPartsActionMapping(t *testing.T) {
|
||||
"part-number-marker": "50",
|
||||
},
|
||||
fallbackAction: s3_constants.ACTION_READ,
|
||||
expectedAction: "s3:ListParts",
|
||||
description: "GET request with uploadId plus other multipart params should map to s3:ListParts",
|
||||
expectedAction: "s3:ListMultipartUploadParts",
|
||||
description: "GET request with uploadId plus other multipart params should map to s3:ListMultipartUploadParts",
|
||||
},
|
||||
{
|
||||
name: "get_object_versions",
|
||||
name: "get_object_with_versionId",
|
||||
method: "GET",
|
||||
bucket: "test-bucket",
|
||||
objectKey: "test-object.txt",
|
||||
queryParams: map[string]string{"versions": ""},
|
||||
queryParams: map[string]string{"versionId": "version-123"},
|
||||
fallbackAction: s3_constants.ACTION_READ,
|
||||
expectedAction: "s3:GetObjectVersion",
|
||||
description: "GET request with versions should still map to s3:GetObjectVersion (precedence check)",
|
||||
description: "GET request with versionId should map to s3:GetObjectVersion",
|
||||
},
|
||||
{
|
||||
name: "get_object_acl_without_uploadId",
|
||||
@@ -103,8 +103,8 @@ func TestListPartsActionMapping(t *testing.T) {
|
||||
}
|
||||
req.URL.RawQuery = query.Encode()
|
||||
|
||||
// Call the granular action determination function
|
||||
action := determineGranularS3Action(req, tc.fallbackAction, tc.bucket, tc.objectKey)
|
||||
// Call the action resolver directly
|
||||
action := ResolveS3Action(req, string(tc.fallbackAction), tc.bucket, tc.objectKey)
|
||||
|
||||
// Verify the action mapping
|
||||
assert.Equal(t, tc.expectedAction, action,
|
||||
@@ -127,17 +127,17 @@ func TestListPartsActionMappingSecurityScenarios(t *testing.T) {
|
||||
query1 := req1.URL.Query()
|
||||
query1.Set("uploadId", "active-upload-123")
|
||||
req1.URL.RawQuery = query1.Encode()
|
||||
action1 := determineGranularS3Action(req1, s3_constants.ACTION_READ, "secure-bucket", "confidential-document.pdf")
|
||||
action1 := ResolveS3Action(req1, string(s3_constants.ACTION_READ), "secure-bucket", "confidential-document.pdf")
|
||||
|
||||
// Test request 2: Get object without uploadId
|
||||
req2 := &http.Request{
|
||||
Method: "GET",
|
||||
URL: &url.URL{Path: "/secure-bucket/confidential-document.pdf"},
|
||||
}
|
||||
action2 := determineGranularS3Action(req2, s3_constants.ACTION_READ, "secure-bucket", "confidential-document.pdf")
|
||||
action2 := ResolveS3Action(req2, string(s3_constants.ACTION_READ), "secure-bucket", "confidential-document.pdf")
|
||||
|
||||
// These should be different actions, allowing different permissions
|
||||
assert.Equal(t, "s3:ListParts", action1, "Listing multipart parts should require s3:ListParts permission")
|
||||
assert.Equal(t, "s3:ListMultipartUploadParts", action1, "Listing multipart parts should require s3:ListMultipartUploadParts permission")
|
||||
assert.Equal(t, "s3:GetObject", action2, "Reading object content should require s3:GetObject permission")
|
||||
assert.NotEqual(t, action1, action2, "ListParts and GetObject should be separate permissions for security")
|
||||
})
|
||||
@@ -155,8 +155,8 @@ func TestListPartsActionMappingSecurityScenarios(t *testing.T) {
|
||||
{
|
||||
description: "List multipart upload parts",
|
||||
queryParams: map[string]string{"uploadId": "upload-abc123"},
|
||||
expectedAction: "s3:ListParts",
|
||||
securityNote: "FIXED: Now correctly maps to s3:ListParts instead of s3:GetObject",
|
||||
expectedAction: "s3:ListMultipartUploadParts",
|
||||
securityNote: "FIXED: Now correctly maps to s3:ListMultipartUploadParts instead of s3:GetObject",
|
||||
},
|
||||
{
|
||||
description: "Get actual object content",
|
||||
@@ -167,7 +167,7 @@ func TestListPartsActionMappingSecurityScenarios(t *testing.T) {
|
||||
{
|
||||
description: "Get object with complex upload ID",
|
||||
queryParams: map[string]string{"uploadId": "complex-upload-id-with-hyphens-123-abc-def"},
|
||||
expectedAction: "s3:ListParts",
|
||||
expectedAction: "s3:ListMultipartUploadParts",
|
||||
securityNote: "FIXED: Complex upload IDs now correctly detected",
|
||||
},
|
||||
}
|
||||
@@ -184,7 +184,7 @@ func TestListPartsActionMappingSecurityScenarios(t *testing.T) {
|
||||
}
|
||||
req.URL.RawQuery = query.Encode()
|
||||
|
||||
action := determineGranularS3Action(req, s3_constants.ACTION_READ, "test-bucket", "test-object")
|
||||
action := ResolveS3Action(req, string(s3_constants.ACTION_READ), "test-bucket", "test-object")
|
||||
|
||||
assert.Equal(t, tc.expectedAction, action,
|
||||
"%s - %s", tc.description, tc.securityNote)
|
||||
@@ -205,7 +205,7 @@ func TestListPartsActionRealWorldScenarios(t *testing.T) {
|
||||
query1 := req1.URL.Query()
|
||||
query1.Set("uploads", "")
|
||||
req1.URL.RawQuery = query1.Encode()
|
||||
action1 := determineGranularS3Action(req1, s3_constants.ACTION_WRITE, "data", "large-dataset.csv")
|
||||
action1 := ResolveS3Action(req1, string(s3_constants.ACTION_WRITE), "data", "large-dataset.csv")
|
||||
|
||||
// Step 2: List existing parts (GET with uploadId query) - THIS WAS THE MISSING MAPPING
|
||||
req2 := &http.Request{
|
||||
@@ -215,7 +215,7 @@ func TestListPartsActionRealWorldScenarios(t *testing.T) {
|
||||
query2 := req2.URL.Query()
|
||||
query2.Set("uploadId", "dataset-upload-20240827-001")
|
||||
req2.URL.RawQuery = query2.Encode()
|
||||
action2 := determineGranularS3Action(req2, s3_constants.ACTION_READ, "data", "large-dataset.csv")
|
||||
action2 := ResolveS3Action(req2, string(s3_constants.ACTION_READ), "data", "large-dataset.csv")
|
||||
|
||||
// Step 3: Upload a part (PUT with uploadId and partNumber)
|
||||
req3 := &http.Request{
|
||||
@@ -226,7 +226,7 @@ func TestListPartsActionRealWorldScenarios(t *testing.T) {
|
||||
query3.Set("uploadId", "dataset-upload-20240827-001")
|
||||
query3.Set("partNumber", "5")
|
||||
req3.URL.RawQuery = query3.Encode()
|
||||
action3 := determineGranularS3Action(req3, s3_constants.ACTION_WRITE, "data", "large-dataset.csv")
|
||||
action3 := ResolveS3Action(req3, string(s3_constants.ACTION_WRITE), "data", "large-dataset.csv")
|
||||
|
||||
// Step 4: Complete multipart upload (POST with uploadId)
|
||||
req4 := &http.Request{
|
||||
@@ -236,11 +236,11 @@ func TestListPartsActionRealWorldScenarios(t *testing.T) {
|
||||
query4 := req4.URL.Query()
|
||||
query4.Set("uploadId", "dataset-upload-20240827-001")
|
||||
req4.URL.RawQuery = query4.Encode()
|
||||
action4 := determineGranularS3Action(req4, s3_constants.ACTION_WRITE, "data", "large-dataset.csv")
|
||||
action4 := ResolveS3Action(req4, string(s3_constants.ACTION_WRITE), "data", "large-dataset.csv")
|
||||
|
||||
// Verify each step has the correct action mapping
|
||||
assert.Equal(t, "s3:CreateMultipartUpload", action1, "Step 1: Initiate upload")
|
||||
assert.Equal(t, "s3:ListParts", action2, "Step 2: List parts (FIXED by this PR)")
|
||||
assert.Equal(t, "s3:ListMultipartUploadParts", action2, "Step 2: List parts (FIXED by this PR)")
|
||||
assert.Equal(t, "s3:UploadPart", action3, "Step 3: Upload part")
|
||||
assert.Equal(t, "s3:CompleteMultipartUpload", action4, "Step 4: Complete upload")
|
||||
|
||||
@@ -277,10 +277,10 @@ func TestListPartsActionRealWorldScenarios(t *testing.T) {
|
||||
query.Set("uploadId", uploadId)
|
||||
req.URL.RawQuery = query.Encode()
|
||||
|
||||
action := determineGranularS3Action(req, s3_constants.ACTION_READ, "test-bucket", "test-file.bin")
|
||||
action := ResolveS3Action(req, string(s3_constants.ACTION_READ), "test-bucket", "test-file.bin")
|
||||
|
||||
assert.Equal(t, "s3:ListParts", action,
|
||||
"Upload ID format %s should be correctly detected and mapped to s3:ListParts", uploadId)
|
||||
assert.Equal(t, "s3:ListMultipartUploadParts", action,
|
||||
"Upload ID format %s should be correctly detected and mapped to s3:ListMultipartUploadParts", uploadId)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user