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 }