Browse Source

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.
pull/8019/merge
Chris Lu 3 days ago
committed by GitHub
parent
commit
8814c2a07d
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 687
      weed/iam/policy/condition_set_test.go
  2. 101
      weed/iam/policy/negation_test.go
  3. 914
      weed/iam/policy/policy_engine.go
  4. 4
      weed/iam/policy/policy_variable_matching_test.go

687
weed/iam/policy/condition_set_test.go

@ -0,0 +1,687 @@
package policy
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestConditionSetOperators(t *testing.T) {
engine := setupTestPolicyEngine(t)
t.Run("ForAnyValue:StringEquals", func(t *testing.T) {
trustPolicy := &PolicyDocument{
Version: "2012-10-17",
Statement: []Statement{
{
Sid: "AllowOIDC",
Effect: "Allow",
Action: []string{"sts:AssumeRoleWithWebIdentity"},
Condition: map[string]map[string]interface{}{
"ForAnyValue:StringEquals": {
"oidc:roles": []string{"Dev.SeaweedFS.TestBucket.ReadWrite", "Dev.SeaweedFS.Admin"},
},
},
},
},
}
// Match: Admin is in the requested roles
evalCtxMatch := &EvaluationContext{
Principal: "web-identity-user",
Action: "sts:AssumeRoleWithWebIdentity",
Resource: "arn:aws:iam::role/test-role",
RequestContext: map[string]interface{}{
"oidc:roles": []string{"Dev.SeaweedFS.Admin", "OtherRole"},
},
}
resultMatch, err := engine.EvaluateTrustPolicy(context.Background(), trustPolicy, evalCtxMatch)
require.NoError(t, err)
assert.Equal(t, EffectAllow, resultMatch.Effect)
// No Match
evalCtxNoMatch := &EvaluationContext{
Principal: "web-identity-user",
Action: "sts:AssumeRoleWithWebIdentity",
Resource: "arn:aws:iam::role/test-role",
RequestContext: map[string]interface{}{
"oidc:roles": []string{"OtherRole1", "OtherRole2"},
},
}
resultNoMatch, err := engine.EvaluateTrustPolicy(context.Background(), trustPolicy, evalCtxNoMatch)
require.NoError(t, err)
assert.Equal(t, EffectDeny, resultNoMatch.Effect)
// No Match: Empty context for ForAnyValue (should deny)
evalCtxEmpty := &EvaluationContext{
Principal: "web-identity-user",
Action: "sts:AssumeRoleWithWebIdentity",
Resource: "arn:aws:iam::role/test-role",
RequestContext: map[string]interface{}{
"oidc:roles": []string{},
},
}
resultEmpty, err := engine.EvaluateTrustPolicy(context.Background(), trustPolicy, evalCtxEmpty)
require.NoError(t, err)
assert.Equal(t, EffectDeny, resultEmpty.Effect, "ForAnyValue should deny when context is empty")
})
t.Run("ForAllValues:StringEquals", func(t *testing.T) {
trustPolicyAll := &PolicyDocument{
Version: "2012-10-17",
Statement: []Statement{
{
Sid: "AllowOIDCAll",
Effect: "Allow",
Action: []string{"sts:AssumeRoleWithWebIdentity"},
Condition: map[string]map[string]interface{}{
"ForAllValues:StringEquals": {
"oidc:roles": []string{"RoleA", "RoleB", "RoleC"},
},
},
},
},
}
// Match: All requested roles ARE in the allowed set
evalCtxAllMatch := &EvaluationContext{
Principal: "web-identity-user",
Action: "sts:AssumeRoleWithWebIdentity",
Resource: "arn:aws:iam::role/test-role",
RequestContext: map[string]interface{}{
"oidc:roles": []string{"RoleA", "RoleB"},
},
}
resultAllMatch, err := engine.EvaluateTrustPolicy(context.Background(), trustPolicyAll, evalCtxAllMatch)
require.NoError(t, err)
assert.Equal(t, EffectAllow, resultAllMatch.Effect)
// Fail: RoleD is NOT in the allowed set
evalCtxAllFail := &EvaluationContext{
Principal: "web-identity-user",
Action: "sts:AssumeRoleWithWebIdentity",
Resource: "arn:aws:iam::role/test-role",
RequestContext: map[string]interface{}{
"oidc:roles": []string{"RoleA", "RoleD"},
},
}
resultAllFail, err := engine.EvaluateTrustPolicy(context.Background(), trustPolicyAll, evalCtxAllFail)
require.NoError(t, err)
assert.Equal(t, EffectDeny, resultAllFail.Effect)
// Vacuously true: Request has NO roles
evalCtxEmpty := &EvaluationContext{
Principal: "web-identity-user",
Action: "sts:AssumeRoleWithWebIdentity",
Resource: "arn:aws:iam::role/test-role",
RequestContext: map[string]interface{}{
"oidc:roles": []string{},
},
}
resultEmpty, err := engine.EvaluateTrustPolicy(context.Background(), trustPolicyAll, evalCtxEmpty)
require.NoError(t, err)
assert.Equal(t, EffectAllow, resultEmpty.Effect)
})
t.Run("ForAllValues:NumericEqualsVacuouslyTrue", func(t *testing.T) {
policy := &PolicyDocument{
Version: "2012-10-17",
Statement: []Statement{
{
Sid: "AllowNumericAll",
Effect: "Allow",
Action: []string{"sts:AssumeRole"},
Condition: map[string]map[string]interface{}{
"ForAllValues:NumericEquals": {
"aws:MultiFactorAuthAge": []string{"3600", "7200"},
},
},
},
},
}
// Vacuously true: Request has NO MFA age info
evalCtxEmpty := &EvaluationContext{
Principal: "user",
Action: "sts:AssumeRole",
Resource: "arn:aws:iam::role/test-role",
RequestContext: map[string]interface{}{
"aws:MultiFactorAuthAge": []string{},
},
}
resultEmpty, err := engine.EvaluateTrustPolicy(context.Background(), policy, evalCtxEmpty)
require.NoError(t, err)
assert.Equal(t, EffectAllow, resultEmpty.Effect, "Should allow when numeric context is empty for ForAllValues")
})
t.Run("ForAllValues:BoolVacuouslyTrue", func(t *testing.T) {
policy := &PolicyDocument{
Version: "2012-10-17",
Statement: []Statement{
{
Sid: "AllowBoolAll",
Effect: "Allow",
Action: []string{"sts:AssumeRole"},
Condition: map[string]map[string]interface{}{
"ForAllValues:Bool": {
"aws:SecureTransport": "true",
},
},
},
},
}
// Vacuously true
evalCtxEmpty := &EvaluationContext{
Principal: "user",
Action: "sts:AssumeRole",
Resource: "arn:aws:iam::role/test-role",
RequestContext: map[string]interface{}{
"aws:SecureTransport": []interface{}{},
},
}
resultEmpty, err := engine.EvaluateTrustPolicy(context.Background(), policy, evalCtxEmpty)
require.NoError(t, err)
assert.Equal(t, EffectAllow, resultEmpty.Effect, "Should allow when bool context is empty for ForAllValues")
})
t.Run("ForAllValues:DateVacuouslyTrue", func(t *testing.T) {
policy := &PolicyDocument{
Version: "2012-10-17",
Statement: []Statement{
{
Sid: "AllowDateAll",
Effect: "Allow",
Action: []string{"sts:AssumeRole"},
Condition: map[string]map[string]interface{}{
"ForAllValues:DateGreaterThan": {
"aws:CurrentTime": "2020-01-01T00:00:00Z",
},
},
},
},
}
// Vacuously true
evalCtxEmpty := &EvaluationContext{
Principal: "user",
Action: "sts:AssumeRole",
Resource: "arn:aws:iam::role/test-role",
RequestContext: map[string]interface{}{
"aws:CurrentTime": []interface{}{},
},
}
resultEmpty, err := engine.EvaluateTrustPolicy(context.Background(), policy, evalCtxEmpty)
require.NoError(t, err)
assert.Equal(t, EffectAllow, resultEmpty.Effect, "Should allow when date context is empty for ForAllValues")
})
t.Run("ForAllValues:DateWithLabelsAsStrings", func(t *testing.T) {
policy := &PolicyDocument{
Version: "2012-10-17",
Statement: []Statement{
{
Sid: "AllowDateStrings",
Effect: "Allow",
Action: []string{"sts:AssumeRole"},
Condition: map[string]map[string]interface{}{
"ForAllValues:DateGreaterThan": {
"aws:CurrentTime": "2020-01-01T00:00:00Z",
},
},
},
},
}
evalCtx := &EvaluationContext{
Principal: "user",
Action: "sts:AssumeRole",
Resource: "arn:aws:iam::role/test-role",
RequestContext: map[string]interface{}{
"aws:CurrentTime": []string{"2021-01-01T00:00:00Z", "2022-01-01T00:00:00Z"},
},
}
result, err := engine.EvaluateTrustPolicy(context.Background(), policy, evalCtx)
require.NoError(t, err)
assert.Equal(t, EffectAllow, result.Effect, "Should allow when date context is a slice of strings")
})
t.Run("ForAllValues:BoolWithLabelsAsStrings", func(t *testing.T) {
policy := &PolicyDocument{
Version: "2012-10-17",
Statement: []Statement{
{
Sid: "AllowBoolStrings",
Effect: "Allow",
Action: []string{"sts:AssumeRole"},
Condition: map[string]map[string]interface{}{
"ForAllValues:Bool": {
"aws:SecureTransport": "true",
},
},
},
},
}
evalCtx := &EvaluationContext{
Principal: "user",
Action: "sts:AssumeRole",
Resource: "arn:aws:iam::role/test-role",
RequestContext: map[string]interface{}{
"aws:SecureTransport": []string{"true", "true"},
},
}
result, err := engine.EvaluateTrustPolicy(context.Background(), policy, evalCtx)
require.NoError(t, err)
assert.Equal(t, EffectAllow, result.Effect, "Should allow when bool context is a slice of strings")
})
t.Run("StringEqualsIgnoreCaseWithVariable", func(t *testing.T) {
policyDoc := &PolicyDocument{
Version: "2012-10-17",
Statement: []Statement{
{
Sid: "AllowVar",
Effect: "Allow",
Action: []string{"s3:GetObject"},
Resource: []string{"arn:aws:s3:::bucket/*"},
Condition: map[string]map[string]interface{}{
"StringEqualsIgnoreCase": {
"s3:prefix": "${aws:username}/",
},
},
},
},
}
err := engine.AddPolicy("", "var-policy", policyDoc)
require.NoError(t, err)
evalCtx := &EvaluationContext{
Principal: "user",
Action: "s3:GetObject",
Resource: "arn:aws:s3:::bucket/ALICE/file.txt",
RequestContext: map[string]interface{}{
"s3:prefix": "ALICE/",
"aws:username": "alice",
},
}
result, err := engine.Evaluate(context.Background(), "", evalCtx, []string{"var-policy"})
require.NoError(t, err)
assert.Equal(t, EffectAllow, result.Effect, "Should allow when variable expands and matches case-insensitively")
})
t.Run("StringLike:CaseSensitivity", func(t *testing.T) {
policyDoc := &PolicyDocument{
Version: "2012-10-17",
Statement: []Statement{
{
Sid: "AllowCaseSensitiveLike",
Effect: "Allow",
Action: []string{"s3:GetObject"},
Resource: []string{"arn:aws:s3:::bucket/*"},
Condition: map[string]map[string]interface{}{
"StringLike": {
"s3:prefix": "Project/*",
},
},
},
},
}
err := engine.AddPolicy("", "like-policy", policyDoc)
require.NoError(t, err)
// Match: Case sensitive match
evalCtxMatch := &EvaluationContext{
Principal: "user",
Action: "s3:GetObject",
Resource: "arn:aws:s3:::bucket/Project/file.txt",
RequestContext: map[string]interface{}{
"s3:prefix": "Project/data",
},
}
resultMatch, err := engine.Evaluate(context.Background(), "", evalCtxMatch, []string{"like-policy"})
require.NoError(t, err)
assert.Equal(t, EffectAllow, resultMatch.Effect, "Should allow when case matches exactly")
// Fail: Case insensitive match (should fail for StringLike)
evalCtxFail := &EvaluationContext{
Principal: "user",
Action: "s3:GetObject",
Resource: "arn:aws:s3:::bucket/project/file.txt",
RequestContext: map[string]interface{}{
"s3:prefix": "project/data", // lowercase 'p'
},
}
resultFail, err := engine.Evaluate(context.Background(), "", evalCtxFail, []string{"like-policy"})
require.NoError(t, err)
assert.Equal(t, EffectDeny, resultFail.Effect, "Should deny when case does not match for StringLike")
})
t.Run("NumericNotEquals:Logic", func(t *testing.T) {
policy := &PolicyDocument{
Version: "2012-10-17",
Statement: []Statement{
{
Sid: "DenySpecificAges",
Effect: "Allow",
Action: []string{"sts:AssumeRole"},
Resource: []string{"*"},
Condition: map[string]map[string]interface{}{
"ForAllValues:NumericNotEquals": {
"aws:MultiFactorAuthAge": []string{"3600", "7200"},
},
},
},
},
}
err := engine.AddPolicy("", "numeric-not-equals-policy", policy)
require.NoError(t, err)
// Fail: One age matches an excluded value (3600)
evalCtxFail := &EvaluationContext{
Principal: "user",
Action: "sts:AssumeRole",
Resource: "arn:aws:iam::role/test-role",
RequestContext: map[string]interface{}{
"aws:MultiFactorAuthAge": []string{"3600", "1800"},
},
}
resultFail, err := engine.Evaluate(context.Background(), "", evalCtxFail, []string{"numeric-not-equals-policy"})
require.NoError(t, err)
assert.Equal(t, EffectDeny, resultFail.Effect, "Should deny when one age matches an excluded value")
// Pass: No age matches any excluded value
evalCtxPass := &EvaluationContext{
Principal: "user",
Action: "sts:AssumeRole",
Resource: "arn:aws:iam::role/test-role",
RequestContext: map[string]interface{}{
"aws:MultiFactorAuthAge": []string{"1800", "900"},
},
}
resultPass, err := engine.Evaluate(context.Background(), "", evalCtxPass, []string{"numeric-not-equals-policy"})
require.NoError(t, err)
assert.Equal(t, EffectAllow, resultPass.Effect, "Should allow when no age matches excluded values")
})
t.Run("DateNotEquals:Logic", func(t *testing.T) {
policy := &PolicyDocument{
Version: "2012-10-17",
Statement: []Statement{
{
Sid: "DenySpecificTimes",
Effect: "Allow",
Action: []string{"sts:AssumeRole"},
Resource: []string{"*"},
Condition: map[string]map[string]interface{}{
"ForAllValues:DateNotEquals": {
"aws:CurrentTime": []string{"2024-01-01T00:00:00Z", "2024-01-02T00:00:00Z"},
},
},
},
},
}
err := engine.AddPolicy("", "date-not-equals-policy", policy)
require.NoError(t, err)
// Fail: One time matches an excluded value
evalCtxFail := &EvaluationContext{
Principal: "user",
Action: "sts:AssumeRole",
Resource: "arn:aws:iam::role/test-role",
RequestContext: map[string]interface{}{
"aws:CurrentTime": []string{"2024-01-01T00:00:00Z", "2024-01-03T00:00:00Z"},
},
}
resultFail, err := engine.Evaluate(context.Background(), "", evalCtxFail, []string{"date-not-equals-policy"})
require.NoError(t, err)
assert.Equal(t, EffectDeny, resultFail.Effect, "Should deny when one date matches an excluded value")
})
t.Run("IpAddress:SetOperators", func(t *testing.T) {
policy := &PolicyDocument{
Version: "2012-10-17",
Statement: []Statement{
{
Sid: "AllowSpecificIPs",
Effect: "Allow",
Action: []string{"s3:GetObject"},
Resource: []string{"*"},
Condition: map[string]map[string]interface{}{
"ForAllValues:IpAddress": {
"aws:SourceIp": []string{"192.168.1.0/24", "10.0.0.1"},
},
},
},
},
}
err := engine.AddPolicy("", "ip-set-policy", policy)
require.NoError(t, err)
// Match: All source IPs are in allowed ranges
evalCtxMatch := &EvaluationContext{
Principal: "user",
Action: "s3:GetObject",
Resource: "arn:aws:s3:::bucket/file.txt",
RequestContext: map[string]interface{}{
"aws:SourceIp": []string{"192.168.1.10", "10.0.0.1"},
},
}
resultMatch, err := engine.Evaluate(context.Background(), "", evalCtxMatch, []string{"ip-set-policy"})
require.NoError(t, err)
assert.Equal(t, EffectAllow, resultMatch.Effect)
// Fail: One source IP is NOT in allowed ranges
evalCtxFail := &EvaluationContext{
Principal: "user",
Action: "s3:GetObject",
Resource: "arn:aws:s3:::bucket/file.txt",
RequestContext: map[string]interface{}{
"aws:SourceIp": []string{"192.168.1.10", "172.16.0.1"},
},
}
resultFail, err := engine.Evaluate(context.Background(), "", evalCtxFail, []string{"ip-set-policy"})
require.NoError(t, err)
assert.Equal(t, EffectDeny, resultFail.Effect)
// ForAnyValue: IPAddress
policyAny := &PolicyDocument{
Version: "2012-10-17",
Statement: []Statement{
{
Sid: "AllowAnySpecificIPs",
Effect: "Allow",
Action: []string{"s3:GetObject"},
Resource: []string{"*"},
Condition: map[string]map[string]interface{}{
"ForAnyValue:IpAddress": {
"aws:SourceIp": []string{"192.168.1.0/24"},
},
},
},
},
}
err = engine.AddPolicy("", "ip-any-policy", policyAny)
require.NoError(t, err)
evalCtxAnyMatch := &EvaluationContext{
Principal: "user",
Action: "s3:GetObject",
Resource: "arn:aws:s3:::bucket/file.txt",
RequestContext: map[string]interface{}{
"aws:SourceIp": []string{"192.168.1.10", "172.16.0.1"},
},
}
resultAnyMatch, err := engine.Evaluate(context.Background(), "", evalCtxAnyMatch, []string{"ip-any-policy"})
require.NoError(t, err)
assert.Equal(t, EffectAllow, resultAnyMatch.Effect)
})
t.Run("IpAddress:SingleStringValue", func(t *testing.T) {
policy := &PolicyDocument{
Version: "2012-10-17",
Statement: []Statement{
{
Sid: "AllowSingleIP",
Effect: "Allow",
Action: []string{"s3:GetObject"},
Resource: []string{"*"},
Condition: map[string]map[string]interface{}{
"IpAddress": {
"aws:SourceIp": "192.168.1.1",
},
},
},
},
}
err := engine.AddPolicy("", "ip-single-policy", policy)
require.NoError(t, err)
evalCtxMatch := &EvaluationContext{
Principal: "user",
Action: "s3:GetObject",
Resource: "arn:aws:s3:::bucket/file.txt",
RequestContext: map[string]interface{}{
"aws:SourceIp": "192.168.1.1",
},
}
resultMatch, err := engine.Evaluate(context.Background(), "", evalCtxMatch, []string{"ip-single-policy"})
require.NoError(t, err)
assert.Equal(t, EffectAllow, resultMatch.Effect)
evalCtxNoMatch := &EvaluationContext{
Principal: "user",
Action: "s3:GetObject",
Resource: "arn:aws:s3:::bucket/file.txt",
RequestContext: map[string]interface{}{
"aws:SourceIp": "10.0.0.1",
},
}
resultNoMatch, err := engine.Evaluate(context.Background(), "", evalCtxNoMatch, []string{"ip-single-policy"})
require.NoError(t, err)
assert.Equal(t, EffectDeny, resultNoMatch.Effect)
})
t.Run("Bool:StringSlicePolicyValues", func(t *testing.T) {
policy := &PolicyDocument{
Version: "2012-10-17",
Statement: []Statement{
{
Sid: "AllowWithBoolStrings",
Effect: "Allow",
Action: []string{"s3:GetObject"},
Resource: []string{"*"},
Condition: map[string]map[string]interface{}{
"Bool": {
"aws:SecureTransport": []string{"true", "false"},
},
},
},
},
}
err := engine.AddPolicy("", "bool-string-slice-policy", policy)
require.NoError(t, err)
evalCtx := &EvaluationContext{
Principal: "user",
Action: "s3:GetObject",
Resource: "arn:aws:s3:::bucket/file.txt",
RequestContext: map[string]interface{}{
"aws:SecureTransport": "true",
},
}
result, err := engine.Evaluate(context.Background(), "", evalCtx, []string{"bool-string-slice-policy"})
require.NoError(t, err)
assert.Equal(t, EffectAllow, result.Effect)
})
t.Run("StringEqualsIgnoreCase:StringSlicePolicyValues", func(t *testing.T) {
policy := &PolicyDocument{
Version: "2012-10-17",
Statement: []Statement{
{
Sid: "AllowWithIgnoreCaseStrings",
Effect: "Allow",
Action: []string{"s3:GetObject"},
Resource: []string{"*"},
Condition: map[string]map[string]interface{}{
"StringEqualsIgnoreCase": {
"s3:x-amz-server-side-encryption": []string{"AES256", "aws:kms"},
},
},
},
},
}
err := engine.AddPolicy("", "string-ignorecase-slice-policy", policy)
require.NoError(t, err)
evalCtx := &EvaluationContext{
Principal: "user",
Action: "s3:GetObject",
Resource: "arn:aws:s3:::bucket/file.txt",
RequestContext: map[string]interface{}{
"s3:x-amz-server-side-encryption": "aes256",
},
}
result, err := engine.Evaluate(context.Background(), "", evalCtx, []string{"string-ignorecase-slice-policy"})
require.NoError(t, err)
assert.Equal(t, EffectAllow, result.Effect)
})
t.Run("IpAddress:CustomContextKey", func(t *testing.T) {
policy := &PolicyDocument{
Version: "2012-10-17",
Statement: []Statement{
{
Sid: "AllowCustomIPKey",
Effect: "Allow",
Action: []string{"s3:GetObject"},
Resource: []string{"*"},
Condition: map[string]map[string]interface{}{
"IpAddress": {
"custom:VpcIp": "10.0.0.0/16",
},
},
},
},
}
err := engine.AddPolicy("", "ip-custom-key-policy", policy)
require.NoError(t, err)
evalCtxMatch := &EvaluationContext{
Principal: "user",
Action: "s3:GetObject",
Resource: "arn:aws:s3:::bucket/file.txt",
RequestContext: map[string]interface{}{
"custom:VpcIp": "10.0.5.1",
},
}
resultMatch, err := engine.Evaluate(context.Background(), "", evalCtxMatch, []string{"ip-custom-key-policy"})
require.NoError(t, err)
assert.Equal(t, EffectAllow, resultMatch.Effect)
evalCtxNoMatch := &EvaluationContext{
Principal: "user",
Action: "s3:GetObject",
Resource: "arn:aws:s3:::bucket/file.txt",
RequestContext: map[string]interface{}{
"custom:VpcIp": "192.168.1.1",
},
}
resultNoMatch, err := engine.Evaluate(context.Background(), "", evalCtxNoMatch, []string{"ip-custom-key-policy"})
require.NoError(t, err)
assert.Equal(t, EffectDeny, resultNoMatch.Effect)
})
}

