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.
		
		
		
		
		
			
		
			
				
					
					
						
							794 lines
						
					
					
						
							25 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							794 lines
						
					
					
						
							25 KiB
						
					
					
				
								package s3api
							 | 
						|
								
							 | 
						|
								import (
							 | 
						|
									"context"
							 | 
						|
									"fmt"
							 | 
						|
									"net"
							 | 
						|
									"net/http"
							 | 
						|
									"net/url"
							 | 
						|
									"strings"
							 | 
						|
									"time"
							 | 
						|
								
							 | 
						|
									"github.com/golang-jwt/jwt/v5"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/glog"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/iam/integration"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/iam/providers"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/iam/sts"
							 | 
						|
									"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
							 | 
						|
									stsService   *sts.STSService
							 | 
						|
									filerAddress string
							 | 
						|
									enabled      bool
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// NewS3IAMIntegration creates a new S3 IAM integration
							 | 
						|
								func NewS3IAMIntegration(iamManager *integration.IAMManager, filerAddress string) *S3IAMIntegration {
							 | 
						|
									var stsService *sts.STSService
							 | 
						|
									if iamManager != nil {
							 | 
						|
										stsService = iamManager.GetSTSService()
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return &S3IAMIntegration{
							 | 
						|
										iamManager:   iamManager,
							 | 
						|
										stsService:   stsService,
							 | 
						|
										filerAddress: filerAddress,
							 | 
						|
										enabled:      iamManager != nil,
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// AuthenticateJWT authenticates JWT tokens using our STS service
							 | 
						|
								func (s3iam *S3IAMIntegration) AuthenticateJWT(ctx context.Context, r *http.Request) (*IAMIdentity, s3err.ErrorCode) {
							 | 
						|
								
							 | 
						|
									if !s3iam.enabled {
							 | 
						|
										return nil, s3err.ErrNotImplemented
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Extract bearer token from Authorization header
							 | 
						|
									authHeader := r.Header.Get("Authorization")
							 | 
						|
									if !strings.HasPrefix(authHeader, "Bearer ") {
							 | 
						|
										return nil, s3err.ErrAccessDenied
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									sessionToken := strings.TrimPrefix(authHeader, "Bearer ")
							 | 
						|
									if sessionToken == "" {
							 | 
						|
										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
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Try to parse as STS session token first
							 | 
						|
									tokenClaims, err := parseJWTToken(sessionToken)
							 | 
						|
									if err != nil {
							 | 
						|
										glog.V(3).Infof("Failed to parse JWT token: %v", err)
							 | 
						|
										return nil, s3err.ErrAccessDenied
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Determine token type by issuer claim (more robust than checking role claim)
							 | 
						|
									issuer, issuerOk := tokenClaims["iss"].(string)
							 | 
						|
									if !issuerOk {
							 | 
						|
										glog.V(3).Infof("Token missing issuer claim - invalid JWT")
							 | 
						|
										return nil, s3err.ErrAccessDenied
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Check if this is an STS-issued token by examining the issuer
							 | 
						|
									if !s3iam.isSTSIssuer(issuer) {
							 | 
						|
								
							 | 
						|
										// Not an STS session token, try to validate as OIDC token with timeout
							 | 
						|
										// Create a context with a reasonable timeout to prevent hanging
							 | 
						|
										ctx, cancel := context.WithTimeout(ctx, 15*time.Second)
							 | 
						|
										defer cancel()
							 | 
						|
								
							 | 
						|
										identity, err := s3iam.validateExternalOIDCToken(ctx, sessionToken)
							 | 
						|
								
							 | 
						|
										if err != nil {
							 | 
						|
											return nil, s3err.ErrAccessDenied
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										// Extract role from OIDC identity
							 | 
						|
										if identity.RoleArn == "" {
							 | 
						|
											return nil, s3err.ErrAccessDenied
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										// Return IAM identity for OIDC token
							 | 
						|
										return &IAMIdentity{
							 | 
						|
											Name:         identity.UserID,
							 | 
						|
											Principal:    identity.RoleArn,
							 | 
						|
											SessionToken: sessionToken,
							 | 
						|
											Account: &Account{
							 | 
						|
												DisplayName:  identity.UserID,
							 | 
						|
												EmailAddress: identity.UserID + "@oidc.local",
							 | 
						|
												Id:           identity.UserID,
							 | 
						|
											},
							 | 
						|
										}, s3err.ErrNone
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// This is an STS-issued token - extract STS session information
							 | 
						|
								
							 | 
						|
									// Extract role claim from STS token
							 | 
						|
									roleName, roleOk := tokenClaims["role"].(string)
							 | 
						|
									if !roleOk || roleName == "" {
							 | 
						|
										glog.V(3).Infof("STS token missing role claim")
							 | 
						|
										return nil, s3err.ErrAccessDenied
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									sessionName, ok := tokenClaims["snam"].(string)
							 | 
						|
									if !ok || sessionName == "" {
							 | 
						|
										sessionName = "jwt-session" // Default fallback
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									subject, ok := tokenClaims["sub"].(string)
							 | 
						|
									if !ok || subject == "" {
							 | 
						|
										subject = "jwt-user" // Default fallback
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Use the principal ARN directly from token claims, or build it if not available
							 | 
						|
									principalArn, ok := tokenClaims["principal"].(string)
							 | 
						|
									if !ok || principalArn == "" {
							 | 
						|
										// Fallback: extract role name from role ARN and build principal ARN
							 | 
						|
										roleNameOnly := roleName
							 | 
						|
										if strings.Contains(roleName, "/") {
							 | 
						|
											parts := strings.Split(roleName, "/")
							 | 
						|
											roleNameOnly = parts[len(parts)-1]
							 | 
						|
										}
							 | 
						|
										principalArn = fmt.Sprintf("arn:seaweed:sts::assumed-role/%s/%s", roleNameOnly, sessionName)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Validate the JWT token directly using STS service (avoid circular dependency)
							 | 
						|
									// Note: We don't call IsActionAllowed here because that would create a circular dependency
							 | 
						|
									// Authentication should only validate the token, authorization happens later
							 | 
						|
									_, err = s3iam.stsService.ValidateSessionToken(ctx, sessionToken)
							 | 
						|
									if err != nil {
							 | 
						|
										glog.V(3).Infof("STS session validation failed: %v", 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 {
							 | 
						|
									if !s3iam.enabled {
							 | 
						|
										return s3err.ErrNone // Fallback to existing authorization
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if identity.SessionToken == "" {
							 | 
						|
										return s3err.ErrAccessDenied
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Build resource ARN for the S3 operation
							 | 
						|
									resourceArn := buildS3ResourceArn(bucket, objectKey)
							 | 
						|
								
							 | 
						|
									// Extract request context for policy conditions
							 | 
						|
									requestContext := extractRequestContext(r)
							 | 
						|
								
							 | 
						|
									// Determine the specific S3 action based on the HTTP request details
							 | 
						|
									specificAction := determineGranularS3Action(r, action, bucket, objectKey)
							 | 
						|
								
							 | 
						|
									// Create action request
							 | 
						|
									actionRequest := &integration.ActionRequest{
							 | 
						|
										Principal:      identity.Principal,
							 | 
						|
										Action:         specificAction,
							 | 
						|
										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 {
							 | 
						|
										return s3err.ErrAccessDenied
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if !allowed {
							 | 
						|
										return s3err.ErrAccessDenied
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									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
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// determineGranularS3Action determines the specific S3 IAM action based on HTTP request details
							 | 
						|
								// This provides granular, operation-specific actions for accurate IAM policy enforcement
							 | 
						|
								func determineGranularS3Action(r *http.Request, fallbackAction Action, bucket string, objectKey string) string {
							 | 
						|
									method := r.Method
							 | 
						|
									query := r.URL.Query()
							 | 
						|
								
							 | 
						|
									// Check if there are specific query parameters indicating granular operations
							 | 
						|
									// If there are, always use granular mapping regardless of method-action alignment
							 | 
						|
									hasGranularIndicators := hasSpecificQueryParameters(query)
							 | 
						|
								
							 | 
						|
									// Only check for method-action mismatch when there are NO granular indicators
							 | 
						|
									// This provides fallback behavior for cases where HTTP method doesn't align with intended action
							 | 
						|
									if !hasGranularIndicators && isMethodActionMismatch(method, fallbackAction) {
							 | 
						|
										return mapLegacyActionToIAM(fallbackAction)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Handle object-level operations when method and action are aligned
							 | 
						|
									if objectKey != "" && objectKey != "/" {
							 | 
						|
										switch method {
							 | 
						|
										case "GET", "HEAD":
							 | 
						|
											// Object read operations - check for specific query parameters
							 | 
						|
											if _, hasAcl := query["acl"]; hasAcl {
							 | 
						|
												return "s3:GetObjectAcl"
							 | 
						|
											}
							 | 
						|
											if _, hasTagging := query["tagging"]; hasTagging {
							 | 
						|
												return "s3:GetObjectTagging"
							 | 
						|
											}
							 | 
						|
											if _, hasRetention := query["retention"]; hasRetention {
							 | 
						|
												return "s3:GetObjectRetention"
							 | 
						|
											}
							 | 
						|
											if _, hasLegalHold := query["legal-hold"]; hasLegalHold {
							 | 
						|
												return "s3:GetObjectLegalHold"
							 | 
						|
											}
							 | 
						|
											if _, hasVersions := query["versions"]; hasVersions {
							 | 
						|
												return "s3:GetObjectVersion"
							 | 
						|
											}
							 | 
						|
											if _, hasUploadId := query["uploadId"]; hasUploadId {
							 | 
						|
												return "s3:ListParts"
							 | 
						|
											}
							 | 
						|
											// Default object read
							 | 
						|
											return "s3:GetObject"
							 | 
						|
								
							 | 
						|
										case "PUT", "POST":
							 | 
						|
											// Object write operations - check for specific query parameters
							 | 
						|
											if _, hasAcl := query["acl"]; hasAcl {
							 | 
						|
												return "s3:PutObjectAcl"
							 | 
						|
											}
							 | 
						|
											if _, hasTagging := query["tagging"]; hasTagging {
							 | 
						|
												return "s3:PutObjectTagging"
							 | 
						|
											}
							 | 
						|
											if _, hasRetention := query["retention"]; hasRetention {
							 | 
						|
												return "s3:PutObjectRetention"
							 | 
						|
											}
							 | 
						|
											if _, hasLegalHold := query["legal-hold"]; hasLegalHold {
							 | 
						|
												return "s3:PutObjectLegalHold"
							 | 
						|
											}
							 | 
						|
											// Check for multipart upload operations
							 | 
						|
											if _, hasUploads := query["uploads"]; hasUploads {
							 | 
						|
												return "s3:CreateMultipartUpload"
							 | 
						|
											}
							 | 
						|
											if _, hasUploadId := query["uploadId"]; hasUploadId {
							 | 
						|
												if _, hasPartNumber := query["partNumber"]; hasPartNumber {
							 | 
						|
													return "s3:UploadPart"
							 | 
						|
												}
							 | 
						|
												return "s3:CompleteMultipartUpload" // Complete multipart upload
							 | 
						|
											}
							 | 
						|
											// Default object write
							 | 
						|
											return "s3:PutObject"
							 | 
						|
								
							 | 
						|
										case "DELETE":
							 | 
						|
											// Object delete operations
							 | 
						|
											if _, hasTagging := query["tagging"]; hasTagging {
							 | 
						|
												return "s3:DeleteObjectTagging"
							 | 
						|
											}
							 | 
						|
											if _, hasUploadId := query["uploadId"]; hasUploadId {
							 | 
						|
												return "s3:AbortMultipartUpload"
							 | 
						|
											}
							 | 
						|
											// Default object delete
							 | 
						|
											return "s3:DeleteObject"
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Handle bucket-level operations
							 | 
						|
									if bucket != "" {
							 | 
						|
										switch method {
							 | 
						|
										case "GET", "HEAD":
							 | 
						|
											// Bucket read operations - check for specific query parameters
							 | 
						|
											if _, hasAcl := query["acl"]; hasAcl {
							 | 
						|
												return "s3:GetBucketAcl"
							 | 
						|
											}
							 | 
						|
											if _, hasPolicy := query["policy"]; hasPolicy {
							 | 
						|
												return "s3:GetBucketPolicy"
							 | 
						|
											}
							 | 
						|
											if _, hasTagging := query["tagging"]; hasTagging {
							 | 
						|
												return "s3:GetBucketTagging"
							 | 
						|
											}
							 | 
						|
											if _, hasCors := query["cors"]; hasCors {
							 | 
						|
												return "s3:GetBucketCors"
							 | 
						|
											}
							 | 
						|
											if _, hasVersioning := query["versioning"]; hasVersioning {
							 | 
						|
												return "s3:GetBucketVersioning"
							 | 
						|
											}
							 | 
						|
											if _, hasNotification := query["notification"]; hasNotification {
							 | 
						|
												return "s3:GetBucketNotification"
							 | 
						|
											}
							 | 
						|
											if _, hasObjectLock := query["object-lock"]; hasObjectLock {
							 | 
						|
												return "s3:GetBucketObjectLockConfiguration"
							 | 
						|
											}
							 | 
						|
											if _, hasUploads := query["uploads"]; hasUploads {
							 | 
						|
												return "s3:ListMultipartUploads"
							 | 
						|
											}
							 | 
						|
											if _, hasVersions := query["versions"]; hasVersions {
							 | 
						|
												return "s3:ListBucketVersions"
							 | 
						|
											}
							 | 
						|
											// Default bucket read/list
							 | 
						|
											return "s3:ListBucket"
							 | 
						|
								
							 | 
						|
										case "PUT":
							 | 
						|
											// Bucket write operations - check for specific query parameters
							 | 
						|
											if _, hasAcl := query["acl"]; hasAcl {
							 | 
						|
												return "s3:PutBucketAcl"
							 | 
						|
											}
							 | 
						|
											if _, hasPolicy := query["policy"]; hasPolicy {
							 | 
						|
												return "s3:PutBucketPolicy"
							 | 
						|
											}
							 | 
						|
											if _, hasTagging := query["tagging"]; hasTagging {
							 | 
						|
												return "s3:PutBucketTagging"
							 | 
						|
											}
							 | 
						|
											if _, hasCors := query["cors"]; hasCors {
							 | 
						|
												return "s3:PutBucketCors"
							 | 
						|
											}
							 | 
						|
											if _, hasVersioning := query["versioning"]; hasVersioning {
							 | 
						|
												return "s3:PutBucketVersioning"
							 | 
						|
											}
							 | 
						|
											if _, hasNotification := query["notification"]; hasNotification {
							 | 
						|
												return "s3:PutBucketNotification"
							 | 
						|
											}
							 | 
						|
											if _, hasObjectLock := query["object-lock"]; hasObjectLock {
							 | 
						|
												return "s3:PutBucketObjectLockConfiguration"
							 | 
						|
											}
							 | 
						|
											// Default bucket creation
							 | 
						|
											return "s3:CreateBucket"
							 | 
						|
								
							 | 
						|
										case "DELETE":
							 | 
						|
											// Bucket delete operations - check for specific query parameters
							 | 
						|
											if _, hasPolicy := query["policy"]; hasPolicy {
							 | 
						|
												return "s3:DeleteBucketPolicy"
							 | 
						|
											}
							 | 
						|
											if _, hasTagging := query["tagging"]; hasTagging {
							 | 
						|
												return "s3:DeleteBucketTagging"
							 | 
						|
											}
							 | 
						|
											if _, hasCors := query["cors"]; hasCors {
							 | 
						|
												return "s3:DeleteBucketCors"
							 | 
						|
											}
							 | 
						|
											// Default bucket delete
							 | 
						|
											return "s3:DeleteBucket"
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Fallback to legacy mapping for specific known actions
							 | 
						|
									return mapLegacyActionToIAM(fallbackAction)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// hasSpecificQueryParameters checks if the request has query parameters that indicate specific granular operations
							 | 
						|
								func hasSpecificQueryParameters(query url.Values) bool {
							 | 
						|
									// Check for object-level operation indicators
							 | 
						|
									objectParams := []string{
							 | 
						|
										"acl",        // ACL operations
							 | 
						|
										"tagging",    // Tagging operations
							 | 
						|
										"retention",  // Object retention
							 | 
						|
										"legal-hold", // Legal hold
							 | 
						|
										"versions",   // Versioning operations
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Check for multipart operation indicators
							 | 
						|
									multipartParams := []string{
							 | 
						|
										"uploads",    // List/initiate multipart uploads
							 | 
						|
										"uploadId",   // Part operations, complete, abort
							 | 
						|
										"partNumber", // Upload part
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Check for bucket-level operation indicators
							 | 
						|
									bucketParams := []string{
							 | 
						|
										"policy",         // Bucket policy operations
							 | 
						|
										"website",        // Website configuration
							 | 
						|
										"cors",           // CORS configuration
							 | 
						|
										"lifecycle",      // Lifecycle configuration
							 | 
						|
										"notification",   // Event notification
							 | 
						|
										"replication",    // Cross-region replication
							 | 
						|
										"encryption",     // Server-side encryption
							 | 
						|
										"accelerate",     // Transfer acceleration
							 | 
						|
										"requestPayment", // Request payment
							 | 
						|
										"logging",        // Access logging
							 | 
						|
										"versioning",     // Versioning configuration
							 | 
						|
										"inventory",      // Inventory configuration
							 | 
						|
										"analytics",      // Analytics configuration
							 | 
						|
										"metrics",        // CloudWatch metrics
							 | 
						|
										"location",       // Bucket location
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Check if any of these parameters are present
							 | 
						|
									allParams := append(append(objectParams, multipartParams...), bucketParams...)
							 | 
						|
									for _, param := range allParams {
							 | 
						|
										if _, exists := query[param]; exists {
							 | 
						|
											return true
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return false
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// isMethodActionMismatch detects when HTTP method doesn't align with the intended S3 action
							 | 
						|
								// This provides a mechanism to use fallback action mapping when there's a semantic mismatch
							 | 
						|
								func isMethodActionMismatch(method string, fallbackAction Action) bool {
							 | 
						|
									switch fallbackAction {
							 | 
						|
									case s3_constants.ACTION_WRITE:
							 | 
						|
										// WRITE actions should typically use PUT, POST, or DELETE methods
							 | 
						|
										// GET/HEAD methods indicate read-oriented operations
							 | 
						|
										return method == "GET" || method == "HEAD"
							 | 
						|
								
							 | 
						|
									case s3_constants.ACTION_READ:
							 | 
						|
										// READ actions should typically use GET or HEAD methods
							 | 
						|
										// PUT, POST, DELETE methods indicate write-oriented operations
							 | 
						|
										return method == "PUT" || method == "POST" || method == "DELETE"
							 | 
						|
								
							 | 
						|
									case s3_constants.ACTION_LIST:
							 | 
						|
										// LIST actions should typically use GET method
							 | 
						|
										// PUT, POST, DELETE methods indicate write-oriented operations
							 | 
						|
										return method == "PUT" || method == "POST" || method == "DELETE"
							 | 
						|
								
							 | 
						|
									case s3_constants.ACTION_DELETE_BUCKET:
							 | 
						|
										// DELETE_BUCKET should use DELETE method
							 | 
						|
										// Other methods indicate different operation types
							 | 
						|
										return method != "DELETE"
							 | 
						|
								
							 | 
						|
									default:
							 | 
						|
										// For unknown actions or actions that already have s3: prefix, don't assume mismatch
							 | 
						|
										return false
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// mapLegacyActionToIAM provides fallback mapping for legacy actions
							 | 
						|
								// This ensures backward compatibility while the system transitions to granular actions
							 | 
						|
								func mapLegacyActionToIAM(legacyAction Action) string {
							 | 
						|
									switch legacyAction {
							 | 
						|
									case s3_constants.ACTION_READ:
							 | 
						|
										return "s3:GetObject" // Fallback for unmapped read operations
							 | 
						|
									case s3_constants.ACTION_WRITE:
							 | 
						|
										return "s3:PutObject" // Fallback for unmapped write operations
							 | 
						|
									case s3_constants.ACTION_LIST:
							 | 
						|
										return "s3:ListBucket" // Fallback for unmapped list operations
							 | 
						|
									case s3_constants.ACTION_TAGGING:
							 | 
						|
										return "s3:GetObjectTagging" // Fallback for unmapped tagging operations
							 | 
						|
									case s3_constants.ACTION_READ_ACP:
							 | 
						|
										return "s3:GetObjectAcl" // Fallback for unmapped ACL read operations
							 | 
						|
									case s3_constants.ACTION_WRITE_ACP:
							 | 
						|
										return "s3:PutObjectAcl" // Fallback for unmapped ACL write operations
							 | 
						|
									case s3_constants.ACTION_DELETE_BUCKET:
							 | 
						|
										return "s3:DeleteBucket" // Fallback for unmapped bucket delete operations
							 | 
						|
									case s3_constants.ACTION_ADMIN:
							 | 
						|
										return "s3:*" // Fallback for unmapped admin operations
							 | 
						|
								
							 | 
						|
									// Handle granular multipart actions (already correctly mapped)
							 | 
						|
									case s3_constants.ACTION_CREATE_MULTIPART_UPLOAD:
							 | 
						|
										return "s3:CreateMultipartUpload"
							 | 
						|
									case s3_constants.ACTION_UPLOAD_PART:
							 | 
						|
										return "s3:UploadPart"
							 | 
						|
									case s3_constants.ACTION_COMPLETE_MULTIPART:
							 | 
						|
										return "s3:CompleteMultipartUpload"
							 | 
						|
									case s3_constants.ACTION_ABORT_MULTIPART:
							 | 
						|
										return "s3:AbortMultipartUpload"
							 | 
						|
									case s3_constants.ACTION_LIST_MULTIPART_UPLOADS:
							 | 
						|
										return "s3:ListMultipartUploads"
							 | 
						|
									case s3_constants.ACTION_LIST_PARTS:
							 | 
						|
										return "s3:ListParts"
							 | 
						|
								
							 | 
						|
									default:
							 | 
						|
										// If it's already a properly formatted S3 action, return as-is
							 | 
						|
										actionStr := string(legacyAction)
							 | 
						|
										if strings.HasPrefix(actionStr, "s3:") {
							 | 
						|
											return actionStr
							 | 
						|
										}
							 | 
						|
										// Fallback: convert to S3 action format
							 | 
						|
										return "s3:" + actionStr
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// 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
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// 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, "localhost:8888")
							 | 
						|
										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, "localhost:8888"),
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// 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)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// OIDCIdentity represents an identity validated through OIDC
							 | 
						|
								type OIDCIdentity struct {
							 | 
						|
									UserID   string
							 | 
						|
									RoleArn  string
							 | 
						|
									Provider string
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// validateExternalOIDCToken validates an external OIDC token using the STS service's secure issuer-based lookup
							 | 
						|
								// This method delegates to the STS service's validateWebIdentityToken for better security and efficiency
							 | 
						|
								func (s3iam *S3IAMIntegration) validateExternalOIDCToken(ctx context.Context, token string) (*OIDCIdentity, error) {
							 | 
						|
								
							 | 
						|
									if s3iam.iamManager == nil {
							 | 
						|
										return nil, fmt.Errorf("IAM manager not available")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Get STS service for secure token validation
							 | 
						|
									stsService := s3iam.iamManager.GetSTSService()
							 | 
						|
									if stsService == nil {
							 | 
						|
										return nil, fmt.Errorf("STS service not available")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Use the STS service's secure validateWebIdentityToken method
							 | 
						|
									// This method uses issuer-based lookup to select the correct provider, which is more secure and efficient
							 | 
						|
									externalIdentity, provider, err := stsService.ValidateWebIdentityToken(ctx, token)
							 | 
						|
									if err != nil {
							 | 
						|
										return nil, fmt.Errorf("token validation failed: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if externalIdentity == nil {
							 | 
						|
										return nil, fmt.Errorf("authentication succeeded but no identity returned")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Extract role from external identity attributes
							 | 
						|
									rolesAttr, exists := externalIdentity.Attributes["roles"]
							 | 
						|
									if !exists || rolesAttr == "" {
							 | 
						|
										glog.V(3).Infof("No roles found in external identity")
							 | 
						|
										return nil, fmt.Errorf("no roles found in external identity")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Parse roles (stored as comma-separated string)
							 | 
						|
									rolesStr := strings.TrimSpace(rolesAttr)
							 | 
						|
									roles := strings.Split(rolesStr, ",")
							 | 
						|
								
							 | 
						|
									// Clean up role names
							 | 
						|
									var cleanRoles []string
							 | 
						|
									for _, role := range roles {
							 | 
						|
										cleanRole := strings.TrimSpace(role)
							 | 
						|
										if cleanRole != "" {
							 | 
						|
											cleanRoles = append(cleanRoles, cleanRole)
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if len(cleanRoles) == 0 {
							 | 
						|
										glog.V(3).Infof("Empty roles list after parsing")
							 | 
						|
										return nil, fmt.Errorf("no valid roles found in token")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Determine the primary role using intelligent selection
							 | 
						|
									roleArn := s3iam.selectPrimaryRole(cleanRoles, externalIdentity)
							 | 
						|
								
							 | 
						|
									return &OIDCIdentity{
							 | 
						|
										UserID:   externalIdentity.UserID,
							 | 
						|
										RoleArn:  roleArn,
							 | 
						|
										Provider: fmt.Sprintf("%T", provider), // Use provider type as identifier
							 | 
						|
									}, nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// selectPrimaryRole simply picks the first role from the list
							 | 
						|
								// The OIDC provider should return roles in priority order (most important first)
							 | 
						|
								func (s3iam *S3IAMIntegration) selectPrimaryRole(roles []string, externalIdentity *providers.ExternalIdentity) string {
							 | 
						|
									if len(roles) == 0 {
							 | 
						|
										return ""
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Just pick the first one - keep it simple
							 | 
						|
									selectedRole := roles[0]
							 | 
						|
									return selectedRole
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// isSTSIssuer determines if an issuer belongs to the STS service
							 | 
						|
								// Uses exact match against configured STS issuer for security and correctness
							 | 
						|
								func (s3iam *S3IAMIntegration) isSTSIssuer(issuer string) bool {
							 | 
						|
									if s3iam.stsService == nil || s3iam.stsService.Config == nil {
							 | 
						|
										return false
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Directly compare with the configured STS issuer for exact match
							 | 
						|
									// This prevents false positives from external OIDC providers that might
							 | 
						|
									// contain STS-related keywords in their issuer URLs
							 | 
						|
									return issuer == s3iam.stsService.Config.Issuer
							 | 
						|
								}
							 |