package s3api import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "net/http" "github.com/seaweedfs/seaweedfs/weed/glog" "github.com/seaweedfs/seaweedfs/weed/iam/integration" "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" "github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine" "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" "github.com/seaweedfs/seaweedfs/weed/s3api/s3err" ) // Bucket policy metadata key for storing policies in filer. // Also consumed directly by weed/admin/dash for the admin UI's bucket // policy management, so keep it exported and don't change its value // without updating that package too. const BUCKET_POLICY_METADATA_KEY = "s3-bucket-policy" // Sentinel errors for bucket policy operations var ( ErrPolicyNotFound = errors.New("bucket policy not found") // ErrBucketNotFound is already defined in s3api_object_retention.go ) // 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) // Validate bucket exists first for correct error mapping _, err := s3a.getBucketEntry(bucket) if err != nil { if errors.Is(err, filer_pb.ErrNotFound) { s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucket) } else { glog.Errorf("Failed to check bucket existence for %s: %v", bucket, err) s3err.WriteErrorResponse(w, r, s3err.ErrInternalError) } return } // Get bucket policy from filer metadata policyDocument, err := s3a.getBucketPolicy(bucket) if err != nil { if errors.Is(err, ErrPolicyNotFound) { s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucketPolicy) } else if errors.Is(err, ErrBucketNotFound) { s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucket) } 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(io.LimitReader(r.Body, policy_engine.MaxBucketPolicySize+1)) 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() if len(body) > policy_engine.MaxBucketPolicySize { s3err.WriteErrorResponse(w, r, s3err.ErrPolicyTooLarge) return } // Parse and validate policy document var policyDoc policy_engine.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 core policy structure (Effect, Action, etc.) if err := policy_engine.ValidatePolicy(&policyDoc); err != nil { glog.Errorf("Policy validation failed: %v", err) s3err.WriteErrorResponse(w, r, s3err.ErrInvalidPolicyDocument) return } // Additional bucket policy specific validation if err := policy_engine.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 } // Immediately load into policy engine to avoid race condition // (The subscription system will also do this async, but we want immediate effect) if s3a.policyEngine != nil { if err := s3a.policyEngine.LoadBucketPolicyFromCache(bucket, &policyDoc); err != nil { glog.Warningf("Failed to immediately load bucket policy into engine for %s: %v", bucket, err) // Don't fail the request since the subscription will eventually sync it } } // 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) // Validate bucket exists first for correct error mapping _, err := s3a.getBucketEntry(bucket) if err != nil { if errors.Is(err, filer_pb.ErrNotFound) { s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucket) } else { glog.Errorf("Failed to check bucket existence for %s: %v", bucket, err) s3err.WriteErrorResponse(w, r, s3err.ErrInternalError) } return } // Check if bucket policy exists if _, err := s3a.getBucketPolicy(bucket); err != nil { if errors.Is(err, ErrPolicyNotFound) { s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucketPolicy) } else if errors.Is(err, ErrBucketNotFound) { s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucket) } 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 } // Immediately remove from policy engine to avoid race condition // (The subscription system will also do this async, but we want immediate effect) if s3a.policyEngine != nil { if err := s3a.policyEngine.DeleteBucketPolicy(bucket); err != nil { glog.Warningf("Failed to immediately remove bucket policy from engine for %s: %v", bucket, err) // Don't fail the request since the subscription will eventually sync it } } // 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 // getBucketPolicy retrieves the bucket policy from filer func (s3a *S3ApiServer) getBucketPolicy(bucket string) (*policy_engine.PolicyDocument, error) { var policyDoc policy_engine.PolicyDocument err := s3a.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { resp, err := client.LookupDirectoryEntry(context.Background(), &filer_pb.LookupDirectoryEntryRequest{ Directory: s3a.bucketRoot(bucket), Name: bucket, }) if err != nil { // Return sentinel error for bucket not found return fmt.Errorf("%w: %v", ErrBucketNotFound, err) } if resp.Entry == nil { return ErrPolicyNotFound } policyJSON, exists := resp.Entry.Extended[BUCKET_POLICY_METADATA_KEY] if !exists || len(policyJSON) == 0 { return ErrPolicyNotFound } 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_engine.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.bucketRoot(bucket), 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.bucketRoot(bucket), 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.bucketRoot(bucket), 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.bucketRoot(bucket), Entry: entry, }) return err }) } // IAM integration functions // updateBucketPolicyInIAM updates the IAM system with the new bucket policy func (s3a *S3ApiServer) updateBucketPolicyInIAM(bucket string, policyDoc *policy_engine.PolicyDocument) error { iamManager := s3a.bucketPolicyIAMManager() if iamManager == nil { return nil } policyJSON, err := json.Marshal(policyDoc) if err != nil { return fmt.Errorf("failed to marshal policy: %w", err) } glog.V(2).Infof("Updated bucket policy for %s in IAM system", bucket) return iamManager.UpdateBucketPolicy(context.Background(), bucket, policyJSON) } // ensureBucketPolicyInIAM backfills the IAM mirror for a policy the // subscription never saw change (one that predates the IAM integration). // Called from the lazy bucket-config load; a present mirror is left alone. func (s3a *S3ApiServer) ensureBucketPolicyInIAM(bucket string, policyJSON []byte) { iamManager := s3a.bucketPolicyIAMManager() if iamManager == nil { return } wrote, err := iamManager.EnsureBucketPolicy(context.Background(), bucket, policyJSON) if err != nil { glog.Warningf("backfill bucket policy for %s into IAM: %v", bucket, err) return } if !wrote { return } // The check-then-write above can race a concurrent policy change or // delete: the event-driven mirror may have landed in between, and this // write would then have re-stored bytes that are already stale - with // no later event to heal it. Reconcile against a fresh entry read, // which is authoritative; anything changing after this read fires its // own event, and the mirror for it finds this write already present. entry, err := s3a.getBucketEntry(bucket) if err != nil { if errors.Is(err, filer_pb.ErrNotFound) { if err := s3a.removeBucketPolicyFromIAM(bucket); err != nil { glog.Warningf("remove bucket policy for %s from IAM: %v", bucket, err) } } return } current := entry.Extended[BUCKET_POLICY_METADATA_KEY] if bytes.Equal(current, policyJSON) { return } if len(current) == 0 { if err := s3a.removeBucketPolicyFromIAM(bucket); err != nil { glog.Warningf("remove bucket policy for %s from IAM: %v", bucket, err) } return } var policyDoc policy_engine.PolicyDocument if err := json.Unmarshal(current, &policyDoc); err != nil { glog.Warningf("backfill bucket policy for %s into IAM: parse: %v", bucket, err) return } if err := s3a.updateBucketPolicyInIAM(bucket, &policyDoc); err != nil { glog.Warningf("backfill bucket policy for %s into IAM: %v", bucket, err) } } // removeBucketPolicyFromIAM removes the bucket policy from the IAM system func (s3a *S3ApiServer) removeBucketPolicyFromIAM(bucket string) error { iamManager := s3a.bucketPolicyIAMManager() if iamManager == nil { return nil } glog.V(2).Infof("Removed bucket policy for %s from IAM system", bucket) return iamManager.RemoveBucketPolicy(context.Background(), bucket) } // bucketPolicyIAMManager returns the advanced-IAM manager the // "bucket-policy:" mirror lives in, or nil when the integration is // not enabled. func (s3a *S3ApiServer) bucketPolicyIAMManager() *integration.IAMManager { if s3a.iam == nil || s3a.iam.iamIntegration == nil { return nil } if s3Integration, ok := s3a.iam.iamIntegration.(*S3IAMIntegration); ok { return s3Integration.iamManager } return nil } // mirrorBucketPolicyToIAM keeps the "bucket-policy:" IAM mirror in // sync with the policy stored on a bucket's filer entry. Driven from the // metadata subscription so it covers every writer - this gateway's own // PutBucketPolicy, another gateway's, the admin UI, bucket deletion, and // rename - where the handlers' direct calls only ever covered the first. func (s3a *S3ApiServer) mirrorBucketPolicyToIAM(oldEntry, newEntry *filer_pb.Entry) { if s3a.bucketPolicyIAMManager() == nil { return } removeName, updateName, updatePolicy := bucketPolicyMirrorOps(oldEntry, newEntry) if removeName != "" { if err := s3a.removeBucketPolicyFromIAM(removeName); err != nil { glog.Warningf("remove bucket policy for %s from IAM: %v", removeName, err) } } if updateName == "" { return } var policyDoc policy_engine.PolicyDocument if err := json.Unmarshal(updatePolicy, &policyDoc); err != nil { glog.Warningf("mirror bucket policy for %s to IAM: parse: %v", updateName, err) return } if err := s3a.updateBucketPolicyInIAM(updateName, &policyDoc); err != nil { glog.Warningf("mirror bucket policy for %s to IAM: %v", updateName, err) } } // bucketPolicyMirrorOps computes what a bucket entry change means for the // IAM mirror: a name whose mirror must be removed, and a (name, policy) to // write. A rename delivers both entries under different names in one event, // and the old name's mirror has to move even when the policy bytes are // unchanged - equality only short-circuits same-name updates. func bucketPolicyMirrorOps(oldEntry, newEntry *filer_pb.Entry) (removeName, updateName string, updatePolicy []byte) { var oldName, newName string var oldPolicy, newPolicy []byte if oldEntry != nil { oldName = oldEntry.Name oldPolicy = oldEntry.Extended[BUCKET_POLICY_METADATA_KEY] } if newEntry != nil { newName = newEntry.Name newPolicy = newEntry.Extended[BUCKET_POLICY_METADATA_KEY] } if oldName != "" && oldName != newName && len(oldPolicy) > 0 { removeName = oldName oldPolicy = nil } if newName == "" || bytes.Equal(oldPolicy, newPolicy) { return } if len(newPolicy) == 0 { removeName = newName return } updateName = newName updatePolicy = newPolicy return } // 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) }