Files
seaweedFS/weed/iam/policy/policy_engine_principal_test.go
Chris Lu d75162370c Fix trust policy wildcard principal handling (#7970)
* Fix trust policy wildcard principal handling

This change fixes the trust policy validation to properly support
AWS-standard wildcard principals like {"Federated": "*"}.

Previously, the evaluatePrincipalValue() function would check for
context existence before evaluating wildcards, causing wildcard
principals to fail when the context key didn't exist. This forced
users to use the plain "*" workaround instead of the more specific
{"Federated": "*"} format.

Changes:
- Modified evaluatePrincipalValue() to check for "*" FIRST before
  validating against context
- Added support for wildcards in principal arrays
- Added comprehensive tests for wildcard principal handling
- All existing tests continue to pass (no regressions)

This matches AWS IAM behavior where "*" in a principal field means
"allow any value" without requiring context validation.

Fixes: https://github.com/seaweedfs/seaweedfs/issues/7917

* Refactor: Move Principal matching to PolicyEngine

This refactoring consolidates all policy evaluation logic into the
PolicyEngine, improving code organization and eliminating duplication.

Changes:
- Added matchesPrincipal() and evaluatePrincipalValue() to PolicyEngine
- Added EvaluateTrustPolicy() method for direct trust policy evaluation
- Updated statementMatches() to check Principal field when present
- Made resource matching optional (trust policies don't have Resources)
- Simplified evaluateTrustPolicy() in iam_manager.go to delegate to PolicyEngine
- Removed ~170 lines of duplicate code from iam_manager.go

Benefits:
- Single source of truth for all policy evaluation
- Better code reusability and maintainability
- Consistent evaluation rules for all policy types
- Easier to test and debug

All tests pass with no regressions.

* Make PolicyEngine AWS-compatible and add unit tests

Changes:
1. AWS-Compatible Context Keys:
   - Changed "seaweed:FederatedProvider" -> "aws:FederatedProvider"
   - Changed "seaweed:AWSPrincipal" -> "aws:PrincipalArn"
   - Changed "seaweed:ServicePrincipal" -> "aws:PrincipalServiceName"
   - This ensures 100% AWS compatibility for trust policies

2. Added Comprehensive Unit Tests:
   - TestPrincipalMatching: 8 test cases for Principal matching
   - TestEvaluatePrincipalValue: 7 test cases for value evaluation
   - TestTrustPolicyEvaluation: 6 test cases for trust policy evaluation
   - TestGetPrincipalContextKey: 4 test cases for context key mapping
   - Total: 25 new unit tests for PolicyEngine

All tests pass:
- Policy engine tests: 54 passed
- Integration tests: 9 passed
- Total: 63 tests passing

* Update context keys to standard AWS/OIDC formats

Replaced remaining seaweed: context keys with standard AWS and OIDC
keys to ensure 100% compatibility with AWS IAM policies.

Mappings:
- seaweed:TokenIssuer -> oidc:iss
- seaweed:Issuer -> oidc:iss
- seaweed:Subject -> oidc:sub
- seaweed:SourceIP -> aws:SourceIp

Also updated unit tests to reflect these changes.

All 63 tests pass successfully.

* Add advanced policy tests for variable substitution and conditions

Added comprehensive tests inspired by AWS IAM patterns:
- TestPolicyVariableSubstitution: Tests ${oidc:sub} variable in resources
- TestConditionWithNumericComparison: Tests sts:DurationSeconds condition
- TestMultipleConditionOperators: Tests combining StringEquals and StringLike

Results:
- TestMultipleConditionOperators:  All 3 subtests pass
- Other tests reveal need for sts:DurationSeconds context population

These tests validate the PolicyEngine's ability to handle complex
AWS-compatible policy scenarios.

* Fix federated provider context and add DurationSeconds support

Changes:
- Use iss claim as aws:FederatedProvider (AWS standard)
- Add sts:DurationSeconds to trust policy evaluation context
- TestPolicyVariableSubstitution now passes 

Remaining work:
- TestConditionWithNumericComparison partially works (1/3 pass)
- Need to investigate NumericLessThanEquals evaluation

* Update trust policies to use issuer URL for AWS compatibility

Changed trust policy from using provider name ("test-oidc") to
using the issuer URL ("https://test-issuer.com") to match AWS
standard behavior where aws:FederatedProvider contains the OIDC
issuer URL.

Test Results:
- 10/12 test suites passing
- TestFullOIDCWorkflow:  All subtests pass
- TestPolicyEnforcement:  All subtests pass
- TestSessionExpiration:  Pass
- TestPolicyVariableSubstitution:  Pass
- TestMultipleConditionOperators:  All subtests pass

Remaining work:
- TestConditionWithNumericComparison needs investigation
- One subtest in TestTrustPolicyValidation needs fix

* Fix S3 API tests for AWS compatibility

Updated all S3 API tests to use AWS-compatible context keys and
trust policy principals:

Changes:
- seaweed:SourceIP → aws:SourceIp (IP-based conditions)
- Federated: "test-oidc" → "https://test-issuer.com" (trust policies)

Test Results:
- TestS3EndToEndWithJWT:  All 13 subtests pass
- TestIPBasedPolicyEnforcement:  All 3 subtests pass

This ensures policies are 100% AWS-compatible and portable.

* Fix ValidateTrustPolicy for AWS compatibility

Updated ValidateTrustPolicy method to check for:
- OIDC: issuer URL ("https://test-issuer.com")
- LDAP: provider name ("test-ldap")
- Wildcard: "*"

Test Results:
- TestTrustPolicyValidation:  All 3 subtests pass

This ensures trust policy validation uses the same AWS-compatible
principals as the PolicyEngine.

* Fix multipart and presigned URL tests for AWS compatibility

Updated trust policies in:
- s3_multipart_iam_test.go
- s3_presigned_url_iam_test.go

Changed "Federated": "test-oidc" → "https://test-issuer.com"

Test Results:
- TestMultipartIAMValidation:  All 7 subtests pass
- TestPresignedURLIAMValidation:  All 4 subtests pass
- TestPresignedURLGeneration:  All 4 subtests pass
- TestPresignedURLExpiration:  All 4 subtests pass
- TestPresignedURLSecurityPolicy:  All 4 subtests pass

All S3 API tests now use AWS-compatible trust policies.

* Fix numeric condition evaluation and trust policy validation interface

Major updates to ensure robust AWS-compatible policy evaluation:
1.  **Policy Engine**: Added support for `int` and `int64` types in `evaluateNumericCondition`, fixing issues where raw numbers in policy documents caused evaluation failures.
2.  **Trust Policy Validation**: Updated `TrustPolicyValidator` interface and `STSService` to propagate `DurationSeconds` correctly during the double-validation flow (Validation -> STS -> Validation callback).
3.  **IAM Manager**: Updated implementation to match the new interface and correctly pass `sts:DurationSeconds` context key.

Test Results:
- TestConditionWithNumericComparison:  All 3 subtests pass
- All IAM and S3 integration tests pass (100%)

This resolves the final edge case with DurationSeconds numeric conditions.

* Fix MockTrustPolicyValidator interface and unreachable code warnings

Updates:
1. Updated MockTrustPolicyValidator.ValidateTrustPolicyForWebIdentity to match new interface signature with durationSeconds parameter
2. Removed unreachable code after infinite loops in filer_backup.go and filer_meta_backup.go to satisfy linter

Test Results:
- All STS tests pass 
- Build warnings resolved 

* Refactor matchesPrincipal to consolidate array handling logic

Consolidated duplicated logic for []interface{} and []string types by converting them to a unified []interface{} upfront.

* Fix malformed AWS docs URL in iam_manager.go comment

* dup

* Enhance IAM integration tests with negative cases and interface array support

Added test cases to TestTrustPolicyWildcardPrincipal to:
1. Verify rejection of roles when principal context does not match (negative test)
2. Verify support for principal arrays as []interface{} (simulating JSON unmarshaled roles)

* Fix syntax errors in filer_backup and filer_meta_backup

Restored missing closing braces for for-loops and re-added return statements.
The previous attempt to remove unreachable code accidentally broke the function structure.
Build now passes successfully.
2026-01-05 15:55:24 -08:00

422 lines
10 KiB
Go

package policy
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestPrincipalMatching tests the matchesPrincipal method
func TestPrincipalMatching(t *testing.T) {
engine := setupTestPolicyEngine(t)
tests := []struct {
name string
principal interface{}
evalCtx *EvaluationContext
want bool
}{
{
name: "plain wildcard principal",
principal: "*",
evalCtx: &EvaluationContext{
RequestContext: map[string]interface{}{},
},
want: true,
},
{
name: "structured wildcard federated principal",
principal: map[string]interface{}{
"Federated": "*",
},
evalCtx: &EvaluationContext{
RequestContext: map[string]interface{}{},
},
want: true,
},
{
name: "wildcard in array",
principal: map[string]interface{}{
"Federated": []interface{}{"specific-provider", "*"},
},
evalCtx: &EvaluationContext{
RequestContext: map[string]interface{}{},
},
want: true,
},
{
name: "specific federated provider match",
principal: map[string]interface{}{
"Federated": "https://example.com/oidc",
},
evalCtx: &EvaluationContext{
RequestContext: map[string]interface{}{
"aws:FederatedProvider": "https://example.com/oidc",
},
},
want: true,
},
{
name: "specific federated provider no match",
principal: map[string]interface{}{
"Federated": "https://example.com/oidc",
},
evalCtx: &EvaluationContext{
RequestContext: map[string]interface{}{
"aws:FederatedProvider": "https://other.com/oidc",
},
},
want: false,
},
{
name: "array with specific provider match",
principal: map[string]interface{}{
"Federated": []string{"https://provider1.com", "https://provider2.com"},
},
evalCtx: &EvaluationContext{
RequestContext: map[string]interface{}{
"aws:FederatedProvider": "https://provider2.com",
},
},
want: true,
},
{
name: "AWS principal match",
principal: map[string]interface{}{
"AWS": "arn:aws:iam::123456789012:user/alice",
},
evalCtx: &EvaluationContext{
RequestContext: map[string]interface{}{
"aws:PrincipalArn": "arn:aws:iam::123456789012:user/alice",
},
},
want: true,
},
{
name: "Service principal match",
principal: map[string]interface{}{
"Service": "s3.amazonaws.com",
},
evalCtx: &EvaluationContext{
RequestContext: map[string]interface{}{
"aws:PrincipalServiceName": "s3.amazonaws.com",
},
},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := engine.matchesPrincipal(tt.principal, tt.evalCtx)
assert.Equal(t, tt.want, result, "Principal matching failed for: %s", tt.name)
})
}
}
// TestEvaluatePrincipalValue tests the evaluatePrincipalValue method
func TestEvaluatePrincipalValue(t *testing.T) {
engine := setupTestPolicyEngine(t)
tests := []struct {
name string
principalValue interface{}
contextKey string
evalCtx *EvaluationContext
want bool
}{
{
name: "wildcard string",
principalValue: "*",
contextKey: "aws:FederatedProvider",
evalCtx: &EvaluationContext{
RequestContext: map[string]interface{}{},
},
want: true,
},
{
name: "specific string match",
principalValue: "https://example.com",
contextKey: "aws:FederatedProvider",
evalCtx: &EvaluationContext{
RequestContext: map[string]interface{}{
"aws:FederatedProvider": "https://example.com",
},
},
want: true,
},
{
name: "specific string no match",
principalValue: "https://example.com",
contextKey: "aws:FederatedProvider",
evalCtx: &EvaluationContext{
RequestContext: map[string]interface{}{
"aws:FederatedProvider": "https://other.com",
},
},
want: false,
},
{
name: "wildcard in array",
principalValue: []interface{}{"provider1", "*"},
contextKey: "aws:FederatedProvider",
evalCtx: &EvaluationContext{
RequestContext: map[string]interface{}{},
},
want: true,
},
{
name: "array match",
principalValue: []string{"provider1", "provider2", "provider3"},
contextKey: "aws:FederatedProvider",
evalCtx: &EvaluationContext{
RequestContext: map[string]interface{}{
"aws:FederatedProvider": "provider2",
},
},
want: true,
},
{
name: "array no match",
principalValue: []string{"provider1", "provider2"},
contextKey: "aws:FederatedProvider",
evalCtx: &EvaluationContext{
RequestContext: map[string]interface{}{
"aws:FederatedProvider": "provider3",
},
},
want: false,
},
{
name: "missing context key",
principalValue: "specific-value",
contextKey: "aws:FederatedProvider",
evalCtx: &EvaluationContext{
RequestContext: map[string]interface{}{},
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := engine.evaluatePrincipalValue(tt.principalValue, tt.evalCtx, tt.contextKey)
assert.Equal(t, tt.want, result, "Principal value evaluation failed for: %s", tt.name)
})
}
}
// TestTrustPolicyEvaluation tests the EvaluateTrustPolicy method
func TestTrustPolicyEvaluation(t *testing.T) {
engine := setupTestPolicyEngine(t)
tests := []struct {
name string
trustPolicy *PolicyDocument
evalCtx *EvaluationContext
wantEffect Effect
wantErr bool
}{
{
name: "wildcard federated principal allows any provider",
trustPolicy: &PolicyDocument{
Version: "2012-10-17",
Statement: []Statement{
{
Effect: "Allow",
Principal: map[string]interface{}{
"Federated": "*",
},
Action: []string{"sts:AssumeRoleWithWebIdentity"},
},
},
},
evalCtx: &EvaluationContext{
Action: "sts:AssumeRoleWithWebIdentity",
RequestContext: map[string]interface{}{
"aws:FederatedProvider": "https://any-provider.com",
},
},
wantEffect: EffectAllow,
wantErr: false,
},
{
name: "specific federated principal matches",
trustPolicy: &PolicyDocument{
Version: "2012-10-17",
Statement: []Statement{
{
Effect: "Allow",
Principal: map[string]interface{}{
"Federated": "https://example.com/oidc",
},
Action: []string{"sts:AssumeRoleWithWebIdentity"},
},
},
},
evalCtx: &EvaluationContext{
Action: "sts:AssumeRoleWithWebIdentity",
RequestContext: map[string]interface{}{
"aws:FederatedProvider": "https://example.com/oidc",
},
},
wantEffect: EffectAllow,
wantErr: false,
},
{
name: "specific federated principal does not match",
trustPolicy: &PolicyDocument{
Version: "2012-10-17",
Statement: []Statement{
{
Effect: "Allow",
Principal: map[string]interface{}{
"Federated": "https://example.com/oidc",
},
Action: []string{"sts:AssumeRoleWithWebIdentity"},
},
},
},
evalCtx: &EvaluationContext{
Action: "sts:AssumeRoleWithWebIdentity",
RequestContext: map[string]interface{}{
"aws:FederatedProvider": "https://other.com/oidc",
},
},
wantEffect: EffectDeny,
wantErr: false,
},
{
name: "plain wildcard principal",
trustPolicy: &PolicyDocument{
Version: "2012-10-17",
Statement: []Statement{
{
Effect: "Allow",
Principal: "*",
Action: []string{"sts:AssumeRoleWithWebIdentity"},
},
},
},
evalCtx: &EvaluationContext{
Action: "sts:AssumeRoleWithWebIdentity",
RequestContext: map[string]interface{}{
"aws:FederatedProvider": "https://any-provider.com",
},
},
wantEffect: EffectAllow,
wantErr: false,
},
{
name: "trust policy with conditions",
trustPolicy: &PolicyDocument{
Version: "2012-10-17",
Statement: []Statement{
{
Effect: "Allow",
Principal: map[string]interface{}{
"Federated": "*",
},
Action: []string{"sts:AssumeRoleWithWebIdentity"},
Condition: map[string]map[string]interface{}{
"StringEquals": {
"oidc:aud": "my-app-id",
},
},
},
},
},
evalCtx: &EvaluationContext{
Action: "sts:AssumeRoleWithWebIdentity",
RequestContext: map[string]interface{}{
"aws:FederatedProvider": "https://provider.com",
"oidc:aud": "my-app-id",
},
},
wantEffect: EffectAllow,
wantErr: false,
},
{
name: "trust policy condition not met",
trustPolicy: &PolicyDocument{
Version: "2012-10-17",
Statement: []Statement{
{
Effect: "Allow",
Principal: map[string]interface{}{
"Federated": "*",
},
Action: []string{"sts:AssumeRoleWithWebIdentity"},
Condition: map[string]map[string]interface{}{
"StringEquals": {
"oidc:aud": "my-app-id",
},
},
},
},
},
evalCtx: &EvaluationContext{
Action: "sts:AssumeRoleWithWebIdentity",
RequestContext: map[string]interface{}{
"aws:FederatedProvider": "https://provider.com",
"oidc:aud": "wrong-app-id",
},
},
wantEffect: EffectDeny,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := engine.EvaluateTrustPolicy(context.Background(), tt.trustPolicy, tt.evalCtx)
if tt.wantErr {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, tt.wantEffect, result.Effect, "Trust policy evaluation failed for: %s", tt.name)
}
})
}
}
// TestGetPrincipalContextKey tests the context key mapping
func TestGetPrincipalContextKey(t *testing.T) {
tests := []struct {
name string
principalType string
want string
}{
{
name: "Federated principal",
principalType: "Federated",
want: "aws:FederatedProvider",
},
{
name: "AWS principal",
principalType: "AWS",
want: "aws:PrincipalArn",
},
{
name: "Service principal",
principalType: "Service",
want: "aws:PrincipalServiceName",
},
{
name: "Custom principal type",
principalType: "CustomType",
want: "aws:PrincipalCustomType",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := getPrincipalContextKey(tt.principalType)
assert.Equal(t, tt.want, result, "Context key mapping failed for: %s", tt.name)
})
}
}