| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861 |
- package iam
- import (
- "context"
- cryptorand "crypto/rand"
- "crypto/rsa"
- "encoding/base64"
- "encoding/json"
- "fmt"
- "io"
- mathrand "math/rand"
- "net/http"
- "net/http/httptest"
- "net/url"
- "os"
- "strings"
- "testing"
- "time"
- "github.com/aws/aws-sdk-go/aws"
- "github.com/aws/aws-sdk-go/aws/awserr"
- "github.com/aws/aws-sdk-go/aws/credentials"
- "github.com/aws/aws-sdk-go/aws/session"
- "github.com/aws/aws-sdk-go/service/s3"
- "github.com/golang-jwt/jwt/v5"
- "github.com/stretchr/testify/require"
- )
- const (
- TestS3Endpoint = "http://localhost:8333"
- TestRegion = "us-west-2"
- // Keycloak configuration
- DefaultKeycloakURL = "http://localhost:8080"
- KeycloakRealm = "seaweedfs-test"
- KeycloakClientID = "seaweedfs-s3"
- KeycloakClientSecret = "seaweedfs-s3-secret"
- )
- // S3IAMTestFramework provides utilities for S3+IAM integration testing
- type S3IAMTestFramework struct {
- t *testing.T
- mockOIDC *httptest.Server
- privateKey *rsa.PrivateKey
- publicKey *rsa.PublicKey
- createdBuckets []string
- ctx context.Context
- keycloakClient *KeycloakClient
- useKeycloak bool
- }
- // KeycloakClient handles authentication with Keycloak
- type KeycloakClient struct {
- baseURL string
- realm string
- clientID string
- clientSecret string
- httpClient *http.Client
- }
- // KeycloakTokenResponse represents Keycloak token response
- type KeycloakTokenResponse struct {
- AccessToken string `json:"access_token"`
- TokenType string `json:"token_type"`
- ExpiresIn int `json:"expires_in"`
- RefreshToken string `json:"refresh_token,omitempty"`
- Scope string `json:"scope,omitempty"`
- }
- // NewS3IAMTestFramework creates a new test framework instance
- func NewS3IAMTestFramework(t *testing.T) *S3IAMTestFramework {
- framework := &S3IAMTestFramework{
- t: t,
- ctx: context.Background(),
- createdBuckets: make([]string, 0),
- }
- // Check if we should use Keycloak or mock OIDC
- keycloakURL := os.Getenv("KEYCLOAK_URL")
- if keycloakURL == "" {
- keycloakURL = DefaultKeycloakURL
- }
- // Test if Keycloak is available
- framework.useKeycloak = framework.isKeycloakAvailable(keycloakURL)
- if framework.useKeycloak {
- t.Logf("Using real Keycloak instance at %s", keycloakURL)
- framework.keycloakClient = NewKeycloakClient(keycloakURL, KeycloakRealm, KeycloakClientID, KeycloakClientSecret)
- } else {
- t.Logf("Using mock OIDC server for testing")
- // Generate RSA keys for JWT signing (mock mode)
- var err error
- framework.privateKey, err = rsa.GenerateKey(cryptorand.Reader, 2048)
- require.NoError(t, err)
- framework.publicKey = &framework.privateKey.PublicKey
- // Setup mock OIDC server
- framework.setupMockOIDCServer()
- }
- return framework
- }
- // NewKeycloakClient creates a new Keycloak client
- func NewKeycloakClient(baseURL, realm, clientID, clientSecret string) *KeycloakClient {
- return &KeycloakClient{
- baseURL: baseURL,
- realm: realm,
- clientID: clientID,
- clientSecret: clientSecret,
- httpClient: &http.Client{Timeout: 30 * time.Second},
- }
- }
- // isKeycloakAvailable checks if Keycloak is running and accessible
- func (f *S3IAMTestFramework) isKeycloakAvailable(keycloakURL string) bool {
- client := &http.Client{Timeout: 5 * time.Second}
- // Use realms endpoint instead of health/ready for Keycloak v26+
- // First, verify master realm is reachable
- masterURL := fmt.Sprintf("%s/realms/master", keycloakURL)
- resp, err := client.Get(masterURL)
- if err != nil {
- return false
- }
- defer resp.Body.Close()
- if resp.StatusCode != http.StatusOK {
- return false
- }
- // Also ensure the specific test realm exists; otherwise fall back to mock
- testRealmURL := fmt.Sprintf("%s/realms/%s", keycloakURL, KeycloakRealm)
- resp2, err := client.Get(testRealmURL)
- if err != nil {
- return false
- }
- defer resp2.Body.Close()
- return resp2.StatusCode == http.StatusOK
- }
- // AuthenticateUser authenticates a user with Keycloak and returns an access token
- func (kc *KeycloakClient) AuthenticateUser(username, password string) (*KeycloakTokenResponse, error) {
- tokenURL := fmt.Sprintf("%s/realms/%s/protocol/openid-connect/token", kc.baseURL, kc.realm)
- data := url.Values{}
- data.Set("grant_type", "password")
- data.Set("client_id", kc.clientID)
- data.Set("client_secret", kc.clientSecret)
- data.Set("username", username)
- data.Set("password", password)
- data.Set("scope", "openid profile email")
- resp, err := kc.httpClient.PostForm(tokenURL, data)
- if err != nil {
- return nil, fmt.Errorf("failed to authenticate with Keycloak: %w", err)
- }
- defer resp.Body.Close()
- if resp.StatusCode != 200 {
- // Read the response body for debugging
- body, readErr := io.ReadAll(resp.Body)
- bodyStr := ""
- if readErr == nil {
- bodyStr = string(body)
- }
- return nil, fmt.Errorf("Keycloak authentication failed with status: %d, response: %s", resp.StatusCode, bodyStr)
- }
- var tokenResp KeycloakTokenResponse
- if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
- return nil, fmt.Errorf("failed to decode token response: %w", err)
- }
- return &tokenResp, nil
- }
- // getKeycloakToken authenticates with Keycloak and returns a JWT token
- func (f *S3IAMTestFramework) getKeycloakToken(username string) (string, error) {
- if f.keycloakClient == nil {
- return "", fmt.Errorf("Keycloak client not initialized")
- }
- // Map username to password for test users
- password := f.getTestUserPassword(username)
- if password == "" {
- return "", fmt.Errorf("unknown test user: %s", username)
- }
- tokenResp, err := f.keycloakClient.AuthenticateUser(username, password)
- if err != nil {
- return "", fmt.Errorf("failed to authenticate user %s: %w", username, err)
- }
- return tokenResp.AccessToken, nil
- }
- // getTestUserPassword returns the password for test users
- func (f *S3IAMTestFramework) getTestUserPassword(username string) string {
- // Password generation matches setup_keycloak_docker.sh logic:
- // password="${username//[^a-zA-Z]/}123" (removes non-alphabetic chars + "123")
- userPasswords := map[string]string{
- "admin-user": "adminuser123", // "admin-user" -> "adminuser" + "123"
- "read-user": "readuser123", // "read-user" -> "readuser" + "123"
- "write-user": "writeuser123", // "write-user" -> "writeuser" + "123"
- "write-only-user": "writeonlyuser123", // "write-only-user" -> "writeonlyuser" + "123"
- }
- return userPasswords[username]
- }
- // setupMockOIDCServer creates a mock OIDC server for testing
- func (f *S3IAMTestFramework) setupMockOIDCServer() {
- f.mockOIDC = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- switch r.URL.Path {
- case "/.well-known/openid_configuration":
- config := map[string]interface{}{
- "issuer": "http://" + r.Host,
- "jwks_uri": "http://" + r.Host + "/jwks",
- "userinfo_endpoint": "http://" + r.Host + "/userinfo",
- }
- w.Header().Set("Content-Type", "application/json")
- fmt.Fprintf(w, `{
- "issuer": "%s",
- "jwks_uri": "%s",
- "userinfo_endpoint": "%s"
- }`, config["issuer"], config["jwks_uri"], config["userinfo_endpoint"])
- case "/jwks":
- w.Header().Set("Content-Type", "application/json")
- fmt.Fprintf(w, `{
- "keys": [
- {
- "kty": "RSA",
- "kid": "test-key-id",
- "use": "sig",
- "alg": "RS256",
- "n": "%s",
- "e": "AQAB"
- }
- ]
- }`, f.encodePublicKey())
- case "/userinfo":
- authHeader := r.Header.Get("Authorization")
- if !strings.HasPrefix(authHeader, "Bearer ") {
- w.WriteHeader(http.StatusUnauthorized)
- return
- }
- token := strings.TrimPrefix(authHeader, "Bearer ")
- userInfo := map[string]interface{}{
- "sub": "test-user",
- "email": "test@example.com",
- "name": "Test User",
- "groups": []string{"users", "developers"},
- }
- if strings.Contains(token, "admin") {
- userInfo["groups"] = []string{"admins"}
- }
- w.Header().Set("Content-Type", "application/json")
- fmt.Fprintf(w, `{
- "sub": "%s",
- "email": "%s",
- "name": "%s",
- "groups": %v
- }`, userInfo["sub"], userInfo["email"], userInfo["name"], userInfo["groups"])
- default:
- http.NotFound(w, r)
- }
- }))
- }
- // encodePublicKey encodes the RSA public key for JWKS
- func (f *S3IAMTestFramework) encodePublicKey() string {
- return base64.RawURLEncoding.EncodeToString(f.publicKey.N.Bytes())
- }
- // BearerTokenTransport is an HTTP transport that adds Bearer token authentication
- type BearerTokenTransport struct {
- Transport http.RoundTripper
- Token string
- }
- // RoundTrip implements the http.RoundTripper interface
- func (t *BearerTokenTransport) RoundTrip(req *http.Request) (*http.Response, error) {
- // Clone the request to avoid modifying the original
- newReq := req.Clone(req.Context())
- // Remove ALL existing Authorization headers first to prevent conflicts
- newReq.Header.Del("Authorization")
- newReq.Header.Del("X-Amz-Date")
- newReq.Header.Del("X-Amz-Content-Sha256")
- newReq.Header.Del("X-Amz-Signature")
- newReq.Header.Del("X-Amz-Algorithm")
- newReq.Header.Del("X-Amz-Credential")
- newReq.Header.Del("X-Amz-SignedHeaders")
- newReq.Header.Del("X-Amz-Security-Token")
- // Add Bearer token authorization header
- newReq.Header.Set("Authorization", "Bearer "+t.Token)
- // Extract and set the principal ARN from JWT token for security compliance
- if principal := t.extractPrincipalFromJWT(t.Token); principal != "" {
- newReq.Header.Set("X-SeaweedFS-Principal", principal)
- }
- // Token preview for logging (first 50 chars for security)
- tokenPreview := t.Token
- if len(tokenPreview) > 50 {
- tokenPreview = tokenPreview[:50] + "..."
- }
- // Use underlying transport
- transport := t.Transport
- if transport == nil {
- transport = http.DefaultTransport
- }
- return transport.RoundTrip(newReq)
- }
- // extractPrincipalFromJWT extracts the principal ARN from a JWT token without validating it
- // This is used to set the X-SeaweedFS-Principal header that's required after our security fix
- func (t *BearerTokenTransport) extractPrincipalFromJWT(tokenString string) string {
- // Parse the JWT token without validation to extract the principal claim
- token, _ := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
- // We don't validate the signature here, just extract the claims
- // This is safe because the actual validation happens server-side
- return []byte("dummy-key"), nil
- })
-
- // Even if parsing fails due to signature verification, we might still get claims
- if claims, ok := token.Claims.(jwt.MapClaims); ok {
- // Try multiple possible claim names for the principal ARN
- if principal, exists := claims["principal"]; exists {
- if principalStr, ok := principal.(string); ok {
- return principalStr
- }
- }
- if assumed, exists := claims["assumed"]; exists {
- if assumedStr, ok := assumed.(string); ok {
- return assumedStr
- }
- }
- }
-
- return ""
- }
- // generateSTSSessionToken creates a session token using the actual STS service for proper validation
- func (f *S3IAMTestFramework) generateSTSSessionToken(username, roleName string, validDuration time.Duration) (string, error) {
- // For now, simulate what the STS service would return by calling AssumeRoleWithWebIdentity
- // In a real test, we'd make an actual HTTP call to the STS endpoint
- // But for unit testing, we'll create a realistic JWT manually that will pass validation
- now := time.Now()
- signingKeyB64 := "dGVzdC1zaWduaW5nLWtleS0zMi1jaGFyYWN0ZXJzLWxvbmc="
- signingKey, err := base64.StdEncoding.DecodeString(signingKeyB64)
- if err != nil {
- return "", fmt.Errorf("failed to decode signing key: %v", err)
- }
- // Generate a session ID that would be created by the STS service
- sessionId := fmt.Sprintf("test-session-%s-%s-%d", username, roleName, now.Unix())
- // Create session token claims exactly matching STSSessionClaims struct
- roleArn := fmt.Sprintf("arn:seaweed:iam::role/%s", roleName)
- sessionName := fmt.Sprintf("test-session-%s", username)
- principalArn := fmt.Sprintf("arn:seaweed:sts::assumed-role/%s/%s", roleName, sessionName)
- // Use jwt.MapClaims but with exact field names that STSSessionClaims expects
- sessionClaims := jwt.MapClaims{
- // RegisteredClaims fields
- "iss": "seaweedfs-sts",
- "sub": sessionId,
- "iat": now.Unix(),
- "exp": now.Add(validDuration).Unix(),
- "nbf": now.Unix(),
- // STSSessionClaims fields (using exact JSON tags from the struct)
- "sid": sessionId, // SessionId
- "snam": sessionName, // SessionName
- "typ": "session", // TokenType
- "role": roleArn, // RoleArn
- "assumed": principalArn, // AssumedRole
- "principal": principalArn, // Principal
- "idp": "test-oidc", // IdentityProvider
- "ext_uid": username, // ExternalUserId
- "assumed_at": now.Format(time.RFC3339Nano), // AssumedAt
- "max_dur": int64(validDuration.Seconds()), // MaxDuration
- }
- token := jwt.NewWithClaims(jwt.SigningMethodHS256, sessionClaims)
- tokenString, err := token.SignedString(signingKey)
- if err != nil {
- return "", err
- }
- // The generated JWT is self-contained and includes all necessary session information.
- // The stateless design of the STS service means no external session storage is required.
- return tokenString, nil
- }
- // CreateS3ClientWithJWT creates an S3 client authenticated with a JWT token for the specified role
- func (f *S3IAMTestFramework) CreateS3ClientWithJWT(username, roleName string) (*s3.S3, error) {
- var token string
- var err error
- if f.useKeycloak {
- // Use real Keycloak authentication
- token, err = f.getKeycloakToken(username)
- if err != nil {
- return nil, fmt.Errorf("failed to get Keycloak token: %v", err)
- }
- } else {
- // Generate STS session token (mock mode)
- token, err = f.generateSTSSessionToken(username, roleName, time.Hour)
- if err != nil {
- return nil, fmt.Errorf("failed to generate STS session token: %v", err)
- }
- }
- // Create custom HTTP client with Bearer token transport
- httpClient := &http.Client{
- Transport: &BearerTokenTransport{
- Token: token,
- },
- }
- sess, err := session.NewSession(&aws.Config{
- Region: aws.String(TestRegion),
- Endpoint: aws.String(TestS3Endpoint),
- HTTPClient: httpClient,
- // Use anonymous credentials to avoid AWS signature generation
- Credentials: credentials.AnonymousCredentials,
- DisableSSL: aws.Bool(true),
- S3ForcePathStyle: aws.Bool(true),
- })
- if err != nil {
- return nil, fmt.Errorf("failed to create AWS session: %v", err)
- }
- return s3.New(sess), nil
- }
- // CreateS3ClientWithInvalidJWT creates an S3 client with an invalid JWT token
- func (f *S3IAMTestFramework) CreateS3ClientWithInvalidJWT() (*s3.S3, error) {
- invalidToken := "invalid.jwt.token"
- // Create custom HTTP client with Bearer token transport
- httpClient := &http.Client{
- Transport: &BearerTokenTransport{
- Token: invalidToken,
- },
- }
- sess, err := session.NewSession(&aws.Config{
- Region: aws.String(TestRegion),
- Endpoint: aws.String(TestS3Endpoint),
- HTTPClient: httpClient,
- // Use anonymous credentials to avoid AWS signature generation
- Credentials: credentials.AnonymousCredentials,
- DisableSSL: aws.Bool(true),
- S3ForcePathStyle: aws.Bool(true),
- })
- if err != nil {
- return nil, fmt.Errorf("failed to create AWS session: %v", err)
- }
- return s3.New(sess), nil
- }
- // CreateS3ClientWithExpiredJWT creates an S3 client with an expired JWT token
- func (f *S3IAMTestFramework) CreateS3ClientWithExpiredJWT(username, roleName string) (*s3.S3, error) {
- // Generate expired STS session token (expired 1 hour ago)
- token, err := f.generateSTSSessionToken(username, roleName, -time.Hour)
- if err != nil {
- return nil, fmt.Errorf("failed to generate expired STS session token: %v", err)
- }
- // Create custom HTTP client with Bearer token transport
- httpClient := &http.Client{
- Transport: &BearerTokenTransport{
- Token: token,
- },
- }
- sess, err := session.NewSession(&aws.Config{
- Region: aws.String(TestRegion),
- Endpoint: aws.String(TestS3Endpoint),
- HTTPClient: httpClient,
- // Use anonymous credentials to avoid AWS signature generation
- Credentials: credentials.AnonymousCredentials,
- DisableSSL: aws.Bool(true),
- S3ForcePathStyle: aws.Bool(true),
- })
- if err != nil {
- return nil, fmt.Errorf("failed to create AWS session: %v", err)
- }
- return s3.New(sess), nil
- }
- // CreateS3ClientWithSessionToken creates an S3 client with a session token
- func (f *S3IAMTestFramework) CreateS3ClientWithSessionToken(sessionToken string) (*s3.S3, error) {
- sess, err := session.NewSession(&aws.Config{
- Region: aws.String(TestRegion),
- Endpoint: aws.String(TestS3Endpoint),
- Credentials: credentials.NewStaticCredentials(
- "session-access-key",
- "session-secret-key",
- sessionToken,
- ),
- DisableSSL: aws.Bool(true),
- S3ForcePathStyle: aws.Bool(true),
- })
- if err != nil {
- return nil, fmt.Errorf("failed to create AWS session: %v", err)
- }
- return s3.New(sess), nil
- }
- // CreateS3ClientWithKeycloakToken creates an S3 client using a Keycloak JWT token
- func (f *S3IAMTestFramework) CreateS3ClientWithKeycloakToken(keycloakToken string) (*s3.S3, error) {
- // Determine response header timeout based on environment
- responseHeaderTimeout := 10 * time.Second
- overallTimeout := 30 * time.Second
- if os.Getenv("GITHUB_ACTIONS") == "true" {
- responseHeaderTimeout = 30 * time.Second // Longer timeout for CI JWT validation
- overallTimeout = 60 * time.Second
- }
- // Create a fresh HTTP transport with appropriate timeouts
- transport := &http.Transport{
- DisableKeepAlives: true, // Force new connections for each request
- DisableCompression: true, // Disable compression to simplify requests
- MaxIdleConns: 0, // No connection pooling
- MaxIdleConnsPerHost: 0, // No connection pooling per host
- IdleConnTimeout: 1 * time.Second,
- TLSHandshakeTimeout: 5 * time.Second,
- ResponseHeaderTimeout: responseHeaderTimeout, // Adjustable for CI environments
- ExpectContinueTimeout: 1 * time.Second,
- }
- // Create a custom HTTP client with appropriate timeouts
- httpClient := &http.Client{
- Timeout: overallTimeout, // Overall request timeout (adjustable for CI)
- Transport: &BearerTokenTransport{
- Token: keycloakToken,
- Transport: transport,
- },
- }
- sess, err := session.NewSession(&aws.Config{
- Region: aws.String(TestRegion),
- Endpoint: aws.String(TestS3Endpoint),
- Credentials: credentials.AnonymousCredentials,
- DisableSSL: aws.Bool(true),
- S3ForcePathStyle: aws.Bool(true),
- HTTPClient: httpClient,
- MaxRetries: aws.Int(0), // No retries to avoid delays
- })
- if err != nil {
- return nil, fmt.Errorf("failed to create AWS session: %v", err)
- }
- return s3.New(sess), nil
- }
- // TestKeycloakTokenDirectly tests a Keycloak token with direct HTTP request (bypassing AWS SDK)
- func (f *S3IAMTestFramework) TestKeycloakTokenDirectly(keycloakToken string) error {
- // Create a simple HTTP client with timeout
- client := &http.Client{
- Timeout: 10 * time.Second,
- }
- // Create request to list buckets
- req, err := http.NewRequest("GET", TestS3Endpoint, nil)
- if err != nil {
- return fmt.Errorf("failed to create request: %v", err)
- }
- // Add Bearer token
- req.Header.Set("Authorization", "Bearer "+keycloakToken)
- req.Header.Set("Host", "localhost:8333")
- // Make request
- resp, err := client.Do(req)
- if err != nil {
- return fmt.Errorf("request failed: %v", err)
- }
- defer resp.Body.Close()
- // Read response
- _, err = io.ReadAll(resp.Body)
- if err != nil {
- return fmt.Errorf("failed to read response: %v", err)
- }
- return nil
- }
- // generateJWTToken creates a JWT token for testing
- func (f *S3IAMTestFramework) generateJWTToken(username, roleName string, validDuration time.Duration) (string, error) {
- now := time.Now()
- claims := jwt.MapClaims{
- "sub": username,
- "iss": f.mockOIDC.URL,
- "aud": "test-client",
- "exp": now.Add(validDuration).Unix(),
- "iat": now.Unix(),
- "email": username + "@example.com",
- "name": strings.Title(username),
- }
- // Add role-specific groups
- switch roleName {
- case "TestAdminRole":
- claims["groups"] = []string{"admins"}
- case "TestReadOnlyRole":
- claims["groups"] = []string{"users"}
- case "TestWriteOnlyRole":
- claims["groups"] = []string{"writers"}
- default:
- claims["groups"] = []string{"users"}
- }
- token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
- token.Header["kid"] = "test-key-id"
- tokenString, err := token.SignedString(f.privateKey)
- if err != nil {
- return "", fmt.Errorf("failed to sign token: %v", err)
- }
- return tokenString, nil
- }
- // CreateShortLivedSessionToken creates a mock session token for testing
- func (f *S3IAMTestFramework) CreateShortLivedSessionToken(username, roleName string, durationSeconds int64) (string, error) {
- // For testing purposes, create a mock session token
- // In reality, this would be generated by the STS service
- return fmt.Sprintf("mock-session-token-%s-%s-%d", username, roleName, time.Now().Unix()), nil
- }
- // ExpireSessionForTesting simulates session expiration for testing
- func (f *S3IAMTestFramework) ExpireSessionForTesting(sessionToken string) error {
- // For integration tests, this would typically involve calling the STS service
- // For now, we just simulate success since the actual expiration will be handled by SeaweedFS
- return nil
- }
- // GenerateUniqueBucketName generates a unique bucket name for testing
- func (f *S3IAMTestFramework) GenerateUniqueBucketName(prefix string) string {
- // Use test name and timestamp to ensure uniqueness
- testName := strings.ToLower(f.t.Name())
- testName = strings.ReplaceAll(testName, "/", "-")
- testName = strings.ReplaceAll(testName, "_", "-")
- // Add random suffix to handle parallel tests
- randomSuffix := mathrand.Intn(10000)
- return fmt.Sprintf("%s-%s-%d", prefix, testName, randomSuffix)
- }
- // CreateBucket creates a bucket and tracks it for cleanup
- func (f *S3IAMTestFramework) CreateBucket(s3Client *s3.S3, bucketName string) error {
- _, err := s3Client.CreateBucket(&s3.CreateBucketInput{
- Bucket: aws.String(bucketName),
- })
- if err != nil {
- return err
- }
- // Track bucket for cleanup
- f.createdBuckets = append(f.createdBuckets, bucketName)
- return nil
- }
- // CreateBucketWithCleanup creates a bucket, cleaning up any existing bucket first
- func (f *S3IAMTestFramework) CreateBucketWithCleanup(s3Client *s3.S3, bucketName string) error {
- // First try to create the bucket normally
- _, err := s3Client.CreateBucket(&s3.CreateBucketInput{
- Bucket: aws.String(bucketName),
- })
- if err != nil {
- // If bucket already exists, clean it up first
- if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "BucketAlreadyExists" {
- f.t.Logf("Bucket %s already exists, cleaning up first", bucketName)
- // Empty the existing bucket
- f.emptyBucket(s3Client, bucketName)
- // Don't need to recreate - bucket already exists and is now empty
- } else {
- return err
- }
- }
- // Track bucket for cleanup
- f.createdBuckets = append(f.createdBuckets, bucketName)
- return nil
- }
- // emptyBucket removes all objects from a bucket
- func (f *S3IAMTestFramework) emptyBucket(s3Client *s3.S3, bucketName string) {
- // Delete all objects
- listResult, err := s3Client.ListObjects(&s3.ListObjectsInput{
- Bucket: aws.String(bucketName),
- })
- if err == nil {
- for _, obj := range listResult.Contents {
- _, err := s3Client.DeleteObject(&s3.DeleteObjectInput{
- Bucket: aws.String(bucketName),
- Key: obj.Key,
- })
- if err != nil {
- f.t.Logf("Warning: Failed to delete object %s/%s: %v", bucketName, *obj.Key, err)
- }
- }
- }
- }
- // Cleanup cleans up test resources
- func (f *S3IAMTestFramework) Cleanup() {
- // Clean up buckets (best effort)
- if len(f.createdBuckets) > 0 {
- // Create admin client for cleanup
- adminClient, err := f.CreateS3ClientWithJWT("admin-user", "TestAdminRole")
- if err == nil {
- for _, bucket := range f.createdBuckets {
- // Try to empty bucket first
- listResult, err := adminClient.ListObjects(&s3.ListObjectsInput{
- Bucket: aws.String(bucket),
- })
- if err == nil {
- for _, obj := range listResult.Contents {
- adminClient.DeleteObject(&s3.DeleteObjectInput{
- Bucket: aws.String(bucket),
- Key: obj.Key,
- })
- }
- }
- // Delete bucket
- adminClient.DeleteBucket(&s3.DeleteBucketInput{
- Bucket: aws.String(bucket),
- })
- }
- }
- }
- // Close mock OIDC server
- if f.mockOIDC != nil {
- f.mockOIDC.Close()
- }
- }
- // WaitForS3Service waits for the S3 service to be available
- func (f *S3IAMTestFramework) WaitForS3Service() error {
- // Create a basic S3 client
- sess, err := session.NewSession(&aws.Config{
- Region: aws.String(TestRegion),
- Endpoint: aws.String(TestS3Endpoint),
- Credentials: credentials.NewStaticCredentials(
- "test-access-key",
- "test-secret-key",
- "",
- ),
- DisableSSL: aws.Bool(true),
- S3ForcePathStyle: aws.Bool(true),
- })
- if err != nil {
- return fmt.Errorf("failed to create AWS session: %v", err)
- }
- s3Client := s3.New(sess)
- // Try to list buckets to check if service is available
- maxRetries := 30
- for i := 0; i < maxRetries; i++ {
- _, err := s3Client.ListBuckets(&s3.ListBucketsInput{})
- if err == nil {
- return nil
- }
- time.Sleep(1 * time.Second)
- }
- return fmt.Errorf("S3 service not available after %d retries", maxRetries)
- }
- // PutTestObject puts a test object in the specified bucket
- func (f *S3IAMTestFramework) PutTestObject(client *s3.S3, bucket, key, content string) error {
- _, err := client.PutObject(&s3.PutObjectInput{
- Bucket: aws.String(bucket),
- Key: aws.String(key),
- Body: strings.NewReader(content),
- })
- return err
- }
- // GetTestObject retrieves a test object from the specified bucket
- func (f *S3IAMTestFramework) GetTestObject(client *s3.S3, bucket, key string) (string, error) {
- result, err := client.GetObject(&s3.GetObjectInput{
- Bucket: aws.String(bucket),
- Key: aws.String(key),
- })
- if err != nil {
- return "", err
- }
- defer result.Body.Close()
- content := strings.Builder{}
- _, err = io.Copy(&content, result.Body)
- if err != nil {
- return "", err
- }
- return content.String(), nil
- }
- // ListTestObjects lists objects in the specified bucket
- func (f *S3IAMTestFramework) ListTestObjects(client *s3.S3, bucket string) ([]string, error) {
- result, err := client.ListObjects(&s3.ListObjectsInput{
- Bucket: aws.String(bucket),
- })
- if err != nil {
- return nil, err
- }
- var keys []string
- for _, obj := range result.Contents {
- keys = append(keys, *obj.Key)
- }
- return keys, nil
- }
- // DeleteTestObject deletes a test object from the specified bucket
- func (f *S3IAMTestFramework) DeleteTestObject(client *s3.S3, bucket, key string) error {
- _, err := client.DeleteObject(&s3.DeleteObjectInput{
- Bucket: aws.String(bucket),
- Key: aws.String(key),
- })
- return err
- }
- // WaitForS3Service waits for the S3 service to be available (simplified version)
- func (f *S3IAMTestFramework) WaitForS3ServiceSimple() error {
- // This is a simplified version that just checks if the endpoint responds
- // The full implementation would be in the Makefile's wait-for-services target
- return nil
- }
|