mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-30 17:00:48 +01:00
* s3: answer NoSuchBucket for encryption and policy writes to a missing bucket PutBucketEncryption, DeleteBucketEncryption, and PutBucketPolicy surfaced the filer's bucket-miss as InternalError; GetBucketEncryption masked it as ServerSideEncryptionConfigurationNotFoundError. Map the miss to NoSuchBucket instead, matching AWS. Internal readers still treat a missing bucket as "no encryption configured" so uploads keep their behavior. * s3: wrap ErrBucketNotFound only for verified misses in bucket policy ops The filer lookup helpers in get/set/deleteBucketPolicy marked every LookupDirectoryEntry failure as bucket-not-found, so a transient filer outage answered NoSuchBucket for a live bucket. Route them through filer_pb.LookupEntry, which normalizes only true misses to ErrNotFound. * s3: answer NoSuchBucket when the policy delete's own write loses the bucket * s3: classify a filer NotFound on the policy UpdateEntry as ErrBucketNotFound * s3: require the bucket entry to exist on config patch transactions * s3: answer NoSuchBucket when tagging and CORS writes lose the bucket mid-request * s3: keep the existence check on no-op bucket config patches An unchanged config write produced an empty patch that early-returned before the transaction, so a bucket deleted between the read and the write still reported success. Ship the condition-only transaction so IF_EXISTS runs on its own. * s3: guard the iam step and cover failure-vs-miss classification Nil-guard s3a.iam in the bucket policy handlers so the handlers do not panic when no IAM server is wired (unit tests, embedded servers), treat a "precondition failed" response string the same as the error code when mapping a failed patch to ErrNotFound, and add handler-level tests that pin the miss/failure distinction: missing buckets and vanished mid-write buckets answer 404 NoSuchBucket while transient filer failures stay 500 InternalError.
492 lines
16 KiB
Go
492 lines
16 KiB
Go
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 {
|
|
if errors.Is(err, ErrBucketNotFound) {
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucket)
|
|
} else {
|
|
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 != nil && 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 {
|
|
if errors.Is(err, ErrBucketNotFound) {
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucket)
|
|
} else {
|
|
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 != nil && 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 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 := filer_pb.LookupEntry(context.Background(), client, &filer_pb.LookupDirectoryEntryRequest{
|
|
Directory: s3a.bucketRoot(bucket),
|
|
Name: bucket,
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, filer_pb.ErrNotFound) {
|
|
return fmt.Errorf("%w: %v", ErrBucketNotFound, err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
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 := filer_pb.LookupEntry(context.Background(), client, &filer_pb.LookupDirectoryEntryRequest{
|
|
Directory: s3a.bucketRoot(bucket),
|
|
Name: bucket,
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, filer_pb.ErrNotFound) {
|
|
return fmt.Errorf("%w: %v", ErrBucketNotFound, err)
|
|
}
|
|
return 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; a bucket deleted between the
|
|
// lookup above and this write surfaces as a filer NotFound.
|
|
_, err = client.UpdateEntry(context.Background(), &filer_pb.UpdateEntryRequest{
|
|
Directory: s3a.bucketRoot(bucket),
|
|
Entry: entry,
|
|
})
|
|
if err != nil && isBucketNotFoundErr(err) {
|
|
return fmt.Errorf("%w: %v", ErrBucketNotFound, err)
|
|
}
|
|
|
|
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 := filer_pb.LookupEntry(context.Background(), client, &filer_pb.LookupDirectoryEntryRequest{
|
|
Directory: s3a.bucketRoot(bucket),
|
|
Name: bucket,
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, filer_pb.ErrNotFound) {
|
|
return fmt.Errorf("%w: %v", ErrBucketNotFound, err)
|
|
}
|
|
return 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; a bucket deleted between the lookup above and this
|
|
// write surfaces as a filer NotFound.
|
|
_, err = client.UpdateEntry(context.Background(), &filer_pb.UpdateEntryRequest{
|
|
Directory: s3a.bucketRoot(bucket),
|
|
Entry: entry,
|
|
})
|
|
if err != nil && isBucketNotFoundErr(err) {
|
|
return fmt.Errorf("%w: %v", ErrBucketNotFound, err)
|
|
}
|
|
|
|
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:<bucket>" 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:<bucket>" 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)
|
|
}
|