101
weed/iam/policy/negation_test.go

@ -0,0 +1,101 @@
package policy
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNegationSetOperators(t *testing.T) {
engine := setupTestPolicyEngine(t)
t.Run("ForAllValues:StringNotEquals", func(t *testing.T) {
policy := &PolicyDocument{
Version: "2012-10-17",
Statement: []Statement{
{
Sid: "DenyAdmin",
Effect: "Allow",
Action: []string{"sts:AssumeRole"},
Condition: map[string]map[string]interface{}{
"ForAllValues:StringNotEquals": {
"oidc:roles": []string{"Admin"},
},
},
},
},
}
// All roles are NOT "Admin" -> Should Allow
evalCtxAllow := &EvaluationContext{
Principal: "user",
Action: "sts:AssumeRole",
Resource: "arn:aws:iam::role/test-role",
RequestContext: map[string]interface{}{
"oidc:roles": []string{"User", "Developer"},
},
}
resultAllow, err := engine.EvaluateTrustPolicy(context.Background(), policy, evalCtxAllow)
require.NoError(t, err)
assert.Equal(t, EffectAllow, resultAllow.Effect, "Should allow when ALL roles satisfy StringNotEquals Admin")
// One role is "Admin" -> Should Deny
evalCtxDeny := &EvaluationContext{
Principal: "user",
Action: "sts:AssumeRole",
Resource: "arn:aws:iam::role/test-role",
RequestContext: map[string]interface{}{
"oidc:roles": []string{"Admin", "User"},
},
}
resultDeny, err := engine.EvaluateTrustPolicy(context.Background(), policy, evalCtxDeny)
require.NoError(t, err)
assert.Equal(t, EffectDeny, resultDeny.Effect, "Should deny when one role is Admin and fails StringNotEquals")
})
t.Run("ForAnyValue:StringNotEquals", func(t *testing.T) {
policy := &PolicyDocument{
Version: "2012-10-17",
Statement: []Statement{
{
Sid: "Requirement",
Effect: "Allow",
Action: []string{"sts:AssumeRole"},
Condition: map[string]map[string]interface{}{
"ForAnyValue:StringNotEquals": {
"oidc:roles": []string{"Prohibited"},
},
},
},
},
}
// At least one role is NOT prohibited -> Should Allow
evalCtxAllow := &EvaluationContext{
Principal: "user",
Action: "sts:AssumeRole",
Resource: "arn:aws:iam::role/test-role",
RequestContext: map[string]interface{}{
"oidc:roles": []string{"Prohibited", "Allowed"},
},
}
resultAllow, err := engine.EvaluateTrustPolicy(context.Background(), policy, evalCtxAllow)
require.NoError(t, err)
assert.Equal(t, EffectAllow, resultAllow.Effect, "Should allow when at least one role is NOT Prohibited")
// All roles are Prohibited -> Should Deny
evalCtxDeny := &EvaluationContext{
Principal: "user",
Action: "sts:AssumeRole",
Resource: "arn:aws:iam::role/test-role",
RequestContext: map[string]interface{}{
"oidc:roles": []string{"Prohibited", "Prohibited"},
},
}
resultDeny, err := engine.EvaluateTrustPolicy(context.Background(), policy, evalCtxDeny)
require.NoError(t, err)
assert.Equal(t, EffectDeny, resultDeny.Effect, "Should deny when ALL roles are Prohibited")
})
}

