| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557 |
- package s3api
- import (
- "context"
- "net/http"
- "net/http/httptest"
- "testing"
- "time"
- "github.com/golang-jwt/jwt/v5"
- "github.com/seaweedfs/seaweedfs/weed/iam/integration"
- "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/seaweedfs/seaweedfs/weed/s3api/s3_constants"
- "github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- )
- // createTestJWTAuth creates a test JWT token with the specified issuer, subject and signing key
- func createTestJWTAuth(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
- }
- // TestJWTAuthenticationFlow tests the JWT authentication flow without full S3 server
- func TestJWTAuthenticationFlow(t *testing.T) {
- // Set up IAM system
- iamManager := setupTestIAMManager(t)
- // Create IAM integration
- s3iam := NewS3IAMIntegration(iamManager, "localhost:8888")
- // Create IAM server with integration
- iamServer := setupIAMWithIntegration(t, iamManager, s3iam)
- // Test scenarios
- tests := []struct {
- name string
- roleArn string
- setupRole func(ctx context.Context, mgr *integration.IAMManager)
- testOperations []JWTTestOperation
- }{
- {
- name: "Read-Only JWT Authentication",
- roleArn: "arn:seaweed:iam::role/S3ReadOnlyRole",
- setupRole: setupTestReadOnlyRole,
- testOperations: []JWTTestOperation{
- {Action: s3_constants.ACTION_READ, Bucket: "test-bucket", Object: "test-file.txt", ExpectedAllow: true},
- {Action: s3_constants.ACTION_WRITE, Bucket: "test-bucket", Object: "new-file.txt", ExpectedAllow: false},
- {Action: s3_constants.ACTION_LIST, Bucket: "test-bucket", Object: "", ExpectedAllow: true},
- },
- },
- {
- name: "Admin JWT Authentication",
- roleArn: "arn:seaweed:iam::role/S3AdminRole",
- setupRole: setupTestAdminRole,
- testOperations: []JWTTestOperation{
- {Action: s3_constants.ACTION_READ, Bucket: "admin-bucket", Object: "admin-file.txt", ExpectedAllow: true},
- {Action: s3_constants.ACTION_WRITE, Bucket: "admin-bucket", Object: "new-admin-file.txt", ExpectedAllow: true},
- {Action: s3_constants.ACTION_DELETE_BUCKET, Bucket: "admin-bucket", Object: "", ExpectedAllow: true},
- },
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- ctx := context.Background()
- // Set up role
- tt.setupRole(ctx, iamManager)
- // Create a valid JWT token for testing
- validJWTToken := createTestJWTAuth(t, "https://test-issuer.com", "test-user-123", "test-signing-key")
- // Assume role to get JWT
- response, err := iamManager.AssumeRoleWithWebIdentity(ctx, &sts.AssumeRoleWithWebIdentityRequest{
- RoleArn: tt.roleArn,
- WebIdentityToken: validJWTToken,
- RoleSessionName: "jwt-auth-test",
- })
- require.NoError(t, err)
- jwtToken := response.Credentials.SessionToken
- // Test each operation
- for _, op := range tt.testOperations {
- t.Run(string(op.Action), func(t *testing.T) {
- // Test JWT authentication
- identity, errCode := testJWTAuthentication(t, iamServer, jwtToken)
- require.Equal(t, s3err.ErrNone, errCode, "JWT authentication should succeed")
- require.NotNil(t, identity)
- // Test authorization with appropriate role based on test case
- var testRoleName string
- if tt.name == "Read-Only JWT Authentication" {
- testRoleName = "TestReadRole"
- } else {
- testRoleName = "TestAdminRole"
- }
- allowed := testJWTAuthorizationWithRole(t, iamServer, identity, op.Action, op.Bucket, op.Object, jwtToken, testRoleName)
- assert.Equal(t, op.ExpectedAllow, allowed, "Operation %s should have expected result", op.Action)
- })
- }
- })
- }
- }
- // TestJWTTokenValidation tests JWT token validation edge cases
- func TestJWTTokenValidation(t *testing.T) {
- iamManager := setupTestIAMManager(t)
- s3iam := NewS3IAMIntegration(iamManager, "localhost:8888")
- iamServer := setupIAMWithIntegration(t, iamManager, s3iam)
- tests := []struct {
- name string
- token string
- expectedErr s3err.ErrorCode
- }{
- {
- name: "Empty token",
- token: "",
- expectedErr: s3err.ErrAccessDenied,
- },
- {
- name: "Invalid token format",
- token: "invalid-token",
- expectedErr: s3err.ErrAccessDenied,
- },
- {
- name: "Expired token",
- token: "expired-session-token",
- expectedErr: s3err.ErrAccessDenied,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- identity, errCode := testJWTAuthentication(t, iamServer, tt.token)
- assert.Equal(t, tt.expectedErr, errCode)
- assert.Nil(t, identity)
- })
- }
- }
- // TestRequestContextExtraction tests context extraction for policy conditions
- func TestRequestContextExtraction(t *testing.T) {
- tests := []struct {
- name string
- setupRequest func() *http.Request
- expectedIP string
- expectedUA string
- }{
- {
- name: "Standard request with IP",
- setupRequest: func() *http.Request {
- req := httptest.NewRequest("GET", "/test-bucket/test-file.txt", http.NoBody)
- req.Header.Set("X-Forwarded-For", "192.168.1.100")
- req.Header.Set("User-Agent", "aws-sdk-go/1.0")
- return req
- },
- expectedIP: "192.168.1.100",
- expectedUA: "aws-sdk-go/1.0",
- },
- {
- name: "Request with X-Real-IP",
- setupRequest: func() *http.Request {
- req := httptest.NewRequest("GET", "/test-bucket/test-file.txt", http.NoBody)
- req.Header.Set("X-Real-IP", "10.0.0.1")
- req.Header.Set("User-Agent", "boto3/1.0")
- return req
- },
- expectedIP: "10.0.0.1",
- expectedUA: "boto3/1.0",
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- req := tt.setupRequest()
- // Extract request context
- context := extractRequestContext(req)
- if tt.expectedIP != "" {
- assert.Equal(t, tt.expectedIP, context["sourceIP"])
- }
- if tt.expectedUA != "" {
- assert.Equal(t, tt.expectedUA, context["userAgent"])
- }
- })
- }
- }
- // TestIPBasedPolicyEnforcement tests IP-based conditional policies
- func TestIPBasedPolicyEnforcement(t *testing.T) {
- iamManager := setupTestIAMManager(t)
- s3iam := NewS3IAMIntegration(iamManager, "localhost:8888")
- ctx := context.Background()
- // Set up IP-restricted role
- setupTestIPRestrictedRole(ctx, iamManager)
- // Create a valid JWT token for testing
- validJWTToken := createTestJWTAuth(t, "https://test-issuer.com", "test-user-123", "test-signing-key")
- // Assume role
- response, err := iamManager.AssumeRoleWithWebIdentity(ctx, &sts.AssumeRoleWithWebIdentityRequest{
- RoleArn: "arn:seaweed:iam::role/S3IPRestrictedRole",
- WebIdentityToken: validJWTToken,
- RoleSessionName: "ip-test-session",
- })
- require.NoError(t, err)
- tests := []struct {
- name string
- sourceIP string
- shouldAllow bool
- }{
- {
- name: "Allow from office IP",
- sourceIP: "192.168.1.100",
- shouldAllow: true,
- },
- {
- name: "Block from external IP",
- sourceIP: "8.8.8.8",
- shouldAllow: false,
- },
- {
- name: "Allow from internal range",
- sourceIP: "10.0.0.1",
- shouldAllow: true,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- // Create request with specific IP
- req := httptest.NewRequest("GET", "/restricted-bucket/file.txt", http.NoBody)
- req.Header.Set("Authorization", "Bearer "+response.Credentials.SessionToken)
- req.Header.Set("X-Forwarded-For", tt.sourceIP)
- // Create IAM identity for testing
- identity := &IAMIdentity{
- Name: "test-user",
- Principal: response.AssumedRoleUser.Arn,
- SessionToken: response.Credentials.SessionToken,
- }
- // Test authorization with IP condition
- errCode := s3iam.AuthorizeAction(ctx, identity, s3_constants.ACTION_READ, "restricted-bucket", "file.txt", req)
- if tt.shouldAllow {
- assert.Equal(t, s3err.ErrNone, errCode, "Should allow access from IP %s", tt.sourceIP)
- } else {
- assert.Equal(t, s3err.ErrAccessDenied, errCode, "Should deny access from IP %s", tt.sourceIP)
- }
- })
- }
- }
- // JWTTestOperation represents a test operation for JWT testing
- type JWTTestOperation struct {
- Action Action
- Bucket string
- Object string
- ExpectedAllow bool
- }
- // Helper functions
- func setupTestIAMManager(t *testing.T) *integration.IAMManager {
- // Create IAM manager
- manager := integration.NewIAMManager()
- // Initialize with test configuration
- config := &integration.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",
- },
- Roles: &integration.RoleStoreConfig{
- StoreType: "memory",
- },
- }
- err := manager.Initialize(config, func() string {
- return "localhost:8888" // Mock filer address for testing
- })
- require.NoError(t, err)
- // Set up test identity providers
- setupTestIdentityProviders(t, manager)
- return manager
- }
- func setupTestIdentityProviders(t *testing.T, manager *integration.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 provider
- 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 setupIAMWithIntegration(t *testing.T, iamManager *integration.IAMManager, s3iam *S3IAMIntegration) *IdentityAccessManagement {
- // Create a minimal IdentityAccessManagement for testing
- iam := &IdentityAccessManagement{
- isAuthEnabled: true,
- }
- // Set IAM integration
- iam.SetIAMIntegration(s3iam)
- return iam
- }
- func setupTestReadOnlyRole(ctx context.Context, manager *integration.IAMManager) {
- // Create read-only policy
- readPolicy := &policy.PolicyDocument{
- Version: "2012-10-17",
- Statement: []policy.Statement{
- {
- Sid: "AllowS3Read",
- Effect: "Allow",
- Action: []string{"s3:GetObject", "s3:ListBucket"},
- Resource: []string{
- "arn:seaweed:s3:::*",
- "arn:seaweed:s3:::*/*",
- },
- },
- {
- Sid: "AllowSTSSessionValidation",
- Effect: "Allow",
- Action: []string{"sts:ValidateSession"},
- Resource: []string{"*"},
- },
- },
- }
- manager.CreatePolicy(ctx, "", "S3ReadOnlyPolicy", readPolicy)
- // Create role
- manager.CreateRole(ctx, "", "S3ReadOnlyRole", &integration.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"},
- })
- // Also create a TestReadRole for read-only authorization testing
- manager.CreateRole(ctx, "", "TestReadRole", &integration.RoleDefinition{
- RoleName: "TestReadRole",
- 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"},
- })
- }
- func setupTestAdminRole(ctx context.Context, manager *integration.IAMManager) {
- // Create admin policy
- adminPolicy := &policy.PolicyDocument{
- Version: "2012-10-17",
- Statement: []policy.Statement{
- {
- Sid: "AllowAllS3",
- Effect: "Allow",
- Action: []string{"s3:*"},
- Resource: []string{
- "arn:seaweed:s3:::*",
- "arn:seaweed:s3:::*/*",
- },
- },
- {
- Sid: "AllowSTSSessionValidation",
- Effect: "Allow",
- Action: []string{"sts:ValidateSession"},
- Resource: []string{"*"},
- },
- },
- }
- manager.CreatePolicy(ctx, "", "S3AdminPolicy", adminPolicy)
- // Create role
- manager.CreateRole(ctx, "", "S3AdminRole", &integration.RoleDefinition{
- RoleName: "S3AdminRole",
- 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{"S3AdminPolicy"},
- })
- // Also create a TestAdminRole with admin policy for authorization testing
- manager.CreateRole(ctx, "", "TestAdminRole", &integration.RoleDefinition{
- RoleName: "TestAdminRole",
- 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{"S3AdminPolicy"}, // Admin gets full access
- })
- }
- func setupTestIPRestrictedRole(ctx context.Context, manager *integration.IAMManager) {
- // Create IP-restricted policy
- restrictedPolicy := &policy.PolicyDocument{
- Version: "2012-10-17",
- Statement: []policy.Statement{
- {
- Sid: "AllowFromOffice",
- Effect: "Allow",
- Action: []string{"s3:GetObject", "s3:ListBucket"},
- Resource: []string{
- "arn:seaweed:s3:::*",
- "arn:seaweed:s3:::*/*",
- },
- Condition: map[string]map[string]interface{}{
- "IpAddress": {
- "seaweed:SourceIP": []string{"192.168.1.0/24", "10.0.0.0/8"},
- },
- },
- },
- },
- }
- manager.CreatePolicy(ctx, "", "S3IPRestrictedPolicy", restrictedPolicy)
- // Create role
- manager.CreateRole(ctx, "", "S3IPRestrictedRole", &integration.RoleDefinition{
- RoleName: "S3IPRestrictedRole",
- 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{"S3IPRestrictedPolicy"},
- })
- }
- func testJWTAuthentication(t *testing.T, iam *IdentityAccessManagement, token string) (*Identity, s3err.ErrorCode) {
- // Create test request with JWT
- req := httptest.NewRequest("GET", "/test-bucket/test-object", http.NoBody)
- req.Header.Set("Authorization", "Bearer "+token)
- // Test authentication
- if iam.iamIntegration == nil {
- return nil, s3err.ErrNotImplemented
- }
- return iam.authenticateJWTWithIAM(req)
- }
- func testJWTAuthorization(t *testing.T, iam *IdentityAccessManagement, identity *Identity, action Action, bucket, object, token string) bool {
- return testJWTAuthorizationWithRole(t, iam, identity, action, bucket, object, token, "TestRole")
- }
- func testJWTAuthorizationWithRole(t *testing.T, iam *IdentityAccessManagement, identity *Identity, action Action, bucket, object, token, roleName string) bool {
- // Create test request
- req := httptest.NewRequest("GET", "/"+bucket+"/"+object, http.NoBody)
- req.Header.Set("Authorization", "Bearer "+token)
- req.Header.Set("X-SeaweedFS-Session-Token", token)
- // Use a proper principal ARN format that matches what STS would generate
- principalArn := "arn:seaweed:sts::assumed-role/" + roleName + "/test-session"
- req.Header.Set("X-SeaweedFS-Principal", principalArn)
- // Test authorization
- if iam.iamIntegration == nil {
- return false
- }
- errCode := iam.authorizeWithIAM(req, identity, action, bucket, object)
- return errCode == s3err.ErrNone
- }
|