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.
 
 
 
 
 
 

331 lines
12 KiB

package s3api
import (
"context"
"net/http"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/seaweedfs/seaweedfs/weed/iam/sts"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
)
// TestSTSIdentityPolicyNamesPopulation tests that STS identities have PolicyNames
// populated from sessionInfo.Policies, enabling IAM-based authorization.
// This is a regression test for GitHub issue #7985.
func TestSTSIdentityPolicyNamesPopulation(t *testing.T) {
// Setup: Create a mock STS service
stsService, config := setupTestSTSService(t)
// Create IAM with STS integration
iam := NewIdentityAccessManagementWithStore(&S3ApiServerOption{}, "memory")
s3iam := &S3IAMIntegration{
stsService: stsService,
}
iam.SetIAMIntegration(s3iam)
// Generate a session token with policies
sessionId := "test-session-id"
expiresAt := time.Now().Add(time.Hour)
// Create session claims with policies
sessionClaims := sts.NewSTSSessionClaims(sessionId, config.Issuer, expiresAt).
WithSessionName("test-session").
WithRoleInfo("arn:aws:iam::role/adminRole", "arn:aws:sts::assumed-role/adminRole/test-session", "arn:aws:sts::assumed-role/adminRole/test-session")
// Add policies to the session claims
sessionClaims.Policies = []string{"AdminPolicy", "S3FullAccess"}
// Generate JWT token
tokenGen := sts.NewTokenGenerator(config.SigningKey, config.Issuer)
sessionToken, err := tokenGen.GenerateJWTWithClaims(sessionClaims)
require.NoError(t, err, "Should generate session token successfully")
// Validate the session token
sessionInfo, err := stsService.ValidateSessionToken(context.Background(), sessionToken)
require.NoError(t, err, "Session token should be valid")
require.NotNil(t, sessionInfo, "Session info should not be nil")
// Verify that sessionInfo has policies
assert.NotEmpty(t, sessionInfo.Policies, "SessionInfo should have policies")
assert.Equal(t, []string{"AdminPolicy", "S3FullAccess"}, sessionInfo.Policies, "Policies should match")
// Create a mock credential from session info
cred := &Credential{
AccessKey: sessionInfo.Credentials.AccessKeyId,
SecretKey: sessionInfo.Credentials.SecretAccessKey,
Status: "Active",
Expiration: sessionInfo.ExpiresAt.Unix(),
}
// Create identity as validateSTSSessionToken does
identity := &Identity{
Name: sessionInfo.AssumedRoleUser,
Account: &AccountAdmin,
Credentials: []*Credential{cred},
PrincipalArn: sessionInfo.Principal,
PolicyNames: sessionInfo.Policies, // This is the fix for issue #7985
}
// Verify the fix: PolicyNames should be populated
assert.NotNil(t, identity.PolicyNames, "Identity PolicyNames should not be nil")
assert.Equal(t, []string{"AdminPolicy", "S3FullAccess"}, identity.PolicyNames, "PolicyNames should be populated from sessionInfo.Policies")
// Verify that Actions is empty (STS identities should use IAM authorization, not legacy Actions)
assert.Empty(t, identity.Actions, "STS identities should have empty Actions to trigger IAM authorization path")
// Verify legacy canDo returns false (forcing fallback to IAM)
assert.False(t, identity.canDo("Read", "test-bucket", "/any/path"),
"canDo should return false for STS identities with no Actions")
// Verify authorization path selection
// When identity.Actions is empty and iamIntegration is available, it should use IAM authorization
hasActions := len(identity.Actions) > 0
hasIAMIntegration := iam.iamIntegration != nil
assert.False(t, hasActions, "STS identity should not have Actions")
assert.True(t, hasIAMIntegration, "IAM integration should be available")
// This combination means authorization will go through the IAM path
t.Log("✓ STS identity will use IAM authorization path (correct behavior)")
}
// TestSTSIdentityAuthorizationFlow tests the complete authorization flow for STS identities
// to ensure they are properly authorized via IAM integration.
func TestSTSIdentityAuthorizationFlow(t *testing.T) {
// This test validates that:
// 1. STS identities have empty Actions
// 2. STS identities have PolicyNames populated
// 3. Authorization logic routes to IAM path when Actions is empty and iamIntegration exists
// Create a mock STS session info
sessionInfo := &sts.SessionInfo{
SessionId: "test-session",
SessionName: "s3-session",
RoleArn: "arn:aws:iam::role/adminRole",
AssumedRoleUser: "arn:aws:sts::assumed-role/adminRole/s3-session",
Principal: "arn:aws:sts::assumed-role/adminRole/s3-session",
Policies: []string{"AdminPolicy", "S3WritePolicy"},
ExpiresAt: time.Now().Add(time.Hour),
Credentials: &sts.Credentials{
AccessKeyId: "AKIAIOSFODNN7EXAMPLE",
SecretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
SessionToken: "test-session-token",
Expiration: time.Now().Add(time.Hour),
},
}
// Create credential from session info
cred := &Credential{
AccessKey: sessionInfo.Credentials.AccessKeyId,
SecretKey: sessionInfo.Credentials.SecretAccessKey,
Status: "Active",
Expiration: sessionInfo.ExpiresAt.Unix(),
}
// Create identity as validateSTSSessionToken does (with the fix)
identity := &Identity{
Name: sessionInfo.AssumedRoleUser,
Account: &AccountAdmin,
Credentials: []*Credential{cred},
PrincipalArn: sessionInfo.Principal,
PolicyNames: sessionInfo.Policies, // Fix for #7985
}
// Test 1: Verify PolicyNames are populated
assert.Equal(t, []string{"AdminPolicy", "S3WritePolicy"}, identity.PolicyNames,
"PolicyNames should be populated from sessionInfo.Policies")
assert.Empty(t, identity.Actions,
"STS identities should have empty Actions to trigger the IAM authorization path")
// Test 2: Verify canDo returns false (legacy auth should be bypassed)
// This is important because it confirms that identity.Actions being empty
// correctly forces the authorization logic to fall back to iam.authorizeWithIAM
assert.False(t, identity.canDo("Read", "test-bucket", "/any/path"),
"canDo should return false for STS identities with no Actions")
// With empty Actions and populated PolicyNames, IAM authorization path will be used
// as per auth_credentials.go:703-713
t.Log("✓ Verified: STS identity correctly bypasses legacy canDo() to use IAM authorization path")
}
// TestSTSIdentityWithoutPolicyNames tests the bug scenario where PolicyNames is not populated
// This reproduces the original issue #7985
func TestSTSIdentityWithoutPolicyNames(t *testing.T) {
// Create identity WITHOUT PolicyNames (the bug scenario)
identityBuggy := newTestIdentity(nil)
// Create identity WITH PolicyNames (the fix)
identityFixed := newTestIdentity([]string{"AdminPolicy"})
// Verify the bug scenario
assert.Empty(t, identityBuggy.Actions, "Buggy identity has no Actions")
assert.Empty(t, identityBuggy.PolicyNames, "Buggy identity has no PolicyNames (the bug)")
// Verify the fix
assert.Empty(t, identityFixed.Actions, "Fixed identity has no Actions (correct)")
assert.NotEmpty(t, identityFixed.PolicyNames, "Fixed identity has PolicyNames (the fix)")
t.Logf("Bug scenario: Actions=%d, PolicyNames=%d → Would be denied",
len(identityBuggy.Actions), len(identityBuggy.PolicyNames))
t.Logf("Fixed scenario: Actions=%d, PolicyNames=%d → Can use IAM authorization",
len(identityFixed.Actions), len(identityFixed.PolicyNames))
}
// TestCanDoPathConstruction tests that bucket and objectKey are properly concatenated
// with a slash separator. This is a regression test for the second issue mentioned in #7985.
func TestCanDoPathConstruction(t *testing.T) {
// Create a test identity with wildcard actions (standard pattern)
identity := &Identity{
Name: "test-user",
Account: &AccountAdmin,
Actions: []Action{
"Write:test-bucket/*", // Wildcard for all objects
"Read:test-bucket/docs/*", // Wildcard for specific path
},
}
// Test cases for path construction
testCases := []struct {
name string
action Action
bucket string
objectKey string
shouldPass bool
}{
{
name: "Wildcard match for write",
action: "Write",
bucket: "test-bucket",
objectKey: "path/to/object.txt",
shouldPass: true,
},
{
name: "Wildcard match for read in docs path",
action: "Read",
bucket: "test-bucket",
objectKey: "docs/file.txt",
shouldPass: true,
},
{
name: "No match - wrong action",
action: "Delete",
bucket: "test-bucket",
objectKey: "file.txt",
shouldPass: false,
},
{
name: "No match - read outside docs path",
action: "Read",
bucket: "test-bucket",
objectKey: "other/file.txt",
shouldPass: false,
},
{
name: "ObjectKey with leading slash",
action: "Read",
bucket: "test-bucket",
objectKey: "/docs/file.txt", // Already has leading slash
shouldPass: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := identity.canDo(tc.action, tc.bucket, tc.objectKey)
// Robust path construction for verification
fullPath := tc.bucket
if tc.objectKey != "" && !strings.HasPrefix(tc.objectKey, "/") {
fullPath += "/"
}
fullPath += tc.objectKey
t.Logf("Testing path: %s", fullPath)
if tc.shouldPass {
assert.True(t, result, "Should allow action %s on %s", tc.action, fullPath)
} else {
assert.False(t, result, "Should deny action %s on %s", tc.action, fullPath)
}
})
}
}
// TestValidateSTSSessionTokenIntegration is an integration test that validates
// the complete flow from session token validation to identity creation
func TestValidateSTSSessionTokenIntegration(t *testing.T) {
// Setup STS service
stsService, config := setupTestSTSService(t)
// Create IAM with STS integration
iam := NewIdentityAccessManagementWithStore(&S3ApiServerOption{}, "memory")
s3iam := &S3IAMIntegration{
stsService: stsService,
}
iam.SetIAMIntegration(s3iam)
// Create a mock HTTP request with STS session token
req, err := http.NewRequest("PUT", "/test-bucket/test-object.txt", nil)
require.NoError(t, err)
// Generate session token
sessionId := "integration-test-session"
expiresAt := time.Now().Add(time.Hour)
sessionClaims := sts.NewSTSSessionClaims(sessionId, config.Issuer, expiresAt).
WithSessionName("integration-test").
WithRoleInfo("arn:aws:iam::role/testRole", "arn:aws:sts::assumed-role/testRole/integration-test", "arn:aws:sts::assumed-role/testRole/integration-test")
sessionClaims.Policies = []string{"TestPolicy"}
tokenGen := sts.NewTokenGenerator(config.SigningKey, config.Issuer)
sessionToken, err := tokenGen.GenerateJWTWithClaims(sessionClaims)
require.NoError(t, err)
// Validate session token
sessionInfo, err := stsService.ValidateSessionToken(context.Background(), sessionToken)
require.NoError(t, err)
require.NotNil(t, sessionInfo)
// Validate session token and check identity creation
identity, _, errCode := iam.validateSTSSessionToken(req, sessionToken, sessionInfo.Credentials.AccessKeyId)
require.Equal(t, s3err.ErrNone, errCode)
require.NotNil(t, identity)
// Verify the identity is properly configured for IAM authorization
assert.Empty(t, identity.Actions, "STS identity should have empty Actions")
assert.Equal(t, []string{"TestPolicy"}, identity.PolicyNames, "PolicyNames should be populated (this requires the fix in #7985)")
assert.NotEmpty(t, identity.PrincipalArn, "PrincipalArn should be set")
t.Log("✓ Integration test passed: STS identity properly configured for IAM authorization")
}
// Helper functions for tests
func setupTestSTSService(t *testing.T) (*sts.STSService, *sts.STSConfig) {
t.Helper()
stsService := sts.NewSTSService()
config := &sts.STSConfig{
TokenDuration: sts.FlexibleDuration{Duration: time.Hour},
MaxSessionLength: sts.FlexibleDuration{Duration: 12 * time.Hour},
Issuer: "test-issuer",
SigningKey: []byte("test-signing-key-at-least-32-bytes-long-for-security"),
}
err := stsService.Initialize(config)
require.NoError(t, err, "STS service should initialize successfully")
return stsService, config
}
func newTestIdentity(policyNames []string) *Identity {
return &Identity{
Name: "arn:aws:sts::assumed-role/adminRole/s3-session",
Account: &AccountAdmin,
Credentials: []*Credential{{AccessKey: "test", SecretKey: "test"}},
PrincipalArn: "arn:aws:sts::assumed-role/adminRole/s3-session",
PolicyNames: policyNames,
}
}