You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
373 lines
15 KiB
373 lines
15 KiB
package policy_engine
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
// TestConvertSingleActionDeleteObject tests support for s3:DeleteObject action (Issue #7864)
|
|
func TestConvertSingleActionDeleteObject(t *testing.T) {
|
|
// Test that Write action includes DeleteObject S3 action
|
|
stmt, err := convertSingleAction("Write:bucket")
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, stmt)
|
|
|
|
// Check that s3:DeleteObject is included in the actions
|
|
actions := stmt.Action.Strings()
|
|
assert.Contains(t, actions, "s3:DeleteObject", "Write action should include s3:DeleteObject")
|
|
assert.Contains(t, actions, "s3:PutObject", "Write action should include s3:PutObject")
|
|
}
|
|
|
|
// TestConvertSingleActionSubpath tests subpath handling for legacy actions (Issue #7864)
|
|
func TestConvertSingleActionSubpath(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
action string
|
|
expectedActions []string
|
|
expectedResources []string
|
|
description string
|
|
}{
|
|
{
|
|
name: "Write_on_bucket",
|
|
action: "Write:mybucket",
|
|
expectedActions: []string{"s3:PutObject", "s3:DeleteObject", "s3:PutObjectAcl", "s3:DeleteObjectVersion", "s3:PutObjectTagging", "s3:DeleteObjectTagging", "s3:AbortMultipartUpload", "s3:ListMultipartUploads", "s3:ListParts", "s3:PutBucketAcl", "s3:PutBucketCors", "s3:PutBucketTagging", "s3:PutBucketNotification", "s3:PutBucketVersioning", "s3:DeleteBucketTagging", "s3:DeleteBucketCors"},
|
|
expectedResources: []string{"arn:aws:s3:::mybucket", "arn:aws:s3:::mybucket/*"},
|
|
description: "Write permission on bucket should include bucket and object ARNs",
|
|
},
|
|
{
|
|
name: "Write_on_bucket_with_wildcard",
|
|
action: "Write:mybucket/*",
|
|
expectedActions: []string{"s3:PutObject", "s3:DeleteObject", "s3:PutObjectAcl", "s3:DeleteObjectVersion", "s3:PutObjectTagging", "s3:DeleteObjectTagging", "s3:AbortMultipartUpload", "s3:ListMultipartUploads", "s3:ListParts", "s3:PutBucketAcl", "s3:PutBucketCors", "s3:PutBucketTagging", "s3:PutBucketNotification", "s3:PutBucketVersioning", "s3:DeleteBucketTagging", "s3:DeleteBucketCors"},
|
|
expectedResources: []string{"arn:aws:s3:::mybucket", "arn:aws:s3:::mybucket/*"},
|
|
description: "Write permission with /* should include bucket and object ARNs",
|
|
},
|
|
{
|
|
name: "Write_on_subpath",
|
|
action: "Write:mybucket/sub_path/*",
|
|
expectedActions: []string{"s3:PutObject", "s3:DeleteObject", "s3:PutObjectAcl", "s3:DeleteObjectVersion", "s3:PutObjectTagging", "s3:DeleteObjectTagging", "s3:AbortMultipartUpload", "s3:ListMultipartUploads", "s3:ListParts", "s3:PutBucketAcl", "s3:PutBucketCors", "s3:PutBucketTagging", "s3:PutBucketNotification", "s3:PutBucketVersioning", "s3:DeleteBucketTagging", "s3:DeleteBucketCors"},
|
|
expectedResources: []string{"arn:aws:s3:::mybucket", "arn:aws:s3:::mybucket/sub_path/*"},
|
|
description: "Write permission on subpath should include bucket and subpath objects ARNs",
|
|
},
|
|
{
|
|
name: "Read_on_subpath",
|
|
action: "Read:mybucket/documents/*",
|
|
expectedActions: []string{"s3:GetObject", "s3:GetObjectVersion", "s3:ListBucket", "s3:ListBucketVersions", "s3:GetObjectAcl", "s3:GetObjectVersionAcl", "s3:GetObjectTagging", "s3:GetObjectVersionTagging", "s3:GetBucketLocation", "s3:GetBucketVersioning", "s3:GetBucketAcl", "s3:GetBucketCors", "s3:GetBucketTagging", "s3:GetBucketNotification"},
|
|
expectedResources: []string{"arn:aws:s3:::mybucket", "arn:aws:s3:::mybucket/documents/*"},
|
|
description: "Read permission on subpath should include bucket ARN and subpath objects",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
stmt, err := convertSingleAction(tc.action)
|
|
assert.NoError(t, err, tc.description)
|
|
assert.NotNil(t, stmt)
|
|
|
|
// Check actions
|
|
actions := stmt.Action.Strings()
|
|
for _, expectedAction := range tc.expectedActions {
|
|
assert.Contains(t, actions, expectedAction,
|
|
"Action %s should be included for %s", expectedAction, tc.action)
|
|
}
|
|
|
|
// Check resources - verify all expected resources are present
|
|
resources := stmt.Resource.Strings()
|
|
assert.ElementsMatch(t, resources, tc.expectedResources,
|
|
"Resources should match exactly for %s. Got %v, expected %v", tc.action, resources, tc.expectedResources)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestConvertSingleActionSubpathDeleteAllowed tests that DeleteObject works on subpaths
|
|
func TestConvertSingleActionSubpathDeleteAllowed(t *testing.T) {
|
|
// This test specifically addresses Issue #7864 part 1:
|
|
// "when a user is granted permission to a subpath, eg s3.configure -user someuser
|
|
// -actions Write -buckets some_bucket/sub_path/* -apply
|
|
// the user will only be able to put, but not delete object under somebucket/sub_path"
|
|
|
|
stmt, err := convertSingleAction("Write:some_bucket/sub_path/*")
|
|
assert.NoError(t, err)
|
|
|
|
// The fix: s3:DeleteObject should be in the allowed actions
|
|
actions := stmt.Action.Strings()
|
|
assert.Contains(t, actions, "s3:DeleteObject",
|
|
"Write permission on subpath should allow deletion of objects in that path")
|
|
|
|
// The resource should be restricted to the subpath
|
|
resources := stmt.Resource.Strings()
|
|
assert.Contains(t, resources, "arn:aws:s3:::some_bucket/sub_path/*",
|
|
"Delete permission should apply to objects under the subpath")
|
|
}
|
|
|
|
// TestConvertSingleActionNestedPaths tests deeply nested paths
|
|
func TestConvertSingleActionNestedPaths(t *testing.T) {
|
|
testCases := []struct {
|
|
action string
|
|
expectedResources []string
|
|
}{
|
|
{
|
|
action: "Write:bucket/a/b/c/*",
|
|
expectedResources: []string{"arn:aws:s3:::bucket", "arn:aws:s3:::bucket/a/b/c/*"},
|
|
},
|
|
{
|
|
action: "Read:bucket/data/documents/2024/*",
|
|
expectedResources: []string{"arn:aws:s3:::bucket", "arn:aws:s3:::bucket/data/documents/2024/*"},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
stmt, err := convertSingleAction(tc.action)
|
|
assert.NoError(t, err)
|
|
|
|
resources := stmt.Resource.Strings()
|
|
assert.ElementsMatch(t, resources, tc.expectedResources)
|
|
}
|
|
}
|
|
|
|
// TestGetResourcesFromLegacyAction tests that GetResourcesFromLegacyAction generates
|
|
// action-appropriate resources consistent with convertSingleAction
|
|
func TestGetResourcesFromLegacyAction(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
action string
|
|
expectedResources []string
|
|
description string
|
|
}{
|
|
// List actions - bucket-only (no object ARNs)
|
|
{
|
|
name: "List_on_bucket",
|
|
action: "List:mybucket",
|
|
expectedResources: []string{"arn:aws:s3:::mybucket"},
|
|
description: "List action should only have bucket ARN",
|
|
},
|
|
{
|
|
name: "List_on_bucket_with_wildcard",
|
|
action: "List:mybucket/*",
|
|
expectedResources: []string{"arn:aws:s3:::mybucket"},
|
|
description: "List action should only have bucket ARN regardless of wildcard",
|
|
},
|
|
// Read actions - bucket and object-level ARNs (includes List* and Get* operations)
|
|
{
|
|
name: "Read_on_bucket",
|
|
action: "Read:mybucket",
|
|
expectedResources: []string{"arn:aws:s3:::mybucket", "arn:aws:s3:::mybucket/*"},
|
|
description: "Read action should have both bucket and object ARNs",
|
|
},
|
|
{
|
|
name: "Read_on_subpath",
|
|
action: "Read:mybucket/documents/*",
|
|
expectedResources: []string{"arn:aws:s3:::mybucket", "arn:aws:s3:::mybucket/documents/*"},
|
|
description: "Read action on subpath should have bucket ARN and object ARN for subpath",
|
|
},
|
|
// Write actions - bucket and object ARNs (includes bucket-level operations)
|
|
{
|
|
name: "Write_on_subpath",
|
|
action: "Write:mybucket/sub_path/*",
|
|
expectedResources: []string{"arn:aws:s3:::mybucket", "arn:aws:s3:::mybucket/sub_path/*"},
|
|
description: "Write action should have bucket and object ARNs",
|
|
},
|
|
// Admin actions - both bucket and object ARNs
|
|
{
|
|
name: "Admin_on_bucket",
|
|
action: "Admin:mybucket",
|
|
expectedResources: []string{"arn:aws:s3:::mybucket", "arn:aws:s3:::mybucket/*"},
|
|
description: "Admin action should have both bucket and object ARNs",
|
|
},
|
|
{
|
|
name: "Admin_on_subpath",
|
|
action: "Admin:mybucket/admin/section/*",
|
|
expectedResources: []string{"arn:aws:s3:::mybucket", "arn:aws:s3:::mybucket/admin/section/*"},
|
|
description: "Admin action on subpath should restrict to subpath, preventing privilege escalation",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
resources, err := GetResourcesFromLegacyAction(tc.action)
|
|
assert.NoError(t, err, tc.description)
|
|
assert.ElementsMatch(t, resources, tc.expectedResources,
|
|
"Resources should match expected. Got %v, expected %v", resources, tc.expectedResources)
|
|
|
|
// Also verify consistency with convertSingleAction where applicable
|
|
stmt, err := convertSingleAction(tc.action)
|
|
assert.NoError(t, err)
|
|
|
|
stmtResources := stmt.Resource.Strings()
|
|
assert.ElementsMatch(t, resources, stmtResources,
|
|
"GetResourcesFromLegacyAction should match convertSingleAction resources for %s", tc.action)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestExtractBucketAndPrefixEdgeCases validates edge case handling in extractBucketAndPrefix
|
|
func TestExtractBucketAndPrefixEdgeCases(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
pattern string
|
|
expectedBucket string
|
|
expectedPrefix string
|
|
description string
|
|
}{
|
|
{
|
|
name: "Empty string",
|
|
pattern: "",
|
|
expectedBucket: "",
|
|
expectedPrefix: "",
|
|
description: "Empty pattern should return empty strings",
|
|
},
|
|
{
|
|
name: "Whitespace only",
|
|
pattern: " ",
|
|
expectedBucket: "",
|
|
expectedPrefix: "",
|
|
description: "Whitespace-only pattern should return empty strings",
|
|
},
|
|
{
|
|
name: "Slash only",
|
|
pattern: "/",
|
|
expectedBucket: "",
|
|
expectedPrefix: "",
|
|
description: "Slash-only pattern should return empty strings",
|
|
},
|
|
{
|
|
name: "Double slash prefix",
|
|
pattern: "bucket//prefix/*",
|
|
expectedBucket: "bucket",
|
|
expectedPrefix: "prefix",
|
|
description: "Double slash should be normalized (trailing slashes removed)",
|
|
},
|
|
{
|
|
name: "Normal bucket",
|
|
pattern: "mybucket",
|
|
expectedBucket: "mybucket",
|
|
expectedPrefix: "",
|
|
description: "Bucket-only pattern should work correctly",
|
|
},
|
|
{
|
|
name: "Bucket with prefix",
|
|
pattern: "mybucket/myprefix/*",
|
|
expectedBucket: "mybucket",
|
|
expectedPrefix: "myprefix",
|
|
description: "Bucket with prefix should be parsed correctly",
|
|
},
|
|
{
|
|
name: "Nested prefix",
|
|
pattern: "mybucket/a/b/c/*",
|
|
expectedBucket: "mybucket",
|
|
expectedPrefix: "a/b/c",
|
|
description: "Nested prefix should be preserved",
|
|
},
|
|
{
|
|
name: "Bucket with trailing slash",
|
|
pattern: "mybucket/",
|
|
expectedBucket: "mybucket",
|
|
expectedPrefix: "",
|
|
description: "Trailing slash on bucket should be normalized",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
bucket, prefix := extractBucketAndPrefix(tc.pattern)
|
|
assert.Equal(t, tc.expectedBucket, bucket, tc.description)
|
|
assert.Equal(t, tc.expectedPrefix, prefix, tc.description)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestCreatePolicyFromLegacyIdentityMultipleActions validates correct resource ARN aggregation
|
|
// when multiple action types target the same resource pattern
|
|
func TestCreatePolicyFromLegacyIdentityMultipleActions(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
identityName string
|
|
actions []string
|
|
expectedStatements int
|
|
expectedActionsInStmt1 []string
|
|
expectedResourcesInStmt1 []string
|
|
description string
|
|
}{
|
|
{
|
|
name: "List_and_Write_on_subpath",
|
|
identityName: "data-manager",
|
|
actions: []string{"List:mybucket/data/*", "Write:mybucket/data/*"},
|
|
expectedStatements: 1,
|
|
expectedActionsInStmt1: []string{
|
|
"s3:ListBucket", "s3:ListBucketVersions", "s3:ListAllMyBuckets",
|
|
"s3:PutObject", "s3:DeleteObject", "s3:PutObjectAcl", "s3:DeleteObjectVersion",
|
|
"s3:PutObjectTagging", "s3:DeleteObjectTagging", "s3:AbortMultipartUpload",
|
|
"s3:ListMultipartUploads", "s3:ListParts", "s3:PutBucketAcl", "s3:PutBucketCors",
|
|
"s3:PutBucketTagging", "s3:PutBucketNotification", "s3:PutBucketVersioning",
|
|
"s3:DeleteBucketTagging", "s3:DeleteBucketCors",
|
|
},
|
|
expectedResourcesInStmt1: []string{
|
|
"arn:aws:s3:::mybucket", // From List and Write actions
|
|
"arn:aws:s3:::mybucket/data/*", // From Write action
|
|
},
|
|
description: "List + Write on same subpath should aggregate all actions and both bucket and object ARNs",
|
|
},
|
|
{
|
|
name: "Read_and_Tagging_on_bucket",
|
|
identityName: "tag-reader",
|
|
actions: []string{"Read:mybucket", "Tagging:mybucket"},
|
|
expectedStatements: 1,
|
|
expectedActionsInStmt1: []string{
|
|
"s3:GetObject", "s3:GetObjectVersion",
|
|
"s3:ListBucket", "s3:ListBucketVersions",
|
|
"s3:GetObjectAcl", "s3:GetObjectVersionAcl",
|
|
"s3:GetObjectTagging", "s3:GetObjectVersionTagging",
|
|
"s3:PutObjectTagging", "s3:DeleteObjectTagging",
|
|
"s3:GetBucketLocation", "s3:GetBucketVersioning",
|
|
"s3:GetBucketAcl", "s3:GetBucketCors", "s3:GetBucketTagging",
|
|
"s3:GetBucketNotification", "s3:PutBucketTagging", "s3:DeleteBucketTagging",
|
|
},
|
|
expectedResourcesInStmt1: []string{
|
|
"arn:aws:s3:::mybucket",
|
|
"arn:aws:s3:::mybucket/*",
|
|
},
|
|
description: "Read + Tagging on same bucket should aggregate all bucket and object-level actions and ARNs",
|
|
},
|
|
{
|
|
name: "Admin_with_other_actions",
|
|
identityName: "admin-user",
|
|
actions: []string{"Admin:mybucket/admin/*", "Write:mybucket/admin/*"},
|
|
expectedStatements: 1,
|
|
expectedActionsInStmt1: []string{"s3:*"},
|
|
expectedResourcesInStmt1: []string{
|
|
"arn:aws:s3:::mybucket",
|
|
"arn:aws:s3:::mybucket/admin/*",
|
|
},
|
|
description: "Admin action should dominate and set s3:*, other actions still processed for resources",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
policy, err := CreatePolicyFromLegacyIdentity(tc.identityName, tc.actions)
|
|
assert.NoError(t, err, tc.description)
|
|
assert.NotNil(t, policy)
|
|
|
|
// Check statement count
|
|
assert.Equal(t, tc.expectedStatements, len(policy.Statement),
|
|
"Expected %d statement(s), got %d", tc.expectedStatements, len(policy.Statement))
|
|
|
|
if tc.expectedStatements > 0 {
|
|
stmt := policy.Statement[0]
|
|
|
|
// Check actions
|
|
actualActions := stmt.Action.Strings()
|
|
for _, expectedAction := range tc.expectedActionsInStmt1 {
|
|
assert.Contains(t, actualActions, expectedAction,
|
|
"Action %s should be included in statement", expectedAction)
|
|
}
|
|
|
|
// Check resources - all expected resources should be present
|
|
actualResources := stmt.Resource.Strings()
|
|
assert.ElementsMatch(t, tc.expectedResourcesInStmt1, actualResources,
|
|
"Statement should aggregate all required resource ARNs. Got %v, expected %v",
|
|
actualResources, tc.expectedResourcesInStmt1)
|
|
}
|
|
})
|
|
}
|
|
}
|