s3_iam_framework.go 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861
  1. package iam
  2. import (
  3. "context"
  4. cryptorand "crypto/rand"
  5. "crypto/rsa"
  6. "encoding/base64"
  7. "encoding/json"
  8. "fmt"
  9. "io"
  10. mathrand "math/rand"
  11. "net/http"
  12. "net/http/httptest"
  13. "net/url"
  14. "os"
  15. "strings"
  16. "testing"
  17. "time"
  18. "github.com/aws/aws-sdk-go/aws"
  19. "github.com/aws/aws-sdk-go/aws/awserr"
  20. "github.com/aws/aws-sdk-go/aws/credentials"
  21. "github.com/aws/aws-sdk-go/aws/session"
  22. "github.com/aws/aws-sdk-go/service/s3"
  23. "github.com/golang-jwt/jwt/v5"
  24. "github.com/stretchr/testify/require"
  25. )
  26. const (
  27. TestS3Endpoint = "http://localhost:8333"
  28. TestRegion = "us-west-2"
  29. // Keycloak configuration
  30. DefaultKeycloakURL = "http://localhost:8080"
  31. KeycloakRealm = "seaweedfs-test"
  32. KeycloakClientID = "seaweedfs-s3"
  33. KeycloakClientSecret = "seaweedfs-s3-secret"
  34. )
  35. // S3IAMTestFramework provides utilities for S3+IAM integration testing
  36. type S3IAMTestFramework struct {
  37. t *testing.T
  38. mockOIDC *httptest.Server
  39. privateKey *rsa.PrivateKey
  40. publicKey *rsa.PublicKey
  41. createdBuckets []string
  42. ctx context.Context
  43. keycloakClient *KeycloakClient
  44. useKeycloak bool
  45. }
  46. // KeycloakClient handles authentication with Keycloak
  47. type KeycloakClient struct {
  48. baseURL string
  49. realm string
  50. clientID string
  51. clientSecret string
  52. httpClient *http.Client
  53. }
  54. // KeycloakTokenResponse represents Keycloak token response
  55. type KeycloakTokenResponse struct {
  56. AccessToken string `json:"access_token"`
  57. TokenType string `json:"token_type"`
  58. ExpiresIn int `json:"expires_in"`
  59. RefreshToken string `json:"refresh_token,omitempty"`
  60. Scope string `json:"scope,omitempty"`
  61. }
  62. // NewS3IAMTestFramework creates a new test framework instance
  63. func NewS3IAMTestFramework(t *testing.T) *S3IAMTestFramework {
  64. framework := &S3IAMTestFramework{
  65. t: t,
  66. ctx: context.Background(),
  67. createdBuckets: make([]string, 0),
  68. }
  69. // Check if we should use Keycloak or mock OIDC
  70. keycloakURL := os.Getenv("KEYCLOAK_URL")
  71. if keycloakURL == "" {
  72. keycloakURL = DefaultKeycloakURL
  73. }
  74. // Test if Keycloak is available
  75. framework.useKeycloak = framework.isKeycloakAvailable(keycloakURL)
  76. if framework.useKeycloak {
  77. t.Logf("Using real Keycloak instance at %s", keycloakURL)
  78. framework.keycloakClient = NewKeycloakClient(keycloakURL, KeycloakRealm, KeycloakClientID, KeycloakClientSecret)
  79. } else {
  80. t.Logf("Using mock OIDC server for testing")
  81. // Generate RSA keys for JWT signing (mock mode)
  82. var err error
  83. framework.privateKey, err = rsa.GenerateKey(cryptorand.Reader, 2048)
  84. require.NoError(t, err)
  85. framework.publicKey = &framework.privateKey.PublicKey
  86. // Setup mock OIDC server
  87. framework.setupMockOIDCServer()
  88. }
  89. return framework
  90. }
  91. // NewKeycloakClient creates a new Keycloak client
  92. func NewKeycloakClient(baseURL, realm, clientID, clientSecret string) *KeycloakClient {
  93. return &KeycloakClient{
  94. baseURL: baseURL,
  95. realm: realm,
  96. clientID: clientID,
  97. clientSecret: clientSecret,
  98. httpClient: &http.Client{Timeout: 30 * time.Second},
  99. }
  100. }
  101. // isKeycloakAvailable checks if Keycloak is running and accessible
  102. func (f *S3IAMTestFramework) isKeycloakAvailable(keycloakURL string) bool {
  103. client := &http.Client{Timeout: 5 * time.Second}
  104. // Use realms endpoint instead of health/ready for Keycloak v26+
  105. // First, verify master realm is reachable
  106. masterURL := fmt.Sprintf("%s/realms/master", keycloakURL)
  107. resp, err := client.Get(masterURL)
  108. if err != nil {
  109. return false
  110. }
  111. defer resp.Body.Close()
  112. if resp.StatusCode != http.StatusOK {
  113. return false
  114. }
  115. // Also ensure the specific test realm exists; otherwise fall back to mock
  116. testRealmURL := fmt.Sprintf("%s/realms/%s", keycloakURL, KeycloakRealm)
  117. resp2, err := client.Get(testRealmURL)
  118. if err != nil {
  119. return false
  120. }
  121. defer resp2.Body.Close()
  122. return resp2.StatusCode == http.StatusOK
  123. }
  124. // AuthenticateUser authenticates a user with Keycloak and returns an access token
  125. func (kc *KeycloakClient) AuthenticateUser(username, password string) (*KeycloakTokenResponse, error) {
  126. tokenURL := fmt.Sprintf("%s/realms/%s/protocol/openid-connect/token", kc.baseURL, kc.realm)
  127. data := url.Values{}
  128. data.Set("grant_type", "password")
  129. data.Set("client_id", kc.clientID)
  130. data.Set("client_secret", kc.clientSecret)
  131. data.Set("username", username)
  132. data.Set("password", password)
  133. data.Set("scope", "openid profile email")
  134. resp, err := kc.httpClient.PostForm(tokenURL, data)
  135. if err != nil {
  136. return nil, fmt.Errorf("failed to authenticate with Keycloak: %w", err)
  137. }
  138. defer resp.Body.Close()
  139. if resp.StatusCode != 200 {
  140. // Read the response body for debugging
  141. body, readErr := io.ReadAll(resp.Body)
  142. bodyStr := ""
  143. if readErr == nil {
  144. bodyStr = string(body)
  145. }
  146. return nil, fmt.Errorf("Keycloak authentication failed with status: %d, response: %s", resp.StatusCode, bodyStr)
  147. }
  148. var tokenResp KeycloakTokenResponse
  149. if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
  150. return nil, fmt.Errorf("failed to decode token response: %w", err)
  151. }
  152. return &tokenResp, nil
  153. }
  154. // getKeycloakToken authenticates with Keycloak and returns a JWT token
  155. func (f *S3IAMTestFramework) getKeycloakToken(username string) (string, error) {
  156. if f.keycloakClient == nil {
  157. return "", fmt.Errorf("Keycloak client not initialized")
  158. }
  159. // Map username to password for test users
  160. password := f.getTestUserPassword(username)
  161. if password == "" {
  162. return "", fmt.Errorf("unknown test user: %s", username)
  163. }
  164. tokenResp, err := f.keycloakClient.AuthenticateUser(username, password)
  165. if err != nil {
  166. return "", fmt.Errorf("failed to authenticate user %s: %w", username, err)
  167. }
  168. return tokenResp.AccessToken, nil
  169. }
  170. // getTestUserPassword returns the password for test users
  171. func (f *S3IAMTestFramework) getTestUserPassword(username string) string {
  172. // Password generation matches setup_keycloak_docker.sh logic:
  173. // password="${username//[^a-zA-Z]/}123" (removes non-alphabetic chars + "123")
  174. userPasswords := map[string]string{
  175. "admin-user": "adminuser123", // "admin-user" -> "adminuser" + "123"
  176. "read-user": "readuser123", // "read-user" -> "readuser" + "123"
  177. "write-user": "writeuser123", // "write-user" -> "writeuser" + "123"
  178. "write-only-user": "writeonlyuser123", // "write-only-user" -> "writeonlyuser" + "123"
  179. }
  180. return userPasswords[username]
  181. }
  182. // setupMockOIDCServer creates a mock OIDC server for testing
  183. func (f *S3IAMTestFramework) setupMockOIDCServer() {
  184. f.mockOIDC = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  185. switch r.URL.Path {
  186. case "/.well-known/openid_configuration":
  187. config := map[string]interface{}{
  188. "issuer": "http://" + r.Host,
  189. "jwks_uri": "http://" + r.Host + "/jwks",
  190. "userinfo_endpoint": "http://" + r.Host + "/userinfo",
  191. }
  192. w.Header().Set("Content-Type", "application/json")
  193. fmt.Fprintf(w, `{
  194. "issuer": "%s",
  195. "jwks_uri": "%s",
  196. "userinfo_endpoint": "%s"
  197. }`, config["issuer"], config["jwks_uri"], config["userinfo_endpoint"])
  198. case "/jwks":
  199. w.Header().Set("Content-Type", "application/json")
  200. fmt.Fprintf(w, `{
  201. "keys": [
  202. {
  203. "kty": "RSA",
  204. "kid": "test-key-id",
  205. "use": "sig",
  206. "alg": "RS256",
  207. "n": "%s",
  208. "e": "AQAB"
  209. }
  210. ]
  211. }`, f.encodePublicKey())
  212. case "/userinfo":
  213. authHeader := r.Header.Get("Authorization")
  214. if !strings.HasPrefix(authHeader, "Bearer ") {
  215. w.WriteHeader(http.StatusUnauthorized)
  216. return
  217. }
  218. token := strings.TrimPrefix(authHeader, "Bearer ")
  219. userInfo := map[string]interface{}{
  220. "sub": "test-user",
  221. "email": "test@example.com",
  222. "name": "Test User",
  223. "groups": []string{"users", "developers"},
  224. }
  225. if strings.Contains(token, "admin") {
  226. userInfo["groups"] = []string{"admins"}
  227. }
  228. w.Header().Set("Content-Type", "application/json")
  229. fmt.Fprintf(w, `{
  230. "sub": "%s",
  231. "email": "%s",
  232. "name": "%s",
  233. "groups": %v
  234. }`, userInfo["sub"], userInfo["email"], userInfo["name"], userInfo["groups"])
  235. default:
  236. http.NotFound(w, r)
  237. }
  238. }))
  239. }
  240. // encodePublicKey encodes the RSA public key for JWKS
  241. func (f *S3IAMTestFramework) encodePublicKey() string {
  242. return base64.RawURLEncoding.EncodeToString(f.publicKey.N.Bytes())
  243. }
  244. // BearerTokenTransport is an HTTP transport that adds Bearer token authentication
  245. type BearerTokenTransport struct {
  246. Transport http.RoundTripper
  247. Token string
  248. }
  249. // RoundTrip implements the http.RoundTripper interface
  250. func (t *BearerTokenTransport) RoundTrip(req *http.Request) (*http.Response, error) {
  251. // Clone the request to avoid modifying the original
  252. newReq := req.Clone(req.Context())
  253. // Remove ALL existing Authorization headers first to prevent conflicts
  254. newReq.Header.Del("Authorization")
  255. newReq.Header.Del("X-Amz-Date")
  256. newReq.Header.Del("X-Amz-Content-Sha256")
  257. newReq.Header.Del("X-Amz-Signature")
  258. newReq.Header.Del("X-Amz-Algorithm")
  259. newReq.Header.Del("X-Amz-Credential")
  260. newReq.Header.Del("X-Amz-SignedHeaders")
  261. newReq.Header.Del("X-Amz-Security-Token")
  262. // Add Bearer token authorization header
  263. newReq.Header.Set("Authorization", "Bearer "+t.Token)
  264. // Extract and set the principal ARN from JWT token for security compliance
  265. if principal := t.extractPrincipalFromJWT(t.Token); principal != "" {
  266. newReq.Header.Set("X-SeaweedFS-Principal", principal)
  267. }
  268. // Token preview for logging (first 50 chars for security)
  269. tokenPreview := t.Token
  270. if len(tokenPreview) > 50 {
  271. tokenPreview = tokenPreview[:50] + "..."
  272. }
  273. // Use underlying transport
  274. transport := t.Transport
  275. if transport == nil {
  276. transport = http.DefaultTransport
  277. }
  278. return transport.RoundTrip(newReq)
  279. }
  280. // extractPrincipalFromJWT extracts the principal ARN from a JWT token without validating it
  281. // This is used to set the X-SeaweedFS-Principal header that's required after our security fix
  282. func (t *BearerTokenTransport) extractPrincipalFromJWT(tokenString string) string {
  283. // Parse the JWT token without validation to extract the principal claim
  284. token, _ := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
  285. // We don't validate the signature here, just extract the claims
  286. // This is safe because the actual validation happens server-side
  287. return []byte("dummy-key"), nil
  288. })
  289. // Even if parsing fails due to signature verification, we might still get claims
  290. if claims, ok := token.Claims.(jwt.MapClaims); ok {
  291. // Try multiple possible claim names for the principal ARN
  292. if principal, exists := claims["principal"]; exists {
  293. if principalStr, ok := principal.(string); ok {
  294. return principalStr
  295. }
  296. }
  297. if assumed, exists := claims["assumed"]; exists {
  298. if assumedStr, ok := assumed.(string); ok {
  299. return assumedStr
  300. }
  301. }
  302. }
  303. return ""
  304. }
  305. // generateSTSSessionToken creates a session token using the actual STS service for proper validation
  306. func (f *S3IAMTestFramework) generateSTSSessionToken(username, roleName string, validDuration time.Duration) (string, error) {
  307. // For now, simulate what the STS service would return by calling AssumeRoleWithWebIdentity
  308. // In a real test, we'd make an actual HTTP call to the STS endpoint
  309. // But for unit testing, we'll create a realistic JWT manually that will pass validation
  310. now := time.Now()
  311. signingKeyB64 := "dGVzdC1zaWduaW5nLWtleS0zMi1jaGFyYWN0ZXJzLWxvbmc="
  312. signingKey, err := base64.StdEncoding.DecodeString(signingKeyB64)
  313. if err != nil {
  314. return "", fmt.Errorf("failed to decode signing key: %v", err)
  315. }
  316. // Generate a session ID that would be created by the STS service
  317. sessionId := fmt.Sprintf("test-session-%s-%s-%d", username, roleName, now.Unix())
  318. // Create session token claims exactly matching STSSessionClaims struct
  319. roleArn := fmt.Sprintf("arn:seaweed:iam::role/%s", roleName)
  320. sessionName := fmt.Sprintf("test-session-%s", username)
  321. principalArn := fmt.Sprintf("arn:seaweed:sts::assumed-role/%s/%s", roleName, sessionName)
  322. // Use jwt.MapClaims but with exact field names that STSSessionClaims expects
  323. sessionClaims := jwt.MapClaims{
  324. // RegisteredClaims fields
  325. "iss": "seaweedfs-sts",
  326. "sub": sessionId,
  327. "iat": now.Unix(),
  328. "exp": now.Add(validDuration).Unix(),
  329. "nbf": now.Unix(),
  330. // STSSessionClaims fields (using exact JSON tags from the struct)
  331. "sid": sessionId, // SessionId
  332. "snam": sessionName, // SessionName
  333. "typ": "session", // TokenType
  334. "role": roleArn, // RoleArn
  335. "assumed": principalArn, // AssumedRole
  336. "principal": principalArn, // Principal
  337. "idp": "test-oidc", // IdentityProvider
  338. "ext_uid": username, // ExternalUserId
  339. "assumed_at": now.Format(time.RFC3339Nano), // AssumedAt
  340. "max_dur": int64(validDuration.Seconds()), // MaxDuration
  341. }
  342. token := jwt.NewWithClaims(jwt.SigningMethodHS256, sessionClaims)
  343. tokenString, err := token.SignedString(signingKey)
  344. if err != nil {
  345. return "", err
  346. }
  347. // The generated JWT is self-contained and includes all necessary session information.
  348. // The stateless design of the STS service means no external session storage is required.
  349. return tokenString, nil
  350. }
  351. // CreateS3ClientWithJWT creates an S3 client authenticated with a JWT token for the specified role
  352. func (f *S3IAMTestFramework) CreateS3ClientWithJWT(username, roleName string) (*s3.S3, error) {
  353. var token string
  354. var err error
  355. if f.useKeycloak {
  356. // Use real Keycloak authentication
  357. token, err = f.getKeycloakToken(username)
  358. if err != nil {
  359. return nil, fmt.Errorf("failed to get Keycloak token: %v", err)
  360. }
  361. } else {
  362. // Generate STS session token (mock mode)
  363. token, err = f.generateSTSSessionToken(username, roleName, time.Hour)
  364. if err != nil {
  365. return nil, fmt.Errorf("failed to generate STS session token: %v", err)
  366. }
  367. }
  368. // Create custom HTTP client with Bearer token transport
  369. httpClient := &http.Client{
  370. Transport: &BearerTokenTransport{
  371. Token: token,
  372. },
  373. }
  374. sess, err := session.NewSession(&aws.Config{
  375. Region: aws.String(TestRegion),
  376. Endpoint: aws.String(TestS3Endpoint),
  377. HTTPClient: httpClient,
  378. // Use anonymous credentials to avoid AWS signature generation
  379. Credentials: credentials.AnonymousCredentials,
  380. DisableSSL: aws.Bool(true),
  381. S3ForcePathStyle: aws.Bool(true),
  382. })
  383. if err != nil {
  384. return nil, fmt.Errorf("failed to create AWS session: %v", err)
  385. }
  386. return s3.New(sess), nil
  387. }
  388. // CreateS3ClientWithInvalidJWT creates an S3 client with an invalid JWT token
  389. func (f *S3IAMTestFramework) CreateS3ClientWithInvalidJWT() (*s3.S3, error) {
  390. invalidToken := "invalid.jwt.token"
  391. // Create custom HTTP client with Bearer token transport
  392. httpClient := &http.Client{
  393. Transport: &BearerTokenTransport{
  394. Token: invalidToken,
  395. },
  396. }
  397. sess, err := session.NewSession(&aws.Config{
  398. Region: aws.String(TestRegion),
  399. Endpoint: aws.String(TestS3Endpoint),
  400. HTTPClient: httpClient,
  401. // Use anonymous credentials to avoid AWS signature generation
  402. Credentials: credentials.AnonymousCredentials,
  403. DisableSSL: aws.Bool(true),
  404. S3ForcePathStyle: aws.Bool(true),
  405. })
  406. if err != nil {
  407. return nil, fmt.Errorf("failed to create AWS session: %v", err)
  408. }
  409. return s3.New(sess), nil
  410. }
  411. // CreateS3ClientWithExpiredJWT creates an S3 client with an expired JWT token
  412. func (f *S3IAMTestFramework) CreateS3ClientWithExpiredJWT(username, roleName string) (*s3.S3, error) {
  413. // Generate expired STS session token (expired 1 hour ago)
  414. token, err := f.generateSTSSessionToken(username, roleName, -time.Hour)
  415. if err != nil {
  416. return nil, fmt.Errorf("failed to generate expired STS session token: %v", err)
  417. }
  418. // Create custom HTTP client with Bearer token transport
  419. httpClient := &http.Client{
  420. Transport: &BearerTokenTransport{
  421. Token: token,
  422. },
  423. }
  424. sess, err := session.NewSession(&aws.Config{
  425. Region: aws.String(TestRegion),
  426. Endpoint: aws.String(TestS3Endpoint),
  427. HTTPClient: httpClient,
  428. // Use anonymous credentials to avoid AWS signature generation
  429. Credentials: credentials.AnonymousCredentials,
  430. DisableSSL: aws.Bool(true),
  431. S3ForcePathStyle: aws.Bool(true),
  432. })
  433. if err != nil {
  434. return nil, fmt.Errorf("failed to create AWS session: %v", err)
  435. }
  436. return s3.New(sess), nil
  437. }
  438. // CreateS3ClientWithSessionToken creates an S3 client with a session token
  439. func (f *S3IAMTestFramework) CreateS3ClientWithSessionToken(sessionToken string) (*s3.S3, error) {
  440. sess, err := session.NewSession(&aws.Config{
  441. Region: aws.String(TestRegion),
  442. Endpoint: aws.String(TestS3Endpoint),
  443. Credentials: credentials.NewStaticCredentials(
  444. "session-access-key",
  445. "session-secret-key",
  446. sessionToken,
  447. ),
  448. DisableSSL: aws.Bool(true),
  449. S3ForcePathStyle: aws.Bool(true),
  450. })
  451. if err != nil {
  452. return nil, fmt.Errorf("failed to create AWS session: %v", err)
  453. }
  454. return s3.New(sess), nil
  455. }
  456. // CreateS3ClientWithKeycloakToken creates an S3 client using a Keycloak JWT token
  457. func (f *S3IAMTestFramework) CreateS3ClientWithKeycloakToken(keycloakToken string) (*s3.S3, error) {
  458. // Determine response header timeout based on environment
  459. responseHeaderTimeout := 10 * time.Second
  460. overallTimeout := 30 * time.Second
  461. if os.Getenv("GITHUB_ACTIONS") == "true" {
  462. responseHeaderTimeout = 30 * time.Second // Longer timeout for CI JWT validation
  463. overallTimeout = 60 * time.Second
  464. }
  465. // Create a fresh HTTP transport with appropriate timeouts
  466. transport := &http.Transport{
  467. DisableKeepAlives: true, // Force new connections for each request
  468. DisableCompression: true, // Disable compression to simplify requests
  469. MaxIdleConns: 0, // No connection pooling
  470. MaxIdleConnsPerHost: 0, // No connection pooling per host
  471. IdleConnTimeout: 1 * time.Second,
  472. TLSHandshakeTimeout: 5 * time.Second,
  473. ResponseHeaderTimeout: responseHeaderTimeout, // Adjustable for CI environments
  474. ExpectContinueTimeout: 1 * time.Second,
  475. }
  476. // Create a custom HTTP client with appropriate timeouts
  477. httpClient := &http.Client{
  478. Timeout: overallTimeout, // Overall request timeout (adjustable for CI)
  479. Transport: &BearerTokenTransport{
  480. Token: keycloakToken,
  481. Transport: transport,
  482. },
  483. }
  484. sess, err := session.NewSession(&aws.Config{
  485. Region: aws.String(TestRegion),
  486. Endpoint: aws.String(TestS3Endpoint),
  487. Credentials: credentials.AnonymousCredentials,
  488. DisableSSL: aws.Bool(true),
  489. S3ForcePathStyle: aws.Bool(true),
  490. HTTPClient: httpClient,
  491. MaxRetries: aws.Int(0), // No retries to avoid delays
  492. })
  493. if err != nil {
  494. return nil, fmt.Errorf("failed to create AWS session: %v", err)
  495. }
  496. return s3.New(sess), nil
  497. }
  498. // TestKeycloakTokenDirectly tests a Keycloak token with direct HTTP request (bypassing AWS SDK)
  499. func (f *S3IAMTestFramework) TestKeycloakTokenDirectly(keycloakToken string) error {
  500. // Create a simple HTTP client with timeout
  501. client := &http.Client{
  502. Timeout: 10 * time.Second,
  503. }
  504. // Create request to list buckets
  505. req, err := http.NewRequest("GET", TestS3Endpoint, nil)
  506. if err != nil {
  507. return fmt.Errorf("failed to create request: %v", err)
  508. }
  509. // Add Bearer token
  510. req.Header.Set("Authorization", "Bearer "+keycloakToken)
  511. req.Header.Set("Host", "localhost:8333")
  512. // Make request
  513. resp, err := client.Do(req)
  514. if err != nil {
  515. return fmt.Errorf("request failed: %v", err)
  516. }
  517. defer resp.Body.Close()
  518. // Read response
  519. _, err = io.ReadAll(resp.Body)
  520. if err != nil {
  521. return fmt.Errorf("failed to read response: %v", err)
  522. }
  523. return nil
  524. }
  525. // generateJWTToken creates a JWT token for testing
  526. func (f *S3IAMTestFramework) generateJWTToken(username, roleName string, validDuration time.Duration) (string, error) {
  527. now := time.Now()
  528. claims := jwt.MapClaims{
  529. "sub": username,
  530. "iss": f.mockOIDC.URL,
  531. "aud": "test-client",
  532. "exp": now.Add(validDuration).Unix(),
  533. "iat": now.Unix(),
  534. "email": username + "@example.com",
  535. "name": strings.Title(username),
  536. }
  537. // Add role-specific groups
  538. switch roleName {
  539. case "TestAdminRole":
  540. claims["groups"] = []string{"admins"}
  541. case "TestReadOnlyRole":
  542. claims["groups"] = []string{"users"}
  543. case "TestWriteOnlyRole":
  544. claims["groups"] = []string{"writers"}
  545. default:
  546. claims["groups"] = []string{"users"}
  547. }
  548. token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
  549. token.Header["kid"] = "test-key-id"
  550. tokenString, err := token.SignedString(f.privateKey)
  551. if err != nil {
  552. return "", fmt.Errorf("failed to sign token: %v", err)
  553. }
  554. return tokenString, nil
  555. }
  556. // CreateShortLivedSessionToken creates a mock session token for testing
  557. func (f *S3IAMTestFramework) CreateShortLivedSessionToken(username, roleName string, durationSeconds int64) (string, error) {
  558. // For testing purposes, create a mock session token
  559. // In reality, this would be generated by the STS service
  560. return fmt.Sprintf("mock-session-token-%s-%s-%d", username, roleName, time.Now().Unix()), nil
  561. }
  562. // ExpireSessionForTesting simulates session expiration for testing
  563. func (f *S3IAMTestFramework) ExpireSessionForTesting(sessionToken string) error {
  564. // For integration tests, this would typically involve calling the STS service
  565. // For now, we just simulate success since the actual expiration will be handled by SeaweedFS
  566. return nil
  567. }
  568. // GenerateUniqueBucketName generates a unique bucket name for testing
  569. func (f *S3IAMTestFramework) GenerateUniqueBucketName(prefix string) string {
  570. // Use test name and timestamp to ensure uniqueness
  571. testName := strings.ToLower(f.t.Name())
  572. testName = strings.ReplaceAll(testName, "/", "-")
  573. testName = strings.ReplaceAll(testName, "_", "-")
  574. // Add random suffix to handle parallel tests
  575. randomSuffix := mathrand.Intn(10000)
  576. return fmt.Sprintf("%s-%s-%d", prefix, testName, randomSuffix)
  577. }
  578. // CreateBucket creates a bucket and tracks it for cleanup
  579. func (f *S3IAMTestFramework) CreateBucket(s3Client *s3.S3, bucketName string) error {
  580. _, err := s3Client.CreateBucket(&s3.CreateBucketInput{
  581. Bucket: aws.String(bucketName),
  582. })
  583. if err != nil {
  584. return err
  585. }
  586. // Track bucket for cleanup
  587. f.createdBuckets = append(f.createdBuckets, bucketName)
  588. return nil
  589. }
  590. // CreateBucketWithCleanup creates a bucket, cleaning up any existing bucket first
  591. func (f *S3IAMTestFramework) CreateBucketWithCleanup(s3Client *s3.S3, bucketName string) error {
  592. // First try to create the bucket normally
  593. _, err := s3Client.CreateBucket(&s3.CreateBucketInput{
  594. Bucket: aws.String(bucketName),
  595. })
  596. if err != nil {
  597. // If bucket already exists, clean it up first
  598. if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "BucketAlreadyExists" {
  599. f.t.Logf("Bucket %s already exists, cleaning up first", bucketName)
  600. // Empty the existing bucket
  601. f.emptyBucket(s3Client, bucketName)
  602. // Don't need to recreate - bucket already exists and is now empty
  603. } else {
  604. return err
  605. }
  606. }
  607. // Track bucket for cleanup
  608. f.createdBuckets = append(f.createdBuckets, bucketName)
  609. return nil
  610. }
  611. // emptyBucket removes all objects from a bucket
  612. func (f *S3IAMTestFramework) emptyBucket(s3Client *s3.S3, bucketName string) {
  613. // Delete all objects
  614. listResult, err := s3Client.ListObjects(&s3.ListObjectsInput{
  615. Bucket: aws.String(bucketName),
  616. })
  617. if err == nil {
  618. for _, obj := range listResult.Contents {
  619. _, err := s3Client.DeleteObject(&s3.DeleteObjectInput{
  620. Bucket: aws.String(bucketName),
  621. Key: obj.Key,
  622. })
  623. if err != nil {
  624. f.t.Logf("Warning: Failed to delete object %s/%s: %v", bucketName, *obj.Key, err)
  625. }
  626. }
  627. }
  628. }
  629. // Cleanup cleans up test resources
  630. func (f *S3IAMTestFramework) Cleanup() {
  631. // Clean up buckets (best effort)
  632. if len(f.createdBuckets) > 0 {
  633. // Create admin client for cleanup
  634. adminClient, err := f.CreateS3ClientWithJWT("admin-user", "TestAdminRole")
  635. if err == nil {
  636. for _, bucket := range f.createdBuckets {
  637. // Try to empty bucket first
  638. listResult, err := adminClient.ListObjects(&s3.ListObjectsInput{
  639. Bucket: aws.String(bucket),
  640. })
  641. if err == nil {
  642. for _, obj := range listResult.Contents {
  643. adminClient.DeleteObject(&s3.DeleteObjectInput{
  644. Bucket: aws.String(bucket),
  645. Key: obj.Key,
  646. })
  647. }
  648. }
  649. // Delete bucket
  650. adminClient.DeleteBucket(&s3.DeleteBucketInput{
  651. Bucket: aws.String(bucket),
  652. })
  653. }
  654. }
  655. }
  656. // Close mock OIDC server
  657. if f.mockOIDC != nil {
  658. f.mockOIDC.Close()
  659. }
  660. }
  661. // WaitForS3Service waits for the S3 service to be available
  662. func (f *S3IAMTestFramework) WaitForS3Service() error {
  663. // Create a basic S3 client
  664. sess, err := session.NewSession(&aws.Config{
  665. Region: aws.String(TestRegion),
  666. Endpoint: aws.String(TestS3Endpoint),
  667. Credentials: credentials.NewStaticCredentials(
  668. "test-access-key",
  669. "test-secret-key",
  670. "",
  671. ),
  672. DisableSSL: aws.Bool(true),
  673. S3ForcePathStyle: aws.Bool(true),
  674. })
  675. if err != nil {
  676. return fmt.Errorf("failed to create AWS session: %v", err)
  677. }
  678. s3Client := s3.New(sess)
  679. // Try to list buckets to check if service is available
  680. maxRetries := 30
  681. for i := 0; i < maxRetries; i++ {
  682. _, err := s3Client.ListBuckets(&s3.ListBucketsInput{})
  683. if err == nil {
  684. return nil
  685. }
  686. time.Sleep(1 * time.Second)
  687. }
  688. return fmt.Errorf("S3 service not available after %d retries", maxRetries)
  689. }
  690. // PutTestObject puts a test object in the specified bucket
  691. func (f *S3IAMTestFramework) PutTestObject(client *s3.S3, bucket, key, content string) error {
  692. _, err := client.PutObject(&s3.PutObjectInput{
  693. Bucket: aws.String(bucket),
  694. Key: aws.String(key),
  695. Body: strings.NewReader(content),
  696. })
  697. return err
  698. }
  699. // GetTestObject retrieves a test object from the specified bucket
  700. func (f *S3IAMTestFramework) GetTestObject(client *s3.S3, bucket, key string) (string, error) {
  701. result, err := client.GetObject(&s3.GetObjectInput{
  702. Bucket: aws.String(bucket),
  703. Key: aws.String(key),
  704. })
  705. if err != nil {
  706. return "", err
  707. }
  708. defer result.Body.Close()
  709. content := strings.Builder{}
  710. _, err = io.Copy(&content, result.Body)
  711. if err != nil {
  712. return "", err
  713. }
  714. return content.String(), nil
  715. }
  716. // ListTestObjects lists objects in the specified bucket
  717. func (f *S3IAMTestFramework) ListTestObjects(client *s3.S3, bucket string) ([]string, error) {
  718. result, err := client.ListObjects(&s3.ListObjectsInput{
  719. Bucket: aws.String(bucket),
  720. })
  721. if err != nil {
  722. return nil, err
  723. }
  724. var keys []string
  725. for _, obj := range result.Contents {
  726. keys = append(keys, *obj.Key)
  727. }
  728. return keys, nil
  729. }
  730. // DeleteTestObject deletes a test object from the specified bucket
  731. func (f *S3IAMTestFramework) DeleteTestObject(client *s3.S3, bucket, key string) error {
  732. _, err := client.DeleteObject(&s3.DeleteObjectInput{
  733. Bucket: aws.String(bucket),
  734. Key: aws.String(key),
  735. })
  736. return err
  737. }
  738. // WaitForS3Service waits for the S3 service to be available (simplified version)
  739. func (f *S3IAMTestFramework) WaitForS3ServiceSimple() error {
  740. // This is a simplified version that just checks if the endpoint responds
  741. // The full implementation would be in the Makefile's wait-for-services target
  742. return nil
  743. }