storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/cmd/bucket-metadata.go (about) 1 /* 2 * MinIO Cloud Storage, (C) 2020 MinIO, Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package cmd 18 19 import ( 20 "bytes" 21 "context" 22 "crypto/rand" 23 "encoding/binary" 24 "encoding/json" 25 "encoding/xml" 26 "errors" 27 "fmt" 28 "path" 29 "time" 30 31 "github.com/minio/minio-go/v7/pkg/tags" 32 "github.com/minio/sio" 33 34 "storj.io/minio/cmd/crypto" 35 "storj.io/minio/cmd/logger" 36 bucketsse "storj.io/minio/pkg/bucket/encryption" 37 "storj.io/minio/pkg/bucket/lifecycle" 38 objectlock "storj.io/minio/pkg/bucket/object/lock" 39 "storj.io/minio/pkg/bucket/policy" 40 "storj.io/minio/pkg/bucket/replication" 41 "storj.io/minio/pkg/bucket/versioning" 42 "storj.io/minio/pkg/event" 43 "storj.io/minio/pkg/fips" 44 "storj.io/minio/pkg/kms" 45 "storj.io/minio/pkg/madmin" 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 85 // Unexported fields. Must be updated atomically. 86 policyConfig *policy.Policy 87 notificationConfig *event.Config 88 lifecycleConfig *lifecycle.Lifecycle 89 objectLockConfig *objectlock.Config 90 versioningConfig *versioning.Versioning 91 sseConfig *bucketsse.BucketSSEConfig 92 taggingConfig *tags.Tags 93 quotaConfig *madmin.BucketQuota 94 replicationConfig *replication.Config 95 bucketTargetConfig *madmin.BucketTargets 96 bucketTargetConfigMeta map[string]string 97 } 98 99 // newBucketMetadata creates BucketMetadata with the supplied name and Created to Now. 100 func newBucketMetadata(name string) BucketMetadata { 101 return BucketMetadata{ 102 Name: name, 103 Created: UTCNow(), 104 notificationConfig: &event.Config{ 105 XMLNS: "http://s3.amazonaws.com/doc/2006-03-01/", 106 }, 107 quotaConfig: &madmin.BucketQuota{}, 108 versioningConfig: &versioning.Versioning{ 109 XMLNS: "http://s3.amazonaws.com/doc/2006-03-01/", 110 }, 111 bucketTargetConfig: &madmin.BucketTargets{}, 112 bucketTargetConfigMeta: make(map[string]string), 113 } 114 } 115 116 // Load - loads the metadata of bucket by name from ObjectLayer api. 117 // If an error is returned the returned metadata will be default initialized. 118 func (b *BucketMetadata) Load(ctx context.Context, api ObjectLayer, name string) error { 119 if name == "" { 120 logger.LogIf(ctx, errors.New("bucket name cannot be empty")) 121 return errors.New("bucket name cannot be empty") 122 } 123 configFile := path.Join(bucketConfigPrefix, name, bucketMetadataFile) 124 data, err := readConfig(ctx, api, configFile) 125 if err != nil { 126 return err 127 } 128 if len(data) <= 4 { 129 return fmt.Errorf("loadBucketMetadata: no data") 130 } 131 // Read header 132 switch binary.LittleEndian.Uint16(data[0:2]) { 133 case bucketMetadataFormat: 134 default: 135 return fmt.Errorf("loadBucketMetadata: unknown format: %d", binary.LittleEndian.Uint16(data[0:2])) 136 } 137 switch binary.LittleEndian.Uint16(data[2:4]) { 138 case bucketMetadataVersion: 139 default: 140 return fmt.Errorf("loadBucketMetadata: unknown version: %d", binary.LittleEndian.Uint16(data[2:4])) 141 } 142 // OK, parse data. 143 _, err = b.UnmarshalMsg(data[4:]) 144 b.Name = name // in-case parsing failed for some reason, make sure bucket name is not empty. 145 return err 146 } 147 148 // loadBucketMetadata loads and migrates to bucket metadata. 149 func loadBucketMetadata(ctx context.Context, objectAPI ObjectLayer, bucket string) (BucketMetadata, error) { 150 b := newBucketMetadata(bucket) 151 err := b.Load(ctx, objectAPI, b.Name) 152 if err != nil && !errors.Is(err, errConfigNotFound) { 153 return b, err 154 } 155 156 // Old bucket without bucket metadata. Hence we migrate existing settings. 157 if err := b.convertLegacyConfigs(ctx, objectAPI); err != nil { 158 return b, err 159 } 160 // migrate unencrypted remote targets 161 return b, b.migrateTargetConfig(ctx, objectAPI) 162 } 163 164 // parseAllConfigs will parse all configs and populate the private fields. 165 // The first error encountered is returned. 166 func (b *BucketMetadata) parseAllConfigs(ctx context.Context, objectAPI ObjectLayer) (err error) { 167 if len(b.PolicyConfigJSON) != 0 { 168 b.policyConfig, err = policy.ParseConfig(bytes.NewReader(b.PolicyConfigJSON), b.Name) 169 if err != nil { 170 return err 171 } 172 } else { 173 b.policyConfig = nil 174 } 175 176 if len(b.NotificationConfigXML) != 0 { 177 if err = xml.Unmarshal(b.NotificationConfigXML, b.notificationConfig); err != nil { 178 return err 179 } 180 } 181 182 if len(b.LifecycleConfigXML) != 0 { 183 b.lifecycleConfig, err = lifecycle.ParseLifecycleConfig(bytes.NewReader(b.LifecycleConfigXML)) 184 if err != nil { 185 return err 186 } 187 } else { 188 b.lifecycleConfig = nil 189 } 190 191 if len(b.EncryptionConfigXML) != 0 { 192 b.sseConfig, err = bucketsse.ParseBucketSSEConfig(bytes.NewReader(b.EncryptionConfigXML)) 193 if err != nil { 194 return err 195 } 196 } else { 197 b.sseConfig = nil 198 } 199 200 if len(b.TaggingConfigXML) != 0 { 201 b.taggingConfig, err = tags.ParseBucketXML(bytes.NewReader(b.TaggingConfigXML)) 202 if err != nil { 203 return err 204 } 205 } else { 206 b.taggingConfig = nil 207 } 208 209 if bytes.Equal(b.ObjectLockConfigXML, enabledBucketObjectLockConfig) { 210 b.VersioningConfigXML = enabledBucketVersioningConfig 211 } 212 213 if len(b.ObjectLockConfigXML) != 0 { 214 b.objectLockConfig, err = objectlock.ParseObjectLockConfig(bytes.NewReader(b.ObjectLockConfigXML)) 215 if err != nil { 216 return err 217 } 218 } else { 219 b.objectLockConfig = nil 220 } 221 222 if len(b.VersioningConfigXML) != 0 { 223 b.versioningConfig, err = versioning.ParseConfig(bytes.NewReader(b.VersioningConfigXML)) 224 if err != nil { 225 return err 226 } 227 } 228 229 if len(b.QuotaConfigJSON) != 0 { 230 b.quotaConfig, err = parseBucketQuota(b.Name, b.QuotaConfigJSON) 231 if err != nil { 232 return err 233 } 234 } 235 236 if len(b.ReplicationConfigXML) != 0 { 237 b.replicationConfig, err = replication.ParseConfig(bytes.NewReader(b.ReplicationConfigXML)) 238 if err != nil { 239 return err 240 } 241 } else { 242 b.replicationConfig = nil 243 } 244 245 if len(b.BucketTargetsConfigJSON) != 0 { 246 b.bucketTargetConfig, err = parseBucketTargetConfig(b.Name, b.BucketTargetsConfigJSON, b.BucketTargetsConfigMetaJSON) 247 if err != nil { 248 return err 249 } 250 } else { 251 b.bucketTargetConfig = &madmin.BucketTargets{} 252 } 253 return nil 254 } 255 256 func (b *BucketMetadata) convertLegacyConfigs(ctx context.Context, objectAPI ObjectLayer) error { 257 legacyConfigs := []string{ 258 legacyBucketObjectLockEnabledConfigFile, 259 bucketPolicyConfig, 260 bucketNotificationConfig, 261 bucketLifecycleConfig, 262 bucketQuotaConfigFile, 263 bucketSSEConfig, 264 bucketTaggingConfig, 265 bucketReplicationConfig, 266 bucketTargetsFile, 267 objectLockConfig, 268 } 269 270 configs := make(map[string][]byte) 271 272 // Handle migration from lockEnabled to newer format. 273 if b.LockEnabled { 274 configs[objectLockConfig] = enabledBucketObjectLockConfig 275 b.LockEnabled = false // legacy value unset it 276 // we are only interested in b.ObjectLockConfigXML or objectLockConfig value 277 } 278 279 for _, legacyFile := range legacyConfigs { 280 configFile := path.Join(bucketConfigPrefix, b.Name, legacyFile) 281 282 configData, err := readConfig(ctx, objectAPI, configFile) 283 if err != nil { 284 switch err.(type) { 285 case ObjectExistsAsDirectory: 286 // in FS mode it possible that we have actual 287 // files in this folder with `.minio.sys/buckets/bucket/configFile` 288 continue 289 } 290 if errors.Is(err, errConfigNotFound) { 291 // legacy file config not found, proceed to look for new metadata. 292 continue 293 } 294 295 return err 296 } 297 configs[legacyFile] = configData 298 } 299 300 if len(configs) == 0 { 301 // nothing to update, return right away. 302 return b.parseAllConfigs(ctx, objectAPI) 303 } 304 305 for legacyFile, configData := range configs { 306 switch legacyFile { 307 case legacyBucketObjectLockEnabledConfigFile: 308 if string(configData) == legacyBucketObjectLockEnabledConfig { 309 b.ObjectLockConfigXML = enabledBucketObjectLockConfig 310 b.VersioningConfigXML = enabledBucketVersioningConfig 311 b.LockEnabled = false // legacy value unset it 312 // we are only interested in b.ObjectLockConfigXML 313 } 314 case bucketPolicyConfig: 315 b.PolicyConfigJSON = configData 316 case bucketNotificationConfig: 317 b.NotificationConfigXML = configData 318 case bucketLifecycleConfig: 319 b.LifecycleConfigXML = configData 320 case bucketSSEConfig: 321 b.EncryptionConfigXML = configData 322 case bucketTaggingConfig: 323 b.TaggingConfigXML = configData 324 case objectLockConfig: 325 b.ObjectLockConfigXML = configData 326 b.VersioningConfigXML = enabledBucketVersioningConfig 327 case bucketQuotaConfigFile: 328 b.QuotaConfigJSON = configData 329 case bucketReplicationConfig: 330 b.ReplicationConfigXML = configData 331 case bucketTargetsFile: 332 b.BucketTargetsConfigJSON = configData 333 } 334 } 335 336 if err := b.Save(ctx, objectAPI); err != nil { 337 return err 338 } 339 340 for legacyFile := range configs { 341 configFile := path.Join(bucketConfigPrefix, b.Name, legacyFile) 342 if err := deleteConfig(ctx, objectAPI, configFile); err != nil && !errors.Is(err, errConfigNotFound) { 343 logger.LogIf(ctx, err) 344 } 345 } 346 347 return nil 348 } 349 350 // Save config to supplied ObjectLayer api. 351 func (b *BucketMetadata) Save(ctx context.Context, api ObjectLayer) error { 352 if err := b.parseAllConfigs(ctx, api); err != nil { 353 return err 354 } 355 356 data := make([]byte, 4, b.Msgsize()+4) 357 358 // Initialize the header. 359 binary.LittleEndian.PutUint16(data[0:2], bucketMetadataFormat) 360 binary.LittleEndian.PutUint16(data[2:4], bucketMetadataVersion) 361 362 // Marshal the bucket metadata 363 data, err := b.MarshalMsg(data) 364 if err != nil { 365 return err 366 } 367 368 configFile := path.Join(bucketConfigPrefix, b.Name, bucketMetadataFile) 369 return saveConfig(ctx, api, configFile, data) 370 } 371 372 // deleteBucketMetadata deletes bucket metadata 373 // If config does not exist no error is returned. 374 func deleteBucketMetadata(ctx context.Context, obj objectDeleter, bucket string) error { 375 metadataFiles := []string{ 376 dataUsageCacheName, 377 bucketMetadataFile, 378 } 379 for _, metaFile := range metadataFiles { 380 configFile := path.Join(bucketConfigPrefix, bucket, metaFile) 381 if err := deleteConfig(ctx, obj, configFile); err != nil && err != errConfigNotFound { 382 return err 383 } 384 } 385 return nil 386 } 387 388 // migrate config for remote targets by encrypting data if currently unencrypted and kms is configured. 389 func (b *BucketMetadata) migrateTargetConfig(ctx context.Context, objectAPI ObjectLayer) error { 390 var err error 391 // early return if no targets or already encrypted 392 if len(b.BucketTargetsConfigJSON) == 0 || GlobalKMS == nil || len(b.BucketTargetsConfigMetaJSON) != 0 { 393 return nil 394 } 395 396 encBytes, metaBytes, err := encryptBucketMetadata(b.Name, b.BucketTargetsConfigJSON, kms.Context{b.Name: b.Name, bucketTargetsFile: bucketTargetsFile}) 397 if err != nil { 398 return err 399 } 400 401 b.BucketTargetsConfigJSON = encBytes 402 b.BucketTargetsConfigMetaJSON = metaBytes 403 return b.Save(ctx, objectAPI) 404 } 405 406 // encrypt bucket metadata if kms is configured. 407 func encryptBucketMetadata(bucket string, input []byte, kmsContext kms.Context) (output, metabytes []byte, err error) { 408 if GlobalKMS == nil { 409 output = input 410 return 411 } 412 413 metadata := make(map[string]string) 414 key, err := GlobalKMS.GenerateKey("", kmsContext) 415 if err != nil { 416 return 417 } 418 419 outbuf := bytes.NewBuffer(nil) 420 objectKey := crypto.GenerateKey(key.Plaintext, rand.Reader) 421 sealedKey := objectKey.Seal(key.Plaintext, crypto.GenerateIV(rand.Reader), crypto.S3.String(), bucket, "") 422 crypto.S3.CreateMetadata(metadata, key.KeyID, key.Ciphertext, sealedKey) 423 _, err = sio.Encrypt(outbuf, bytes.NewBuffer(input), sio.Config{Key: objectKey[:], MinVersion: sio.Version20, CipherSuites: fips.CipherSuitesDARE()}) 424 if err != nil { 425 return output, metabytes, err 426 } 427 metabytes, err = json.Marshal(metadata) 428 if err != nil { 429 return 430 } 431 return outbuf.Bytes(), metabytes, nil 432 } 433 434 // decrypt bucket metadata if kms is configured. 435 func decryptBucketMetadata(input []byte, bucket string, meta map[string]string, kmsContext kms.Context) ([]byte, error) { 436 if GlobalKMS == nil { 437 return nil, errKMSNotConfigured 438 } 439 keyID, kmsKey, sealedKey, err := crypto.S3.ParseMetadata(meta) 440 if err != nil { 441 return nil, err 442 } 443 extKey, err := GlobalKMS.DecryptKey(keyID, kmsKey, kmsContext) 444 if err != nil { 445 return nil, err 446 } 447 var objectKey crypto.ObjectKey 448 if err = objectKey.Unseal(extKey, sealedKey, crypto.S3.String(), bucket, ""); err != nil { 449 return nil, err 450 } 451 452 outbuf := bytes.NewBuffer(nil) 453 _, err = sio.Decrypt(outbuf, bytes.NewBuffer(input), sio.Config{Key: objectKey[:], MinVersion: sio.Version20, CipherSuites: fips.CipherSuitesDARE()}) 454 return outbuf.Bytes(), err 455 }