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  }