Merge branch 'master' of https://github.com/seaweedfs/seaweedfs
This commit is contained in:
687
weed/iam/policy/condition_set_test.go
Normal file
687
weed/iam/policy/condition_set_test.go
Normal file
@@ -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
Normal file
101
weed/iam/policy/negation_test.go
Normal file
@@ -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")
|
||||
})
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -156,7 +156,7 @@ func TestActionResourceConsistencyWithStringConditions(t *testing.T) {
|
||||
Action: []string{"S3:GET*"}, // Uppercase action pattern
|
||||
Resource: []string{"arn:aws:s3:::TEST-BUCKET/*"}, // Uppercase resource pattern
|
||||
Condition: map[string]map[string]interface{}{
|
||||
"StringLike": {
|
||||
"StringLikeIgnoreCase": {
|
||||
"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")
|
||||
|
||||
// Verify that matching statements were found
|
||||
assert.Len(t, result.MatchingStatements, 1,
|
||||
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")
|
||||
|
||||
Reference in New Issue
Block a user