You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							405 lines
						
					
					
						
							13 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							405 lines
						
					
					
						
							13 KiB
						
					
					
				
								package s3api
							 | 
						|
								
							 | 
						|
								import (
							 | 
						|
									"context"
							 | 
						|
									"fmt"
							 | 
						|
									"net"
							 | 
						|
									"net/http"
							 | 
						|
									"strings"
							 | 
						|
								
							 | 
						|
									"github.com/golang-jwt/jwt/v5"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/glog"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/iam/integration"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
							 | 
						|
								)
							 | 
						|
								
							 | 
						|
								// S3IAMIntegration provides IAM integration for S3 API
							 | 
						|
								type S3IAMIntegration struct {
							 | 
						|
									iamManager *integration.IAMManager
							 | 
						|
									enabled    bool
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// NewS3IAMIntegration creates a new S3 IAM integration
							 | 
						|
								func NewS3IAMIntegration(iamManager *integration.IAMManager) *S3IAMIntegration {
							 | 
						|
									return &S3IAMIntegration{
							 | 
						|
										iamManager: iamManager,
							 | 
						|
										enabled:    iamManager != nil,
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// AuthenticateJWT authenticates JWT tokens using our STS service
							 | 
						|
								func (s3iam *S3IAMIntegration) AuthenticateJWT(ctx context.Context, r *http.Request) (*IAMIdentity, s3err.ErrorCode) {
							 | 
						|
									glog.V(0).Infof("AuthenticateJWT: enabled=%t", s3iam.enabled)
							 | 
						|
									if !s3iam.enabled {
							 | 
						|
										glog.V(0).Info("S3 IAM integration not enabled")
							 | 
						|
										return nil, s3err.ErrNotImplemented
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Extract bearer token from Authorization header
							 | 
						|
									authHeader := r.Header.Get("Authorization")
							 | 
						|
									glog.V(0).Infof("AuthenticateJWT: authHeader='%s'", authHeader)
							 | 
						|
									if !strings.HasPrefix(authHeader, "Bearer ") {
							 | 
						|
										glog.V(0).Info("Invalid JWT authorization header format")
							 | 
						|
										return nil, s3err.ErrAccessDenied
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									sessionToken := strings.TrimPrefix(authHeader, "Bearer ")
							 | 
						|
									glog.V(0).Infof("AuthenticateJWT: sessionToken length=%d", len(sessionToken))
							 | 
						|
									if sessionToken == "" {
							 | 
						|
										glog.V(0).Info("Empty session token")
							 | 
						|
										return nil, s3err.ErrAccessDenied
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Basic token format validation - reject obviously invalid tokens
							 | 
						|
									if sessionToken == "invalid-token" || len(sessionToken) < 10 {
							 | 
						|
										glog.V(3).Info("Session token format is invalid")
							 | 
						|
										return nil, s3err.ErrAccessDenied
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Parse JWT token to extract claims
							 | 
						|
									tokenClaims, err := parseJWTToken(sessionToken)
							 | 
						|
									if err != nil {
							 | 
						|
										glog.V(3).Infof("Failed to parse JWT token: %v", err)
							 | 
						|
										return nil, s3err.ErrAccessDenied
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Extract role information from token claims
							 | 
						|
									roleName, ok := tokenClaims["role_name"].(string)
							 | 
						|
									if !ok || roleName == "" {
							 | 
						|
										glog.V(3).Info("No role_name found in JWT token")
							 | 
						|
										return nil, s3err.ErrAccessDenied
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									sessionName, ok := tokenClaims["session_name"].(string)
							 | 
						|
									if !ok || sessionName == "" {
							 | 
						|
										sessionName = "jwt-session" // Default fallback
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									subject, ok := tokenClaims["sub"].(string)
							 | 
						|
									if !ok || subject == "" {
							 | 
						|
										subject = "jwt-user" // Default fallback
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Build principal ARN from token claims
							 | 
						|
									principalArn := fmt.Sprintf("arn:seaweed:sts::assumed-role/%s/%s", roleName, sessionName)
							 | 
						|
								
							 | 
						|
									// Validate the session using our IAM system
							 | 
						|
									testRequest := &integration.ActionRequest{
							 | 
						|
										Principal:    principalArn,
							 | 
						|
										Action:       "sts:ValidateSession",
							 | 
						|
										Resource:     "*",
							 | 
						|
										SessionToken: sessionToken,
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									glog.V(0).Infof("AuthenticateJWT: calling IsActionAllowed for principal=%s", principalArn)
							 | 
						|
									allowed, err := s3iam.iamManager.IsActionAllowed(ctx, testRequest)
							 | 
						|
									glog.V(0).Infof("AuthenticateJWT: IsActionAllowed returned allowed=%t, err=%v", allowed, err)
							 | 
						|
									if err != nil || !allowed {
							 | 
						|
										glog.V(0).Infof("IAM validation failed for %s: %v", principalArn, err)
							 | 
						|
										return nil, s3err.ErrAccessDenied
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Create IAM identity from validated token
							 | 
						|
									identity := &IAMIdentity{
							 | 
						|
										Name:         subject,
							 | 
						|
										Principal:    principalArn,
							 | 
						|
										SessionToken: sessionToken,
							 | 
						|
										Account: &Account{
							 | 
						|
											DisplayName:  roleName,
							 | 
						|
											EmailAddress: subject + "@seaweedfs.local",
							 | 
						|
											Id:           subject,
							 | 
						|
										},
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									glog.V(3).Infof("JWT authentication successful for principal: %s", identity.Principal)
							 | 
						|
									return identity, s3err.ErrNone
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// AuthorizeAction authorizes actions using our policy engine
							 | 
						|
								func (s3iam *S3IAMIntegration) AuthorizeAction(ctx context.Context, identity *IAMIdentity, action Action, bucket string, objectKey string, r *http.Request) s3err.ErrorCode {
							 | 
						|
									glog.V(0).Infof("AuthorizeAction called: enabled=%t, action=%s, bucket=%s, principal=%s", s3iam.enabled, action, bucket, identity.Principal)
							 | 
						|
									if !s3iam.enabled {
							 | 
						|
										glog.V(3).Info("S3 IAM integration not enabled, using fallback authorization")
							 | 
						|
										return s3err.ErrNone // Fallback to existing authorization
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if identity.SessionToken == "" {
							 | 
						|
										glog.V(3).Info("No session token for authorization")
							 | 
						|
										return s3err.ErrAccessDenied
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Build resource ARN for the S3 operation
							 | 
						|
									resourceArn := buildS3ResourceArn(bucket, objectKey)
							 | 
						|
								
							 | 
						|
									// Extract request context for policy conditions
							 | 
						|
									requestContext := extractRequestContext(r)
							 | 
						|
								
							 | 
						|
									// Create action request
							 | 
						|
									actionRequest := &integration.ActionRequest{
							 | 
						|
										Principal:      identity.Principal,
							 | 
						|
										Action:         mapS3ActionToIAMAction(action),
							 | 
						|
										Resource:       resourceArn,
							 | 
						|
										SessionToken:   identity.SessionToken,
							 | 
						|
										RequestContext: requestContext,
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Check if action is allowed using our policy engine
							 | 
						|
									allowed, err := s3iam.iamManager.IsActionAllowed(ctx, actionRequest)
							 | 
						|
									if err != nil {
							 | 
						|
										// Log the error but treat authentication/authorization failures as access denied
							 | 
						|
										// rather than internal errors to provide better user experience
							 | 
						|
										glog.V(3).Infof("Policy evaluation failed: %v", err)
							 | 
						|
										return s3err.ErrAccessDenied
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if !allowed {
							 | 
						|
										glog.V(3).Infof("Action %s denied for principal %s on resource %s", action, identity.Principal, resourceArn)
							 | 
						|
										return s3err.ErrAccessDenied
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									glog.V(3).Infof("Action %s allowed for principal %s on resource %s", action, identity.Principal, resourceArn)
							 | 
						|
									return s3err.ErrNone
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// IAMIdentity represents an authenticated identity with session information
							 | 
						|
								type IAMIdentity struct {
							 | 
						|
									Name         string
							 | 
						|
									Principal    string
							 | 
						|
									SessionToken string
							 | 
						|
									Account      *Account
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// IsAdmin checks if the identity has admin privileges
							 | 
						|
								func (identity *IAMIdentity) IsAdmin() bool {
							 | 
						|
									// In our IAM system, admin status is determined by policies, not identity
							 | 
						|
									// This is handled by the policy engine during authorization
							 | 
						|
									return false
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// Mock session structures for validation
							 | 
						|
								type MockSessionInfo struct {
							 | 
						|
									AssumedRoleUser MockAssumedRoleUser
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								type MockAssumedRoleUser struct {
							 | 
						|
									AssumedRoleId string
							 | 
						|
									Arn           string
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// Helper functions
							 | 
						|
								
							 | 
						|
								// buildS3ResourceArn builds an S3 resource ARN from bucket and object
							 | 
						|
								func buildS3ResourceArn(bucket string, objectKey string) string {
							 | 
						|
									if bucket == "" {
							 | 
						|
										return "arn:seaweed:s3:::*"
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if objectKey == "" || objectKey == "/" {
							 | 
						|
										return "arn:seaweed:s3:::" + bucket
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Remove leading slash from object key if present
							 | 
						|
									if strings.HasPrefix(objectKey, "/") {
							 | 
						|
										objectKey = objectKey[1:]
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return "arn:seaweed:s3:::" + bucket + "/" + objectKey
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// mapS3ActionToIAMAction maps S3 API actions to IAM policy actions
							 | 
						|
								func mapS3ActionToIAMAction(s3Action Action) string {
							 | 
						|
									// Map S3 actions to standard IAM policy actions
							 | 
						|
									actionMap := map[Action]string{
							 | 
						|
										s3_constants.ACTION_READ:          "s3:GetObject",
							 | 
						|
										s3_constants.ACTION_WRITE:         "s3:PutObject",
							 | 
						|
										s3_constants.ACTION_LIST:          "s3:ListBucket",
							 | 
						|
										s3_constants.ACTION_TAGGING:       "s3:GetObjectTagging",
							 | 
						|
										s3_constants.ACTION_READ_ACP:      "s3:GetObjectAcl",
							 | 
						|
										s3_constants.ACTION_WRITE_ACP:     "s3:PutObjectAcl",
							 | 
						|
										s3_constants.ACTION_DELETE_BUCKET: "s3:DeleteBucket",
							 | 
						|
										s3_constants.ACTION_ADMIN:         "s3:*",
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if iamAction, exists := actionMap[s3Action]; exists {
							 | 
						|
										return iamAction
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Default to the string representation of the action
							 | 
						|
									return string(s3Action)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// extractRequestContext extracts request context for policy conditions
							 | 
						|
								func extractRequestContext(r *http.Request) map[string]interface{} {
							 | 
						|
									context := make(map[string]interface{})
							 | 
						|
								
							 | 
						|
									// Extract source IP for IP-based conditions
							 | 
						|
									sourceIP := extractSourceIP(r)
							 | 
						|
									if sourceIP != "" {
							 | 
						|
										context["sourceIP"] = sourceIP
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Extract user agent
							 | 
						|
									if userAgent := r.Header.Get("User-Agent"); userAgent != "" {
							 | 
						|
										context["userAgent"] = userAgent
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Extract request time
							 | 
						|
									context["requestTime"] = r.Context().Value("requestTime")
							 | 
						|
								
							 | 
						|
									// Extract additional headers that might be useful for conditions
							 | 
						|
									if referer := r.Header.Get("Referer"); referer != "" {
							 | 
						|
										context["referer"] = referer
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return context
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// extractSourceIP extracts the real source IP from the request
							 | 
						|
								func extractSourceIP(r *http.Request) string {
							 | 
						|
									// Check X-Forwarded-For header (most common for proxied requests)
							 | 
						|
									if forwardedFor := r.Header.Get("X-Forwarded-For"); forwardedFor != "" {
							 | 
						|
										// X-Forwarded-For can contain multiple IPs, take the first one
							 | 
						|
										if ips := strings.Split(forwardedFor, ","); len(ips) > 0 {
							 | 
						|
											return strings.TrimSpace(ips[0])
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Check X-Real-IP header
							 | 
						|
									if realIP := r.Header.Get("X-Real-IP"); realIP != "" {
							 | 
						|
										return strings.TrimSpace(realIP)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Fall back to RemoteAddr
							 | 
						|
									if ip, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
							 | 
						|
										return ip
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return r.RemoteAddr
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// extractRoleNameFromPrincipal extracts role name from assumed role principal ARN
							 | 
						|
								func extractRoleNameFromPrincipal(principal string) string {
							 | 
						|
									// Expected format: arn:seaweed:sts::assumed-role/RoleName/SessionName
							 | 
						|
									prefix := "arn:seaweed:sts::assumed-role/"
							 | 
						|
									if len(principal) > len(prefix) && principal[:len(prefix)] == prefix {
							 | 
						|
										remainder := principal[len(prefix):]
							 | 
						|
										// Split on first '/' to get role name
							 | 
						|
										if slashIndex := strings.Index(remainder, "/"); slashIndex != -1 {
							 | 
						|
											return remainder[:slashIndex]
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
									return principal // Return original if parsing fails
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// parseJWTToken parses a JWT token and returns its claims without verification
							 | 
						|
								// Note: This is for extracting claims only. Verification is done by the IAM system.
							 | 
						|
								func parseJWTToken(tokenString string) (jwt.MapClaims, error) {
							 | 
						|
									token, _, err := new(jwt.Parser).ParseUnverified(tokenString, jwt.MapClaims{})
							 | 
						|
									if err != nil {
							 | 
						|
										return nil, fmt.Errorf("failed to parse JWT token: %v", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									claims, ok := token.Claims.(jwt.MapClaims)
							 | 
						|
									if !ok {
							 | 
						|
										return nil, fmt.Errorf("invalid token claims")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return claims, nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// minInt returns the minimum of two integers
							 | 
						|
								func minInt(a, b int) int {
							 | 
						|
									if a < b {
							 | 
						|
										return a
							 | 
						|
									}
							 | 
						|
									return b
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// SetIAMIntegration adds advanced IAM integration to the S3ApiServer
							 | 
						|
								func (s3a *S3ApiServer) SetIAMIntegration(iamManager *integration.IAMManager) {
							 | 
						|
									if s3a.iam != nil {
							 | 
						|
										s3a.iam.iamIntegration = NewS3IAMIntegration(iamManager)
							 | 
						|
										glog.V(0).Infof("IAM integration successfully set on S3ApiServer")
							 | 
						|
									} else {
							 | 
						|
										glog.Errorf("Cannot set IAM integration: s3a.iam is nil")
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// EnhancedS3ApiServer extends S3ApiServer with IAM integration
							 | 
						|
								type EnhancedS3ApiServer struct {
							 | 
						|
									*S3ApiServer
							 | 
						|
									iamIntegration *S3IAMIntegration
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// NewEnhancedS3ApiServer creates an S3 API server with IAM integration
							 | 
						|
								func NewEnhancedS3ApiServer(baseServer *S3ApiServer, iamManager *integration.IAMManager) *EnhancedS3ApiServer {
							 | 
						|
									// Set the IAM integration on the base server
							 | 
						|
									baseServer.SetIAMIntegration(iamManager)
							 | 
						|
								
							 | 
						|
									return &EnhancedS3ApiServer{
							 | 
						|
										S3ApiServer:    baseServer,
							 | 
						|
										iamIntegration: NewS3IAMIntegration(iamManager),
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// AuthenticateJWTRequest handles JWT authentication for S3 requests
							 | 
						|
								func (enhanced *EnhancedS3ApiServer) AuthenticateJWTRequest(r *http.Request) (*Identity, s3err.ErrorCode) {
							 | 
						|
									ctx := r.Context()
							 | 
						|
								
							 | 
						|
									// Use our IAM integration for JWT authentication
							 | 
						|
									iamIdentity, errCode := enhanced.iamIntegration.AuthenticateJWT(ctx, r)
							 | 
						|
									if errCode != s3err.ErrNone {
							 | 
						|
										return nil, errCode
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Convert IAMIdentity to the existing Identity structure
							 | 
						|
									identity := &Identity{
							 | 
						|
										Name:    iamIdentity.Name,
							 | 
						|
										Account: iamIdentity.Account,
							 | 
						|
										// Note: Actions will be determined by policy evaluation
							 | 
						|
										Actions: []Action{}, // Empty - authorization handled by policy engine
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Store session token for later authorization
							 | 
						|
									r.Header.Set("X-SeaweedFS-Session-Token", iamIdentity.SessionToken)
							 | 
						|
									r.Header.Set("X-SeaweedFS-Principal", iamIdentity.Principal)
							 | 
						|
								
							 | 
						|
									return identity, s3err.ErrNone
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// AuthorizeRequest handles authorization for S3 requests using policy engine
							 | 
						|
								func (enhanced *EnhancedS3ApiServer) AuthorizeRequest(r *http.Request, identity *Identity, action Action) s3err.ErrorCode {
							 | 
						|
									ctx := r.Context()
							 | 
						|
								
							 | 
						|
									// Get session info from request headers (set during authentication)
							 | 
						|
									sessionToken := r.Header.Get("X-SeaweedFS-Session-Token")
							 | 
						|
									principal := r.Header.Get("X-SeaweedFS-Principal")
							 | 
						|
								
							 | 
						|
									if sessionToken == "" || principal == "" {
							 | 
						|
										glog.V(3).Info("No session information available for authorization")
							 | 
						|
										return s3err.ErrAccessDenied
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Extract bucket and object from request
							 | 
						|
									bucket, object := s3_constants.GetBucketAndObject(r)
							 | 
						|
									prefix := s3_constants.GetPrefix(r)
							 | 
						|
								
							 | 
						|
									// For List operations, use prefix for permission checking if available
							 | 
						|
									if action == s3_constants.ACTION_LIST && object == "" && prefix != "" {
							 | 
						|
										object = prefix
							 | 
						|
									} else if (object == "/" || object == "") && prefix != "" {
							 | 
						|
										object = prefix
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Create IAM identity for authorization
							 | 
						|
									iamIdentity := &IAMIdentity{
							 | 
						|
										Name:         identity.Name,
							 | 
						|
										Principal:    principal,
							 | 
						|
										SessionToken: sessionToken,
							 | 
						|
										Account:      identity.Account,
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Use our IAM integration for authorization
							 | 
						|
									return enhanced.iamIntegration.AuthorizeAction(ctx, iamIdentity, action, bucket, object, r)
							 | 
						|
								}
							 |