s3_jwt_auth_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557
  1. package s3api
  2. import (
  3. "context"
  4. "net/http"
  5. "net/http/httptest"
  6. "testing"
  7. "time"
  8. "github.com/golang-jwt/jwt/v5"
  9. "github.com/seaweedfs/seaweedfs/weed/iam/integration"
  10. "github.com/seaweedfs/seaweedfs/weed/iam/ldap"
  11. "github.com/seaweedfs/seaweedfs/weed/iam/oidc"
  12. "github.com/seaweedfs/seaweedfs/weed/iam/policy"
  13. "github.com/seaweedfs/seaweedfs/weed/iam/sts"
  14. "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
  15. "github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
  16. "github.com/stretchr/testify/assert"
  17. "github.com/stretchr/testify/require"
  18. )
  19. // createTestJWTAuth creates a test JWT token with the specified issuer, subject and signing key
  20. func createTestJWTAuth(t *testing.T, issuer, subject, signingKey string) string {
  21. token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
  22. "iss": issuer,
  23. "sub": subject,
  24. "aud": "test-client-id",
  25. "exp": time.Now().Add(time.Hour).Unix(),
  26. "iat": time.Now().Unix(),
  27. // Add claims that trust policy validation expects
  28. "idp": "test-oidc", // Identity provider claim for trust policy matching
  29. })
  30. tokenString, err := token.SignedString([]byte(signingKey))
  31. require.NoError(t, err)
  32. return tokenString
  33. }
  34. // TestJWTAuthenticationFlow tests the JWT authentication flow without full S3 server
  35. func TestJWTAuthenticationFlow(t *testing.T) {
  36. // Set up IAM system
  37. iamManager := setupTestIAMManager(t)
  38. // Create IAM integration
  39. s3iam := NewS3IAMIntegration(iamManager, "localhost:8888")
  40. // Create IAM server with integration
  41. iamServer := setupIAMWithIntegration(t, iamManager, s3iam)
  42. // Test scenarios
  43. tests := []struct {
  44. name string
  45. roleArn string
  46. setupRole func(ctx context.Context, mgr *integration.IAMManager)
  47. testOperations []JWTTestOperation
  48. }{
  49. {
  50. name: "Read-Only JWT Authentication",
  51. roleArn: "arn:seaweed:iam::role/S3ReadOnlyRole",
  52. setupRole: setupTestReadOnlyRole,
  53. testOperations: []JWTTestOperation{
  54. {Action: s3_constants.ACTION_READ, Bucket: "test-bucket", Object: "test-file.txt", ExpectedAllow: true},
  55. {Action: s3_constants.ACTION_WRITE, Bucket: "test-bucket", Object: "new-file.txt", ExpectedAllow: false},
  56. {Action: s3_constants.ACTION_LIST, Bucket: "test-bucket", Object: "", ExpectedAllow: true},
  57. },
  58. },
  59. {
  60. name: "Admin JWT Authentication",
  61. roleArn: "arn:seaweed:iam::role/S3AdminRole",
  62. setupRole: setupTestAdminRole,
  63. testOperations: []JWTTestOperation{
  64. {Action: s3_constants.ACTION_READ, Bucket: "admin-bucket", Object: "admin-file.txt", ExpectedAllow: true},
  65. {Action: s3_constants.ACTION_WRITE, Bucket: "admin-bucket", Object: "new-admin-file.txt", ExpectedAllow: true},
  66. {Action: s3_constants.ACTION_DELETE_BUCKET, Bucket: "admin-bucket", Object: "", ExpectedAllow: true},
  67. },
  68. },
  69. }
  70. for _, tt := range tests {
  71. t.Run(tt.name, func(t *testing.T) {
  72. ctx := context.Background()
  73. // Set up role
  74. tt.setupRole(ctx, iamManager)
  75. // Create a valid JWT token for testing
  76. validJWTToken := createTestJWTAuth(t, "https://test-issuer.com", "test-user-123", "test-signing-key")
  77. // Assume role to get JWT
  78. response, err := iamManager.AssumeRoleWithWebIdentity(ctx, &sts.AssumeRoleWithWebIdentityRequest{
  79. RoleArn: tt.roleArn,
  80. WebIdentityToken: validJWTToken,
  81. RoleSessionName: "jwt-auth-test",
  82. })
  83. require.NoError(t, err)
  84. jwtToken := response.Credentials.SessionToken
  85. // Test each operation
  86. for _, op := range tt.testOperations {
  87. t.Run(string(op.Action), func(t *testing.T) {
  88. // Test JWT authentication
  89. identity, errCode := testJWTAuthentication(t, iamServer, jwtToken)
  90. require.Equal(t, s3err.ErrNone, errCode, "JWT authentication should succeed")
  91. require.NotNil(t, identity)
  92. // Test authorization with appropriate role based on test case
  93. var testRoleName string
  94. if tt.name == "Read-Only JWT Authentication" {
  95. testRoleName = "TestReadRole"
  96. } else {
  97. testRoleName = "TestAdminRole"
  98. }
  99. allowed := testJWTAuthorizationWithRole(t, iamServer, identity, op.Action, op.Bucket, op.Object, jwtToken, testRoleName)
  100. assert.Equal(t, op.ExpectedAllow, allowed, "Operation %s should have expected result", op.Action)
  101. })
  102. }
  103. })
  104. }
  105. }
  106. // TestJWTTokenValidation tests JWT token validation edge cases
  107. func TestJWTTokenValidation(t *testing.T) {
  108. iamManager := setupTestIAMManager(t)
  109. s3iam := NewS3IAMIntegration(iamManager, "localhost:8888")
  110. iamServer := setupIAMWithIntegration(t, iamManager, s3iam)
  111. tests := []struct {
  112. name string
  113. token string
  114. expectedErr s3err.ErrorCode
  115. }{
  116. {
  117. name: "Empty token",
  118. token: "",
  119. expectedErr: s3err.ErrAccessDenied,
  120. },
  121. {
  122. name: "Invalid token format",
  123. token: "invalid-token",
  124. expectedErr: s3err.ErrAccessDenied,
  125. },
  126. {
  127. name: "Expired token",
  128. token: "expired-session-token",
  129. expectedErr: s3err.ErrAccessDenied,
  130. },
  131. }
  132. for _, tt := range tests {
  133. t.Run(tt.name, func(t *testing.T) {
  134. identity, errCode := testJWTAuthentication(t, iamServer, tt.token)
  135. assert.Equal(t, tt.expectedErr, errCode)
  136. assert.Nil(t, identity)
  137. })
  138. }
  139. }
  140. // TestRequestContextExtraction tests context extraction for policy conditions
  141. func TestRequestContextExtraction(t *testing.T) {
  142. tests := []struct {
  143. name string
  144. setupRequest func() *http.Request
  145. expectedIP string
  146. expectedUA string
  147. }{
  148. {
  149. name: "Standard request with IP",
  150. setupRequest: func() *http.Request {
  151. req := httptest.NewRequest("GET", "/test-bucket/test-file.txt", http.NoBody)
  152. req.Header.Set("X-Forwarded-For", "192.168.1.100")
  153. req.Header.Set("User-Agent", "aws-sdk-go/1.0")
  154. return req
  155. },
  156. expectedIP: "192.168.1.100",
  157. expectedUA: "aws-sdk-go/1.0",
  158. },
  159. {
  160. name: "Request with X-Real-IP",
  161. setupRequest: func() *http.Request {
  162. req := httptest.NewRequest("GET", "/test-bucket/test-file.txt", http.NoBody)
  163. req.Header.Set("X-Real-IP", "10.0.0.1")
  164. req.Header.Set("User-Agent", "boto3/1.0")
  165. return req
  166. },
  167. expectedIP: "10.0.0.1",
  168. expectedUA: "boto3/1.0",
  169. },
  170. }
  171. for _, tt := range tests {
  172. t.Run(tt.name, func(t *testing.T) {
  173. req := tt.setupRequest()
  174. // Extract request context
  175. context := extractRequestContext(req)
  176. if tt.expectedIP != "" {
  177. assert.Equal(t, tt.expectedIP, context["sourceIP"])
  178. }
  179. if tt.expectedUA != "" {
  180. assert.Equal(t, tt.expectedUA, context["userAgent"])
  181. }
  182. })
  183. }
  184. }
  185. // TestIPBasedPolicyEnforcement tests IP-based conditional policies
  186. func TestIPBasedPolicyEnforcement(t *testing.T) {
  187. iamManager := setupTestIAMManager(t)
  188. s3iam := NewS3IAMIntegration(iamManager, "localhost:8888")
  189. ctx := context.Background()
  190. // Set up IP-restricted role
  191. setupTestIPRestrictedRole(ctx, iamManager)
  192. // Create a valid JWT token for testing
  193. validJWTToken := createTestJWTAuth(t, "https://test-issuer.com", "test-user-123", "test-signing-key")
  194. // Assume role
  195. response, err := iamManager.AssumeRoleWithWebIdentity(ctx, &sts.AssumeRoleWithWebIdentityRequest{
  196. RoleArn: "arn:seaweed:iam::role/S3IPRestrictedRole",
  197. WebIdentityToken: validJWTToken,
  198. RoleSessionName: "ip-test-session",
  199. })
  200. require.NoError(t, err)
  201. tests := []struct {
  202. name string
  203. sourceIP string
  204. shouldAllow bool
  205. }{
  206. {
  207. name: "Allow from office IP",
  208. sourceIP: "192.168.1.100",
  209. shouldAllow: true,
  210. },
  211. {
  212. name: "Block from external IP",
  213. sourceIP: "8.8.8.8",
  214. shouldAllow: false,
  215. },
  216. {
  217. name: "Allow from internal range",
  218. sourceIP: "10.0.0.1",
  219. shouldAllow: true,
  220. },
  221. }
  222. for _, tt := range tests {
  223. t.Run(tt.name, func(t *testing.T) {
  224. // Create request with specific IP
  225. req := httptest.NewRequest("GET", "/restricted-bucket/file.txt", http.NoBody)
  226. req.Header.Set("Authorization", "Bearer "+response.Credentials.SessionToken)
  227. req.Header.Set("X-Forwarded-For", tt.sourceIP)
  228. // Create IAM identity for testing
  229. identity := &IAMIdentity{
  230. Name: "test-user",
  231. Principal: response.AssumedRoleUser.Arn,
  232. SessionToken: response.Credentials.SessionToken,
  233. }
  234. // Test authorization with IP condition
  235. errCode := s3iam.AuthorizeAction(ctx, identity, s3_constants.ACTION_READ, "restricted-bucket", "file.txt", req)
  236. if tt.shouldAllow {
  237. assert.Equal(t, s3err.ErrNone, errCode, "Should allow access from IP %s", tt.sourceIP)
  238. } else {
  239. assert.Equal(t, s3err.ErrAccessDenied, errCode, "Should deny access from IP %s", tt.sourceIP)
  240. }
  241. })
  242. }
  243. }
  244. // JWTTestOperation represents a test operation for JWT testing
  245. type JWTTestOperation struct {
  246. Action Action
  247. Bucket string
  248. Object string
  249. ExpectedAllow bool
  250. }
  251. // Helper functions
  252. func setupTestIAMManager(t *testing.T) *integration.IAMManager {
  253. // Create IAM manager
  254. manager := integration.NewIAMManager()
  255. // Initialize with test configuration
  256. config := &integration.IAMConfig{
  257. STS: &sts.STSConfig{
  258. TokenDuration: sts.FlexibleDuration{time.Hour},
  259. MaxSessionLength: sts.FlexibleDuration{time.Hour * 12},
  260. Issuer: "test-sts",
  261. SigningKey: []byte("test-signing-key-32-characters-long"),
  262. },
  263. Policy: &policy.PolicyEngineConfig{
  264. DefaultEffect: "Deny",
  265. StoreType: "memory",
  266. },
  267. Roles: &integration.RoleStoreConfig{
  268. StoreType: "memory",
  269. },
  270. }
  271. err := manager.Initialize(config, func() string {
  272. return "localhost:8888" // Mock filer address for testing
  273. })
  274. require.NoError(t, err)
  275. // Set up test identity providers
  276. setupTestIdentityProviders(t, manager)
  277. return manager
  278. }
  279. func setupTestIdentityProviders(t *testing.T, manager *integration.IAMManager) {
  280. // Set up OIDC provider
  281. oidcProvider := oidc.NewMockOIDCProvider("test-oidc")
  282. oidcConfig := &oidc.OIDCConfig{
  283. Issuer: "https://test-issuer.com",
  284. ClientID: "test-client-id",
  285. }
  286. err := oidcProvider.Initialize(oidcConfig)
  287. require.NoError(t, err)
  288. oidcProvider.SetupDefaultTestData()
  289. // Set up LDAP provider
  290. ldapProvider := ldap.NewMockLDAPProvider("test-ldap")
  291. err = ldapProvider.Initialize(nil) // Mock doesn't need real config
  292. require.NoError(t, err)
  293. ldapProvider.SetupDefaultTestData()
  294. // Register providers
  295. err = manager.RegisterIdentityProvider(oidcProvider)
  296. require.NoError(t, err)
  297. err = manager.RegisterIdentityProvider(ldapProvider)
  298. require.NoError(t, err)
  299. }
  300. func setupIAMWithIntegration(t *testing.T, iamManager *integration.IAMManager, s3iam *S3IAMIntegration) *IdentityAccessManagement {
  301. // Create a minimal IdentityAccessManagement for testing
  302. iam := &IdentityAccessManagement{
  303. isAuthEnabled: true,
  304. }
  305. // Set IAM integration
  306. iam.SetIAMIntegration(s3iam)
  307. return iam
  308. }
  309. func setupTestReadOnlyRole(ctx context.Context, manager *integration.IAMManager) {
  310. // Create read-only policy
  311. readPolicy := &policy.PolicyDocument{
  312. Version: "2012-10-17",
  313. Statement: []policy.Statement{
  314. {
  315. Sid: "AllowS3Read",
  316. Effect: "Allow",
  317. Action: []string{"s3:GetObject", "s3:ListBucket"},
  318. Resource: []string{
  319. "arn:seaweed:s3:::*",
  320. "arn:seaweed:s3:::*/*",
  321. },
  322. },
  323. {
  324. Sid: "AllowSTSSessionValidation",
  325. Effect: "Allow",
  326. Action: []string{"sts:ValidateSession"},
  327. Resource: []string{"*"},
  328. },
  329. },
  330. }
  331. manager.CreatePolicy(ctx, "", "S3ReadOnlyPolicy", readPolicy)
  332. // Create role
  333. manager.CreateRole(ctx, "", "S3ReadOnlyRole", &integration.RoleDefinition{
  334. RoleName: "S3ReadOnlyRole",
  335. TrustPolicy: &policy.PolicyDocument{
  336. Version: "2012-10-17",
  337. Statement: []policy.Statement{
  338. {
  339. Effect: "Allow",
  340. Principal: map[string]interface{}{
  341. "Federated": "test-oidc",
  342. },
  343. Action: []string{"sts:AssumeRoleWithWebIdentity"},
  344. },
  345. },
  346. },
  347. AttachedPolicies: []string{"S3ReadOnlyPolicy"},
  348. })
  349. // Also create a TestReadRole for read-only authorization testing
  350. manager.CreateRole(ctx, "", "TestReadRole", &integration.RoleDefinition{
  351. RoleName: "TestReadRole",
  352. TrustPolicy: &policy.PolicyDocument{
  353. Version: "2012-10-17",
  354. Statement: []policy.Statement{
  355. {
  356. Effect: "Allow",
  357. Principal: map[string]interface{}{
  358. "Federated": "test-oidc",
  359. },
  360. Action: []string{"sts:AssumeRoleWithWebIdentity"},
  361. },
  362. },
  363. },
  364. AttachedPolicies: []string{"S3ReadOnlyPolicy"},
  365. })
  366. }
  367. func setupTestAdminRole(ctx context.Context, manager *integration.IAMManager) {
  368. // Create admin policy
  369. adminPolicy := &policy.PolicyDocument{
  370. Version: "2012-10-17",
  371. Statement: []policy.Statement{
  372. {
  373. Sid: "AllowAllS3",
  374. Effect: "Allow",
  375. Action: []string{"s3:*"},
  376. Resource: []string{
  377. "arn:seaweed:s3:::*",
  378. "arn:seaweed:s3:::*/*",
  379. },
  380. },
  381. {
  382. Sid: "AllowSTSSessionValidation",
  383. Effect: "Allow",
  384. Action: []string{"sts:ValidateSession"},
  385. Resource: []string{"*"},
  386. },
  387. },
  388. }
  389. manager.CreatePolicy(ctx, "", "S3AdminPolicy", adminPolicy)
  390. // Create role
  391. manager.CreateRole(ctx, "", "S3AdminRole", &integration.RoleDefinition{
  392. RoleName: "S3AdminRole",
  393. TrustPolicy: &policy.PolicyDocument{
  394. Version: "2012-10-17",
  395. Statement: []policy.Statement{
  396. {
  397. Effect: "Allow",
  398. Principal: map[string]interface{}{
  399. "Federated": "test-oidc",
  400. },
  401. Action: []string{"sts:AssumeRoleWithWebIdentity"},
  402. },
  403. },
  404. },
  405. AttachedPolicies: []string{"S3AdminPolicy"},
  406. })
  407. // Also create a TestAdminRole with admin policy for authorization testing
  408. manager.CreateRole(ctx, "", "TestAdminRole", &integration.RoleDefinition{
  409. RoleName: "TestAdminRole",
  410. TrustPolicy: &policy.PolicyDocument{
  411. Version: "2012-10-17",
  412. Statement: []policy.Statement{
  413. {
  414. Effect: "Allow",
  415. Principal: map[string]interface{}{
  416. "Federated": "test-oidc",
  417. },
  418. Action: []string{"sts:AssumeRoleWithWebIdentity"},
  419. },
  420. },
  421. },
  422. AttachedPolicies: []string{"S3AdminPolicy"}, // Admin gets full access
  423. })
  424. }
  425. func setupTestIPRestrictedRole(ctx context.Context, manager *integration.IAMManager) {
  426. // Create IP-restricted policy
  427. restrictedPolicy := &policy.PolicyDocument{
  428. Version: "2012-10-17",
  429. Statement: []policy.Statement{
  430. {
  431. Sid: "AllowFromOffice",
  432. Effect: "Allow",
  433. Action: []string{"s3:GetObject", "s3:ListBucket"},
  434. Resource: []string{
  435. "arn:seaweed:s3:::*",
  436. "arn:seaweed:s3:::*/*",
  437. },
  438. Condition: map[string]map[string]interface{}{
  439. "IpAddress": {
  440. "seaweed:SourceIP": []string{"192.168.1.0/24", "10.0.0.0/8"},
  441. },
  442. },
  443. },
  444. },
  445. }
  446. manager.CreatePolicy(ctx, "", "S3IPRestrictedPolicy", restrictedPolicy)
  447. // Create role
  448. manager.CreateRole(ctx, "", "S3IPRestrictedRole", &integration.RoleDefinition{
  449. RoleName: "S3IPRestrictedRole",
  450. TrustPolicy: &policy.PolicyDocument{
  451. Version: "2012-10-17",
  452. Statement: []policy.Statement{
  453. {
  454. Effect: "Allow",
  455. Principal: map[string]interface{}{
  456. "Federated": "test-oidc",
  457. },
  458. Action: []string{"sts:AssumeRoleWithWebIdentity"},
  459. },
  460. },
  461. },
  462. AttachedPolicies: []string{"S3IPRestrictedPolicy"},
  463. })
  464. }
  465. func testJWTAuthentication(t *testing.T, iam *IdentityAccessManagement, token string) (*Identity, s3err.ErrorCode) {
  466. // Create test request with JWT
  467. req := httptest.NewRequest("GET", "/test-bucket/test-object", http.NoBody)
  468. req.Header.Set("Authorization", "Bearer "+token)
  469. // Test authentication
  470. if iam.iamIntegration == nil {
  471. return nil, s3err.ErrNotImplemented
  472. }
  473. return iam.authenticateJWTWithIAM(req)
  474. }
  475. func testJWTAuthorization(t *testing.T, iam *IdentityAccessManagement, identity *Identity, action Action, bucket, object, token string) bool {
  476. return testJWTAuthorizationWithRole(t, iam, identity, action, bucket, object, token, "TestRole")
  477. }
  478. func testJWTAuthorizationWithRole(t *testing.T, iam *IdentityAccessManagement, identity *Identity, action Action, bucket, object, token, roleName string) bool {
  479. // Create test request
  480. req := httptest.NewRequest("GET", "/"+bucket+"/"+object, http.NoBody)
  481. req.Header.Set("Authorization", "Bearer "+token)
  482. req.Header.Set("X-SeaweedFS-Session-Token", token)
  483. // Use a proper principal ARN format that matches what STS would generate
  484. principalArn := "arn:seaweed:sts::assumed-role/" + roleName + "/test-session"
  485. req.Header.Set("X-SeaweedFS-Principal", principalArn)
  486. // Test authorization
  487. if iam.iamIntegration == nil {
  488. return false
  489. }
  490. errCode := iam.authorizeWithIAM(req, identity, action, bucket, object)
  491. return errCode == s3err.ErrNone
  492. }