Files
seaweedFS/weed/iam/policy/policy_variable_matching_test.go
Chris Lu 8814c2a07d iam: support ForAnyValue and ForAllValues condition set operators (#8105)
* iam: support ForAnyValue and ForAllValues condition set operators

This implementation adds support for AWS-style IAM condition set operators
`ForAnyValue:` and `ForAllValues:`. These are essential for trust policies
that evaluate collection-based claims like `oidc:roles` or groups.

- Updated EvaluateStringCondition to handle set operators.
- Added set operator support to numeric, date, and boolean conditions.
- ForAnyValue matches if any request value matches any condition value (default).
- ForAllValues matches if every request value matches at least one condition value.

* iam: add test suite for condition set operators

* iam: ensure ForAllValues is vacuously true for all condition types

Aligned Numeric, Date, and Boolean conditions with AWS IAM behavior
where ForAllValues returns true when the request context values are empty.

* iam: add Date vacuously true test case for ForAllValues

* iam: expand policy variables in case-insensitive string conditions

Added expandPolicyVariables support to evaluateStringConditionIgnoreCase
to ensure consistency with case-sensitive counterparts.

* iam: fix negation issues in string set operators

Refactored EvaluateStringCondition and evaluateStringConditionIgnoreCase
to evaluate operators (including negation) per context value before
aggregating. This ensures StringNotEquals and StringNotLike work
correctly with ForAllValues and ForAnyValue.

* iam: add []string support for Date and Boolean context values

Ensures consistency with Numeric conditions by allowing context values
to be provided as slices of strings, which is common in JSON/OIDC claims.

* iam: simplify redundant type check in policy engine

The `evaluateStringConditionIgnoreCase` function had a redundant type
check for `string` in the `default` block of a type switch that
already handled the `string` case.

* iam: remove outdated "currently fails" comment in negation tests

* iam: add StringLikeIgnoreCase condition support

* iam: explicitly handle empty context sets for ForAnyValue

AWS IAM treats empty request sets as "no match" for ForAnyValue.
Added an explicit check and comment to make this behavior clear.

* iam: refactor EvaluateStringCondition to expand policy variables once

Avoid redundant calls to expandPolicyVariables by expanding them once
per condition value instead of inside awsIAMMatch or in the exact
matching branch.

* iam: fix StringLike case sensitivity to match AWS IAM specs

StringLike and StringNotLike condition operators are case-sensitive in
AWS IAM. Changed the implementation to use filepath.Match for
case-sensitive wildcard matching instead of the case-insensitive
awsIAMMatch.

* iam: integrate StringLike case-sensitivity test into suite

Integrated the case-sensitivity verification into condition_set_test.go
and updated the consistency test to use StringLikeIgnoreCase to maintain
its case-insensitive matching verification.

* iam: fix NumericNotEquals logic to follow "not equal to any" semantics

Updated evaluateNumericCondition to correctly handle NumericNotEquals by
ensuring a context value matches only if it is not equal to ANY of the
provided expected values. Also added support for []string expected
values.

* iam: fix DateNotEquals logic and integrate tests

Updated evaluateDateCondition to correctly handle DateNotEquals logic.
Integrated the new test cases for NumericNotEquals and DateNotEquals into
condition_set_test.go.

* iam: fix validation error in integrated NotEquals tests

Added missing Resource field to IAM policy statements in
condition_set_test.go to satisfy validation requirements.

* iam: add set operator support for IP and Null conditions

Implemented ForAllValues and ForAnyValue support for IpAddress,
NotIpAddress, and Null condition operators. Also added test coverage for
ForAnyValue with an empty context to ensure correct behavior.

* iam: refine IP condition evaluation to handle multiple policy value types

Updated evaluateIPCondition to correctly handle string, []string, and
[]interface{} values for IP address conditions in policy documents.
Added IpAddress:SingleStringValue test case to verify consistency.

* iam: refine Null and case-insensitive string conditions

- Reverted evaluateNullCondition to standard AWS behavior (no set operators).
- Refactored evaluateStringConditionIgnoreCase to use idiomatic helpers
  (strings.EqualFold and AwsWildcardMatch).
- Cleaned up tests in condition_set_test.go.

* iam: normalize policy value handling across condition evaluators

- Implemented normalizeRanges helper for consistent IP range extraction.
- Expanded type switches in IP, Bool, and String condition evaluators to
  support string, []string, and []interface{} policy values.
- Fixed ForAnyValue bool matching to support string slices.
- Added targeted tests for []string policy values in condition_set_test.go.

* iam: refactor IP condition to support arbitrary context keys

Refactored evaluateIPCondition to iterate through all keys in the
condition block instead of hardcoding aws:SourceIp. This ensures
consistency with other condition types and allows custom context keys.
Added IpAddress:CustomContextKey test case to verify the change.
2026-01-24 13:34:49 -08:00

192 lines
6.0 KiB
Go

package policy
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestPolicyVariableMatchingInActionsAndResources tests that Actions and Resources
// now support policy variables like ${aws:username} just like string conditions do
func TestPolicyVariableMatchingInActionsAndResources(t *testing.T) {
engine := NewPolicyEngine()
config := &PolicyEngineConfig{
DefaultEffect: "Deny",
StoreType: "memory",
}
err := engine.Initialize(config)
require.NoError(t, err)
ctx := context.Background()
filerAddress := ""
// Create a policy that uses policy variables in Action and Resource fields
policyDoc := &PolicyDocument{
Version: "2012-10-17",
Statement: []Statement{
{
Sid: "AllowUserSpecificActions",
Effect: "Allow",
Action: []string{
"s3:Get*", // Regular wildcard
"s3:${aws:principaltype}*", // Policy variable in action
},
Resource: []string{
"arn:aws:s3:::user-${aws:username}/*", // Policy variable in resource
"arn:aws:s3:::shared/${saml:username}/*", // Different policy variable
},
},
},
}
err = engine.AddPolicy(filerAddress, "user-specific-policy", policyDoc)
require.NoError(t, err)
tests := []struct {
name string
principal string
action string
resource string
requestContext map[string]interface{}
expectedEffect Effect
description string
}{
{
name: "policy_variable_in_action_matches",
principal: "test-user",
action: "s3:AssumedRole", // Should match s3:${aws:principaltype}* when principaltype=AssumedRole
resource: "arn:aws:s3:::user-testuser/file.txt",
requestContext: map[string]interface{}{
"aws:username": "testuser",
"aws:principaltype": "AssumedRole",
},
expectedEffect: EffectAllow,
description: "Action with policy variable should match when variable is expanded",
},
{
name: "policy_variable_in_resource_matches",
principal: "alice",
action: "s3:GetObject",
resource: "arn:aws:s3:::user-alice/document.pdf", // Should match user-${aws:username}/*
requestContext: map[string]interface{}{
"aws:username": "alice",
},
expectedEffect: EffectAllow,
description: "Resource with policy variable should match when variable is expanded",
},
{
name: "saml_username_variable_in_resource",
principal: "bob",
action: "s3:GetObject",
resource: "arn:aws:s3:::shared/bob/data.json", // Should match shared/${saml:username}/*
requestContext: map[string]interface{}{
"saml:username": "bob",
},
expectedEffect: EffectAllow,
description: "SAML username variable should be expanded in resource patterns",
},
{
name: "policy_variable_no_match_wrong_user",
principal: "charlie",
action: "s3:GetObject",
resource: "arn:aws:s3:::user-alice/file.txt", // charlie trying to access alice's files
requestContext: map[string]interface{}{
"aws:username": "charlie",
},
expectedEffect: EffectDeny,
description: "Policy variable should prevent access when username doesn't match",
},
{
name: "missing_policy_variable_context",
principal: "dave",
action: "s3:GetObject",
resource: "arn:aws:s3:::user-dave/file.txt",
requestContext: map[string]interface{}{
// Missing aws:username context
},
expectedEffect: EffectDeny,
description: "Missing policy variable context should result in no match",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
evalCtx := &EvaluationContext{
Principal: tt.principal,
Action: tt.action,
Resource: tt.resource,
RequestContext: tt.requestContext,
}
result, err := engine.Evaluate(ctx, filerAddress, evalCtx, []string{"user-specific-policy"})
require.NoError(t, err, "Policy evaluation should not error")
assert.Equal(t, tt.expectedEffect, result.Effect,
"Test %s: %s. Expected %s but got %s",
tt.name, tt.description, tt.expectedEffect, result.Effect)
})
}
}
// TestActionResourceConsistencyWithStringConditions verifies that Actions, Resources,
// and string conditions all use the same AWS IAM-compliant matching logic
func TestActionResourceConsistencyWithStringConditions(t *testing.T) {
engine := NewPolicyEngine()
config := &PolicyEngineConfig{
DefaultEffect: "Deny",
StoreType: "memory",
}
err := engine.Initialize(config)
require.NoError(t, err)
ctx := context.Background()
filerAddress := ""
// Policy that uses case-insensitive matching in all three areas
policyDoc := &PolicyDocument{
Version: "2012-10-17",
Statement: []Statement{
{
Sid: "CaseInsensitiveMatching",
Effect: "Allow",
Action: []string{"S3:GET*"}, // Uppercase action pattern
Resource: []string{"arn:aws:s3:::TEST-BUCKET/*"}, // Uppercase resource pattern
Condition: map[string]map[string]interface{}{
"StringLikeIgnoreCase": {
"s3:RequestedRegion": "US-*", // Uppercase condition pattern
},
},
},
},
}
err = engine.AddPolicy(filerAddress, "case-insensitive-policy", policyDoc)
require.NoError(t, err)
evalCtx := &EvaluationContext{
Principal: "test-user",
Action: "s3:getobject", // lowercase action
Resource: "arn:aws:s3:::test-bucket/file.txt", // lowercase resource
RequestContext: map[string]interface{}{
"s3:RequestedRegion": "us-east-1", // lowercase condition value
},
}
result, err := engine.Evaluate(ctx, filerAddress, evalCtx, []string{"case-insensitive-policy"})
require.NoError(t, err)
// All should match due to case-insensitive AWS IAM-compliant matching
assert.Equal(t, EffectAllow, result.Effect,
"Actions, Resources, and Conditions should all use case-insensitive AWS IAM matching")
// Verify that matching statements were found
require.Len(t, result.MatchingStatements, 1,
"Should have exactly one matching statement")
assert.Equal(t, "Allow", string(result.MatchingStatements[0].Effect),
"Matching statement should have Allow effect")
}