* 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.
681 lines
20 KiB
Go
681 lines
20 KiB
Go
package integration
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/seaweedfs/seaweedfs/weed/iam/ldap"
|
|
"github.com/seaweedfs/seaweedfs/weed/iam/oidc"
|
|
"github.com/seaweedfs/seaweedfs/weed/iam/policy"
|
|
"github.com/seaweedfs/seaweedfs/weed/iam/sts"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestFullOIDCWorkflow tests the complete OIDC → STS → Policy workflow
|
|
func TestFullOIDCWorkflow(t *testing.T) {
|
|
// Set up integrated IAM system
|
|
iamManager := setupIntegratedIAMSystem(t)
|
|
|
|
// Create JWT tokens for testing with the correct issuer
|
|
validJWTToken := createTestJWT(t, "https://test-issuer.com", "test-user-123", "test-signing-key")
|
|
invalidJWTToken := createTestJWT(t, "https://invalid-issuer.com", "test-user", "wrong-key")
|
|
|
|
tests := []struct {
|
|
name string
|
|
roleArn string
|
|
sessionName string
|
|
webToken string
|
|
expectedAllow bool
|
|
testAction string
|
|
testResource string
|
|
}{
|
|
{
|
|
name: "successful role assumption with policy validation",
|
|
roleArn: "arn:aws:iam::role/S3ReadOnlyRole",
|
|
sessionName: "oidc-session",
|
|
webToken: validJWTToken,
|
|
expectedAllow: true,
|
|
testAction: "s3:GetObject",
|
|
testResource: "arn:aws:s3:::test-bucket/file.txt",
|
|
},
|
|
{
|
|
name: "role assumption denied by trust policy",
|
|
roleArn: "arn:aws:iam::role/RestrictedRole",
|
|
sessionName: "oidc-session",
|
|
webToken: validJWTToken,
|
|
expectedAllow: false,
|
|
},
|
|
{
|
|
name: "invalid token rejected",
|
|
roleArn: "arn:aws:iam::role/S3ReadOnlyRole",
|
|
sessionName: "oidc-session",
|
|
webToken: invalidJWTToken,
|
|
expectedAllow: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// Step 1: Attempt role assumption
|
|
assumeRequest := &sts.AssumeRoleWithWebIdentityRequest{
|
|
RoleArn: tt.roleArn,
|
|
WebIdentityToken: tt.webToken,
|
|
RoleSessionName: tt.sessionName,
|
|
}
|
|
|
|
response, err := iamManager.AssumeRoleWithWebIdentity(ctx, assumeRequest)
|
|
|
|
if !tt.expectedAllow {
|
|
assert.Error(t, err)
|
|
assert.Nil(t, response)
|
|
return
|
|
}
|
|
|
|
// Should succeed if expectedAllow is true
|
|
require.NoError(t, err)
|
|
require.NotNil(t, response)
|
|
require.NotNil(t, response.Credentials)
|
|
|
|
// Step 2: Test policy enforcement with assumed credentials
|
|
if tt.testAction != "" && tt.testResource != "" {
|
|
allowed, err := iamManager.IsActionAllowed(ctx, &ActionRequest{
|
|
Principal: response.AssumedRoleUser.Arn,
|
|
Action: tt.testAction,
|
|
Resource: tt.testResource,
|
|
SessionToken: response.Credentials.SessionToken,
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
assert.True(t, allowed, "Action should be allowed by role policy")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestFullLDAPWorkflow tests the complete LDAP → STS → Policy workflow
|
|
func TestFullLDAPWorkflow(t *testing.T) {
|
|
iamManager := setupIntegratedIAMSystem(t)
|
|
|
|
tests := []struct {
|
|
name string
|
|
roleArn string
|
|
sessionName string
|
|
username string
|
|
password string
|
|
expectedAllow bool
|
|
testAction string
|
|
testResource string
|
|
}{
|
|
{
|
|
name: "successful LDAP role assumption",
|
|
roleArn: "arn:aws:iam::role/LDAPUserRole",
|
|
sessionName: "ldap-session",
|
|
username: "testuser",
|
|
password: "testpass",
|
|
expectedAllow: true,
|
|
testAction: "filer:CreateEntry",
|
|
testResource: "arn:aws:filer::path/user-docs/*",
|
|
},
|
|
{
|
|
name: "invalid LDAP credentials",
|
|
roleArn: "arn:aws:iam::role/LDAPUserRole",
|
|
sessionName: "ldap-session",
|
|
username: "testuser",
|
|
password: "wrongpass",
|
|
expectedAllow: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// Step 1: Attempt role assumption with LDAP credentials
|
|
assumeRequest := &sts.AssumeRoleWithCredentialsRequest{
|
|
RoleArn: tt.roleArn,
|
|
Username: tt.username,
|
|
Password: tt.password,
|
|
RoleSessionName: tt.sessionName,
|
|
ProviderName: "test-ldap",
|
|
}
|
|
|
|
response, err := iamManager.AssumeRoleWithCredentials(ctx, assumeRequest)
|
|
|
|
if !tt.expectedAllow {
|
|
assert.Error(t, err)
|
|
assert.Nil(t, response)
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
require.NotNil(t, response)
|
|
|
|
// Step 2: Test policy enforcement
|
|
if tt.testAction != "" && tt.testResource != "" {
|
|
allowed, err := iamManager.IsActionAllowed(ctx, &ActionRequest{
|
|
Principal: response.AssumedRoleUser.Arn,
|
|
Action: tt.testAction,
|
|
Resource: tt.testResource,
|
|
SessionToken: response.Credentials.SessionToken,
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
assert.True(t, allowed)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestPolicyEnforcement tests policy evaluation for various scenarios
|
|
func TestPolicyEnforcement(t *testing.T) {
|
|
iamManager := setupIntegratedIAMSystem(t)
|
|
|
|
// Create a valid JWT token for testing
|
|
validJWTToken := createTestJWT(t, "https://test-issuer.com", "test-user-123", "test-signing-key")
|
|
|
|
// Create a session for testing
|
|
ctx := context.Background()
|
|
assumeRequest := &sts.AssumeRoleWithWebIdentityRequest{
|
|
RoleArn: "arn:aws:iam::role/S3ReadOnlyRole",
|
|
WebIdentityToken: validJWTToken,
|
|
RoleSessionName: "policy-test-session",
|
|
}
|
|
|
|
response, err := iamManager.AssumeRoleWithWebIdentity(ctx, assumeRequest)
|
|
require.NoError(t, err)
|
|
|
|
sessionToken := response.Credentials.SessionToken
|
|
principal := response.AssumedRoleUser.Arn
|
|
|
|
tests := []struct {
|
|
name string
|
|
action string
|
|
resource string
|
|
shouldAllow bool
|
|
reason string
|
|
}{
|
|
{
|
|
name: "allow read access",
|
|
action: "s3:GetObject",
|
|
resource: "arn:aws:s3:::test-bucket/file.txt",
|
|
shouldAllow: true,
|
|
reason: "S3ReadOnlyRole should allow GetObject",
|
|
},
|
|
{
|
|
name: "allow list bucket",
|
|
action: "s3:ListBucket",
|
|
resource: "arn:aws:s3:::test-bucket",
|
|
shouldAllow: true,
|
|
reason: "S3ReadOnlyRole should allow ListBucket",
|
|
},
|
|
{
|
|
name: "deny write access",
|
|
action: "s3:PutObject",
|
|
resource: "arn:aws:s3:::test-bucket/newfile.txt",
|
|
shouldAllow: false,
|
|
reason: "S3ReadOnlyRole should deny write operations",
|
|
},
|
|
{
|
|
name: "deny delete access",
|
|
action: "s3:DeleteObject",
|
|
resource: "arn:aws:s3:::test-bucket/file.txt",
|
|
shouldAllow: false,
|
|
reason: "S3ReadOnlyRole should deny delete operations",
|
|
},
|
|
{
|
|
name: "deny filer access",
|
|
action: "filer:CreateEntry",
|
|
resource: "arn:aws:filer::path/test",
|
|
shouldAllow: false,
|
|
reason: "S3ReadOnlyRole should not allow filer operations",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
allowed, err := iamManager.IsActionAllowed(ctx, &ActionRequest{
|
|
Principal: principal,
|
|
Action: tt.action,
|
|
Resource: tt.resource,
|
|
SessionToken: sessionToken,
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.shouldAllow, allowed, tt.reason)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSessionExpiration tests session expiration and cleanup
|
|
func TestSessionExpiration(t *testing.T) {
|
|
iamManager := setupIntegratedIAMSystem(t)
|
|
ctx := context.Background()
|
|
|
|
// Create a valid JWT token for testing
|
|
validJWTToken := createTestJWT(t, "https://test-issuer.com", "test-user-123", "test-signing-key")
|
|
|
|
// Create a short-lived session
|
|
assumeRequest := &sts.AssumeRoleWithWebIdentityRequest{
|
|
RoleArn: "arn:aws:iam::role/S3ReadOnlyRole",
|
|
WebIdentityToken: validJWTToken,
|
|
RoleSessionName: "expiration-test",
|
|
DurationSeconds: int64Ptr(900), // 15 minutes
|
|
}
|
|
|
|
response, err := iamManager.AssumeRoleWithWebIdentity(ctx, assumeRequest)
|
|
require.NoError(t, err)
|
|
|
|
sessionToken := response.Credentials.SessionToken
|
|
|
|
// Verify session is initially valid
|
|
allowed, err := iamManager.IsActionAllowed(ctx, &ActionRequest{
|
|
Principal: response.AssumedRoleUser.Arn,
|
|
Action: "s3:GetObject",
|
|
Resource: "arn:aws:s3:::test-bucket/file.txt",
|
|
SessionToken: sessionToken,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.True(t, allowed)
|
|
|
|
// Verify the expiration time is set correctly
|
|
assert.True(t, response.Credentials.Expiration.After(time.Now()))
|
|
assert.True(t, response.Credentials.Expiration.Before(time.Now().Add(16*time.Minute)))
|
|
|
|
// Test session expiration behavior in stateless JWT system
|
|
// In a stateless system, manual expiration is not supported
|
|
err = iamManager.ExpireSessionForTesting(ctx, sessionToken)
|
|
require.Error(t, err, "Manual session expiration should not be supported in stateless system")
|
|
assert.Contains(t, err.Error(), "manual session expiration not supported")
|
|
|
|
// Verify session is still valid (since it hasn't naturally expired)
|
|
allowed, err = iamManager.IsActionAllowed(ctx, &ActionRequest{
|
|
Principal: response.AssumedRoleUser.Arn,
|
|
Action: "s3:GetObject",
|
|
Resource: "arn:aws:s3:::test-bucket/file.txt",
|
|
SessionToken: sessionToken,
|
|
})
|
|
require.NoError(t, err, "Session should still be valid in stateless system")
|
|
assert.True(t, allowed, "Access should still be allowed since token hasn't naturally expired")
|
|
}
|
|
|
|
// TestTrustPolicyValidation tests role trust policy validation
|
|
func TestTrustPolicyValidation(t *testing.T) {
|
|
iamManager := setupIntegratedIAMSystem(t)
|
|
ctx := context.Background()
|
|
|
|
tests := []struct {
|
|
name string
|
|
roleArn string
|
|
provider string
|
|
userID string
|
|
shouldAllow bool
|
|
reason string
|
|
}{
|
|
{
|
|
name: "OIDC user allowed by trust policy",
|
|
roleArn: "arn:aws:iam::role/S3ReadOnlyRole",
|
|
provider: "oidc",
|
|
userID: "test-user-id",
|
|
shouldAllow: true,
|
|
reason: "Trust policy should allow OIDC users",
|
|
},
|
|
{
|
|
name: "LDAP user allowed by different role",
|
|
roleArn: "arn:aws:iam::role/LDAPUserRole",
|
|
provider: "ldap",
|
|
userID: "testuser",
|
|
shouldAllow: true,
|
|
reason: "Trust policy should allow LDAP users for LDAP role",
|
|
},
|
|
{
|
|
name: "Wrong provider for role",
|
|
roleArn: "arn:aws:iam::role/S3ReadOnlyRole",
|
|
provider: "ldap",
|
|
userID: "testuser",
|
|
shouldAllow: false,
|
|
reason: "S3ReadOnlyRole trust policy should reject LDAP users",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// This would test trust policy evaluation
|
|
// For now, we'll implement this as part of the IAM manager
|
|
result := iamManager.ValidateTrustPolicy(ctx, tt.roleArn, tt.provider, tt.userID)
|
|
assert.Equal(t, tt.shouldAllow, result, tt.reason)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestTrustPolicyWildcardPrincipal tests wildcard principal handling in trust policies
|
|
func TestTrustPolicyWildcardPrincipal(t *testing.T) {
|
|
iamManager := setupIntegratedIAMSystem(t)
|
|
ctx := context.Background()
|
|
|
|
// Create a role with wildcard federated principal
|
|
err := iamManager.CreateRole(ctx, "", "WildcardFederatedRole", &RoleDefinition{
|
|
RoleName: "WildcardFederatedRole",
|
|
TrustPolicy: &policy.PolicyDocument{
|
|
Version: "2012-10-17",
|
|
Statement: []policy.Statement{
|
|
{
|
|
Effect: "Allow",
|
|
Principal: map[string]interface{}{
|
|
"Federated": "*", // Wildcard should allow any federated provider
|
|
},
|
|
Action: []string{"sts:AssumeRoleWithWebIdentity"},
|
|
},
|
|
},
|
|
},
|
|
AttachedPolicies: []string{"S3ReadOnlyPolicy"},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Create a role with wildcard in array
|
|
err = iamManager.CreateRole(ctx, "", "WildcardArrayRole", &RoleDefinition{
|
|
RoleName: "WildcardArrayRole",
|
|
TrustPolicy: &policy.PolicyDocument{
|
|
Version: "2012-10-17",
|
|
Statement: []policy.Statement{
|
|
{
|
|
Effect: "Allow",
|
|
Principal: map[string]interface{}{
|
|
"Federated": []string{"specific-provider", "*"}, // Array with wildcard
|
|
},
|
|
Action: []string{"sts:AssumeRoleWithWebIdentity"},
|
|
},
|
|
},
|
|
},
|
|
AttachedPolicies: []string{"S3ReadOnlyPolicy"},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Create a role with plain wildcard principal (regression test)
|
|
err = iamManager.CreateRole(ctx, "", "PlainWildcardRole", &RoleDefinition{
|
|
RoleName: "PlainWildcardRole",
|
|
TrustPolicy: &policy.PolicyDocument{
|
|
Version: "2012-10-17",
|
|
Statement: []policy.Statement{
|
|
{
|
|
Effect: "Allow",
|
|
Principal: "*", // Plain wildcard
|
|
Action: []string{"sts:AssumeRoleWithWebIdentity"},
|
|
},
|
|
},
|
|
},
|
|
AttachedPolicies: []string{"S3ReadOnlyPolicy"},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// NEW: Create a role with specific federated principal (for negative testing)
|
|
err = iamManager.CreateRole(ctx, "", "SpecificFederatedRole", &RoleDefinition{
|
|
RoleName: "SpecificFederatedRole",
|
|
TrustPolicy: &policy.PolicyDocument{
|
|
Version: "2012-10-17",
|
|
Statement: []policy.Statement{
|
|
{
|
|
Effect: "Allow",
|
|
Principal: map[string]interface{}{
|
|
"Federated": "https://test-issuer.com",
|
|
},
|
|
Action: []string{"sts:AssumeRoleWithWebIdentity"},
|
|
},
|
|
},
|
|
},
|
|
AttachedPolicies: []string{"S3ReadOnlyPolicy"},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// NEW: Create a role with principal as []interface{} (simulating JSON unmarshaling)
|
|
err = iamManager.CreateRole(ctx, "", "InterfaceArrayRole", &RoleDefinition{
|
|
RoleName: "InterfaceArrayRole",
|
|
TrustPolicy: &policy.PolicyDocument{
|
|
Version: "2012-10-17",
|
|
Statement: []policy.Statement{
|
|
{
|
|
Effect: "Allow",
|
|
Principal: map[string]interface{}{
|
|
"Federated": []interface{}{"specific-provider", "https://test-issuer.com"},
|
|
},
|
|
Action: []string{"sts:AssumeRoleWithWebIdentity"},
|
|
},
|
|
},
|
|
},
|
|
AttachedPolicies: []string{"S3ReadOnlyPolicy"},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Create JWT token for testing
|
|
validJWTToken := createTestJWT(t, "https://test-issuer.com", "test-user-123", "test-signing-key")
|
|
|
|
tests := []struct {
|
|
name string
|
|
roleArn string
|
|
token string
|
|
shouldAllow bool
|
|
reason string
|
|
}{
|
|
{
|
|
name: "Wildcard federated principal allows any provider",
|
|
roleArn: "arn:aws:iam::role/WildcardFederatedRole",
|
|
token: validJWTToken,
|
|
shouldAllow: true,
|
|
reason: "Wildcard federated principal should allow any provider",
|
|
},
|
|
{
|
|
name: "Wildcard in array allows any provider",
|
|
roleArn: "arn:aws:iam::role/WildcardArrayRole",
|
|
token: validJWTToken,
|
|
shouldAllow: true,
|
|
reason: "Wildcard in principal array should allow any provider",
|
|
},
|
|
{
|
|
name: "Plain wildcard allows any provider (regression)",
|
|
roleArn: "arn:aws:iam::role/PlainWildcardRole",
|
|
token: validJWTToken,
|
|
shouldAllow: true,
|
|
reason: "Plain wildcard principal should still work",
|
|
},
|
|
{
|
|
name: "Non-wildcard federated principal requires matching provider",
|
|
roleArn: "arn:aws:iam::role/SpecificFederatedRole",
|
|
token: createTestJWT(t, "https://different-issuer.com", "test-user", "test-signing-key"),
|
|
shouldAllow: false,
|
|
reason: "Non-wildcard principal should still require matching provider",
|
|
},
|
|
{
|
|
name: "Interface array principal works correctly",
|
|
roleArn: "arn:aws:iam::role/InterfaceArrayRole",
|
|
token: validJWTToken,
|
|
shouldAllow: true,
|
|
reason: "Principal as []interface{} should be handled correctly",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assumeRequest := &sts.AssumeRoleWithWebIdentityRequest{
|
|
RoleArn: tt.roleArn,
|
|
WebIdentityToken: tt.token,
|
|
RoleSessionName: "wildcard-test-session",
|
|
}
|
|
|
|
response, err := iamManager.AssumeRoleWithWebIdentity(ctx, assumeRequest)
|
|
|
|
if tt.shouldAllow {
|
|
require.NoError(t, err, tt.reason)
|
|
require.NotNil(t, response)
|
|
require.NotNil(t, response.Credentials)
|
|
} else {
|
|
assert.Error(t, err, tt.reason)
|
|
assert.Nil(t, response)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Helper functions and test setup
|
|
|
|
// createTestJWT creates a test JWT token with the specified issuer, subject and signing key
|
|
func createTestJWT(t *testing.T, issuer, subject, signingKey string) string {
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
|
"iss": issuer,
|
|
"sub": subject,
|
|
"aud": "test-client-id",
|
|
"exp": time.Now().Add(time.Hour).Unix(),
|
|
"iat": time.Now().Unix(),
|
|
// Add claims that trust policy validation expects
|
|
"idp": "test-oidc", // Identity provider claim for trust policy matching
|
|
})
|
|
|
|
tokenString, err := token.SignedString([]byte(signingKey))
|
|
require.NoError(t, err)
|
|
return tokenString
|
|
}
|
|
|
|
func setupIntegratedIAMSystem(t *testing.T) *IAMManager {
|
|
// Create IAM manager with all components
|
|
manager := NewIAMManager()
|
|
|
|
// Configure and initialize
|
|
config := &IAMConfig{
|
|
STS: &sts.STSConfig{
|
|
TokenDuration: sts.FlexibleDuration{Duration: time.Hour},
|
|
MaxSessionLength: sts.FlexibleDuration{Duration: time.Hour * 12},
|
|
Issuer: "test-sts",
|
|
SigningKey: []byte("test-signing-key-32-characters-long"),
|
|
},
|
|
Policy: &policy.PolicyEngineConfig{
|
|
DefaultEffect: "Deny",
|
|
StoreType: "memory", // Use memory for unit tests
|
|
},
|
|
Roles: &RoleStoreConfig{
|
|
StoreType: "memory", // Use memory for unit tests
|
|
},
|
|
}
|
|
|
|
err := manager.Initialize(config, func() string {
|
|
return "localhost:8888" // Mock filer address for testing
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Set up test providers
|
|
setupTestProviders(t, manager)
|
|
|
|
// Set up test policies and roles
|
|
setupTestPoliciesAndRoles(t, manager)
|
|
|
|
return manager
|
|
}
|
|
|
|
func setupTestProviders(t *testing.T, manager *IAMManager) {
|
|
// Set up OIDC provider
|
|
oidcProvider := oidc.NewMockOIDCProvider("test-oidc")
|
|
oidcConfig := &oidc.OIDCConfig{
|
|
Issuer: "https://test-issuer.com",
|
|
ClientID: "test-client-id",
|
|
}
|
|
err := oidcProvider.Initialize(oidcConfig)
|
|
require.NoError(t, err)
|
|
oidcProvider.SetupDefaultTestData()
|
|
|
|
// Set up LDAP mock provider (no config needed for mock)
|
|
ldapProvider := ldap.NewMockLDAPProvider("test-ldap")
|
|
err = ldapProvider.Initialize(nil) // Mock doesn't need real config
|
|
require.NoError(t, err)
|
|
ldapProvider.SetupDefaultTestData()
|
|
|
|
// Register providers
|
|
err = manager.RegisterIdentityProvider(oidcProvider)
|
|
require.NoError(t, err)
|
|
err = manager.RegisterIdentityProvider(ldapProvider)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func setupTestPoliciesAndRoles(t *testing.T, manager *IAMManager) {
|
|
ctx := context.Background()
|
|
|
|
// Create S3 read-only policy
|
|
s3ReadPolicy := &policy.PolicyDocument{
|
|
Version: "2012-10-17",
|
|
Statement: []policy.Statement{
|
|
{
|
|
Sid: "S3ReadAccess",
|
|
Effect: "Allow",
|
|
Action: []string{"s3:GetObject", "s3:ListBucket"},
|
|
Resource: []string{
|
|
"arn:aws:s3:::*",
|
|
"arn:aws:s3:::*/*",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
err := manager.CreatePolicy(ctx, "", "S3ReadOnlyPolicy", s3ReadPolicy)
|
|
require.NoError(t, err)
|
|
|
|
// Create LDAP user policy
|
|
ldapUserPolicy := &policy.PolicyDocument{
|
|
Version: "2012-10-17",
|
|
Statement: []policy.Statement{
|
|
{
|
|
Sid: "FilerAccess",
|
|
Effect: "Allow",
|
|
Action: []string{"filer:*"},
|
|
Resource: []string{
|
|
"arn:aws:filer::path/user-docs/*",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
err = manager.CreatePolicy(ctx, "", "LDAPUserPolicy", ldapUserPolicy)
|
|
require.NoError(t, err)
|
|
|
|
// Create roles with trust policies
|
|
err = manager.CreateRole(ctx, "", "S3ReadOnlyRole", &RoleDefinition{
|
|
RoleName: "S3ReadOnlyRole",
|
|
TrustPolicy: &policy.PolicyDocument{
|
|
Version: "2012-10-17",
|
|
Statement: []policy.Statement{
|
|
{
|
|
Effect: "Allow",
|
|
Principal: map[string]interface{}{
|
|
"Federated": "https://test-issuer.com",
|
|
},
|
|
Action: []string{"sts:AssumeRoleWithWebIdentity"},
|
|
},
|
|
},
|
|
},
|
|
AttachedPolicies: []string{"S3ReadOnlyPolicy"},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = manager.CreateRole(ctx, "", "LDAPUserRole", &RoleDefinition{
|
|
RoleName: "LDAPUserRole",
|
|
TrustPolicy: &policy.PolicyDocument{
|
|
Version: "2012-10-17",
|
|
Statement: []policy.Statement{
|
|
{
|
|
Effect: "Allow",
|
|
Principal: map[string]interface{}{
|
|
"Federated": "test-ldap",
|
|
},
|
|
Action: []string{"sts:AssumeRoleWithCredentials"},
|
|
},
|
|
},
|
|
},
|
|
AttachedPolicies: []string{"LDAPUserPolicy"},
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func int64Ptr(v int64) *int64 {
|
|
return &v
|
|
}
|