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 }