github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/bucket-metadata.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 "context" 23 "crypto/rand" 24 "encoding/binary" 25 "encoding/json" 26 "encoding/xml" 27 "errors" 28 "fmt" 29 "path" 30 "time" 31 32 "github.com/minio/madmin-go/v3" 33 "github.com/minio/minio-go/v7/pkg/tags" 34 bucketsse "github.com/minio/minio/internal/bucket/encryption" 35 "github.com/minio/minio/internal/bucket/lifecycle" 36 objectlock "github.com/minio/minio/internal/bucket/object/lock" 37 "github.com/minio/minio/internal/bucket/replication" 38 "github.com/minio/minio/internal/bucket/versioning" 39 "github.com/minio/minio/internal/crypto" 40 "github.com/minio/minio/internal/event" 41 "github.com/minio/minio/internal/fips" 42 "github.com/minio/minio/internal/kms" 43 "github.com/minio/minio/internal/logger" 44 "github.com/minio/pkg/v2/policy" 45 "github.com/minio/sio" 46 ) 47 48 const ( 49 legacyBucketObjectLockEnabledConfigFile = "object-lock-enabled.json" 50 legacyBucketObjectLockEnabledConfig = `{"x-amz-bucket-object-lock-enabled":true}` 51 52 bucketMetadataFile = ".metadata.bin" 53 bucketMetadataFormat = 1 54 bucketMetadataVersion = 1 55 ) 56 57 var ( 58 enabledBucketObjectLockConfig = []byte(`<ObjectLockConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><ObjectLockEnabled>Enabled</ObjectLockEnabled></ObjectLockConfiguration>`) 59 enabledBucketVersioningConfig = []byte(`<VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Status>Enabled</Status></VersioningConfiguration>`) 60 ) 61 62 //go:generate msgp -file $GOFILE 63 64 // BucketMetadata contains bucket metadata. 65 // When adding/removing fields, regenerate the marshal code using the go generate above. 66 // Only changing meaning of fields requires a version bump. 67 // bucketMetadataFormat refers to the format. 68 // bucketMetadataVersion can be used to track a rolling upgrade of a field. 69 type BucketMetadata struct { 70 Name string 71 Created time.Time 72 LockEnabled bool // legacy not used anymore. 73 PolicyConfigJSON []byte 74 NotificationConfigXML []byte 75 LifecycleConfigXML []byte 76 ObjectLockConfigXML []byte 77 VersioningConfigXML []byte 78 EncryptionConfigXML []byte 79 TaggingConfigXML []byte 80 QuotaConfigJSON []byte 81 ReplicationConfigXML []byte 82 BucketTargetsConfigJSON []byte 83 BucketTargetsConfigMetaJSON []byte 84 PolicyConfigUpdatedAt time.Time 85 ObjectLockConfigUpdatedAt time.Time 86 EncryptionConfigUpdatedAt time.Time 87 TaggingConfigUpdatedAt time.Time 88 QuotaConfigUpdatedAt time.Time 89 ReplicationConfigUpdatedAt time.Time 90 VersioningConfigUpdatedAt time.Time 91 LifecycleConfigUpdatedAt time.Time 92 93 // Unexported fields. Must be updated atomically. 94 policyConfig *policy.BucketPolicy 95 notificationConfig *event.Config 96 lifecycleConfig *lifecycle.Lifecycle 97 objectLockConfig *objectlock.Config 98 versioningConfig *versioning.Versioning 99 sseConfig *bucketsse.BucketSSEConfig 100 taggingConfig *tags.Tags 101 quotaConfig *madmin.BucketQuota 102 replicationConfig *replication.Config 103 bucketTargetConfig *madmin.BucketTargets 104 bucketTargetConfigMeta map[string]string 105 } 106 107 // newBucketMetadata creates BucketMetadata with the supplied name and Created to Now. 108 func newBucketMetadata(name string) BucketMetadata { 109 return BucketMetadata{ 110 Name: name, 111 notificationConfig: &event.Config{ 112 XMLNS: "http://s3.amazonaws.com/doc/2006-03-01/", 113 }, 114 quotaConfig: &madmin.BucketQuota{}, 115 versioningConfig: &versioning.Versioning{ 116 XMLNS: "http://s3.amazonaws.com/doc/2006-03-01/", 117 }, 118 bucketTargetConfig: &madmin.BucketTargets{}, 119 bucketTargetConfigMeta: make(map[string]string), 120 } 121 } 122 123 // Versioning returns true if versioning is enabled 124 func (b BucketMetadata) Versioning() bool { 125 return b.LockEnabled || (b.versioningConfig != nil && b.versioningConfig.Enabled()) || (b.objectLockConfig != nil && b.objectLockConfig.Enabled()) 126 } 127 128 // ObjectLocking returns true if object locking is enabled 129 func (b BucketMetadata) ObjectLocking() bool { 130 return b.LockEnabled || (b.objectLockConfig != nil && b.objectLockConfig.Enabled()) 131 } 132 133 // SetCreatedAt preserves the CreatedAt time for bucket across sites in site replication. It defaults to 134 // creation time of bucket on this cluster in all other cases. 135 func (b *BucketMetadata) SetCreatedAt(createdAt time.Time) { 136 if b.Created.IsZero() { 137 b.Created = UTCNow() 138 } 139 if !createdAt.IsZero() { 140 b.Created = createdAt.UTC() 141 } 142 } 143 144 // Load - loads the metadata of bucket by name from ObjectLayer api. 145 // If an error is returned the returned metadata will be default initialized. 146 func readBucketMetadata(ctx context.Context, api ObjectLayer, name string) (BucketMetadata, error) { 147 if name == "" { 148 logger.LogIf(ctx, errors.New("bucket name cannot be empty")) 149 return BucketMetadata{}, errInvalidArgument 150 } 151 b := newBucketMetadata(name) 152 configFile := path.Join(bucketMetaPrefix, name, bucketMetadataFile) 153 data, err := readConfig(ctx, api, configFile) 154 if err != nil { 155 return b, err 156 } 157 if len(data) <= 4 { 158 return b, fmt.Errorf("loadBucketMetadata: no data") 159 } 160 // Read header 161 switch binary.LittleEndian.Uint16(data[0:2]) { 162 case bucketMetadataFormat: 163 default: 164 return b, fmt.Errorf("loadBucketMetadata: unknown format: %d", binary.LittleEndian.Uint16(data[0:2])) 165 } 166 switch binary.LittleEndian.Uint16(data[2:4]) { 167 case bucketMetadataVersion: 168 default: 169 return b, fmt.Errorf("loadBucketMetadata: unknown version: %d", binary.LittleEndian.Uint16(data[2:4])) 170 } 171 _, err = b.UnmarshalMsg(data[4:]) 172 return b, err 173 } 174 175 func loadBucketMetadataParse(ctx context.Context, objectAPI ObjectLayer, bucket string, parse bool) (BucketMetadata, error) { 176 b, err := readBucketMetadata(ctx, objectAPI, bucket) 177 b.Name = bucket // in-case parsing failed for some reason, make sure bucket name is not empty. 178 if err != nil && !errors.Is(err, errConfigNotFound) { 179 return b, err 180 } 181 if err == nil { 182 b.defaultTimestamps() 183 } 184 185 // If bucket metadata is missing look for legacy files, 186 // since we only ever had b.Created as non-zero when 187 // migration was complete in 2020-May release. So this 188 // a check to avoid migrating for buckets that already 189 // have this field set. 190 if b.Created.IsZero() { 191 configs, err := b.getAllLegacyConfigs(ctx, objectAPI) 192 if err != nil { 193 return b, err 194 } 195 196 if len(configs) > 0 { 197 // Old bucket without bucket metadata. Hence we migrate existing settings. 198 if err = b.convertLegacyConfigs(ctx, objectAPI, configs); err != nil { 199 return b, err 200 } 201 } 202 } 203 204 if parse { 205 // nothing to update, parse and proceed. 206 if err = b.parseAllConfigs(ctx, objectAPI); err != nil { 207 return b, err 208 } 209 } 210 211 // migrate unencrypted remote targets 212 if err = b.migrateTargetConfig(ctx, objectAPI); err != nil { 213 return b, err 214 } 215 216 return b, nil 217 } 218 219 // loadBucketMetadata loads and migrates to bucket metadata. 220 func loadBucketMetadata(ctx context.Context, objectAPI ObjectLayer, bucket string) (BucketMetadata, error) { 221 return loadBucketMetadataParse(ctx, objectAPI, bucket, true) 222 } 223 224 // parseAllConfigs will parse all configs and populate the private fields. 225 // The first error encountered is returned. 226 func (b *BucketMetadata) parseAllConfigs(ctx context.Context, objectAPI ObjectLayer) (err error) { 227 if len(b.PolicyConfigJSON) != 0 { 228 b.policyConfig, err = policy.ParseBucketPolicyConfig(bytes.NewReader(b.PolicyConfigJSON), b.Name) 229 if err != nil { 230 return err 231 } 232 } else { 233 b.policyConfig = nil 234 } 235 236 if len(b.NotificationConfigXML) != 0 { 237 if err = xml.Unmarshal(b.NotificationConfigXML, b.notificationConfig); err != nil { 238 return err 239 } 240 } 241 242 if len(b.LifecycleConfigXML) != 0 { 243 b.lifecycleConfig, err = lifecycle.ParseLifecycleConfig(bytes.NewReader(b.LifecycleConfigXML)) 244 if err != nil { 245 return err 246 } 247 } else { 248 b.lifecycleConfig = nil 249 } 250 251 if len(b.EncryptionConfigXML) != 0 { 252 b.sseConfig, err = bucketsse.ParseBucketSSEConfig(bytes.NewReader(b.EncryptionConfigXML)) 253 if err != nil { 254 return err 255 } 256 } else { 257 b.sseConfig = nil 258 } 259 260 if len(b.TaggingConfigXML) != 0 { 261 b.taggingConfig, err = tags.ParseBucketXML(bytes.NewReader(b.TaggingConfigXML)) 262 if err != nil { 263 return err 264 } 265 } else { 266 b.taggingConfig = nil 267 } 268 269 if bytes.Equal(b.ObjectLockConfigXML, enabledBucketObjectLockConfig) { 270 b.VersioningConfigXML = enabledBucketVersioningConfig 271 } 272 273 if len(b.ObjectLockConfigXML) != 0 { 274 b.objectLockConfig, err = objectlock.ParseObjectLockConfig(bytes.NewReader(b.ObjectLockConfigXML)) 275 if err != nil { 276 return err 277 } 278 } else { 279 b.objectLockConfig = nil 280 } 281 282 if len(b.VersioningConfigXML) != 0 { 283 b.versioningConfig, err = versioning.ParseConfig(bytes.NewReader(b.VersioningConfigXML)) 284 if err != nil { 285 return err 286 } 287 } 288 289 if len(b.QuotaConfigJSON) != 0 { 290 b.quotaConfig, err = parseBucketQuota(b.Name, b.QuotaConfigJSON) 291 if err != nil { 292 return err 293 } 294 } 295 296 if len(b.ReplicationConfigXML) != 0 { 297 b.replicationConfig, err = replication.ParseConfig(bytes.NewReader(b.ReplicationConfigXML)) 298 if err != nil { 299 return err 300 } 301 } else { 302 b.replicationConfig = nil 303 } 304 305 if len(b.BucketTargetsConfigJSON) != 0 { 306 b.bucketTargetConfig, err = parseBucketTargetConfig(b.Name, b.BucketTargetsConfigJSON, b.BucketTargetsConfigMetaJSON) 307 if err != nil { 308 return err 309 } 310 } else { 311 b.bucketTargetConfig = &madmin.BucketTargets{} 312 } 313 return nil 314 } 315 316 func (b *BucketMetadata) getAllLegacyConfigs(ctx context.Context, objectAPI ObjectLayer) (map[string][]byte, error) { 317 legacyConfigs := []string{ 318 legacyBucketObjectLockEnabledConfigFile, 319 bucketPolicyConfig, 320 bucketNotificationConfig, 321 bucketLifecycleConfig, 322 bucketQuotaConfigFile, 323 bucketSSEConfig, 324 bucketTaggingConfig, 325 bucketReplicationConfig, 326 bucketTargetsFile, 327 objectLockConfig, 328 } 329 330 configs := make(map[string][]byte, len(legacyConfigs)) 331 332 // Handle migration from lockEnabled to newer format. 333 if b.LockEnabled { 334 configs[objectLockConfig] = enabledBucketObjectLockConfig 335 b.LockEnabled = false // legacy value unset it 336 // we are only interested in b.ObjectLockConfigXML or objectLockConfig value 337 } 338 339 for _, legacyFile := range legacyConfigs { 340 configFile := path.Join(bucketMetaPrefix, b.Name, legacyFile) 341 342 configData, info, err := readConfigWithMetadata(ctx, objectAPI, configFile, ObjectOptions{}) 343 if err != nil { 344 if _, ok := err.(ObjectExistsAsDirectory); ok { 345 // in FS mode it possible that we have actual 346 // files in this folder with `.minio.sys/buckets/bucket/configFile` 347 continue 348 } 349 if errors.Is(err, errConfigNotFound) { 350 // legacy file config not found, proceed to look for new metadata. 351 continue 352 } 353 354 return nil, err 355 } 356 configs[legacyFile] = configData 357 b.Created = info.ModTime 358 } 359 360 return configs, nil 361 } 362 363 func (b *BucketMetadata) convertLegacyConfigs(ctx context.Context, objectAPI ObjectLayer, configs map[string][]byte) error { 364 for legacyFile, configData := range configs { 365 switch legacyFile { 366 case legacyBucketObjectLockEnabledConfigFile: 367 if string(configData) == legacyBucketObjectLockEnabledConfig { 368 b.ObjectLockConfigXML = enabledBucketObjectLockConfig 369 b.VersioningConfigXML = enabledBucketVersioningConfig 370 b.LockEnabled = false // legacy value unset it 371 // we are only interested in b.ObjectLockConfigXML 372 } 373 case bucketPolicyConfig: 374 b.PolicyConfigJSON = configData 375 case bucketNotificationConfig: 376 b.NotificationConfigXML = configData 377 case bucketLifecycleConfig: 378 b.LifecycleConfigXML = configData 379 case bucketSSEConfig: 380 b.EncryptionConfigXML = configData 381 case bucketTaggingConfig: 382 b.TaggingConfigXML = configData 383 case objectLockConfig: 384 b.ObjectLockConfigXML = configData 385 b.VersioningConfigXML = enabledBucketVersioningConfig 386 case bucketQuotaConfigFile: 387 b.QuotaConfigJSON = configData 388 case bucketReplicationConfig: 389 b.ReplicationConfigXML = configData 390 case bucketTargetsFile: 391 b.BucketTargetsConfigJSON = configData 392 } 393 } 394 b.defaultTimestamps() 395 396 if err := b.Save(ctx, objectAPI); err != nil { 397 return err 398 } 399 400 for legacyFile := range configs { 401 configFile := path.Join(bucketMetaPrefix, b.Name, legacyFile) 402 if err := deleteConfig(ctx, objectAPI, configFile); err != nil && !errors.Is(err, errConfigNotFound) { 403 logger.LogIf(ctx, err) 404 } 405 } 406 407 return nil 408 } 409 410 // default timestamps to metadata Created timestamp if unset. 411 func (b *BucketMetadata) defaultTimestamps() { 412 if b.PolicyConfigUpdatedAt.IsZero() { 413 b.PolicyConfigUpdatedAt = b.Created 414 } 415 416 if b.EncryptionConfigUpdatedAt.IsZero() { 417 b.EncryptionConfigUpdatedAt = b.Created 418 } 419 420 if b.TaggingConfigUpdatedAt.IsZero() { 421 b.TaggingConfigUpdatedAt = b.Created 422 } 423 424 if b.ObjectLockConfigUpdatedAt.IsZero() { 425 b.ObjectLockConfigUpdatedAt = b.Created 426 } 427 428 if b.QuotaConfigUpdatedAt.IsZero() { 429 b.QuotaConfigUpdatedAt = b.Created 430 } 431 432 if b.ReplicationConfigUpdatedAt.IsZero() { 433 b.ReplicationConfigUpdatedAt = b.Created 434 } 435 436 if b.VersioningConfigUpdatedAt.IsZero() { 437 b.VersioningConfigUpdatedAt = b.Created 438 } 439 440 if b.LifecycleConfigUpdatedAt.IsZero() { 441 b.LifecycleConfigUpdatedAt = b.Created 442 } 443 } 444 445 // Save config to supplied ObjectLayer api. 446 func (b *BucketMetadata) Save(ctx context.Context, api ObjectLayer) error { 447 if err := b.parseAllConfigs(ctx, api); err != nil { 448 return err 449 } 450 451 data := make([]byte, 4, b.Msgsize()+4) 452 453 // Initialize the header. 454 binary.LittleEndian.PutUint16(data[0:2], bucketMetadataFormat) 455 binary.LittleEndian.PutUint16(data[2:4], bucketMetadataVersion) 456 457 // Marshal the bucket metadata 458 data, err := b.MarshalMsg(data) 459 if err != nil { 460 return err 461 } 462 463 configFile := path.Join(bucketMetaPrefix, b.Name, bucketMetadataFile) 464 return saveConfig(ctx, api, configFile, data) 465 } 466 467 // migrate config for remote targets by encrypting data if currently unencrypted and kms is configured. 468 func (b *BucketMetadata) migrateTargetConfig(ctx context.Context, objectAPI ObjectLayer) error { 469 var err error 470 // early return if no targets or already encrypted 471 if len(b.BucketTargetsConfigJSON) == 0 || GlobalKMS == nil || len(b.BucketTargetsConfigMetaJSON) != 0 { 472 return nil 473 } 474 475 encBytes, metaBytes, err := encryptBucketMetadata(ctx, b.Name, b.BucketTargetsConfigJSON, kms.Context{b.Name: b.Name, bucketTargetsFile: bucketTargetsFile}) 476 if err != nil { 477 return err 478 } 479 480 b.BucketTargetsConfigJSON = encBytes 481 b.BucketTargetsConfigMetaJSON = metaBytes 482 return b.Save(ctx, objectAPI) 483 } 484 485 // encrypt bucket metadata if kms is configured. 486 func encryptBucketMetadata(ctx context.Context, bucket string, input []byte, kmsContext kms.Context) (output, metabytes []byte, err error) { 487 if GlobalKMS == nil { 488 output = input 489 return 490 } 491 492 metadata := make(map[string]string) 493 key, err := GlobalKMS.GenerateKey(ctx, "", kmsContext) 494 if err != nil { 495 return 496 } 497 498 outbuf := bytes.NewBuffer(nil) 499 objectKey := crypto.GenerateKey(key.Plaintext, rand.Reader) 500 sealedKey := objectKey.Seal(key.Plaintext, crypto.GenerateIV(rand.Reader), crypto.S3.String(), bucket, "") 501 crypto.S3.CreateMetadata(metadata, key.KeyID, key.Ciphertext, sealedKey) 502 _, err = sio.Encrypt(outbuf, bytes.NewBuffer(input), sio.Config{Key: objectKey[:], MinVersion: sio.Version20, CipherSuites: fips.DARECiphers()}) 503 if err != nil { 504 return output, metabytes, err 505 } 506 metabytes, err = json.Marshal(metadata) 507 if err != nil { 508 return 509 } 510 return outbuf.Bytes(), metabytes, nil 511 } 512 513 // decrypt bucket metadata if kms is configured. 514 func decryptBucketMetadata(input []byte, bucket string, meta map[string]string, kmsContext kms.Context) ([]byte, error) { 515 if GlobalKMS == nil { 516 return nil, errKMSNotConfigured 517 } 518 keyID, kmsKey, sealedKey, err := crypto.S3.ParseMetadata(meta) 519 if err != nil { 520 return nil, err 521 } 522 extKey, err := GlobalKMS.DecryptKey(keyID, kmsKey, kmsContext) 523 if err != nil { 524 return nil, err 525 } 526 var objectKey crypto.ObjectKey 527 if err = objectKey.Unseal(extKey, sealedKey, crypto.S3.String(), bucket, ""); err != nil { 528 return nil, err 529 } 530 531 outbuf := bytes.NewBuffer(nil) 532 _, err = sio.Decrypt(outbuf, bytes.NewBuffer(input), sio.Config{Key: objectKey[:], MinVersion: sio.Version20, CipherSuites: fips.DARECiphers()}) 533 return outbuf.Bytes(), err 534 }