You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
513 lines
15 KiB
513 lines
15 KiB
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:seaweed:iam::role/S3ReadOnlyRole",
|
|
sessionName: "oidc-session",
|
|
webToken: validJWTToken,
|
|
expectedAllow: true,
|
|
testAction: "s3:GetObject",
|
|
testResource: "arn:seaweed:s3:::test-bucket/file.txt",
|
|
},
|
|
{
|
|
name: "role assumption denied by trust policy",
|
|
roleArn: "arn:seaweed:iam::role/RestrictedRole",
|
|
sessionName: "oidc-session",
|
|
webToken: validJWTToken,
|
|
expectedAllow: false,
|
|
},
|
|
{
|
|
name: "invalid token rejected",
|
|
roleArn: "arn:seaweed: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:seaweed:iam::role/LDAPUserRole",
|
|
sessionName: "ldap-session",
|
|
username: "testuser",
|
|
password: "testpass",
|
|
expectedAllow: true,
|
|
testAction: "filer:CreateEntry",
|
|
testResource: "arn:seaweed:filer::path/user-docs/*",
|
|
},
|
|
{
|
|
name: "invalid LDAP credentials",
|
|
roleArn: "arn:seaweed: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:seaweed: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:seaweed:s3:::test-bucket/file.txt",
|
|
shouldAllow: true,
|
|
reason: "S3ReadOnlyRole should allow GetObject",
|
|
},
|
|
{
|
|
name: "allow list bucket",
|
|
action: "s3:ListBucket",
|
|
resource: "arn:seaweed:s3:::test-bucket",
|
|
shouldAllow: true,
|
|
reason: "S3ReadOnlyRole should allow ListBucket",
|
|
},
|
|
{
|
|
name: "deny write access",
|
|
action: "s3:PutObject",
|
|
resource: "arn:seaweed:s3:::test-bucket/newfile.txt",
|
|
shouldAllow: false,
|
|
reason: "S3ReadOnlyRole should deny write operations",
|
|
},
|
|
{
|
|
name: "deny delete access",
|
|
action: "s3:DeleteObject",
|
|
resource: "arn:seaweed:s3:::test-bucket/file.txt",
|
|
shouldAllow: false,
|
|
reason: "S3ReadOnlyRole should deny delete operations",
|
|
},
|
|
{
|
|
name: "deny filer access",
|
|
action: "filer:CreateEntry",
|
|
resource: "arn:seaweed: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:seaweed: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:seaweed: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:seaweed: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:seaweed: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:seaweed: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:seaweed: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)
|
|
})
|
|
}
|
|
}
|
|
|
|
// 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{time.Hour},
|
|
MaxSessionLength: sts.FlexibleDuration{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:seaweed:s3:::*",
|
|
"arn:seaweed: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:seaweed: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": "test-oidc",
|
|
},
|
|
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
|
|
}
|