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

     1  // Copyright (c) 2015-2022 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/json"
    23  	"encoding/xml"
    24  	"errors"
    25  	"fmt"
    26  	"io"
    27  	"net/http"
    28  	"path"
    29  	"time"
    30  
    31  	"github.com/minio/minio-go/v7"
    32  	objectlock "github.com/minio/minio/internal/bucket/object/lock"
    33  	"github.com/minio/minio/internal/bucket/replication"
    34  	xhttp "github.com/minio/minio/internal/http"
    35  	"github.com/minio/minio/internal/logger"
    36  	"github.com/minio/mux"
    37  	"github.com/minio/pkg/v2/policy"
    38  )
    39  
    40  // PutBucketReplicationConfigHandler - PUT Bucket replication configuration.
    41  // ----------
    42  // Add a replication configuration on the specified bucket as specified in https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketReplication.html
    43  func (api objectAPIHandlers) PutBucketReplicationConfigHandler(w http.ResponseWriter, r *http.Request) {
    44  	ctx := newContext(r, w, "PutBucketReplicationConfig")
    45  	defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
    46  
    47  	vars := mux.Vars(r)
    48  	bucket := vars["bucket"]
    49  	objectAPI := api.ObjectAPI()
    50  	if objectAPI == nil {
    51  		writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
    52  		return
    53  	}
    54  	if s3Error := checkRequestAuthType(ctx, r, policy.PutReplicationConfigurationAction, bucket, ""); s3Error != ErrNone {
    55  		writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
    56  		return
    57  	}
    58  	// Check if bucket exists.
    59  	if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
    60  		writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
    61  		return
    62  	}
    63  	if globalSiteReplicationSys.isEnabled() && logger.GetReqInfo(ctx).Cred.AccessKey != globalActiveCred.AccessKey {
    64  		writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationDenyEditError), r.URL)
    65  		return
    66  	}
    67  	if versioned := globalBucketVersioningSys.Enabled(bucket); !versioned {
    68  		writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationNeedsVersioningError), r.URL)
    69  		return
    70  	}
    71  	replicationConfig, err := replication.ParseConfig(io.LimitReader(r.Body, r.ContentLength))
    72  	if err != nil {
    73  		apiErr := errorCodes.ToAPIErr(ErrMalformedXML)
    74  		apiErr.Description = err.Error()
    75  		writeErrorResponse(ctx, w, apiErr, r.URL)
    76  		return
    77  	}
    78  	sameTarget, apiErr := validateReplicationDestination(ctx, bucket, replicationConfig, true)
    79  	if apiErr != noError {
    80  		writeErrorResponse(ctx, w, apiErr, r.URL)
    81  		return
    82  	}
    83  	// Validate the received bucket replication config
    84  	if err = replicationConfig.Validate(bucket, sameTarget); err != nil {
    85  		writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
    86  		return
    87  	}
    88  	configData, err := xml.Marshal(replicationConfig)
    89  	if err != nil {
    90  		writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
    91  		return
    92  	}
    93  	if _, err = globalBucketMetadataSys.Update(ctx, bucket, bucketReplicationConfig, configData); err != nil {
    94  		writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
    95  		return
    96  	}
    97  
    98  	// Write success response.
    99  	writeSuccessResponseHeadersOnly(w)
   100  }
   101  
   102  // GetBucketReplicationConfigHandler - GET Bucket replication configuration.
   103  // ----------
   104  // Gets the replication configuration for a bucket.
   105  func (api objectAPIHandlers) GetBucketReplicationConfigHandler(w http.ResponseWriter, r *http.Request) {
   106  	ctx := newContext(r, w, "GetBucketReplicationConfig")
   107  
   108  	defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
   109  
   110  	vars := mux.Vars(r)
   111  	bucket := vars["bucket"]
   112  
   113  	objectAPI := api.ObjectAPI()
   114  	if objectAPI == nil {
   115  		writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
   116  		return
   117  	}
   118  
   119  	// check if user has permissions to perform this operation
   120  	if s3Error := checkRequestAuthType(ctx, r, policy.GetReplicationConfigurationAction, bucket, ""); s3Error != ErrNone {
   121  		writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
   122  		return
   123  	}
   124  	// Check if bucket exists.
   125  	if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
   126  		writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
   127  		return
   128  	}
   129  
   130  	config, _, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket)
   131  	if err != nil {
   132  		writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
   133  		return
   134  	}
   135  	configData, err := xml.Marshal(config)
   136  	if err != nil {
   137  		writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
   138  		return
   139  	}
   140  
   141  	// Write success response.
   142  	writeSuccessResponseXML(w, configData)
   143  }
   144  
   145  // DeleteBucketReplicationConfigHandler - DELETE Bucket replication config.
   146  // ----------
   147  func (api objectAPIHandlers) DeleteBucketReplicationConfigHandler(w http.ResponseWriter, r *http.Request) {
   148  	ctx := newContext(r, w, "DeleteBucketReplicationConfig")
   149  	defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
   150  	vars := mux.Vars(r)
   151  	bucket := vars["bucket"]
   152  
   153  	objectAPI := api.ObjectAPI()
   154  	if objectAPI == nil {
   155  		writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
   156  		return
   157  	}
   158  
   159  	if s3Error := checkRequestAuthType(ctx, r, policy.PutReplicationConfigurationAction, bucket, ""); s3Error != ErrNone {
   160  		writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
   161  		return
   162  	}
   163  	// Check if bucket exists.
   164  	if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
   165  		writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
   166  		return
   167  	}
   168  	if globalSiteReplicationSys.isEnabled() {
   169  		writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationDenyEditError), r.URL)
   170  		return
   171  	}
   172  	if _, err := globalBucketMetadataSys.Delete(ctx, bucket, bucketReplicationConfig); err != nil {
   173  		writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
   174  		return
   175  	}
   176  
   177  	targets, err := globalBucketTargetSys.ListBucketTargets(ctx, bucket)
   178  	if err != nil {
   179  		writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
   180  		return
   181  	}
   182  	for _, tgt := range targets.Targets {
   183  		if err := globalBucketTargetSys.RemoveTarget(ctx, bucket, tgt.Arn); err != nil {
   184  			writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
   185  			return
   186  		}
   187  	}
   188  	if _, err := globalBucketMetadataSys.Delete(ctx, bucket, bucketTargetsFile); err != nil {
   189  		writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
   190  		return
   191  	}
   192  	// Write success response.
   193  	writeSuccessResponseHeadersOnly(w)
   194  }
   195  
   196  // GetBucketReplicationMetricsHandler - GET Bucket replication metrics.		// Deprecated Aug 2023
   197  // ----------
   198  // Gets the replication metrics for a bucket.
   199  func (api objectAPIHandlers) GetBucketReplicationMetricsHandler(w http.ResponseWriter, r *http.Request) {
   200  	ctx := newContext(r, w, "GetBucketReplicationMetrics")
   201  
   202  	defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
   203  
   204  	vars := mux.Vars(r)
   205  	bucket := vars["bucket"]
   206  
   207  	objectAPI := api.ObjectAPI()
   208  	if objectAPI == nil {
   209  		writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
   210  		return
   211  	}
   212  
   213  	// check if user has permissions to perform this operation
   214  	if s3Error := checkRequestAuthType(ctx, r, policy.GetReplicationConfigurationAction, bucket, ""); s3Error != ErrNone {
   215  		writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
   216  		return
   217  	}
   218  
   219  	// Check if bucket exists.
   220  	if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
   221  		writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
   222  		return
   223  	}
   224  
   225  	if _, _, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket); err != nil {
   226  		writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
   227  		return
   228  	}
   229  
   230  	w.Header().Set(xhttp.ContentType, string(mimeJSON))
   231  
   232  	enc := json.NewEncoder(w)
   233  	stats := globalReplicationStats.getLatestReplicationStats(bucket)
   234  	bwRpt := globalNotificationSys.GetBandwidthReports(ctx, bucket)
   235  	bwMap := bwRpt.BucketStats
   236  	for arn, st := range stats.ReplicationStats.Stats {
   237  		for opts, bw := range bwMap {
   238  			if opts.ReplicationARN != "" && opts.ReplicationARN == arn {
   239  				st.BandWidthLimitInBytesPerSecond = bw.LimitInBytesPerSecond
   240  				st.CurrentBandwidthInBytesPerSecond = bw.CurrentBandwidthInBytesPerSecond
   241  				stats.ReplicationStats.Stats[arn] = st
   242  			}
   243  		}
   244  	}
   245  
   246  	if err := enc.Encode(stats.ReplicationStats); err != nil {
   247  		writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
   248  		return
   249  	}
   250  }
   251  
   252  // GetBucketReplicationMetricsV2Handler - GET Bucket replication metrics.
   253  // ----------
   254  // Gets the replication metrics for a bucket.
   255  func (api objectAPIHandlers) GetBucketReplicationMetricsV2Handler(w http.ResponseWriter, r *http.Request) {
   256  	ctx := newContext(r, w, "GetBucketReplicationMetricsV2")
   257  
   258  	defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
   259  
   260  	vars := mux.Vars(r)
   261  	bucket := vars["bucket"]
   262  
   263  	objectAPI := api.ObjectAPI()
   264  	if objectAPI == nil {
   265  		writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
   266  		return
   267  	}
   268  
   269  	// check if user has permissions to perform this operation
   270  	if s3Error := checkRequestAuthType(ctx, r, policy.GetReplicationConfigurationAction, bucket, ""); s3Error != ErrNone {
   271  		writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
   272  		return
   273  	}
   274  
   275  	// Check if bucket exists.
   276  	if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
   277  		writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
   278  		return
   279  	}
   280  
   281  	if _, _, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket); err != nil {
   282  		writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
   283  		return
   284  	}
   285  
   286  	w.Header().Set(xhttp.ContentType, string(mimeJSON))
   287  
   288  	enc := json.NewEncoder(w)
   289  	stats := globalReplicationStats.getLatestReplicationStats(bucket)
   290  	bwRpt := globalNotificationSys.GetBandwidthReports(ctx, bucket)
   291  	bwMap := bwRpt.BucketStats
   292  	for arn, st := range stats.ReplicationStats.Stats {
   293  		for opts, bw := range bwMap {
   294  			if opts.ReplicationARN != "" && opts.ReplicationARN == arn {
   295  				st.BandWidthLimitInBytesPerSecond = bw.LimitInBytesPerSecond
   296  				st.CurrentBandwidthInBytesPerSecond = bw.CurrentBandwidthInBytesPerSecond
   297  				stats.ReplicationStats.Stats[arn] = st
   298  			}
   299  		}
   300  	}
   301  	stats.Uptime = UTCNow().Unix() - globalBootTime.Unix()
   302  
   303  	if err := enc.Encode(stats); err != nil {
   304  		writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
   305  		return
   306  	}
   307  }
   308  
   309  // ResetBucketReplicationStartHandler - starts a replication reset for all objects in a bucket which
   310  // qualify for replication and re-sync the object(s) to target, provided ExistingObjectReplication is
   311  // enabled for the qualifying rule. This API is a MinIO only extension provided for situations where
   312  // remote target is entirely lost,and previously replicated objects need to be re-synced. If resync is
   313  // already in progress it returns an error
   314  func (api objectAPIHandlers) ResetBucketReplicationStartHandler(w http.ResponseWriter, r *http.Request) {
   315  	ctx := newContext(r, w, "ResetBucketReplicationStart")
   316  	defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
   317  
   318  	vars := mux.Vars(r)
   319  	bucket := vars["bucket"]
   320  	durationStr := r.URL.Query().Get("older-than")
   321  	arn := r.URL.Query().Get("arn")
   322  	resetID := r.URL.Query().Get("reset-id")
   323  	if resetID == "" {
   324  		resetID = mustGetUUID()
   325  	}
   326  	var (
   327  		days time.Duration
   328  		err  error
   329  	)
   330  	if durationStr != "" {
   331  		days, err = time.ParseDuration(durationStr)
   332  		if err != nil {
   333  			writeErrorResponse(ctx, w, toAPIError(ctx, InvalidArgument{
   334  				Bucket: bucket,
   335  				Err:    fmt.Errorf("invalid query parameter older-than %s for %s : %w", durationStr, bucket, err),
   336  			}), r.URL)
   337  			return
   338  		}
   339  	}
   340  	resetBeforeDate := UTCNow().AddDate(0, 0, -1*int(days/24))
   341  
   342  	objectAPI := api.ObjectAPI()
   343  	if objectAPI == nil {
   344  		writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
   345  		return
   346  	}
   347  
   348  	if s3Error := checkRequestAuthType(ctx, r, policy.ResetBucketReplicationStateAction, bucket, ""); s3Error != ErrNone {
   349  		writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
   350  		return
   351  	}
   352  
   353  	// Check if bucket exists.
   354  	if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
   355  		writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
   356  		return
   357  	}
   358  
   359  	config, _, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket)
   360  	if err != nil {
   361  		writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
   362  		return
   363  	}
   364  	hasARN, hasExistingObjEnabled := config.HasExistingObjectReplication(arn)
   365  	if !hasARN {
   366  		writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrRemoteTargetNotFoundError), r.URL)
   367  		return
   368  	}
   369  
   370  	if !hasExistingObjEnabled {
   371  		writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationNoExistingObjects), r.URL)
   372  		return
   373  	}
   374  
   375  	tgtArns := config.FilterTargetArns(
   376  		replication.ObjectOpts{
   377  			OpType:    replication.ResyncReplicationType,
   378  			TargetArn: arn,
   379  		})
   380  
   381  	if len(tgtArns) == 0 {
   382  		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrBadRequest, InvalidArgument{
   383  			Bucket: bucket,
   384  			Err:    fmt.Errorf("Remote target ARN %s missing or ineligible for replication resync", arn),
   385  		}), r.URL)
   386  		return
   387  	}
   388  
   389  	if len(tgtArns) > 1 && arn == "" {
   390  		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrBadRequest, InvalidArgument{
   391  			Bucket: bucket,
   392  			Err:    fmt.Errorf("ARN should be specified for replication reset"),
   393  		}), r.URL)
   394  		return
   395  	}
   396  	var rinfo ResyncTargetsInfo
   397  	target := globalBucketTargetSys.GetRemoteBucketTargetByArn(ctx, bucket, tgtArns[0])
   398  	target.ResetBeforeDate = UTCNow().AddDate(0, 0, -1*int(days/24))
   399  	target.ResetID = resetID
   400  	rinfo.Targets = append(rinfo.Targets, ResyncTarget{Arn: tgtArns[0], ResetID: target.ResetID})
   401  	if err = globalBucketTargetSys.SetTarget(ctx, bucket, &target, true); err != nil {
   402  		switch err.(type) {
   403  		case RemoteTargetConnectionErr:
   404  			writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrReplicationRemoteConnectionError, err), r.URL)
   405  		default:
   406  			writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
   407  		}
   408  		return
   409  	}
   410  	targets, err := globalBucketTargetSys.ListBucketTargets(ctx, bucket)
   411  	if err != nil {
   412  		writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
   413  		return
   414  	}
   415  	tgtBytes, err := json.Marshal(&targets)
   416  	if err != nil {
   417  		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
   418  		return
   419  	}
   420  	if _, err = globalBucketMetadataSys.Update(ctx, bucket, bucketTargetsFile, tgtBytes); err != nil {
   421  		writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
   422  		return
   423  	}
   424  
   425  	if err := globalReplicationPool.resyncer.start(ctx, objectAPI, resyncOpts{
   426  		bucket:       bucket,
   427  		arn:          arn,
   428  		resyncID:     resetID,
   429  		resyncBefore: resetBeforeDate,
   430  	}); err != nil {
   431  		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrBadRequest, InvalidArgument{
   432  			Bucket: bucket,
   433  			Err:    err,
   434  		}), r.URL)
   435  		return
   436  	}
   437  
   438  	data, err := json.Marshal(rinfo)
   439  	if err != nil {
   440  		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
   441  		return
   442  	}
   443  	// Write success response.
   444  	writeSuccessResponseJSON(w, data)
   445  }
   446  
   447  // ResetBucketReplicationStatusHandler - returns the status of replication reset.
   448  // This API is a MinIO only extension
   449  func (api objectAPIHandlers) ResetBucketReplicationStatusHandler(w http.ResponseWriter, r *http.Request) {
   450  	ctx := newContext(r, w, "ResetBucketReplicationStatus")
   451  	defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
   452  
   453  	vars := mux.Vars(r)
   454  	bucket := vars["bucket"]
   455  	arn := r.URL.Query().Get("arn")
   456  	var err error
   457  
   458  	objectAPI := api.ObjectAPI()
   459  	if objectAPI == nil {
   460  		writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
   461  		return
   462  	}
   463  
   464  	if s3Error := checkRequestAuthType(ctx, r, policy.ResetBucketReplicationStateAction, bucket, ""); s3Error != ErrNone {
   465  		writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
   466  		return
   467  	}
   468  
   469  	// Check if bucket exists.
   470  	if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
   471  		writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
   472  		return
   473  	}
   474  
   475  	if _, _, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket); err != nil {
   476  		writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
   477  		return
   478  	}
   479  	brs, err := loadBucketResyncMetadata(ctx, bucket, objectAPI)
   480  	if err != nil {
   481  		writeErrorResponse(ctx, w, errorCodes.ToAPIErrWithErr(ErrBadRequest, InvalidArgument{
   482  			Bucket: bucket,
   483  			Err:    fmt.Errorf("replication resync status not available for %s (%s)", arn, err.Error()),
   484  		}), r.URL)
   485  		return
   486  	}
   487  
   488  	var rinfo ResyncTargetsInfo
   489  	for tarn, st := range brs.TargetsMap {
   490  		if arn != "" && tarn != arn {
   491  			continue
   492  		}
   493  		rinfo.Targets = append(rinfo.Targets, ResyncTarget{
   494  			Arn:             tarn,
   495  			ResetID:         st.ResyncID,
   496  			StartTime:       st.StartTime,
   497  			EndTime:         st.LastUpdate,
   498  			ResyncStatus:    st.ResyncStatus.String(),
   499  			ReplicatedSize:  st.ReplicatedSize,
   500  			ReplicatedCount: st.ReplicatedCount,
   501  			FailedSize:      st.FailedSize,
   502  			FailedCount:     st.FailedCount,
   503  			Bucket:          st.Bucket,
   504  			Object:          st.Object,
   505  		})
   506  	}
   507  	data, err := json.Marshal(rinfo)
   508  	if err != nil {
   509  		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
   510  		return
   511  	}
   512  
   513  	// Write success response.
   514  	writeSuccessResponseJSON(w, data)
   515  }
   516  
   517  // ValidateBucketReplicationCredsHandler - validate replication credentials for a bucket.
   518  // ----------
   519  func (api objectAPIHandlers) ValidateBucketReplicationCredsHandler(w http.ResponseWriter, r *http.Request) {
   520  	ctx := newContext(r, w, "ValidateBucketReplicationCreds")
   521  	defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
   522  
   523  	vars := mux.Vars(r)
   524  	bucket := vars["bucket"]
   525  	objectAPI := api.ObjectAPI()
   526  	if objectAPI == nil {
   527  		writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
   528  		return
   529  	}
   530  	if s3Error := checkRequestAuthType(ctx, r, policy.GetReplicationConfigurationAction, bucket, ""); s3Error != ErrNone {
   531  		writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
   532  		return
   533  	}
   534  	// Check if bucket exists.
   535  	if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
   536  		writeErrorResponse(ctx, w, errorCodes.ToAPIErrWithErr(ErrReplicationValidationError, err), r.URL)
   537  		return
   538  	}
   539  
   540  	if versioned := globalBucketVersioningSys.Enabled(bucket); !versioned {
   541  		writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationNeedsVersioningError), r.URL)
   542  		return
   543  	}
   544  	replicationConfig, _, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket)
   545  	if err != nil {
   546  		writeErrorResponse(ctx, w, errorCodes.ToAPIErrWithErr(ErrReplicationConfigurationNotFoundError, err), r.URL)
   547  		return
   548  	}
   549  
   550  	lockEnabled := false
   551  	lcfg, _, err := globalBucketMetadataSys.GetObjectLockConfig(bucket)
   552  	if err != nil {
   553  		if !errors.Is(err, BucketObjectLockConfigNotFound{Bucket: bucket}) {
   554  			writeErrorResponse(ctx, w, errorCodes.ToAPIErrWithErr(ErrReplicationValidationError, err), r.URL)
   555  			return
   556  		}
   557  	}
   558  	if lcfg != nil {
   559  		lockEnabled = lcfg.Enabled()
   560  	}
   561  
   562  	sameTarget, apiErr := validateReplicationDestination(ctx, bucket, replicationConfig, true)
   563  	if apiErr != noError {
   564  		writeErrorResponse(ctx, w, apiErr, r.URL)
   565  		return
   566  	}
   567  
   568  	// Validate the bucket replication config
   569  	if err = replicationConfig.Validate(bucket, sameTarget); err != nil {
   570  		writeErrorResponse(ctx, w, errorCodes.ToAPIErrWithErr(ErrReplicationValidationError, err), r.URL)
   571  		return
   572  	}
   573  	buf := bytes.Repeat([]byte("a"), 8)
   574  	for _, rule := range replicationConfig.Rules {
   575  		if rule.Status == replication.Disabled {
   576  			continue
   577  		}
   578  		clnt := globalBucketTargetSys.GetRemoteTargetClient(bucket, rule.Destination.Bucket)
   579  		if clnt == nil {
   580  			writeErrorResponse(ctx, w, errorCodes.ToAPIErrWithErr(ErrRemoteTargetNotFoundError, fmt.Errorf("replication config with rule ID %s has a stale target", rule.ID)), r.URL)
   581  			return
   582  		}
   583  		if lockEnabled {
   584  			lock, _, _, _, err := clnt.GetObjectLockConfig(ctx, clnt.Bucket)
   585  			if err != nil {
   586  				writeErrorResponse(ctx, w, errorCodes.ToAPIErrWithErr(ErrReplicationValidationError, err), r.URL)
   587  				return
   588  			}
   589  			if lock != objectlock.Enabled {
   590  				writeErrorResponse(ctx, w, errorCodes.ToAPIErrWithErr(ErrReplicationDestinationMissingLock, fmt.Errorf("target bucket %s is not object lock enabled", clnt.Bucket)), r.URL)
   591  				return
   592  			}
   593  		}
   594  		vcfg, err := clnt.GetBucketVersioning(ctx, clnt.Bucket)
   595  		if err != nil {
   596  			writeErrorResponse(ctx, w, errorCodes.ToAPIErrWithErr(ErrReplicationValidationError, err), r.URL)
   597  			return
   598  		}
   599  		if !vcfg.Enabled() {
   600  			writeErrorResponse(ctx, w, errorCodes.ToAPIErrWithErr(ErrRemoteTargetNotVersionedError, fmt.Errorf("target bucket %s is not versioned", clnt.Bucket)), r.URL)
   601  			return
   602  		}
   603  		if sameTarget && bucket == clnt.Bucket {
   604  			writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrBucketRemoteIdenticalToSource), r.URL)
   605  			return
   606  		}
   607  
   608  		reader := bytes.NewReader(buf)
   609  		// fake a PutObject and RemoveObject call to validate permissions
   610  		c := &minio.Core{Client: clnt.Client}
   611  		putOpts := minio.PutObjectOptions{
   612  			Internal: minio.AdvancedPutOptions{
   613  				SourceVersionID:          mustGetUUID(),
   614  				ReplicationStatus:        minio.ReplicationStatusReplica,
   615  				SourceMTime:              time.Now(),
   616  				ReplicationRequest:       true, // always set this to distinguish between `mc mirror` replication and serverside
   617  				ReplicationValidityCheck: true, // set this to validate the replication config
   618  			},
   619  		}
   620  		obj := path.Join(minioReservedBucket, globalLocalNodeNameHex, "deleteme")
   621  		ui, err := c.PutObject(ctx, clnt.Bucket, obj, reader, int64(len(buf)), "", "", putOpts)
   622  		if err != nil && !isReplicationPermissionCheck(ErrorRespToObjectError(err, bucket, obj)) {
   623  			writeErrorResponse(ctx, w, errorCodes.ToAPIErrWithErr(ErrReplicationValidationError, fmt.Errorf("s3:ReplicateObject permissions missing for replication user: %w", err)), r.URL)
   624  			return
   625  		}
   626  
   627  		err = c.RemoveObject(ctx, clnt.Bucket, obj, minio.RemoveObjectOptions{
   628  			VersionID: ui.VersionID,
   629  			Internal: minio.AdvancedRemoveOptions{
   630  				ReplicationDeleteMarker:  true,
   631  				ReplicationMTime:         time.Now(),
   632  				ReplicationStatus:        minio.ReplicationStatusReplica,
   633  				ReplicationRequest:       true, // always set this to distinguish between `mc mirror` replication and serverside
   634  				ReplicationValidityCheck: true, // set this to validate the replication config
   635  			},
   636  		})
   637  		if err != nil && !isReplicationPermissionCheck(ErrorRespToObjectError(err, bucket, obj)) {
   638  			writeErrorResponse(ctx, w, errorCodes.ToAPIErrWithErr(ErrReplicationValidationError, fmt.Errorf("s3:ReplicateDelete permissions missing for replication user: %w", err)), r.URL)
   639  			return
   640  		}
   641  		// fake a versioned delete - to ensure deny policies are not in place
   642  		err = c.RemoveObject(ctx, clnt.Bucket, obj, minio.RemoveObjectOptions{
   643  			VersionID: ui.VersionID,
   644  			Internal: minio.AdvancedRemoveOptions{
   645  				ReplicationDeleteMarker:  false,
   646  				ReplicationMTime:         time.Now(),
   647  				ReplicationStatus:        minio.ReplicationStatusReplica,
   648  				ReplicationRequest:       true, // always set this to distinguish between `mc mirror` replication and serverside
   649  				ReplicationValidityCheck: true, // set this to validate the replication config
   650  			},
   651  		})
   652  		if err != nil && !isReplicationPermissionCheck(ErrorRespToObjectError(err, bucket, obj)) {
   653  			writeErrorResponse(ctx, w, errorCodes.ToAPIErrWithErr(ErrReplicationValidationError, fmt.Errorf("s3:ReplicateDelete/s3:DeleteObject permissions missing for replication user: %w", err)), r.URL)
   654  			return
   655  		}
   656  	}
   657  
   658  	// Write success response.
   659  	writeSuccessResponseHeadersOnly(w)
   660  }