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  }