s3_keycloak_integration_test.go 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. package iam
  2. import (
  3. "encoding/base64"
  4. "encoding/json"
  5. "os"
  6. "strings"
  7. "testing"
  8. "github.com/aws/aws-sdk-go/service/s3"
  9. "github.com/stretchr/testify/assert"
  10. "github.com/stretchr/testify/require"
  11. )
  12. const (
  13. testKeycloakBucket = "test-keycloak-bucket"
  14. )
  15. // TestKeycloakIntegrationAvailable checks if Keycloak is available for testing
  16. func TestKeycloakIntegrationAvailable(t *testing.T) {
  17. framework := NewS3IAMTestFramework(t)
  18. defer framework.Cleanup()
  19. if !framework.useKeycloak {
  20. t.Skip("Keycloak not available, skipping integration tests")
  21. }
  22. // Test Keycloak health
  23. assert.True(t, framework.useKeycloak, "Keycloak should be available")
  24. assert.NotNil(t, framework.keycloakClient, "Keycloak client should be initialized")
  25. }
  26. // TestKeycloakAuthentication tests authentication flow with real Keycloak
  27. func TestKeycloakAuthentication(t *testing.T) {
  28. framework := NewS3IAMTestFramework(t)
  29. defer framework.Cleanup()
  30. if !framework.useKeycloak {
  31. t.Skip("Keycloak not available, skipping integration tests")
  32. }
  33. t.Run("admin_user_authentication", func(t *testing.T) {
  34. // Test admin user authentication
  35. token, err := framework.getKeycloakToken("admin-user")
  36. require.NoError(t, err)
  37. assert.NotEmpty(t, token, "JWT token should not be empty")
  38. // Verify token can be used to create S3 client
  39. s3Client, err := framework.CreateS3ClientWithKeycloakToken(token)
  40. require.NoError(t, err)
  41. assert.NotNil(t, s3Client, "S3 client should be created successfully")
  42. // Test bucket operations with admin privileges
  43. err = framework.CreateBucket(s3Client, testKeycloakBucket)
  44. assert.NoError(t, err, "Admin user should be able to create buckets")
  45. // Verify bucket exists
  46. buckets, err := s3Client.ListBuckets(&s3.ListBucketsInput{})
  47. require.NoError(t, err)
  48. found := false
  49. for _, bucket := range buckets.Buckets {
  50. if *bucket.Name == testKeycloakBucket {
  51. found = true
  52. break
  53. }
  54. }
  55. assert.True(t, found, "Created bucket should be listed")
  56. })
  57. t.Run("read_only_user_authentication", func(t *testing.T) {
  58. // Test read-only user authentication
  59. token, err := framework.getKeycloakToken("read-user")
  60. require.NoError(t, err)
  61. assert.NotEmpty(t, token, "JWT token should not be empty")
  62. // Debug: decode token to verify it's for read-user
  63. parts := strings.Split(token, ".")
  64. if len(parts) >= 2 {
  65. payload := parts[1]
  66. // JWTs use URL-safe base64 encoding without padding (RFC 4648 §5)
  67. decoded, err := base64.RawURLEncoding.DecodeString(payload)
  68. if err == nil {
  69. var claims map[string]interface{}
  70. if json.Unmarshal(decoded, &claims) == nil {
  71. t.Logf("Token username: %v", claims["preferred_username"])
  72. t.Logf("Token roles: %v", claims["roles"])
  73. }
  74. }
  75. }
  76. // First test with direct HTTP request to verify OIDC authentication works
  77. t.Logf("Testing with direct HTTP request...")
  78. err = framework.TestKeycloakTokenDirectly(token)
  79. require.NoError(t, err, "Direct HTTP test should succeed")
  80. // Create S3 client with Keycloak token
  81. s3Client, err := framework.CreateS3ClientWithKeycloakToken(token)
  82. require.NoError(t, err)
  83. // Test that read-only user can list buckets
  84. t.Logf("Testing ListBuckets with AWS SDK...")
  85. _, err = s3Client.ListBuckets(&s3.ListBucketsInput{})
  86. assert.NoError(t, err, "Read-only user should be able to list buckets")
  87. // Test that read-only user cannot create buckets
  88. t.Logf("Testing CreateBucket with AWS SDK...")
  89. err = framework.CreateBucket(s3Client, testKeycloakBucket+"-readonly")
  90. assert.Error(t, err, "Read-only user should not be able to create buckets")
  91. })
  92. t.Run("invalid_user_authentication", func(t *testing.T) {
  93. // Test authentication with invalid credentials
  94. _, err := framework.keycloakClient.AuthenticateUser("invalid-user", "invalid-password")
  95. assert.Error(t, err, "Authentication with invalid credentials should fail")
  96. })
  97. }
  98. // TestKeycloakTokenExpiration tests JWT token expiration handling
  99. func TestKeycloakTokenExpiration(t *testing.T) {
  100. framework := NewS3IAMTestFramework(t)
  101. defer framework.Cleanup()
  102. if !framework.useKeycloak {
  103. t.Skip("Keycloak not available, skipping integration tests")
  104. }
  105. // Get a short-lived token (if Keycloak is configured for it)
  106. // Use consistent password that matches Docker setup script logic: "adminuser123"
  107. tokenResp, err := framework.keycloakClient.AuthenticateUser("admin-user", "adminuser123")
  108. require.NoError(t, err)
  109. // Verify token properties
  110. assert.NotEmpty(t, tokenResp.AccessToken, "Access token should not be empty")
  111. assert.Equal(t, "Bearer", tokenResp.TokenType, "Token type should be Bearer")
  112. assert.Greater(t, tokenResp.ExpiresIn, 0, "Token should have expiration time")
  113. // Test that token works initially
  114. token, err := framework.getKeycloakToken("admin-user")
  115. require.NoError(t, err)
  116. s3Client, err := framework.CreateS3ClientWithKeycloakToken(token)
  117. require.NoError(t, err)
  118. _, err = s3Client.ListBuckets(&s3.ListBucketsInput{})
  119. assert.NoError(t, err, "Fresh token should work for S3 operations")
  120. }
  121. // TestKeycloakRoleMapping tests role mapping from Keycloak to S3 policies
  122. func TestKeycloakRoleMapping(t *testing.T) {
  123. framework := NewS3IAMTestFramework(t)
  124. defer framework.Cleanup()
  125. if !framework.useKeycloak {
  126. t.Skip("Keycloak not available, skipping integration tests")
  127. }
  128. testCases := []struct {
  129. username string
  130. expectedRole string
  131. canCreateBucket bool
  132. canListBuckets bool
  133. description string
  134. }{
  135. {
  136. username: "admin-user",
  137. expectedRole: "S3AdminRole",
  138. canCreateBucket: true,
  139. canListBuckets: true,
  140. description: "Admin user should have full access",
  141. },
  142. {
  143. username: "read-user",
  144. expectedRole: "S3ReadOnlyRole",
  145. canCreateBucket: false,
  146. canListBuckets: true,
  147. description: "Read-only user should have read-only access",
  148. },
  149. {
  150. username: "write-user",
  151. expectedRole: "S3ReadWriteRole",
  152. canCreateBucket: true,
  153. canListBuckets: true,
  154. description: "Read-write user should have read-write access",
  155. },
  156. }
  157. for _, tc := range testCases {
  158. t.Run(tc.username, func(t *testing.T) {
  159. // Get Keycloak token for the user
  160. token, err := framework.getKeycloakToken(tc.username)
  161. require.NoError(t, err)
  162. // Create S3 client with Keycloak token
  163. s3Client, err := framework.CreateS3ClientWithKeycloakToken(token)
  164. require.NoError(t, err, tc.description)
  165. // Test list buckets permission
  166. _, err = s3Client.ListBuckets(&s3.ListBucketsInput{})
  167. if tc.canListBuckets {
  168. assert.NoError(t, err, "%s should be able to list buckets", tc.username)
  169. } else {
  170. assert.Error(t, err, "%s should not be able to list buckets", tc.username)
  171. }
  172. // Test create bucket permission
  173. testBucketName := testKeycloakBucket + "-" + tc.username
  174. err = framework.CreateBucket(s3Client, testBucketName)
  175. if tc.canCreateBucket {
  176. assert.NoError(t, err, "%s should be able to create buckets", tc.username)
  177. } else {
  178. assert.Error(t, err, "%s should not be able to create buckets", tc.username)
  179. }
  180. })
  181. }
  182. }
  183. // TestKeycloakS3Operations tests comprehensive S3 operations with Keycloak authentication
  184. func TestKeycloakS3Operations(t *testing.T) {
  185. framework := NewS3IAMTestFramework(t)
  186. defer framework.Cleanup()
  187. if !framework.useKeycloak {
  188. t.Skip("Keycloak not available, skipping integration tests")
  189. }
  190. // Use admin user for comprehensive testing
  191. token, err := framework.getKeycloakToken("admin-user")
  192. require.NoError(t, err)
  193. s3Client, err := framework.CreateS3ClientWithKeycloakToken(token)
  194. require.NoError(t, err)
  195. bucketName := testKeycloakBucket + "-operations"
  196. t.Run("bucket_lifecycle", func(t *testing.T) {
  197. // Create bucket
  198. err = framework.CreateBucket(s3Client, bucketName)
  199. require.NoError(t, err, "Should be able to create bucket")
  200. // Verify bucket exists
  201. buckets, err := s3Client.ListBuckets(&s3.ListBucketsInput{})
  202. require.NoError(t, err)
  203. found := false
  204. for _, bucket := range buckets.Buckets {
  205. if *bucket.Name == bucketName {
  206. found = true
  207. break
  208. }
  209. }
  210. assert.True(t, found, "Created bucket should be listed")
  211. })
  212. t.Run("object_operations", func(t *testing.T) {
  213. objectKey := "test-object.txt"
  214. objectContent := "Hello from Keycloak-authenticated SeaweedFS!"
  215. // Put object
  216. err = framework.PutTestObject(s3Client, bucketName, objectKey, objectContent)
  217. require.NoError(t, err, "Should be able to put object")
  218. // Get object
  219. content, err := framework.GetTestObject(s3Client, bucketName, objectKey)
  220. require.NoError(t, err, "Should be able to get object")
  221. assert.Equal(t, objectContent, content, "Object content should match")
  222. // List objects
  223. objects, err := framework.ListTestObjects(s3Client, bucketName)
  224. require.NoError(t, err, "Should be able to list objects")
  225. assert.Contains(t, objects, objectKey, "Object should be listed")
  226. // Delete object
  227. err = framework.DeleteTestObject(s3Client, bucketName, objectKey)
  228. assert.NoError(t, err, "Should be able to delete object")
  229. })
  230. }
  231. // TestKeycloakFailover tests fallback to mock OIDC when Keycloak is unavailable
  232. func TestKeycloakFailover(t *testing.T) {
  233. // Temporarily override Keycloak URL to simulate unavailability
  234. originalURL := os.Getenv("KEYCLOAK_URL")
  235. os.Setenv("KEYCLOAK_URL", "http://localhost:9999") // Non-existent service
  236. defer func() {
  237. if originalURL != "" {
  238. os.Setenv("KEYCLOAK_URL", originalURL)
  239. } else {
  240. os.Unsetenv("KEYCLOAK_URL")
  241. }
  242. }()
  243. framework := NewS3IAMTestFramework(t)
  244. defer framework.Cleanup()
  245. // Should fall back to mock OIDC
  246. assert.False(t, framework.useKeycloak, "Should fall back to mock OIDC when Keycloak is unavailable")
  247. assert.Nil(t, framework.keycloakClient, "Keycloak client should not be initialized")
  248. assert.NotNil(t, framework.mockOIDC, "Mock OIDC server should be initialized")
  249. // Test that mock authentication still works
  250. s3Client, err := framework.CreateS3ClientWithJWT("admin-user", "TestAdminRole")
  251. require.NoError(t, err, "Should be able to create S3 client with mock authentication")
  252. // Basic operation should work
  253. _, err = s3Client.ListBuckets(&s3.ListBucketsInput{})
  254. // Note: This may still fail due to session store issues, but the client creation should work
  255. }