github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/admin-bucket-handlers.go (about)

     1  // Copyright (c) 2015-2021 MinIO, Inc.
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package cmd
    19  
    20  import (
    21  	"bytes"
    22  	"encoding/base64"
    23  	"encoding/json"
    24  	"encoding/xml"
    25  	"errors"
    26  	"fmt"
    27  	"io"
    28  	"net/http"
    29  	"strings"
    30  	"time"
    31  
    32  	jsoniter "github.com/json-iterator/go"
    33  	"github.com/klauspost/compress/zip"
    34  	"github.com/minio/kms-go/kes"
    35  	"github.com/minio/madmin-go/v3"
    36  	"github.com/minio/minio-go/v7/pkg/tags"
    37  	"github.com/minio/minio/internal/bucket/lifecycle"
    38  	objectlock "github.com/minio/minio/internal/bucket/object/lock"
    39  	"github.com/minio/minio/internal/bucket/versioning"
    40  	"github.com/minio/minio/internal/event"
    41  	"github.com/minio/minio/internal/kms"
    42  	"github.com/minio/minio/internal/logger"
    43  	"github.com/minio/mux"
    44  	"github.com/minio/pkg/v2/policy"
    45  )
    46  
    47  const (
    48  	bucketQuotaConfigFile = "quota.json"
    49  	bucketTargetsFile     = "bucket-targets.json"
    50  )
    51  
    52  // PutBucketQuotaConfigHandler - PUT Bucket quota configuration.
    53  // ----------
    54  // Places a quota configuration on the specified bucket. The quota
    55  // specified in the quota configuration will be applied by default
    56  // to enforce total quota for the specified bucket.
    57  func (a adminAPIHandlers) PutBucketQuotaConfigHandler(w http.ResponseWriter, r *http.Request) {
    58  	ctx := r.Context()
    59  
    60  	objectAPI, _ := validateAdminReq(ctx, w, r, policy.SetBucketQuotaAdminAction)
    61  	if objectAPI == nil {
    62  		return
    63  	}
    64  
    65  	vars := mux.Vars(r)
    66  	bucket := pathClean(vars["bucket"])
    67  
    68  	if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
    69  		writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
    70  		return
    71  	}
    72  
    73  	data, err := io.ReadAll(r.Body)
    74  	if err != nil {
    75  		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
    76  		return
    77  	}
    78  
    79  	quotaConfig, err := parseBucketQuota(bucket, data)
    80  	if err != nil {
    81  		writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
    82  		return
    83  	}
    84  
    85  	updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketQuotaConfigFile, data)
    86  	if err != nil {
    87  		writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
    88  		return
    89  	}
    90  
    91  	bucketMeta := madmin.SRBucketMeta{
    92  		Type:      madmin.SRBucketMetaTypeQuotaConfig,
    93  		Bucket:    bucket,
    94  		Quota:     data,
    95  		UpdatedAt: updatedAt,
    96  	}
    97  	if quotaConfig.Size == 0 && quotaConfig.Quota == 0 {
    98  		bucketMeta.Quota = nil
    99  	}
   100  
   101  	// Call site replication hook.
   102  	logger.LogIf(ctx, globalSiteReplicationSys.BucketMetaHook(ctx, bucketMeta))
   103  
   104  	// Write success response.
   105  	writeSuccessResponseHeadersOnly(w)
   106  }
   107  
   108  // GetBucketQuotaConfigHandler - gets bucket quota configuration
   109  func (a adminAPIHandlers) GetBucketQuotaConfigHandler(w http.ResponseWriter, r *http.Request) {
   110  	ctx := r.Context()
   111  
   112  	objectAPI, _ := validateAdminReq(ctx, w, r, policy.GetBucketQuotaAdminAction)
   113  	if objectAPI == nil {
   114  		return
   115  	}
   116  
   117  	vars := mux.Vars(r)
   118  	bucket := pathClean(vars["bucket"])
   119  
   120  	if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
   121  		writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
   122  		return
   123  	}
   124  
   125  	config, _, err := globalBucketMetadataSys.GetQuotaConfig(ctx, bucket)
   126  	if err != nil {
   127  		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
   128  		return
   129  	}
   130  
   131  	configData, err := json.Marshal(config)
   132  	if err != nil {
   133  		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
   134  		return
   135  	}
   136  
   137  	// Write success response.
   138  	writeSuccessResponseJSON(w, configData)
   139  }
   140  
   141  // SetRemoteTargetHandler - sets a remote target for bucket
   142  func (a adminAPIHandlers) SetRemoteTargetHandler(w http.ResponseWriter, r *http.Request) {
   143  	ctx := r.Context()
   144  
   145  	vars := mux.Vars(r)
   146  	bucket := pathClean(vars["bucket"])
   147  	update := r.Form.Get("update") == "true"
   148  
   149  	// Get current object layer instance.
   150  	objectAPI, _ := validateAdminReq(ctx, w, r, policy.SetBucketTargetAction)
   151  	if objectAPI == nil {
   152  		return
   153  	}
   154  
   155  	// Check if bucket exists.
   156  	if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
   157  		writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
   158  		return
   159  	}
   160  
   161  	cred, _, s3Err := validateAdminSignature(ctx, r, "")
   162  	if s3Err != ErrNone {
   163  		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
   164  		return
   165  	}
   166  	password := cred.SecretKey
   167  
   168  	reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
   169  	if err != nil {
   170  		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
   171  		return
   172  	}
   173  	var target madmin.BucketTarget
   174  	json := jsoniter.ConfigCompatibleWithStandardLibrary
   175  	if err = json.Unmarshal(reqBytes, &target); err != nil {
   176  		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
   177  		return
   178  	}
   179  	sameTarget, _ := isLocalHost(target.URL().Hostname(), target.URL().Port(), globalMinioPort)
   180  	if sameTarget && bucket == target.TargetBucket {
   181  		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrBucketRemoteIdenticalToSource), r.URL)
   182  		return
   183  	}
   184  
   185  	target.SourceBucket = bucket
   186  	var ops []madmin.TargetUpdateType
   187  	if update {
   188  		ops = madmin.GetTargetUpdateOps(r.Form)
   189  	} else {
   190  		var exists bool // true if arn exists
   191  		target.Arn, exists = globalBucketTargetSys.getRemoteARN(bucket, &target, "")
   192  		if exists && target.Arn != "" { // return pre-existing ARN
   193  			data, err := json.Marshal(target.Arn)
   194  			if err != nil {
   195  				writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
   196  				return
   197  			}
   198  			// Write success response.
   199  			writeSuccessResponseJSON(w, data)
   200  			return
   201  		}
   202  	}
   203  	if target.Arn == "" {
   204  		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
   205  		return
   206  	}
   207  	if globalSiteReplicationSys.isEnabled() && !update {
   208  		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrRemoteTargetDenyAddError, err), r.URL)
   209  		return
   210  	}
   211  
   212  	if update {
   213  		// overlay the updates on existing target
   214  		tgt := globalBucketTargetSys.GetRemoteBucketTargetByArn(ctx, bucket, target.Arn)
   215  		if tgt.Empty() {
   216  			writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrRemoteTargetNotFoundError, err), r.URL)
   217  			return
   218  		}
   219  		for _, op := range ops {
   220  			switch op {
   221  			case madmin.CredentialsUpdateType:
   222  				if !globalSiteReplicationSys.isEnabled() {
   223  					// credentials update is possible only in bucket replication. User will never
   224  					// know the site replicator creds.
   225  					tgt.Credentials = target.Credentials
   226  					tgt.TargetBucket = target.TargetBucket
   227  					tgt.Secure = target.Secure
   228  					tgt.Endpoint = target.Endpoint
   229  				}
   230  			case madmin.SyncUpdateType:
   231  				tgt.ReplicationSync = target.ReplicationSync
   232  			case madmin.ProxyUpdateType:
   233  				tgt.DisableProxy = target.DisableProxy
   234  			case madmin.PathUpdateType:
   235  				tgt.Path = target.Path
   236  			case madmin.BandwidthLimitUpdateType:
   237  				tgt.BandwidthLimit = target.BandwidthLimit
   238  			case madmin.HealthCheckDurationUpdateType:
   239  				tgt.HealthCheckDuration = target.HealthCheckDuration
   240  			}
   241  		}
   242  		target = tgt
   243  	}
   244  
   245  	// enforce minimum bandwidth limit as 100MBps
   246  	if target.BandwidthLimit > 0 && target.BandwidthLimit < 100*1000*1000 {
   247  		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrReplicationBandwidthLimitError, err), r.URL)
   248  		return
   249  	}
   250  	if err = globalBucketTargetSys.SetTarget(ctx, bucket, &target, update); err != nil {
   251  		switch err.(type) {
   252  		case RemoteTargetConnectionErr:
   253  			writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrReplicationRemoteConnectionError, err), r.URL)
   254  		default:
   255  			writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
   256  		}
   257  		return
   258  	}
   259  	targets, err := globalBucketTargetSys.ListBucketTargets(ctx, bucket)
   260  	if err != nil {
   261  		writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
   262  		return
   263  	}
   264  	tgtBytes, err := json.Marshal(&targets)
   265  	if err != nil {
   266  		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
   267  		return
   268  	}
   269  	if _, err = globalBucketMetadataSys.Update(ctx, bucket, bucketTargetsFile, tgtBytes); err != nil {
   270  		writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
   271  		return
   272  	}
   273  
   274  	data, err := json.Marshal(target.Arn)
   275  	if err != nil {
   276  		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
   277  		return
   278  	}
   279  	// Write success response.
   280  	writeSuccessResponseJSON(w, data)
   281  }
   282  
   283  // ListRemoteTargetsHandler - lists remote target(s) for a bucket or gets a target
   284  // for a particular ARN type
   285  func (a adminAPIHandlers) ListRemoteTargetsHandler(w http.ResponseWriter, r *http.Request) {
   286  	ctx := r.Context()
   287  
   288  	vars := mux.Vars(r)
   289  	bucket := pathClean(vars["bucket"])
   290  	arnType := vars["type"]
   291  
   292  	// Get current object layer instance.
   293  	objectAPI, _ := validateAdminReq(ctx, w, r, policy.GetBucketTargetAction)
   294  	if objectAPI == nil {
   295  		return
   296  	}
   297  	if bucket != "" {
   298  		// Check if bucket exists.
   299  		if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
   300  			writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
   301  			return
   302  		}
   303  		if _, err := globalBucketMetadataSys.GetBucketTargetsConfig(bucket); err != nil {
   304  			writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
   305  			return
   306  		}
   307  	}
   308  	targets := globalBucketTargetSys.ListTargets(ctx, bucket, arnType)
   309  	data, err := json.Marshal(targets)
   310  	if err != nil {
   311  		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
   312  		return
   313  	}
   314  	// Write success response.
   315  	writeSuccessResponseJSON(w, data)
   316  }
   317  
   318  // RemoveRemoteTargetHandler - removes a remote target for bucket with specified ARN
   319  func (a adminAPIHandlers) RemoveRemoteTargetHandler(w http.ResponseWriter, r *http.Request) {
   320  	ctx := r.Context()
   321  
   322  	vars := mux.Vars(r)
   323  	bucket := pathClean(vars["bucket"])
   324  	arn := vars["arn"]
   325  
   326  	// Get current object layer instance.
   327  	objectAPI, _ := validateAdminReq(ctx, w, r, policy.SetBucketTargetAction)
   328  	if objectAPI == nil {
   329  		return
   330  	}
   331  
   332  	// Check if bucket exists.
   333  	if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
   334  		writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
   335  		return
   336  	}
   337  
   338  	if err := globalBucketTargetSys.RemoveTarget(ctx, bucket, arn); err != nil {
   339  		writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
   340  		return
   341  	}
   342  	targets, err := globalBucketTargetSys.ListBucketTargets(ctx, bucket)
   343  	if err != nil {
   344  		writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
   345  		return
   346  	}
   347  	tgtBytes, err := json.Marshal(&targets)
   348  	if err != nil {
   349  		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
   350  		return
   351  	}
   352  	if _, err = globalBucketMetadataSys.Update(ctx, bucket, bucketTargetsFile, tgtBytes); err != nil {
   353  		writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
   354  		return
   355  	}
   356  
   357  	// Write success response.
   358  	writeSuccessNoContent(w)
   359  }
   360  
   361  // ExportBucketMetadataHandler - exports all bucket metadata as a zipped file
   362  func (a adminAPIHandlers) ExportBucketMetadataHandler(w http.ResponseWriter, r *http.Request) {
   363  	ctx := r.Context()
   364  
   365  	bucket := pathClean(r.Form.Get("bucket"))
   366  	// Get current object layer instance.
   367  	objectAPI, _ := validateAdminReq(ctx, w, r, policy.ExportBucketMetadataAction)
   368  	if objectAPI == nil {
   369  		return
   370  	}
   371  
   372  	var (
   373  		buckets []BucketInfo
   374  		err     error
   375  	)
   376  	if bucket != "" {
   377  		// Check if bucket exists.
   378  		if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
   379  			writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
   380  			return
   381  		}
   382  		buckets = append(buckets, BucketInfo{Name: bucket})
   383  	} else {
   384  		buckets, err = objectAPI.ListBuckets(ctx, BucketOptions{})
   385  		if err != nil {
   386  			writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
   387  			return
   388  		}
   389  	}
   390  
   391  	// Initialize a zip writer which will provide a zipped content
   392  	// of bucket metadata
   393  	zipWriter := zip.NewWriter(w)
   394  	defer zipWriter.Close()
   395  
   396  	rawDataFn := func(r io.Reader, filename string, sz int) {
   397  		header, zerr := zip.FileInfoHeader(dummyFileInfo{
   398  			name:    filename,
   399  			size:    int64(sz),
   400  			mode:    0o600,
   401  			modTime: time.Now(),
   402  			isDir:   false,
   403  			sys:     nil,
   404  		})
   405  		if zerr == nil {
   406  			header.Method = zip.Deflate
   407  			zwriter, zerr := zipWriter.CreateHeader(header)
   408  			if zerr == nil {
   409  				io.Copy(zwriter, r)
   410  			}
   411  		}
   412  	}
   413  
   414  	cfgFiles := []string{
   415  		bucketPolicyConfig,
   416  		bucketNotificationConfig,
   417  		bucketLifecycleConfig,
   418  		bucketSSEConfig,
   419  		bucketTaggingConfig,
   420  		bucketQuotaConfigFile,
   421  		objectLockConfig,
   422  		bucketVersioningConfig,
   423  		bucketReplicationConfig,
   424  		bucketTargetsFile,
   425  	}
   426  	for _, bi := range buckets {
   427  		for _, cfgFile := range cfgFiles {
   428  			cfgPath := pathJoin(bi.Name, cfgFile)
   429  			bucket := bi.Name
   430  			switch cfgFile {
   431  			case bucketNotificationConfig:
   432  				config, err := globalBucketMetadataSys.GetNotificationConfig(bucket)
   433  				if err != nil {
   434  					logger.LogIf(ctx, err)
   435  					writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
   436  					return
   437  				}
   438  				configData, err := xml.Marshal(config)
   439  				if err != nil {
   440  					writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
   441  					return
   442  				}
   443  				rawDataFn(bytes.NewReader(configData), cfgPath, len(configData))
   444  			case bucketLifecycleConfig:
   445  				config, _, err := globalBucketMetadataSys.GetLifecycleConfig(bucket)
   446  				if err != nil {
   447  					if errors.Is(err, BucketLifecycleNotFound{Bucket: bucket}) {
   448  						continue
   449  					}
   450  					logger.LogIf(ctx, err)
   451  					writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
   452  					return
   453  				}
   454  				configData, err := xml.Marshal(config)
   455  				if err != nil {
   456  					writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
   457  					return
   458  				}
   459  				rawDataFn(bytes.NewReader(configData), cfgPath, len(configData))
   460  			case bucketQuotaConfigFile:
   461  				config, _, err := globalBucketMetadataSys.GetQuotaConfig(ctx, bucket)
   462  				if err != nil {
   463  					if errors.Is(err, BucketQuotaConfigNotFound{Bucket: bucket}) {
   464  						continue
   465  					}
   466  					writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
   467  					return
   468  				}
   469  				configData, err := json.Marshal(config)
   470  				if err != nil {
   471  					writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
   472  					return
   473  				}
   474  				rawDataFn(bytes.NewReader(configData), cfgPath, len(configData))
   475  			case bucketSSEConfig:
   476  				config, _, err := globalBucketMetadataSys.GetSSEConfig(bucket)
   477  				if err != nil {
   478  					if errors.Is(err, BucketSSEConfigNotFound{Bucket: bucket}) {
   479  						continue
   480  					}
   481  					writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
   482  					return
   483  				}
   484  				configData, err := xml.Marshal(config)
   485  				if err != nil {
   486  					writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
   487  					return
   488  				}
   489  				rawDataFn(bytes.NewReader(configData), cfgPath, len(configData))
   490  			case bucketTaggingConfig:
   491  				config, _, err := globalBucketMetadataSys.GetTaggingConfig(bucket)
   492  				if err != nil {
   493  					if errors.Is(err, BucketTaggingNotFound{Bucket: bucket}) {
   494  						continue
   495  					}
   496  					writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
   497  					return
   498  				}
   499  				configData, err := xml.Marshal(config)
   500  				if err != nil {
   501  					writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
   502  					return
   503  				}
   504  				rawDataFn(bytes.NewReader(configData), cfgPath, len(configData))
   505  			case objectLockConfig:
   506  				config, _, err := globalBucketMetadataSys.GetObjectLockConfig(bucket)
   507  				if err != nil {
   508  					if errors.Is(err, BucketObjectLockConfigNotFound{Bucket: bucket}) {
   509  						continue
   510  					}
   511  					writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
   512  					return
   513  				}
   514  
   515  				configData, err := xml.Marshal(config)
   516  				if err != nil {
   517  					writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
   518  					return
   519  				}
   520  				rawDataFn(bytes.NewReader(configData), cfgPath, len(configData))
   521  			case bucketVersioningConfig:
   522  				config, _, err := globalBucketMetadataSys.GetVersioningConfig(bucket)
   523  				if err != nil {
   524  					writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
   525  					return
   526  				}
   527  				// ignore empty versioning configs
   528  				if config.Status != versioning.Enabled && config.Status != versioning.Suspended {
   529  					continue
   530  				}
   531  				configData, err := xml.Marshal(config)
   532  				if err != nil {
   533  					writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
   534  					return
   535  				}
   536  				rawDataFn(bytes.NewReader(configData), cfgPath, len(configData))
   537  			case bucketReplicationConfig:
   538  				config, _, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket)
   539  				if err != nil {
   540  					if errors.Is(err, BucketReplicationConfigNotFound{Bucket: bucket}) {
   541  						continue
   542  					}
   543  					writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
   544  					return
   545  				}
   546  				configData, err := xml.Marshal(config)
   547  				if err != nil {
   548  					writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
   549  					return
   550  				}
   551  				rawDataFn(bytes.NewReader(configData), cfgPath, len(configData))
   552  			case bucketTargetsFile:
   553  				config, err := globalBucketMetadataSys.GetBucketTargetsConfig(bucket)
   554  				if err != nil {
   555  					if errors.Is(err, BucketRemoteTargetNotFound{Bucket: bucket}) {
   556  						continue
   557  					}
   558  
   559  					writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
   560  					return
   561  				}
   562  				configData, err := xml.Marshal(config)
   563  				if err != nil {
   564  					writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
   565  					return
   566  				}
   567  				rawDataFn(bytes.NewReader(configData), cfgPath, len(configData))
   568  			}
   569  		}
   570  	}
   571  }
   572  
   573  type importMetaReport struct {
   574  	madmin.BucketMetaImportErrs
   575  }
   576  
   577  func (i *importMetaReport) SetStatus(bucket, fname string, err error) {
   578  	st := i.Buckets[bucket]
   579  	var errMsg string
   580  	if err != nil {
   581  		errMsg = err.Error()
   582  	}
   583  	switch fname {
   584  	case bucketPolicyConfig:
   585  		st.Policy = madmin.MetaStatus{IsSet: true, Err: errMsg}
   586  	case bucketNotificationConfig:
   587  		st.Notification = madmin.MetaStatus{IsSet: true, Err: errMsg}
   588  	case bucketLifecycleConfig:
   589  		st.Lifecycle = madmin.MetaStatus{IsSet: true, Err: errMsg}
   590  	case bucketSSEConfig:
   591  		st.SSEConfig = madmin.MetaStatus{IsSet: true, Err: errMsg}
   592  	case bucketTaggingConfig:
   593  		st.Tagging = madmin.MetaStatus{IsSet: true, Err: errMsg}
   594  	case bucketQuotaConfigFile:
   595  		st.Quota = madmin.MetaStatus{IsSet: true, Err: errMsg}
   596  	case objectLockConfig:
   597  		st.ObjectLock = madmin.MetaStatus{IsSet: true, Err: errMsg}
   598  	case bucketVersioningConfig:
   599  		st.Versioning = madmin.MetaStatus{IsSet: true, Err: errMsg}
   600  	default:
   601  		st.Err = errMsg
   602  	}
   603  	i.Buckets[bucket] = st
   604  }
   605  
   606  // ImportBucketMetadataHandler - imports all bucket metadata from a zipped file and overwrite bucket metadata config
   607  // There are some caveats regarding the following:
   608  // 1. object lock config - object lock should have been specified at time of bucket creation. Only default retention settings are imported here.
   609  // 2. Replication config - is omitted from import as remote target credentials are not available from exported data for security reasons.
   610  // 3. lifecycle config - if transition rules are present, tier name needs to have been defined.
   611  func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r *http.Request) {
   612  	ctx := r.Context()
   613  
   614  	// Get current object layer instance.
   615  	objectAPI, _ := validateAdminReq(ctx, w, r, policy.ImportBucketMetadataAction)
   616  	if objectAPI == nil {
   617  		return
   618  	}
   619  	data, err := io.ReadAll(r.Body)
   620  	if err != nil {
   621  		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
   622  		return
   623  	}
   624  	reader := bytes.NewReader(data)
   625  	zr, err := zip.NewReader(reader, int64(len(data)))
   626  	if err != nil {
   627  		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
   628  		return
   629  	}
   630  	rpt := importMetaReport{
   631  		madmin.BucketMetaImportErrs{
   632  			Buckets: make(map[string]madmin.BucketStatus, len(zr.File)),
   633  		},
   634  	}
   635  
   636  	bucketMap := make(map[string]*BucketMetadata, len(zr.File))
   637  
   638  	updatedAt := UTCNow()
   639  
   640  	for _, file := range zr.File {
   641  		slc := strings.Split(file.Name, slashSeparator)
   642  		if len(slc) != 2 { // expecting bucket/configfile in the zipfile
   643  			rpt.SetStatus(file.Name, "", fmt.Errorf("malformed zip - expecting format bucket/<config.json>"))
   644  			continue
   645  		}
   646  		bucket := slc[0]
   647  		meta, err := readBucketMetadata(ctx, objectAPI, bucket)
   648  		if err == nil {
   649  			bucketMap[bucket] = &meta
   650  		} else if err != errConfigNotFound {
   651  			rpt.SetStatus(bucket, "", err)
   652  		}
   653  	}
   654  
   655  	// import object lock config if any - order of import matters here.
   656  	for _, file := range zr.File {
   657  		slc := strings.Split(file.Name, slashSeparator)
   658  		if len(slc) != 2 { // expecting bucket/configfile in the zipfile
   659  			rpt.SetStatus(file.Name, "", fmt.Errorf("malformed zip - expecting format bucket/<config.json>"))
   660  			continue
   661  		}
   662  		bucket, fileName := slc[0], slc[1]
   663  		if fileName == objectLockConfig {
   664  			reader, err := file.Open()
   665  			if err != nil {
   666  				rpt.SetStatus(bucket, fileName, err)
   667  				continue
   668  			}
   669  			config, err := objectlock.ParseObjectLockConfig(reader)
   670  			if err != nil {
   671  				rpt.SetStatus(bucket, fileName, fmt.Errorf("%s (%s)", errorCodes[ErrMalformedXML].Description, err))
   672  				continue
   673  			}
   674  
   675  			configData, err := xml.Marshal(config)
   676  			if err != nil {
   677  				rpt.SetStatus(bucket, fileName, err)
   678  				continue
   679  			}
   680  			if _, ok := bucketMap[bucket]; !ok {
   681  				opts := MakeBucketOptions{
   682  					LockEnabled: config.Enabled(),
   683  					ForceCreate: true, // ignore if it already exists
   684  				}
   685  				err = objectAPI.MakeBucket(ctx, bucket, opts)
   686  				if err != nil {
   687  					rpt.SetStatus(bucket, fileName, err)
   688  					continue
   689  				}
   690  				v, _ := globalBucketMetadataSys.Get(bucket)
   691  				bucketMap[bucket] = &v
   692  			}
   693  
   694  			bucketMap[bucket].ObjectLockConfigXML = configData
   695  			bucketMap[bucket].ObjectLockConfigUpdatedAt = updatedAt
   696  			rpt.SetStatus(bucket, fileName, nil)
   697  		}
   698  	}
   699  
   700  	// import versioning metadata
   701  	for _, file := range zr.File {
   702  		slc := strings.Split(file.Name, slashSeparator)
   703  		if len(slc) != 2 { // expecting bucket/configfile in the zipfile
   704  			rpt.SetStatus(file.Name, "", fmt.Errorf("malformed zip - expecting format bucket/<config.json>"))
   705  			continue
   706  		}
   707  		bucket, fileName := slc[0], slc[1]
   708  		if fileName == bucketVersioningConfig {
   709  			reader, err := file.Open()
   710  			if err != nil {
   711  				rpt.SetStatus(bucket, fileName, err)
   712  				continue
   713  			}
   714  			v, err := versioning.ParseConfig(io.LimitReader(reader, maxBucketVersioningConfigSize))
   715  			if err != nil {
   716  				rpt.SetStatus(bucket, fileName, err)
   717  				continue
   718  			}
   719  			if _, ok := bucketMap[bucket]; !ok {
   720  				if err = objectAPI.MakeBucket(ctx, bucket, MakeBucketOptions{
   721  					ForceCreate: true, // ignore if it already exists
   722  				}); err != nil {
   723  					rpt.SetStatus(bucket, fileName, err)
   724  					continue
   725  				}
   726  				v, _ := globalBucketMetadataSys.Get(bucket)
   727  				bucketMap[bucket] = &v
   728  			}
   729  
   730  			if globalSiteReplicationSys.isEnabled() && v.Suspended() {
   731  				rpt.SetStatus(bucket, fileName, fmt.Errorf("Cluster replication is enabled for this site, so the versioning state cannot be suspended."))
   732  				continue
   733  			}
   734  
   735  			if rcfg, _ := globalBucketObjectLockSys.Get(bucket); rcfg.LockEnabled && v.Suspended() {
   736  				rpt.SetStatus(bucket, fileName, fmt.Errorf("An Object Lock configuration is present on this bucket, so the versioning state cannot be suspended."))
   737  				continue
   738  			}
   739  			if _, err := getReplicationConfig(ctx, bucket); err == nil && v.Suspended() {
   740  				rpt.SetStatus(bucket, fileName, fmt.Errorf("A replication configuration is present on this bucket, so the versioning state cannot be suspended."))
   741  				continue
   742  			}
   743  
   744  			configData, err := xml.Marshal(v)
   745  			if err != nil {
   746  				rpt.SetStatus(bucket, fileName, fmt.Errorf("%s (%s)", errorCodes[ErrMalformedXML].Description, err))
   747  				continue
   748  			}
   749  
   750  			bucketMap[bucket].VersioningConfigXML = configData
   751  			bucketMap[bucket].VersioningConfigUpdatedAt = updatedAt
   752  			rpt.SetStatus(bucket, fileName, nil)
   753  		}
   754  	}
   755  
   756  	for _, file := range zr.File {
   757  		reader, err := file.Open()
   758  		if err != nil {
   759  			rpt.SetStatus(file.Name, "", err)
   760  			continue
   761  		}
   762  		sz := file.FileInfo().Size()
   763  		slc := strings.Split(file.Name, slashSeparator)
   764  		if len(slc) != 2 { // expecting bucket/configfile in the zipfile
   765  			rpt.SetStatus(file.Name, "", fmt.Errorf("malformed zip - expecting format bucket/<config.json>"))
   766  			continue
   767  		}
   768  		bucket, fileName := slc[0], slc[1]
   769  
   770  		// create bucket if it does not exist yet.
   771  		if _, ok := bucketMap[bucket]; !ok {
   772  			err = objectAPI.MakeBucket(ctx, bucket, MakeBucketOptions{
   773  				ForceCreate: true, // ignore if it already exists
   774  			})
   775  			if err != nil {
   776  				rpt.SetStatus(bucket, "", err)
   777  				continue
   778  			}
   779  			v, _ := globalBucketMetadataSys.Get(bucket)
   780  			bucketMap[bucket] = &v
   781  		}
   782  		if _, ok := bucketMap[bucket]; !ok {
   783  			continue
   784  		}
   785  		switch fileName {
   786  		case bucketNotificationConfig:
   787  			config, err := event.ParseConfig(io.LimitReader(reader, sz), globalSite.Region, globalEventNotifier.targetList)
   788  			if err != nil {
   789  				rpt.SetStatus(bucket, fileName, fmt.Errorf("%s (%s)", errorCodes[ErrMalformedXML].Description, err))
   790  				continue
   791  			}
   792  
   793  			configData, err := xml.Marshal(config)
   794  			if err != nil {
   795  				rpt.SetStatus(bucket, fileName, err)
   796  				continue
   797  			}
   798  
   799  			bucketMap[bucket].NotificationConfigXML = configData
   800  			rpt.SetStatus(bucket, fileName, nil)
   801  		case bucketPolicyConfig:
   802  			// Error out if Content-Length is beyond allowed size.
   803  			if sz > maxBucketPolicySize {
   804  				rpt.SetStatus(bucket, fileName, fmt.Errorf(ErrPolicyTooLarge.String()))
   805  				continue
   806  			}
   807  
   808  			bucketPolicyBytes, err := io.ReadAll(io.LimitReader(reader, sz))
   809  			if err != nil {
   810  				rpt.SetStatus(bucket, fileName, err)
   811  				continue
   812  			}
   813  
   814  			bucketPolicy, err := policy.ParseBucketPolicyConfig(bytes.NewReader(bucketPolicyBytes), bucket)
   815  			if err != nil {
   816  				rpt.SetStatus(bucket, fileName, err)
   817  				continue
   818  			}
   819  
   820  			// Version in policy must not be empty
   821  			if bucketPolicy.Version == "" {
   822  				rpt.SetStatus(bucket, fileName, fmt.Errorf(ErrPolicyInvalidVersion.String()))
   823  				continue
   824  			}
   825  
   826  			configData, err := json.Marshal(bucketPolicy)
   827  			if err != nil {
   828  				rpt.SetStatus(bucket, fileName, err)
   829  				continue
   830  			}
   831  
   832  			bucketMap[bucket].PolicyConfigJSON = configData
   833  			bucketMap[bucket].PolicyConfigUpdatedAt = updatedAt
   834  			rpt.SetStatus(bucket, fileName, nil)
   835  		case bucketLifecycleConfig:
   836  			bucketLifecycle, err := lifecycle.ParseLifecycleConfig(io.LimitReader(reader, sz))
   837  			if err != nil {
   838  				rpt.SetStatus(bucket, fileName, err)
   839  				continue
   840  			}
   841  
   842  			// Validate the received bucket policy document
   843  			if err = bucketLifecycle.Validate(); err != nil {
   844  				rpt.SetStatus(bucket, fileName, err)
   845  				continue
   846  			}
   847  
   848  			// Validate the transition storage ARNs
   849  			if err = validateTransitionTier(bucketLifecycle); err != nil {
   850  				rpt.SetStatus(bucket, fileName, err)
   851  				continue
   852  			}
   853  
   854  			configData, err := xml.Marshal(bucketLifecycle)
   855  			if err != nil {
   856  				rpt.SetStatus(bucket, fileName, err)
   857  				continue
   858  			}
   859  
   860  			bucketMap[bucket].LifecycleConfigXML = configData
   861  			bucketMap[bucket].LifecycleConfigUpdatedAt = updatedAt
   862  			rpt.SetStatus(bucket, fileName, nil)
   863  		case bucketSSEConfig:
   864  			// Parse bucket encryption xml
   865  			encConfig, err := validateBucketSSEConfig(io.LimitReader(reader, maxBucketSSEConfigSize))
   866  			if err != nil {
   867  				rpt.SetStatus(bucket, fileName, fmt.Errorf("%s (%s)", errorCodes[ErrMalformedXML].Description, err))
   868  				continue
   869  			}
   870  
   871  			// Return error if KMS is not initialized
   872  			if GlobalKMS == nil {
   873  				rpt.SetStatus(bucket, fileName, fmt.Errorf("%s", errorCodes[ErrKMSNotConfigured].Description))
   874  				continue
   875  			}
   876  			kmsKey := encConfig.KeyID()
   877  			if kmsKey != "" {
   878  				kmsContext := kms.Context{"MinIO admin API": "ServerInfoHandler"} // Context for a test key operation
   879  				_, err := GlobalKMS.GenerateKey(ctx, kmsKey, kmsContext)
   880  				if err != nil {
   881  					if errors.Is(err, kes.ErrKeyNotFound) {
   882  						rpt.SetStatus(bucket, fileName, errKMSKeyNotFound)
   883  						continue
   884  					}
   885  					rpt.SetStatus(bucket, fileName, err)
   886  					continue
   887  				}
   888  			}
   889  
   890  			configData, err := xml.Marshal(encConfig)
   891  			if err != nil {
   892  				rpt.SetStatus(bucket, fileName, err)
   893  				continue
   894  			}
   895  
   896  			bucketMap[bucket].EncryptionConfigXML = configData
   897  			bucketMap[bucket].EncryptionConfigUpdatedAt = updatedAt
   898  			rpt.SetStatus(bucket, fileName, nil)
   899  		case bucketTaggingConfig:
   900  			tags, err := tags.ParseBucketXML(io.LimitReader(reader, sz))
   901  			if err != nil {
   902  				rpt.SetStatus(bucket, fileName, fmt.Errorf("%s (%s)", errorCodes[ErrMalformedXML].Description, err))
   903  				continue
   904  			}
   905  
   906  			configData, err := xml.Marshal(tags)
   907  			if err != nil {
   908  				rpt.SetStatus(bucket, fileName, err)
   909  				continue
   910  			}
   911  
   912  			bucketMap[bucket].TaggingConfigXML = configData
   913  			bucketMap[bucket].TaggingConfigUpdatedAt = updatedAt
   914  			rpt.SetStatus(bucket, fileName, nil)
   915  		case bucketQuotaConfigFile:
   916  			data, err := io.ReadAll(reader)
   917  			if err != nil {
   918  				rpt.SetStatus(bucket, fileName, err)
   919  				continue
   920  			}
   921  
   922  			_, err = parseBucketQuota(bucket, data)
   923  			if err != nil {
   924  				rpt.SetStatus(bucket, fileName, err)
   925  				continue
   926  			}
   927  
   928  			bucketMap[bucket].QuotaConfigJSON = data
   929  			bucketMap[bucket].QuotaConfigUpdatedAt = updatedAt
   930  			rpt.SetStatus(bucket, fileName, nil)
   931  		}
   932  	}
   933  
   934  	enc := func(b []byte) *string {
   935  		if b == nil {
   936  			return nil
   937  		}
   938  		v := base64.StdEncoding.EncodeToString(b)
   939  		return &v
   940  	}
   941  
   942  	for bucket, meta := range bucketMap {
   943  		err := globalBucketMetadataSys.save(ctx, *meta)
   944  		if err != nil {
   945  			rpt.SetStatus(bucket, "", err)
   946  			continue
   947  		}
   948  		// Call site replication hook.
   949  		if err = globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
   950  			Bucket:           bucket,
   951  			Quota:            meta.QuotaConfigJSON,
   952  			Policy:           meta.PolicyConfigJSON,
   953  			Versioning:       enc(meta.VersioningConfigXML),
   954  			Tags:             enc(meta.TaggingConfigXML),
   955  			ObjectLockConfig: enc(meta.ObjectLockConfigXML),
   956  			SSEConfig:        enc(meta.EncryptionConfigXML),
   957  			UpdatedAt:        updatedAt,
   958  		}); err != nil {
   959  			rpt.SetStatus(bucket, "", err)
   960  			continue
   961  		}
   962  
   963  	}
   964  
   965  	rptData, err := json.Marshal(rpt.BucketMetaImportErrs)
   966  	if err != nil {
   967  		writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
   968  		return
   969  	}
   970  
   971  	writeSuccessResponseJSON(w, rptData)
   972  }
   973  
   974  // ReplicationDiffHandler - POST returns info on unreplicated versions for a remote target ARN
   975  // to the connected HTTP client.
   976  func (a adminAPIHandlers) ReplicationDiffHandler(w http.ResponseWriter, r *http.Request) {
   977  	ctx := r.Context()
   978  
   979  	vars := mux.Vars(r)
   980  	bucket := vars["bucket"]
   981  
   982  	objectAPI, _ := validateAdminReq(ctx, w, r, policy.ReplicationDiff)
   983  	if objectAPI == nil {
   984  		return
   985  	}
   986  
   987  	// Check if bucket exists.
   988  	if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
   989  		writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
   990  		return
   991  	}
   992  	opts := extractReplicateDiffOpts(r.Form)
   993  	if opts.ARN != "" {
   994  		tgt := globalBucketTargetSys.GetRemoteBucketTargetByArn(ctx, bucket, opts.ARN)
   995  		if tgt.Empty() {
   996  			writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrInvalidRequest, fmt.Errorf("invalid arn : '%s'", opts.ARN)), r.URL)
   997  			return
   998  		}
   999  	}
  1000  
  1001  	keepAliveTicker := time.NewTicker(500 * time.Millisecond)
  1002  	defer keepAliveTicker.Stop()
  1003  
  1004  	diffCh, err := getReplicationDiff(ctx, objectAPI, bucket, opts)
  1005  	if err != nil {
  1006  		writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
  1007  		return
  1008  	}
  1009  	enc := json.NewEncoder(w)
  1010  	for {
  1011  		select {
  1012  		case entry, ok := <-diffCh:
  1013  			if !ok {
  1014  				return
  1015  			}
  1016  			if err := enc.Encode(entry); err != nil {
  1017  				return
  1018  			}
  1019  			if len(diffCh) == 0 {
  1020  				// Flush if nothing is queued
  1021  				w.(http.Flusher).Flush()
  1022  			}
  1023  		case <-keepAliveTicker.C:
  1024  			if len(diffCh) > 0 {
  1025  				continue
  1026  			}
  1027  			if _, err := w.Write([]byte(" ")); err != nil {
  1028  				return
  1029  			}
  1030  			w.(http.Flusher).Flush()
  1031  		case <-ctx.Done():
  1032  			return
  1033  		}
  1034  	}
  1035  }
  1036  
  1037  // ReplicationMRFHandler - POST returns info on entries in the MRF backlog for a node or all nodes
  1038  func (a adminAPIHandlers) ReplicationMRFHandler(w http.ResponseWriter, r *http.Request) {
  1039  	ctx := r.Context()
  1040  
  1041  	vars := mux.Vars(r)
  1042  	bucket := vars["bucket"]
  1043  
  1044  	objectAPI, _ := validateAdminReq(ctx, w, r, policy.ReplicationDiff)
  1045  	if objectAPI == nil {
  1046  		return
  1047  	}
  1048  
  1049  	// Check if bucket exists.
  1050  	if bucket != "" {
  1051  		if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
  1052  			writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
  1053  			return
  1054  		}
  1055  	}
  1056  
  1057  	q := r.Form
  1058  	node := q.Get("node")
  1059  
  1060  	keepAliveTicker := time.NewTicker(500 * time.Millisecond)
  1061  	defer keepAliveTicker.Stop()
  1062  
  1063  	mrfCh, err := globalNotificationSys.GetReplicationMRF(ctx, bucket, node)
  1064  	if err != nil {
  1065  		writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
  1066  		return
  1067  	}
  1068  	enc := json.NewEncoder(w)
  1069  	for {
  1070  		select {
  1071  		case entry, ok := <-mrfCh:
  1072  			if !ok {
  1073  				return
  1074  			}
  1075  			if err := enc.Encode(entry); err != nil {
  1076  				return
  1077  			}
  1078  			if len(mrfCh) == 0 {
  1079  				// Flush if nothing is queued
  1080  				w.(http.Flusher).Flush()
  1081  			}
  1082  		case <-keepAliveTicker.C:
  1083  			if len(mrfCh) > 0 {
  1084  				continue
  1085  			}
  1086  			if _, err := w.Write([]byte(" ")); err != nil {
  1087  				return
  1088  			}
  1089  			w.(http.Flusher).Flush()
  1090  		case <-ctx.Done():
  1091  			return
  1092  		}
  1093  	}
  1094  }