diff --git a/test/s3/iam/s3_iam_framework.go b/test/s3/iam/s3_iam_framework.go index 0e4e5d7b8..675c5a989 100644 --- a/test/s3/iam/s3_iam_framework.go +++ b/test/s3/iam/s3_iam_framework.go @@ -27,9 +27,9 @@ import ( const ( TestS3Endpoint = "http://localhost:8333" TestRegion = "us-west-2" - + // Keycloak configuration - DefaultKeycloakURL = "http://localhost:8080" + DefaultKeycloakURL = "http://localhost:8080" KeycloakRealm = "seaweedfs-test" KeycloakClientID = "seaweedfs-s3" KeycloakClientSecret = "seaweedfs-s3-secret" @@ -78,10 +78,10 @@ func NewS3IAMTestFramework(t *testing.T) *S3IAMTestFramework { 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) @@ -115,20 +115,20 @@ func NewKeycloakClient(baseURL, realm, clientID, clientSecret string) *KeycloakC func (f *S3IAMTestFramework) isKeycloakAvailable(keycloakURL string) bool { client := &http.Client{Timeout: 5 * time.Second} healthURL := fmt.Sprintf("%s/health/ready", keycloakURL) - + resp, err := client.Get(healthURL) if err != nil { return false } defer resp.Body.Close() - + return resp.StatusCode == 200 } // 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) @@ -136,22 +136,22 @@ func (kc *KeycloakClient) AuthenticateUser(username, password string) (*Keycloak 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 { return nil, fmt.Errorf("Keycloak authentication failed with status: %d", resp.StatusCode) } - + 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 } @@ -160,18 +160,18 @@ 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 } @@ -179,10 +179,10 @@ func (f *S3IAMTestFramework) getKeycloakToken(username string) (string, error) { func (f *S3IAMTestFramework) getTestUserPassword(username string) string { userPasswords := map[string]string{ "admin-user": "admin123", - "read-user": "read123", + "read-user": "read123", "write-user": "write123", } - + return userPasswords[username] } @@ -304,27 +304,31 @@ func (f *S3IAMTestFramework) generateSTSSessionToken(username, roleName string, // 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 as TokenGenerator does + // 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{ - "iss": "seaweedfs-sts", - "sub": sessionId, - "iat": now.Unix(), - "exp": now.Add(validDuration).Unix(), - "nbf": now.Unix(), - "typ": "session", - "role": roleArn, - "snam": sessionName, - "principal": principalArn, - "assumed": principalArn, - "assumed_at": now.Format(time.RFC3339Nano), - "ext_uid": username, - "idp": "test-oidc", - "max_dur": int64(validDuration.Seconds()), - "sid": sessionId, + // 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) @@ -344,7 +348,7 @@ func (f *S3IAMTestFramework) generateSTSSessionToken(username, roleName string, 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) diff --git a/weed/iam/integration/iam_manager.go b/weed/iam/integration/iam_manager.go index 1cade2cb9..239d593b0 100644 --- a/weed/iam/integration/iam_manager.go +++ b/weed/iam/integration/iam_manager.go @@ -8,6 +8,7 @@ import ( "path/filepath" "strings" + "github.com/seaweedfs/seaweedfs/weed/glog" "github.com/seaweedfs/seaweedfs/weed/iam/policy" "github.com/seaweedfs/seaweedfs/weed/iam/providers" "github.com/seaweedfs/seaweedfs/weed/iam/sts" @@ -227,27 +228,38 @@ func (m *IAMManager) AssumeRoleWithCredentials(ctx context.Context, request *sts // IsActionAllowed checks if a principal is allowed to perform an action on a resource func (m *IAMManager) IsActionAllowed(ctx context.Context, request *ActionRequest) (bool, error) { + glog.V(0).Infof("IsActionAllowed: starting validation for principal=%s, action=%s", request.Principal, request.Action) + if !m.initialized { + glog.V(0).Info("IsActionAllowed: IAM manager not initialized") return false, fmt.Errorf("IAM manager not initialized") } // Validate session token first + glog.V(0).Infof("IsActionAllowed: validating session token (length=%d)", len(request.SessionToken)) _, err := m.stsService.ValidateSessionToken(ctx, request.SessionToken) if err != nil { + glog.V(0).Infof("IsActionAllowed: session token validation failed: %v", err) return false, fmt.Errorf("invalid session: %w", err) } + glog.V(0).Info("IsActionAllowed: session token validation successful") // Extract role name from principal ARN roleName := extractRoleNameFromPrincipal(request.Principal) + glog.V(0).Infof("IsActionAllowed: extracted role name=%s from principal=%s", roleName, request.Principal) if roleName == "" { + glog.V(0).Infof("IsActionAllowed: could not extract role from principal: %s", request.Principal) return false, fmt.Errorf("could not extract role from principal: %s", request.Principal) } // Get role definition + glog.V(0).Infof("IsActionAllowed: looking up role definition for role=%s", roleName) roleDef, err := m.roleStore.GetRole(ctx, roleName) if err != nil { + glog.V(0).Infof("IsActionAllowed: role lookup failed for role=%s: %v", roleName, err) return false, fmt.Errorf("role not found: %s", roleName) } + glog.V(0).Infof("IsActionAllowed: found role definition with %d attached policies", len(roleDef.AttachedPolicies)) // Create evaluation context evalCtx := &policy.EvaluationContext{ @@ -258,11 +270,14 @@ func (m *IAMManager) IsActionAllowed(ctx context.Context, request *ActionRequest } // Evaluate policies attached to the role + glog.V(0).Infof("IsActionAllowed: evaluating policies: %v", roleDef.AttachedPolicies) result, err := m.policyEngine.Evaluate(ctx, evalCtx, roleDef.AttachedPolicies) if err != nil { + glog.V(0).Infof("IsActionAllowed: policy evaluation failed: %v", err) return false, fmt.Errorf("policy evaluation failed: %w", err) } + glog.V(0).Infof("IsActionAllowed: policy evaluation result - effect=%s, allowed=%t", result.Effect, result.Effect == policy.EffectAllow) return result.Effect == policy.EffectAllow, nil } @@ -306,7 +321,7 @@ func (m *IAMManager) validateTrustPolicyForWebIdentity(ctx context.Context, role // Create evaluation context for trust policy validation requestContext := make(map[string]interface{}) - + // Add standard context values that trust policies might check if idp, ok := tokenClaims["idp"].(string); ok { requestContext["seaweed:TokenIssuer"] = idp diff --git a/weed/iam/policy/policy_engine.go b/weed/iam/policy/policy_engine.go index a2ea1212f..b23ae92cd 100644 --- a/weed/iam/policy/policy_engine.go +++ b/weed/iam/policy/policy_engine.go @@ -431,7 +431,7 @@ func (e *PolicyEngine) evaluateStringCondition(block map[string]interface{}, eva } continue } - + // Convert context value to string slice var contextStrings []string switch v := contextValues.(type) { @@ -449,7 +449,7 @@ func (e *PolicyEngine) evaluateStringCondition(block map[string]interface{}, eva // Convert to string as fallback contextStrings = []string{fmt.Sprintf("%v", v)} } - + // Convert condition value to string slice var expectedStrings []string switch v := conditionValue.(type) { @@ -468,7 +468,7 @@ func (e *PolicyEngine) evaluateStringCondition(block map[string]interface{}, eva default: expectedStrings = []string{fmt.Sprintf("%v", v)} } - + // Evaluate the condition conditionMet := false for _, expected := range expectedStrings { @@ -492,7 +492,7 @@ func (e *PolicyEngine) evaluateStringCondition(block map[string]interface{}, eva break } } - + // For shouldMatch=true (StringEquals, StringLike): condition must be met // For shouldMatch=false (StringNotEquals): condition must NOT be met if shouldMatch && !conditionMet { @@ -502,7 +502,7 @@ func (e *PolicyEngine) evaluateStringCondition(block map[string]interface{}, eva return false } } - + return true } diff --git a/weed/iam/sts/token_utils.go b/weed/iam/sts/token_utils.go index 12cddfb1c..13e4819a5 100644 --- a/weed/iam/sts/token_utils.go +++ b/weed/iam/sts/token_utils.go @@ -9,6 +9,7 @@ import ( "time" "github.com/golang-jwt/jwt/v5" + "github.com/seaweedfs/seaweedfs/weed/glog" ) // TokenGenerator handles token generation and validation @@ -88,7 +89,10 @@ func (t *TokenGenerator) ValidateSessionToken(tokenString string) (*SessionToken // ValidateJWTWithClaims validates and extracts comprehensive session claims from a JWT token func (t *TokenGenerator) ValidateJWTWithClaims(tokenString string) (*STSSessionClaims, error) { + glog.V(0).Infof("ValidateJWTWithClaims: validating token with length=%d", len(tokenString)) + token, err := jwt.ParseWithClaims(tokenString, &STSSessionClaims{}, func(token *jwt.Token) (interface{}, error) { + glog.V(0).Infof("ValidateJWTWithClaims: signing method=%v", token.Header["alg"]) if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } @@ -96,33 +100,42 @@ func (t *TokenGenerator) ValidateJWTWithClaims(tokenString string) (*STSSessionC }) if err != nil { + glog.V(0).Infof("ValidateJWTWithClaims: token parsing failed: %v", err) return nil, fmt.Errorf(ErrInvalidToken, err) } if !token.Valid { + glog.V(0).Info("ValidateJWTWithClaims: token is not valid") return nil, fmt.Errorf(ErrTokenNotValid) } claims, ok := token.Claims.(*STSSessionClaims) if !ok { + glog.V(0).Infof("ValidateJWTWithClaims: failed to cast claims to STSSessionClaims, got type: %T", token.Claims) return nil, fmt.Errorf(ErrInvalidTokenClaims) } + glog.V(0).Infof("ValidateJWTWithClaims: parsed claims - issuer=%s, sessionId=%s", claims.Issuer, claims.SessionId) + // Validate issuer if claims.Issuer != t.issuer { + glog.V(0).Infof("ValidateJWTWithClaims: issuer mismatch - expected=%s, got=%s", t.issuer, claims.Issuer) return nil, fmt.Errorf(ErrInvalidIssuer) } // Validate that required fields are present if claims.SessionId == "" { + glog.V(0).Info("ValidateJWTWithClaims: missing session ID") return nil, fmt.Errorf(ErrMissingSessionID) } // Additional validation using the claims' own validation method if !claims.IsValid() { + glog.V(0).Info("ValidateJWTWithClaims: claims validation failed") return nil, fmt.Errorf(ErrTokenNotValid) } + glog.V(0).Info("ValidateJWTWithClaims: validation successful") return claims, nil }