package s3api import ( "context" "encoding/json" "fmt" "io" "net/http" "strings" "github.com/seaweedfs/seaweedfs/weed/glog" "github.com/seaweedfs/seaweedfs/weed/iam/policy" "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" "github.com/seaweedfs/seaweedfs/weed/s3api/s3err" ) // Bucket policy metadata key for storing policies in filer const BUCKET_POLICY_METADATA_KEY = "s3-bucket-policy" // GetBucketPolicyHandler handles GET bucket?policy requests func (s3a *S3ApiServer) GetBucketPolicyHandler(w http.ResponseWriter, r *http.Request) { bucket, _ := s3_constants.GetBucketAndObject(r) glog.V(3).Infof("GetBucketPolicyHandler: bucket=%s", bucket) // Get bucket policy from filer metadata policyDocument, err := s3a.getBucketPolicy(bucket) if err != nil { if strings.Contains(err.Error(), "not found") { s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucketPolicy) } else { glog.Errorf("Failed to get bucket policy for %s: %v", bucket, err) s3err.WriteErrorResponse(w, r, s3err.ErrInternalError) } return } // Return policy as JSON w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(policyDocument); err != nil { glog.Errorf("Failed to encode bucket policy response: %v", err) } } // PutBucketPolicyHandler handles PUT bucket?policy requests func (s3a *S3ApiServer) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request) { bucket, _ := s3_constants.GetBucketAndObject(r) glog.V(3).Infof("PutBucketPolicyHandler: bucket=%s", bucket) // Read policy document from request body body, err := io.ReadAll(r.Body) if err != nil { glog.Errorf("Failed to read bucket policy request body: %v", err) s3err.WriteErrorResponse(w, r, s3err.ErrInvalidPolicyDocument) return } defer r.Body.Close() // Parse and validate policy document var policyDoc policy.PolicyDocument if err := json.Unmarshal(body, &policyDoc); err != nil { glog.Errorf("Failed to parse bucket policy JSON: %v", err) s3err.WriteErrorResponse(w, r, s3err.ErrMalformedPolicy) return } // Validate policy document structure if err := policy.ValidatePolicyDocument(&policyDoc); err != nil { glog.Errorf("Invalid bucket policy document: %v", err) s3err.WriteErrorResponse(w, r, s3err.ErrInvalidPolicyDocument) return } // Additional bucket policy specific validation if err := s3a.validateBucketPolicy(&policyDoc, bucket); err != nil { glog.Errorf("Bucket policy validation failed: %v", err) s3err.WriteErrorResponse(w, r, s3err.ErrInvalidPolicyDocument) return } // Store bucket policy if err := s3a.setBucketPolicy(bucket, &policyDoc); err != nil { glog.Errorf("Failed to store bucket policy for %s: %v", bucket, err) s3err.WriteErrorResponse(w, r, s3err.ErrInternalError) return } // Update IAM integration with new bucket policy if s3a.iam.iamIntegration != nil { if err := s3a.updateBucketPolicyInIAM(bucket, &policyDoc); err != nil { glog.Errorf("Failed to update IAM with bucket policy: %v", err) // Don't fail the request, but log the warning } } w.WriteHeader(http.StatusNoContent) } // DeleteBucketPolicyHandler handles DELETE bucket?policy requests func (s3a *S3ApiServer) DeleteBucketPolicyHandler(w http.ResponseWriter, r *http.Request) { bucket, _ := s3_constants.GetBucketAndObject(r) glog.V(3).Infof("DeleteBucketPolicyHandler: bucket=%s", bucket) // Check if bucket policy exists if _, err := s3a.getBucketPolicy(bucket); err != nil { if strings.Contains(err.Error(), "not found") { s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucketPolicy) } else { s3err.WriteErrorResponse(w, r, s3err.ErrInternalError) } return } // Delete bucket policy if err := s3a.deleteBucketPolicy(bucket); err != nil { glog.Errorf("Failed to delete bucket policy for %s: %v", bucket, err) s3err.WriteErrorResponse(w, r, s3err.ErrInternalError) return } // Update IAM integration to remove bucket policy if s3a.iam.iamIntegration != nil { if err := s3a.removeBucketPolicyFromIAM(bucket); err != nil { glog.Errorf("Failed to remove bucket policy from IAM: %v", err) // Don't fail the request, but log the warning } } w.WriteHeader(http.StatusNoContent) } // Helper functions for bucket policy storage and retrieval // getBucketPolicy retrieves a bucket policy from filer metadata func (s3a *S3ApiServer) getBucketPolicy(bucket string) (*policy.PolicyDocument, error) { var policyDoc policy.PolicyDocument err := s3a.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { resp, err := client.LookupDirectoryEntry(context.Background(), &filer_pb.LookupDirectoryEntryRequest{ Directory: s3a.option.BucketsPath, Name: bucket, }) if err != nil { return fmt.Errorf("bucket not found: %v", err) } if resp.Entry == nil { return fmt.Errorf("bucket policy not found: no entry") } policyJSON, exists := resp.Entry.Extended[BUCKET_POLICY_METADATA_KEY] if !exists || len(policyJSON) == 0 { return fmt.Errorf("bucket policy not found: no policy metadata") } if err := json.Unmarshal(policyJSON, &policyDoc); err != nil { return fmt.Errorf("failed to parse stored bucket policy: %v", err) } return nil }) if err != nil { return nil, err } return &policyDoc, nil } // setBucketPolicy stores a bucket policy in filer metadata func (s3a *S3ApiServer) setBucketPolicy(bucket string, policyDoc *policy.PolicyDocument) error { // Serialize policy to JSON policyJSON, err := json.Marshal(policyDoc) if err != nil { return fmt.Errorf("failed to serialize policy: %v", err) } return s3a.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { // First, get the current entry to preserve other attributes resp, err := client.LookupDirectoryEntry(context.Background(), &filer_pb.LookupDirectoryEntryRequest{ Directory: s3a.option.BucketsPath, Name: bucket, }) if err != nil { return fmt.Errorf("bucket not found: %v", err) } entry := resp.Entry if entry.Extended == nil { entry.Extended = make(map[string][]byte) } // Set the bucket policy metadata entry.Extended[BUCKET_POLICY_METADATA_KEY] = policyJSON // Update the entry with new metadata _, err = client.UpdateEntry(context.Background(), &filer_pb.UpdateEntryRequest{ Directory: s3a.option.BucketsPath, Entry: entry, }) return err }) } // deleteBucketPolicy removes a bucket policy from filer metadata func (s3a *S3ApiServer) deleteBucketPolicy(bucket string) error { return s3a.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { // Get the current entry resp, err := client.LookupDirectoryEntry(context.Background(), &filer_pb.LookupDirectoryEntryRequest{ Directory: s3a.option.BucketsPath, Name: bucket, }) if err != nil { return fmt.Errorf("bucket not found: %v", err) } entry := resp.Entry if entry.Extended == nil { return nil // No policy to delete } // Remove the bucket policy metadata delete(entry.Extended, BUCKET_POLICY_METADATA_KEY) // Update the entry _, err = client.UpdateEntry(context.Background(), &filer_pb.UpdateEntryRequest{ Directory: s3a.option.BucketsPath, Entry: entry, }) return err }) } // validateBucketPolicy performs bucket-specific policy validation func (s3a *S3ApiServer) validateBucketPolicy(policyDoc *policy.PolicyDocument, bucket string) error { if policyDoc.Version != "2012-10-17" { return fmt.Errorf("unsupported policy version: %s (must be 2012-10-17)", policyDoc.Version) } if len(policyDoc.Statement) == 0 { return fmt.Errorf("policy document must contain at least one statement") } for i, statement := range policyDoc.Statement { // Bucket policies must have Principal if statement.Principal == nil { return fmt.Errorf("statement %d: bucket policies must specify a Principal", i) } // Validate resources refer to this bucket for _, resource := range statement.Resource { if !s3a.validateResourceForBucket(resource, bucket) { return fmt.Errorf("statement %d: resource %s does not match bucket %s", i, resource, bucket) } } // Validate actions are S3 actions for _, action := range statement.Action { if !strings.HasPrefix(action, "s3:") { return fmt.Errorf("statement %d: bucket policies only support S3 actions, got %s", i, action) } } } return nil } // validateResourceForBucket checks if a resource ARN is valid for the given bucket func (s3a *S3ApiServer) validateResourceForBucket(resource, bucket string) bool { // Expected formats: // arn:seaweed:s3:::bucket-name // arn:seaweed:s3:::bucket-name/* // arn:seaweed:s3:::bucket-name/path/to/object expectedBucketArn := fmt.Sprintf("arn:seaweed:s3:::%s", bucket) expectedBucketWildcard := fmt.Sprintf("arn:seaweed:s3:::%s/*", bucket) expectedBucketPath := fmt.Sprintf("arn:seaweed:s3:::%s/", bucket) return resource == expectedBucketArn || resource == expectedBucketWildcard || strings.HasPrefix(resource, expectedBucketPath) } // IAM integration functions // updateBucketPolicyInIAM updates the IAM system with the new bucket policy func (s3a *S3ApiServer) updateBucketPolicyInIAM(bucket string, policyDoc *policy.PolicyDocument) error { // This would integrate with our advanced IAM system // For now, we'll just log that the policy was updated glog.V(2).Infof("Updated bucket policy for %s in IAM system", bucket) // TODO: Integrate with IAM manager to store resource-based policies // s3a.iam.iamIntegration.iamManager.SetBucketPolicy(bucket, policyDoc) return nil } // removeBucketPolicyFromIAM removes the bucket policy from the IAM system func (s3a *S3ApiServer) removeBucketPolicyFromIAM(bucket string) error { // This would remove the bucket policy from our advanced IAM system glog.V(2).Infof("Removed bucket policy for %s from IAM system", bucket) // TODO: Integrate with IAM manager to remove resource-based policies // s3a.iam.iamIntegration.iamManager.RemoveBucketPolicy(bucket) return nil } // GetPublicAccessBlockHandler Retrieves the PublicAccessBlock configuration for an S3 bucket // https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetPublicAccessBlock.html func (s3a *S3ApiServer) GetPublicAccessBlockHandler(w http.ResponseWriter, r *http.Request) { s3err.WriteErrorResponse(w, r, s3err.ErrNotImplemented) } func (s3a *S3ApiServer) PutPublicAccessBlockHandler(w http.ResponseWriter, r *http.Request) { s3err.WriteErrorResponse(w, r, s3err.ErrNotImplemented) } func (s3a *S3ApiServer) DeletePublicAccessBlockHandler(w http.ResponseWriter, r *http.Request) { s3err.WriteErrorResponse(w, r, s3err.ErrNotImplemented) }