914
weed/iam/policy/policy_engine.go
File diff suppressed because it is too large
View File

4
weed/iam/policy/policy_variable_matching_test.go

@ -156,7 +156,7 @@ func TestActionResourceConsistencyWithStringConditions(t *testing.T) {
Action: []string{"S3:GET*"}, // Uppercase action pattern Action: []string{"S3:GET*"}, // Uppercase action pattern
Resource: []string{"arn:aws:s3:::TEST-BUCKET/*"}, // Uppercase resource pattern Resource: []string{"arn:aws:s3:::TEST-BUCKET/*"}, // Uppercase resource pattern
Condition: map[string]map[string]interface{}{ Condition: map[string]map[string]interface{}{
"StringLike": {
"StringLikeIgnoreCase": {
"s3:RequestedRegion": "US-*", // Uppercase condition pattern "s3:RequestedRegion": "US-*", // Uppercase condition pattern
}, },
}, },
@ -184,7 +184,7 @@ func TestActionResourceConsistencyWithStringConditions(t *testing.T) {
"Actions, Resources, and Conditions should all use case-insensitive AWS IAM matching") "Actions, Resources, and Conditions should all use case-insensitive AWS IAM matching")
// Verify that matching statements were found // Verify that matching statements were found
assert.Len(t, result.MatchingStatements, 1,
require.Len(t, result.MatchingStatements, 1,
"Should have exactly one matching statement") "Should have exactly one matching statement")
assert.Equal(t, "Allow", string(result.MatchingStatements[0].Effect), assert.Equal(t, "Allow", string(result.MatchingStatements[0].Effect),
"Matching statement should have Allow effect") "Matching statement should have Allow effect")

Loading…
Cancel
Save