Files
seaweedFS/weed/s3api/policy_engine/engine_test.go
Chris Lu 995dfc4d5d chore: remove ~50k lines of unreachable dead code (#8913)
* chore: remove unreachable dead code across the codebase

Remove ~50,000 lines of unreachable code identified by static analysis.

Major removals:
- weed/filer/redis_lua: entire unused Redis Lua filer store implementation
- weed/wdclient/net2, resource_pool: unused connection/resource pool packages
- weed/plugin/worker/lifecycle: unused lifecycle plugin worker
- weed/s3api: unused S3 policy templates, presigned URL IAM, streaming copy,
  multipart IAM, key rotation, and various SSE helper functions
- weed/mq/kafka: unused partition mapping, compression, schema, and protocol functions
- weed/mq/offset: unused SQL storage and migration code
- weed/worker: unused registry, task, and monitoring functions
- weed/query: unused SQL engine, parquet scanner, and type functions
- weed/shell: unused EC proportional rebalance functions
- weed/storage/erasure_coding/distribution: unused distribution analysis functions
- Individual unreachable functions removed from 150+ files across admin,
  credential, filer, iam, kms, mount, mq, operation, pb, s3api, server,
  shell, storage, topology, and util packages

* fix(s3): reset shared memory store in IAM test to prevent flaky failure

TestLoadIAMManagerFromConfig_EmptyConfigWithFallbackKey was flaky because
the MemoryStore credential backend is a singleton registered via init().
Earlier tests that create anonymous identities pollute the shared store,
causing LookupAnonymous() to unexpectedly return true.

Fix by calling Reset() on the memory store before the test runs.

* style: run gofmt on changed files

* fix: restore KMS functions used by integration tests

* fix(plugin): prevent panic on send to closed worker session channel

The Plugin.sendToWorker method could panic with "send on closed channel"
when a worker disconnected while a message was being sent. The race was
between streamSession.close() closing the outgoing channel and sendToWorker
writing to it concurrently.

Add a done channel to streamSession that is closed before the outgoing
channel, and check it in sendToWorker's select to safely detect closed
sessions without panicking.
2026-04-03 16:04:27 -07:00

984 lines
24 KiB
Go

package policy_engine
import (
"net/http"
"net/url"
"testing"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
"github.com/seaweedfs/seaweedfs/weed/util/wildcard"
)
// tagsToEntry converts a map of tag key-value pairs to the entry.Extended format
// used for s3:ExistingObjectTag/<key> condition evaluation
func tagsToEntry(tags map[string]string) map[string][]byte {
if tags == nil {
return nil
}
entry := make(map[string][]byte)
for k, v := range tags {
entry[s3_constants.AmzObjectTaggingPrefix+k] = []byte(v)
}
return entry
}
func TestPolicyEngine(t *testing.T) {
engine := NewPolicyEngine()
// Test policy JSON
policyJSON := `{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": ["arn:aws:s3:::test-bucket/*"]
},
{
"Effect": "Deny",
"Action": ["s3:DeleteObject"],
"Resource": ["arn:aws:s3:::test-bucket/*"],
"Condition": {
"StringEquals": {
"s3:RequestMethod": ["DELETE"]
}
}
}
]
}`
// Set bucket policy
err := engine.SetBucketPolicy("test-bucket", policyJSON)
if err != nil {
t.Fatalf("Failed to set bucket policy: %v", err)
}
// Test Allow case
args := &PolicyEvaluationArgs{
Action: "s3:GetObject",
Resource: "arn:aws:s3:::test-bucket/test-object",
Principal: "user1",
Conditions: map[string][]string{},
}
result := engine.EvaluatePolicy("test-bucket", args)
if result != PolicyResultAllow {
t.Errorf("Expected Allow, got %v", result)
}
// Test Deny case
args = &PolicyEvaluationArgs{
Action: "s3:DeleteObject",
Resource: "arn:aws:s3:::test-bucket/test-object",
Principal: "user1",
Conditions: map[string][]string{
"s3:RequestMethod": {"DELETE"},
},
}
result = engine.EvaluatePolicy("test-bucket", args)
if result != PolicyResultDeny {
t.Errorf("Expected Deny, got %v", result)
}
// Test non-matching action
args = &PolicyEvaluationArgs{
Action: "s3:ListBucket",
Resource: "arn:aws:s3:::test-bucket",
Principal: "user1",
Conditions: map[string][]string{},
}
result = engine.EvaluatePolicy("test-bucket", args)
if result != PolicyResultIndeterminate {
t.Errorf("Expected Indeterminate for non-matching action (should fall through to IAM), got %v", result)
}
// Test GetBucketPolicy
policy, err := engine.GetBucketPolicy("test-bucket")
if err != nil {
t.Fatalf("Failed to get bucket policy: %v", err)
}
if policy.Version != "2012-10-17" {
t.Errorf("Expected version 2012-10-17, got %s", policy.Version)
}
// Test DeleteBucketPolicy
err = engine.DeleteBucketPolicy("test-bucket")
if err != nil {
t.Fatalf("Failed to delete bucket policy: %v", err)
}
// Test policy is gone
result = engine.EvaluatePolicy("test-bucket", args)
if result != PolicyResultIndeterminate {
t.Errorf("Expected Indeterminate after policy deletion, got %v", result)
}
}
func TestConditionEvaluators(t *testing.T) {
tests := []struct {
name string
operator string
conditionValue interface{}
contextValues []string
expected bool
}{
{
name: "StringEquals - match",
operator: "StringEquals",
conditionValue: "test-value",
contextValues: []string{"test-value"},
expected: true,
},
{
name: "StringEquals - no match",
operator: "StringEquals",
conditionValue: "test-value",
contextValues: []string{"other-value"},
expected: false,
},
{
name: "StringLike - wildcard match",
operator: "StringLike",
conditionValue: "test-*",
contextValues: []string{"test-value"},
expected: true,
},
{
name: "StringLike - wildcard no match",
operator: "StringLike",
conditionValue: "test-*",
contextValues: []string{"other-value"},
expected: false,
},
{
name: "NumericEquals - match",
operator: "NumericEquals",
conditionValue: "42",
contextValues: []string{"42"},
expected: true,
},
{
name: "NumericLessThan - match",
operator: "NumericLessThan",
conditionValue: "100",
contextValues: []string{"50"},
expected: true,
},
{
name: "NumericLessThan - no match",
operator: "NumericLessThan",
conditionValue: "100",
contextValues: []string{"150"},
expected: false,
},
{
name: "IpAddress - CIDR match",
operator: "IpAddress",
conditionValue: "192.168.1.0/24",
contextValues: []string{"192.168.1.100"},
expected: true,
},
{
name: "IpAddress - CIDR no match",
operator: "IpAddress",
conditionValue: "192.168.1.0/24",
contextValues: []string{"10.0.0.1"},
expected: false,
},
{
name: "Bool - true match",
operator: "Bool",
conditionValue: "true",
contextValues: []string{"true"},
expected: true,
},
{
name: "Bool - false match",
operator: "Bool",
conditionValue: "false",
contextValues: []string{"false"},
expected: true,
},
{
name: "Bool - no match",
operator: "Bool",
conditionValue: "true",
contextValues: []string{"false"},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
evaluator, err := GetConditionEvaluator(tt.operator)
if err != nil {
t.Fatalf("Failed to get condition evaluator: %v", err)
}
result := evaluator.Evaluate(tt.conditionValue, tt.contextValues)
if result != tt.expected {
t.Errorf("Expected %v, got %v", tt.expected, result)
}
})
}
}
func TestPolicyValidation(t *testing.T) {
tests := []struct {
name string
policyJSON string
expectError bool
}{
{
name: "Valid policy",
policyJSON: `{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::test-bucket/*"
}
]
}`,
expectError: false,
},
{
name: "Valid policy with single statement object",
policyJSON: `{
"Version": "2012-10-17",
"Statement": {
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::test-bucket/*"
}
}`,
expectError: false,
},
{
name: "Invalid version",
policyJSON: `{
"Version": "2008-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::test-bucket/*"
}
]
}`,
expectError: true,
},
{
name: "Missing action",
policyJSON: `{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Resource": "arn:aws:s3:::test-bucket/*"
}
]
}`,
expectError: true,
},
{
name: "Invalid JSON",
policyJSON: `{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::test-bucket/*"
}
]
}extra`,
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := ParsePolicy(tt.policyJSON)
if (err != nil) != tt.expectError {
t.Errorf("Expected error: %v, got error: %v", tt.expectError, err)
}
})
}
}
func TestPatternMatching(t *testing.T) {
tests := []struct {
name string
pattern string
value string
expected bool
}{
{
name: "Exact match",
pattern: "s3:GetObject",
value: "s3:GetObject",
expected: true,
},
{
name: "Wildcard match",
pattern: "s3:Get*",
value: "s3:GetObject",
expected: true,
},
{
name: "Wildcard no match",
pattern: "s3:Put*",
value: "s3:GetObject",
expected: false,
},
{
name: "Full wildcard",
pattern: "*",
value: "anything",
expected: true,
},
{
name: "Question mark wildcard",
pattern: "s3:GetObjec?",
value: "s3:GetObject",
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
compiled, err := compilePattern(tt.pattern)
if err != nil {
t.Fatalf("Failed to compile pattern %s: %v", tt.pattern, err)
}
result := compiled.MatchString(tt.value)
if result != tt.expected {
t.Errorf("Pattern %s against %s: expected %v, got %v", tt.pattern, tt.value, tt.expected, result)
}
})
}
}
func TestExtractConditionValuesFromRequest(t *testing.T) {
// Create a test request
req := &http.Request{
Method: "GET",
URL: &url.URL{
Path: "/test-bucket/test-object",
RawQuery: "prefix=test&delimiter=/",
},
Header: map[string][]string{
"User-Agent": {"test-agent"},
"X-Amz-Copy-Source": {"source-bucket/source-object"},
},
RemoteAddr: "192.168.1.100:12345",
}
values := ExtractConditionValuesFromRequest(req)
// Check extracted values
if len(values["aws:SourceIp"]) != 1 || values["aws:SourceIp"][0] != "192.168.1.100" {
t.Errorf("Expected SourceIp to be 192.168.1.100, got %v", values["aws:SourceIp"])
}
if len(values["aws:UserAgent"]) != 1 || values["aws:UserAgent"][0] != "test-agent" {
t.Errorf("Expected UserAgent to be test-agent, got %v", values["aws:UserAgent"])
}
if len(values["s3:prefix"]) != 1 || values["s3:prefix"][0] != "test" {
t.Errorf("Expected prefix to be test, got %v", values["s3:prefix"])
}
if len(values["s3:delimiter"]) != 1 || values["s3:delimiter"][0] != "/" {
t.Errorf("Expected delimiter to be /, got %v", values["s3:delimiter"])
}
if len(values["s3:RequestMethod"]) != 1 || values["s3:RequestMethod"][0] != "GET" {
t.Errorf("Expected RequestMethod to be GET, got %v", values["s3:RequestMethod"])
}
if len(values["s3:x-amz-copy-source"]) != 1 || values["s3:x-amz-copy-source"][0] != "source-bucket/source-object" {
t.Errorf("Expected X-Amz-Copy-Source header to be extracted with s3: prefix, got %v", values["s3:x-amz-copy-source"])
}
// Check that aws:CurrentTime is properly set
if len(values["aws:CurrentTime"]) != 1 {
t.Errorf("Expected aws:CurrentTime to be set, got %v", values["aws:CurrentTime"])
}
// Check that aws:RequestTime is still available for backward compatibility
if len(values["aws:RequestTime"]) != 1 {
t.Errorf("Expected aws:RequestTime to be set for backward compatibility, got %v", values["aws:RequestTime"])
}
}
func TestExtractConditionValuesFromRequestSourceIPPrecedence(t *testing.T) {
tests := []struct {
name string
header map[string][]string
remoteAddr string
expectedIP string
}{
{
name: "uses right-most public X-Forwarded-For entry",
header: map[string][]string{
"X-Forwarded-For": {"bad-ip, 203.0.113.10, 198.51.100.5"},
},
remoteAddr: "192.168.1.100:12345",
expectedIP: "198.51.100.5",
},
{
name: "falls back to X-Real-Ip when X-Forwarded-For has no valid ip",
header: map[string][]string{
"X-Forwarded-For": {"bad-ip"},
"X-Real-Ip": {"198.51.100.7"},
},
remoteAddr: "192.168.1.100:12345",
expectedIP: "198.51.100.7",
},
{
name: "uses RemoteAddr ip when no forwarding headers",
header: map[string][]string{},
remoteAddr: "192.168.1.100:12345",
expectedIP: "192.168.1.100",
},
{
name: "keeps unix socket marker when RemoteAddr is not an ip",
header: map[string][]string{},
remoteAddr: "@",
expectedIP: "@",
},
{
name: "uses IPv6 X-Forwarded-For entry",
header: map[string][]string{
"X-Forwarded-For": {"2001:db8::8, 198.51.100.7"},
},
remoteAddr: "192.168.1.100:12345",
expectedIP: "198.51.100.7",
},
{
name: "ignores spoofed IP when real client is public",
header: map[string][]string{
"X-Forwarded-For": {"8.8.8.8, 203.0.113.10, 10.0.0.1"},
},
remoteAddr: "192.168.1.100:12345",
expectedIP: "203.0.113.10",
},
{
name: "handles bracketed IPv6 remote address",
header: map[string][]string{},
remoteAddr: "[2001:db8::1]:12345",
expectedIP: "2001:db8::1",
},
{
name: "avoids returning DNS host names",
header: map[string][]string{},
remoteAddr: "example.com:9000",
expectedIP: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := &http.Request{
Method: "GET",
URL: &url.URL{Path: "/"},
Header: tt.header,
RemoteAddr: tt.remoteAddr,
}
values := ExtractConditionValuesFromRequest(req)
if len(values["aws:SourceIp"]) != 1 || values["aws:SourceIp"][0] != tt.expectedIP {
t.Errorf("Expected SourceIp %q, got %v", tt.expectedIP, values["aws:SourceIp"])
}
})
}
}
func TestPolicyEvaluationWithConditions(t *testing.T) {
engine := NewPolicyEngine()
// Policy with IP condition
policyJSON := `{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::test-bucket/*",
"Condition": {
"IpAddress": {
"aws:SourceIp": "192.168.1.0/24"
}
}
}
]
}`
err := engine.SetBucketPolicy("test-bucket", policyJSON)
if err != nil {
t.Fatalf("Failed to set bucket policy: %v", err)
}
// Test matching IP
args := &PolicyEvaluationArgs{
Action: "s3:GetObject",
Resource: "arn:aws:s3:::test-bucket/test-object",
Principal: "user1",
Conditions: map[string][]string{
"aws:SourceIp": {"192.168.1.100"},
},
}
result := engine.EvaluatePolicy("test-bucket", args)
if result != PolicyResultAllow {
t.Errorf("Expected Allow for matching IP, got %v", result)
}
// Test non-matching IP
args.Conditions["aws:SourceIp"] = []string{"10.0.0.1"}
result = engine.EvaluatePolicy("test-bucket", args)
if result != PolicyResultIndeterminate {
t.Errorf("Expected Indeterminate for non-matching IP (should fall through to IAM), got %v", result)
}
}
func TestResourceArn(t *testing.T) {
tests := []struct {
name string
bucketName string
objectName string
expected string
}{
{
name: "Bucket only",
bucketName: "test-bucket",
objectName: "",
expected: "arn:aws:s3:::test-bucket",
},
{
name: "Bucket and object",
bucketName: "test-bucket",
objectName: "test-object",
expected: "arn:aws:s3:::test-bucket/test-object",
},
{
name: "Bucket and nested object",
bucketName: "test-bucket",
objectName: "folder/subfolder/test-object",
expected: "arn:aws:s3:::test-bucket/folder/subfolder/test-object",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := BuildResourceArn(tt.bucketName, tt.objectName)
if result != tt.expected {
t.Errorf("Expected %s, got %s", tt.expected, result)
}
})
}
}
func TestActionConversion(t *testing.T) {
tests := []struct {
name string
action string
expected string
}{
{
name: "Already has s3 prefix",
action: "s3:GetObject",
expected: "s3:GetObject",
},
{
name: "Add s3 prefix",
action: "GetObject",
expected: "s3:GetObject",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := BuildActionName(tt.action)
if result != tt.expected {
t.Errorf("Expected %s, got %s", tt.expected, result)
}
})
}
}
func TestPolicyEngineForRequest(t *testing.T) {
engine := NewPolicyEngine()
// Set up a policy
policyJSON := `{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::test-bucket/*",
"Condition": {
"StringEquals": {
"s3:RequestMethod": "GET"
}
}
}
]
}`
err := engine.SetBucketPolicy("test-bucket", policyJSON)
if err != nil {
t.Fatalf("Failed to set bucket policy: %v", err)
}
// Create test request
req := &http.Request{
Method: "GET",
URL: &url.URL{
Path: "/test-bucket/test-object",
},
Header: make(map[string][]string),
RemoteAddr: "192.168.1.100:12345",
}
// Test the request
result := engine.EvaluatePolicyForRequest("test-bucket", "test-object", "GetObject", "user1", req)
if result != PolicyResultAllow {
t.Errorf("Expected Allow for matching request, got %v", result)
}
}
func TestWildcardMatching(t *testing.T) {
tests := []struct {
name string
pattern string
str string
expected bool
}{
{
name: "Exact match",
pattern: "test",
str: "test",
expected: true,
},
{
name: "Single wildcard",
pattern: "*",
str: "anything",
expected: true,
},
{
name: "Prefix wildcard",
pattern: "test*",
str: "test123",
expected: true,
},
{
name: "Suffix wildcard",
pattern: "*test",
str: "123test",
expected: true,
},
{
name: "Middle wildcard",
pattern: "test*123",
str: "testABC123",
expected: true,
},
{
name: "No match",
pattern: "test*",
str: "other",
expected: false,
},
{
name: "Multiple wildcards",
pattern: "test*abc*123",
str: "testXYZabcDEF123",
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := wildcard.MatchesWildcard(tt.pattern, tt.str)
if result != tt.expected {
t.Errorf("Pattern %s against %s: expected %v, got %v", tt.pattern, tt.str, tt.expected, result)
}
})
}
}
func TestCompilePolicy(t *testing.T) {
policyJSON := `{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::test-bucket/*"
}
]
}`
policy, err := ParsePolicy(policyJSON)
if err != nil {
t.Fatalf("Failed to parse policy: %v", err)
}
compiled, err := CompilePolicy(policy)
if err != nil {
t.Fatalf("Failed to compile policy: %v", err)
}
if len(compiled.Statements) != 1 {
t.Errorf("Expected 1 compiled statement, got %d", len(compiled.Statements))
}
stmt := compiled.Statements[0]
if len(stmt.ActionPatterns) != 2 {
t.Errorf("Expected 2 action patterns, got %d", len(stmt.ActionPatterns))
}
if len(stmt.ResourcePatterns) != 1 {
t.Errorf("Expected 1 resource pattern, got %d", len(stmt.ResourcePatterns))
}
}
// TestExistingObjectTagCondition tests s3:ExistingObjectTag/<tag-key> condition support
func TestExistingObjectTagCondition(t *testing.T) {
engine := NewPolicyEngine()
// Policy that allows GetObject only for objects with specific tag
policyJSON := `{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::test-bucket/*",
"Condition": {
"StringEquals": {
"s3:ExistingObjectTag/status": ["public"]
}
}
}
]
}`
err := engine.SetBucketPolicy("test-bucket", policyJSON)
if err != nil {
t.Fatalf("Failed to set bucket policy: %v", err)
}
tests := []struct {
name string
objectTags map[string]string
expected PolicyEvaluationResult
}{
{
name: "Matching tag value - should allow",
objectTags: map[string]string{"status": "public"},
expected: PolicyResultAllow,
},
{
name: "Non-matching tag value - should be indeterminate",
objectTags: map[string]string{"status": "private"},
expected: PolicyResultIndeterminate,
},
{
name: "Missing tag - should be indeterminate",
objectTags: map[string]string{"other": "value"},
expected: PolicyResultIndeterminate,
},
{
name: "No tags - should be indeterminate",
objectTags: nil,
expected: PolicyResultIndeterminate,
},
{
name: "Empty tags - should be indeterminate",
objectTags: map[string]string{},
expected: PolicyResultIndeterminate,
},
{
name: "Multiple tags with matching one - should allow",
objectTags: map[string]string{"status": "public", "owner": "admin"},
expected: PolicyResultAllow,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
args := &PolicyEvaluationArgs{
Action: "s3:GetObject",
Resource: "arn:aws:s3:::test-bucket/test-object",
Principal: "*",
ObjectEntry: tagsToEntry(tt.objectTags),
}
result := engine.EvaluatePolicy("test-bucket", args)
if result != tt.expected {
t.Errorf("Expected %v, got %v", tt.expected, result)
}
})
}
}
// TestExistingObjectTagConditionMultipleTags tests policies with multiple tag conditions
func TestExistingObjectTagConditionMultipleTags(t *testing.T) {
engine := NewPolicyEngine()
// Policy that requires multiple tag conditions
policyJSON := `{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::test-bucket/*",
"Condition": {
"StringEquals": {
"s3:ExistingObjectTag/status": ["public"],
"s3:ExistingObjectTag/tier": ["free", "premium"]
}
}
}
]
}`
err := engine.SetBucketPolicy("test-bucket", policyJSON)
if err != nil {
t.Fatalf("Failed to set bucket policy: %v", err)
}
tests := []struct {
name string
objectTags map[string]string
expected PolicyEvaluationResult
}{
{
name: "Both tags match - should allow",
objectTags: map[string]string{"status": "public", "tier": "free"},
expected: PolicyResultAllow,
},
{
name: "Both tags match (premium tier) - should allow",
objectTags: map[string]string{"status": "public", "tier": "premium"},
expected: PolicyResultAllow,
},
{
name: "Only status matches - should be indeterminate",
objectTags: map[string]string{"status": "public"},
expected: PolicyResultIndeterminate,
},
{
name: "Only tier matches - should be indeterminate",
objectTags: map[string]string{"tier": "free"},
expected: PolicyResultIndeterminate,
},
{
name: "Neither tag matches - should be indeterminate",
objectTags: map[string]string{"status": "private", "tier": "basic"},
expected: PolicyResultIndeterminate,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
args := &PolicyEvaluationArgs{
Action: "s3:GetObject",
Resource: "arn:aws:s3:::test-bucket/test-object",
Principal: "*",
ObjectEntry: tagsToEntry(tt.objectTags),
}
result := engine.EvaluatePolicy("test-bucket", args)
if result != tt.expected {
t.Errorf("Expected %v, got %v", tt.expected, result)
}
})
}
}
// TestExistingObjectTagDenyPolicy tests deny policies with tag conditions
func TestExistingObjectTagDenyPolicy(t *testing.T) {
engine := NewPolicyEngine()
// Policy that denies access to objects with confidential tag
policyJSON := `{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::test-bucket/*"
},
{
"Effect": "Deny",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::test-bucket/*",
"Condition": {
"StringEquals": {
"s3:ExistingObjectTag/classification": ["confidential"]
}
}
}
]
}`
err := engine.SetBucketPolicy("test-bucket", policyJSON)
if err != nil {
t.Fatalf("Failed to set bucket policy: %v", err)
}
tests := []struct {
name string
objectTags map[string]string
expected PolicyEvaluationResult
}{
{
name: "No tags - allow by default statement",
objectTags: nil,
expected: PolicyResultAllow,
},
{
name: "Non-confidential tag - allow",
objectTags: map[string]string{"classification": "public"},
expected: PolicyResultAllow,
},
{
name: "Confidential tag - deny",
objectTags: map[string]string{"classification": "confidential"},
expected: PolicyResultDeny,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
args := &PolicyEvaluationArgs{
Action: "s3:GetObject",
Resource: "arn:aws:s3:::test-bucket/test-object",
Principal: "*",
ObjectEntry: tagsToEntry(tt.objectTags),
}
result := engine.EvaluatePolicy("test-bucket", args)
if result != tt.expected {
t.Errorf("Expected %v, got %v", tt.expected, result)
}
})
}
}