4 changed files with 1517 additions and 189 deletions
-
687weed/iam/policy/condition_set_test.go
-
101weed/iam/policy/negation_test.go
-
914weed/iam/policy/policy_engine.go
-
4weed/iam/policy/policy_variable_matching_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) |
|||
}) |
|||
} |
|||
@ -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
File diff suppressed because it is too large
View File
Write
Preview
Loading…
Cancel
Save
Reference in new issue