From 026265f8f7dc4197ee01f033677f72def82ce911 Mon Sep 17 00:00:00 2001 From: Krishnan Parthasarathi Date: Wed, 5 Feb 2020 01:42:34 -0800 Subject: [PATCH] Add support for bucket encryption feature (#8890) - pkg/bucket/encryption provides support for handling bucket encryption configuration - changes under cmd/ provide support for AES256 algorithm only Co-Authored-By: Poorna Co-authored-by: Harshavardhana --- cmd/api-errors.go | 8 + cmd/api-router.go | 7 + cmd/bucket-encryption-handlers.go | 180 ++++++++++++++++++ cmd/bucket-encryption.go | 169 ++++++++++++++++ cmd/bucket-encryption_test.go | 70 +++++++ cmd/bucket-handlers.go | 4 +- cmd/fs-v1.go | 18 ++ cmd/gateway-unsupported.go | 18 ++ cmd/globals.go | 6 +- cmd/lifecycle.go | 16 +- cmd/notification.go | 38 ++++ cmd/object-api-errors.go | 7 + cmd/object-api-interface.go | 7 + cmd/object-handlers.go | 13 +- cmd/peer-rest-client.go | 32 ++++ cmd/peer-rest-common.go | 2 + cmd/peer-rest-server.go | 46 +++++ cmd/server-main.go | 7 + cmd/test-utils_test.go | 6 + cmd/web-handlers.go | 4 +- cmd/xl-sets.go | 16 ++ cmd/xl-v1-bucket.go | 17 ++ cmd/xl-zones.go | 16 ++ pkg/bucket/encryption/bucket-sse-config.go | 103 ++++++++++ .../encryption/bucket-sse-config_test.go | 159 ++++++++++++++++ pkg/bucket/policy/action.go | 5 + 26 files changed, 961 insertions(+), 13 deletions(-) create mode 100644 cmd/bucket-encryption-handlers.go create mode 100644 cmd/bucket-encryption.go create mode 100644 cmd/bucket-encryption_test.go create mode 100644 pkg/bucket/encryption/bucket-sse-config.go create mode 100644 pkg/bucket/encryption/bucket-sse-config_test.go diff --git a/cmd/api-errors.go b/cmd/api-errors.go index 0f17a1373..33f75f6aa 100644 --- a/cmd/api-errors.go +++ b/cmd/api-errors.go @@ -96,6 +96,7 @@ const ( ErrNoSuchBucket ErrNoSuchBucketPolicy ErrNoSuchBucketLifecycle + ErrNoSuchBucketSSEConfig ErrNoSuchKey ErrNoSuchUpload ErrNoSuchVersion @@ -487,6 +488,11 @@ var errorCodes = errorCodeMap{ Description: "The bucket lifecycle configuration does not exist", HTTPStatusCode: http.StatusNotFound, }, + ErrNoSuchBucketSSEConfig: { + Code: "ServerSideEncryptionConfigurationNotFoundError", + Description: "The server side encryption configuration was not found", + HTTPStatusCode: http.StatusNotFound, + }, ErrNoSuchKey: { Code: "NoSuchKey", Description: "The specified key does not exist.", @@ -1719,6 +1725,8 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) { apiErr = ErrNoSuchBucketPolicy case BucketLifecycleNotFound: apiErr = ErrNoSuchBucketLifecycle + case BucketSSEConfigNotFound: + apiErr = ErrNoSuchBucketSSEConfig case *event.ErrInvalidEventName: apiErr = ErrEventNotification case *event.ErrInvalidARN: diff --git a/cmd/api-router.go b/cmd/api-router.go index 78d831b82..a8e6bf703 100644 --- a/cmd/api-router.go +++ b/cmd/api-router.go @@ -138,6 +138,8 @@ func registerAPIRouter(router *mux.Router, encryptionEnabled, allowSSEKMS bool) bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("getbucketpolicy", httpTraceAll(api.GetBucketPolicyHandler))).Queries("policy", "") // GetBucketLifecycle bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("getbucketlifecycle", httpTraceAll(api.GetBucketLifecycleHandler))).Queries("lifecycle", "") + // GetBucketEncryption + bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("getbucketencryption", httpTraceAll(api.GetBucketEncryptionHandler))).Queries("encryption", "") // Dummy Bucket Calls // GetBucketACL -- this is a dummy call. @@ -183,6 +185,9 @@ func registerAPIRouter(router *mux.Router, encryptionEnabled, allowSSEKMS bool) bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("listobjectsv1", httpTraceAll(api.ListObjectsV1Handler))) // PutBucketLifecycle bucket.Methods(http.MethodPut).HandlerFunc(collectAPIStats("putbucketlifecycle", httpTraceAll(api.PutBucketLifecycleHandler))).Queries("lifecycle", "") + // PutBucketEncryption + bucket.Methods(http.MethodPut).HandlerFunc(collectAPIStats("putbucketencryption", httpTraceAll(api.PutBucketEncryptionHandler))).Queries("encryption", "") + // PutBucketPolicy bucket.Methods(http.MethodPut).HandlerFunc(collectAPIStats("putbucketpolicy", httpTraceAll(api.PutBucketPolicyHandler))).Queries("policy", "") @@ -204,6 +209,8 @@ func registerAPIRouter(router *mux.Router, encryptionEnabled, allowSSEKMS bool) bucket.Methods(http.MethodDelete).HandlerFunc(collectAPIStats("deletebucketpolicy", httpTraceAll(api.DeleteBucketPolicyHandler))).Queries("policy", "") // DeleteBucketLifecycle bucket.Methods(http.MethodDelete).HandlerFunc(collectAPIStats("deletebucketlifecycle", httpTraceAll(api.DeleteBucketLifecycleHandler))).Queries("lifecycle", "") + // DeleteBucketEncryption + bucket.Methods(http.MethodDelete).HandlerFunc(collectAPIStats("deletebucketencryption", httpTraceAll(api.DeleteBucketEncryptionHandler))).Queries("encryption", "") // DeleteBucket bucket.Methods(http.MethodDelete).HandlerFunc(collectAPIStats("deletebucket", httpTraceAll(api.DeleteBucketHandler))) } diff --git a/cmd/bucket-encryption-handlers.go b/cmd/bucket-encryption-handlers.go new file mode 100644 index 000000000..eed04bdef --- /dev/null +++ b/cmd/bucket-encryption-handlers.go @@ -0,0 +1,180 @@ +/* + * MinIO Cloud Storage, (C) 2020 MinIO, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cmd + +import ( + "encoding/xml" + "io" + "net/http" + + "github.com/gorilla/mux" + "github.com/minio/minio/cmd/logger" + bucketsse "github.com/minio/minio/pkg/bucket/encryption" + "github.com/minio/minio/pkg/bucket/policy" +) + +const ( + // Bucket Encryption configuration file name. + bucketSSEConfig = "bucket-encryption.xml" +) + +// PutBucketEncryptionHandler - Stores given bucket encryption configuration +// https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketEncryption.html +func (api objectAPIHandlers) PutBucketEncryptionHandler(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "PutBucketEncryption") + + defer logger.AuditLog(w, r, "PutBucketEncryption", mustGetClaimsFromToken(r)) + + objAPI := api.ObjectAPI() + if objAPI == nil { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r)) + return + } + + vars := mux.Vars(r) + bucket := vars["bucket"] + + if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketEncryptionAction, bucket, ""); s3Error != ErrNone { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r)) + return + } + + // Check if bucket exists. + if _, err := objAPI.GetBucketInfo(ctx, bucket); err != nil { + writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) + return + } + + // PutBucketEncyrption API requires Content-Md5 + if _, ok := r.Header["Content-Md5"]; !ok { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentMD5), r.URL, guessIsBrowserReq(r)) + return + } + + // Parse bucket encryption xml + encConfig, err := validateBucketSSEConfig(io.LimitReader(r.Body, maxBucketSSEConfigSize)) + if err != nil { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMalformedXML), r.URL, guessIsBrowserReq(r)) + return + } + + // Return error if KMS is not initialized + if GlobalKMS == nil { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrKMSNotConfigured), r.URL, guessIsBrowserReq(r)) + return + } + + // Store the bucket encryption configuration in the object layer + if err = objAPI.SetBucketSSEConfig(ctx, bucket, encConfig); err != nil { + writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) + return + } + + // Update the in-memory bucket encryption cache + globalBucketSSEConfigSys.Set(bucket, *encConfig) + + // Update peer MinIO servers of the updated bucket encryption config + globalNotificationSys.SetBucketSSEConfig(ctx, bucket, encConfig) + + writeSuccessResponseHeadersOnly(w) +} + +// GetBucketEncryptionHandler - Returns bucket policy configuration +// https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketEncryption.html +func (api objectAPIHandlers) GetBucketEncryptionHandler(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "GetBucketEncryption") + + defer logger.AuditLog(w, r, "GetBucketEncryption", mustGetClaimsFromToken(r)) + + objAPI := api.ObjectAPI() + if objAPI == nil { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r)) + return + } + + vars := mux.Vars(r) + bucket := vars["bucket"] + + if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketEncryptionAction, bucket, ""); s3Error != ErrNone { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r)) + return + } + + // Check if bucket exists + var err error + if _, err = objAPI.GetBucketInfo(ctx, bucket); err != nil { + writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) + return + } + + // Fetch bucket encryption configuration from object layer + var encConfig *bucketsse.BucketSSEConfig + if encConfig, err = objAPI.GetBucketSSEConfig(ctx, bucket); err != nil { + writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) + return + } + + var encConfigData []byte + if encConfigData, err = xml.Marshal(encConfig); err != nil { + writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) + return + } + + // Write bucket encryption configuration to client + writeSuccessResponseXML(w, encConfigData) +} + +// DeleteBucketEncryptionHandler - Removes bucket encryption configuration +func (api objectAPIHandlers) DeleteBucketEncryptionHandler(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "DeleteBucketEncryption") + + defer logger.AuditLog(w, r, "DeleteBucketEncryption", mustGetClaimsFromToken(r)) + + objAPI := api.ObjectAPI() + if objAPI == nil { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r)) + return + } + + vars := mux.Vars(r) + bucket := vars["bucket"] + + if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketEncryptionAction, bucket, ""); s3Error != ErrNone { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r)) + return + } + + // Check if bucket exists + var err error + if _, err = objAPI.GetBucketInfo(ctx, bucket); err != nil { + writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) + return + } + + // Delete bucket encryption config from object layer + if err = objAPI.DeleteBucketSSEConfig(ctx, bucket); err != nil { + writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) + return + } + + // Remove entry from the in-memory bucket encryption cache + globalBucketSSEConfigSys.Remove(bucket) + // Update peer MinIO servers of the updated bucket encryption config + globalNotificationSys.RemoveBucketSSEConfig(ctx, bucket) + + writeSuccessNoContent(w) +} diff --git a/cmd/bucket-encryption.go b/cmd/bucket-encryption.go new file mode 100644 index 000000000..a28fecb36 --- /dev/null +++ b/cmd/bucket-encryption.go @@ -0,0 +1,169 @@ +/* + * MinIO Cloud Storage, (C) 2020 MinIO, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cmd + +import ( + "bytes" + "context" + "encoding/xml" + "errors" + "io" + "path" + "sync" + + bucketsse "github.com/minio/minio/pkg/bucket/encryption" +) + +// BucketSSEConfigSys - in-memory cache of bucket encryption config +type BucketSSEConfigSys struct { + sync.RWMutex + bucketSSEConfigMap map[string]bucketsse.BucketSSEConfig +} + +// NewBucketSSEConfigSys - Creates an empty in-memory bucket encryption configuration cache +func NewBucketSSEConfigSys() *BucketSSEConfigSys { + return &BucketSSEConfigSys{ + bucketSSEConfigMap: make(map[string]bucketsse.BucketSSEConfig), + } +} + +// load - Loads the bucket encryption configuration for the given list of buckets +func (sys *BucketSSEConfigSys) load(buckets []BucketInfo, objAPI ObjectLayer) error { + for _, bucket := range buckets { + config, err := objAPI.GetBucketSSEConfig(context.Background(), bucket.Name) + if err != nil { + if _, ok := err.(BucketSSEConfigNotFound); ok { + sys.Remove(bucket.Name) + } + continue + } + sys.Set(bucket.Name, *config) + } + + return nil +} + +// Init - Initializes in-memory bucket encryption config cache for the given list of buckets +func (sys *BucketSSEConfigSys) Init(buckets []BucketInfo, objAPI ObjectLayer) error { + if objAPI == nil { + return errServerNotInitialized + } + + // We don't cache bucket encryption config in gateway mode, nothing to do. + if globalIsGateway { + return nil + } + + // Load bucket encryption config cache once during boot. + return sys.load(buckets, objAPI) +} + +// Get - gets bucket encryption config for the given bucket. +func (sys *BucketSSEConfigSys) Get(bucket string) (config bucketsse.BucketSSEConfig, ok bool) { + // We don't cache bucket encryption config in gateway mode. + if globalIsGateway { + objAPI := newObjectLayerWithoutSafeModeFn() + if objAPI == nil { + return + } + + cfg, err := objAPI.GetBucketSSEConfig(context.Background(), bucket) + if err != nil { + return + } + return *cfg, true + } + + sys.Lock() + defer sys.Unlock() + config, ok = sys.bucketSSEConfigMap[bucket] + return +} + +// Set - sets bucket encryption config to given bucket name. +func (sys *BucketSSEConfigSys) Set(bucket string, config bucketsse.BucketSSEConfig) { + // We don't cache bucket encryption config in gateway mode. + if globalIsGateway { + return + } + + sys.Lock() + defer sys.Unlock() + sys.bucketSSEConfigMap[bucket] = config +} + +// Remove - removes bucket encryption config for given bucket. +func (sys *BucketSSEConfigSys) Remove(bucket string) { + sys.Lock() + defer sys.Unlock() + + delete(sys.bucketSSEConfigMap, bucket) +} + +// saveBucketSSEConfig - save bucket encryption config for given bucket. +func saveBucketSSEConfig(ctx context.Context, objAPI ObjectLayer, bucket string, config *bucketsse.BucketSSEConfig) error { + data, err := xml.Marshal(config) + if err != nil { + return err + } + + // Path to store bucket encryption config for the given bucket. + configFile := path.Join(bucketConfigPrefix, bucket, bucketSSEConfig) + return saveConfig(ctx, objAPI, configFile, data) +} + +// getBucketSSEConfig - get bucket encryption config for given bucket. +func getBucketSSEConfig(objAPI ObjectLayer, bucket string) (*bucketsse.BucketSSEConfig, error) { + // Path to bucket-encryption.xml for the given bucket. + configFile := path.Join(bucketConfigPrefix, bucket, bucketSSEConfig) + configData, err := readConfig(context.Background(), objAPI, configFile) + if err != nil { + if err == errConfigNotFound { + err = BucketSSEConfigNotFound{Bucket: bucket} + } + return nil, err + } + + return bucketsse.ParseBucketSSEConfig(bytes.NewReader(configData)) +} + +// removeBucketSSEConfig - removes bucket encryption config for given bucket. +func removeBucketSSEConfig(ctx context.Context, objAPI ObjectLayer, bucket string) error { + // Path to bucket-encryption.xml for the given bucket. + configFile := path.Join(bucketConfigPrefix, bucket, bucketSSEConfig) + + if err := objAPI.DeleteObject(ctx, minioMetaBucket, configFile); err != nil { + if _, ok := err.(ObjectNotFound); ok { + return BucketSSEConfigNotFound{Bucket: bucket} + } + return err + } + return nil +} + +// validateBucketSSEConfig parses bucket encryption configuration and validates if it is supported by MinIO. +func validateBucketSSEConfig(r io.Reader) (*bucketsse.BucketSSEConfig, error) { + encConfig, err := bucketsse.ParseBucketSSEConfig(r) + if err != nil { + return nil, err + } + + if len(encConfig.Rules) == 1 && encConfig.Rules[0].DefaultEncryptionAction.Algorithm == bucketsse.AES256 { + return encConfig, nil + } + return nil, errors.New("Unsupported bucket encryption configuration") +} diff --git a/cmd/bucket-encryption_test.go b/cmd/bucket-encryption_test.go new file mode 100644 index 000000000..25548caf3 --- /dev/null +++ b/cmd/bucket-encryption_test.go @@ -0,0 +1,70 @@ +/* + * MinIO Cloud Storage, (C) 2020 MinIO, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cmd + +import ( + "bytes" + "errors" + "testing" +) + +func TestValidateBucketSSEConfig(t *testing.T) { + testCases := []struct { + inputXML string + expectedErr error + shouldPass bool + }{ + // MinIO supported XML + { + inputXML: ` + + + AES256 + + + `, + expectedErr: nil, + shouldPass: true, + }, + // Unsupported XML + { + inputXML: ` + + + aws:kms + arn:aws:kms:us-east-1:1234/5678example + + + `, + expectedErr: errors.New("Unsupported bucket encryption configuration"), + shouldPass: false, + }, + } + + for i, tc := range testCases { + _, err := validateBucketSSEConfig(bytes.NewReader([]byte(tc.inputXML))) + if tc.shouldPass && err != nil { + t.Fatalf("Test case %d: Expected to succeed but got %s", i+1, err) + } + + if !tc.shouldPass { + if err == nil || err != nil && err.Error() != tc.expectedErr.Error() { + t.Fatalf("Test case %d: Expected %s but got %s", i+1, tc.expectedErr, err) + } + } + } +} diff --git a/cmd/bucket-handlers.go b/cmd/bucket-handlers.go index 1790822ca..f6777aae1 100644 --- a/cmd/bucket-handlers.go +++ b/cmd/bucket-handlers.go @@ -757,8 +757,10 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h pReader := NewPutObjReader(rawReader, nil, nil) var objectEncryptionKey []byte + // Check if bucket encryption is enabled + _, encEnabled := globalBucketSSEConfigSys.Get(bucket) // This request header needs to be set prior to setting ObjectOptions - if globalAutoEncryption && !crypto.SSEC.IsRequested(r.Header) { + if (globalAutoEncryption || encEnabled) && !crypto.SSEC.IsRequested(r.Header) { r.Header.Add(crypto.SSEHeader, crypto.SSEAlgorithmAES256) } // get gateway encryption options diff --git a/cmd/fs-v1.go b/cmd/fs-v1.go index 727d25d43..6be6b4f8c 100644 --- a/cmd/fs-v1.go +++ b/cmd/fs-v1.go @@ -37,9 +37,12 @@ import ( "github.com/minio/minio/cmd/config" xhttp "github.com/minio/minio/cmd/http" "github.com/minio/minio/cmd/logger" + + bucketsse "github.com/minio/minio/pkg/bucket/encryption" "github.com/minio/minio/pkg/bucket/lifecycle" "github.com/minio/minio/pkg/bucket/object/tagging" "github.com/minio/minio/pkg/bucket/policy" + "github.com/minio/minio/pkg/lock" "github.com/minio/minio/pkg/madmin" "github.com/minio/minio/pkg/mimedb" @@ -1292,6 +1295,21 @@ func (fs *FSObjects) DeleteBucketLifecycle(ctx context.Context, bucket string) e return removeLifecycleConfig(ctx, fs, bucket) } +// GetBucketSSEConfig returns bucket encryption config on given bucket +func (fs *FSObjects) GetBucketSSEConfig(ctx context.Context, bucket string) (*bucketsse.BucketSSEConfig, error) { + return getBucketSSEConfig(fs, bucket) +} + +// SetBucketSSEConfig sets bucket encryption config on given bucket +func (fs *FSObjects) SetBucketSSEConfig(ctx context.Context, bucket string, config *bucketsse.BucketSSEConfig) error { + return saveBucketSSEConfig(ctx, fs, bucket, config) +} + +// DeleteBucketSSEConfig deletes bucket encryption config on given bucket +func (fs *FSObjects) DeleteBucketSSEConfig(ctx context.Context, bucket string) error { + return removeBucketSSEConfig(ctx, fs, bucket) +} + // ListObjectsV2 lists all blobs in bucket filtered by prefix func (fs *FSObjects) ListObjectsV2(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int, fetchOwner bool, startAfter string) (result ListObjectsV2Info, err error) { marker := continuationToken diff --git a/cmd/gateway-unsupported.go b/cmd/gateway-unsupported.go index fd8a0759c..000f32cf5 100644 --- a/cmd/gateway-unsupported.go +++ b/cmd/gateway-unsupported.go @@ -21,9 +21,12 @@ import ( "errors" "github.com/minio/minio/cmd/logger" + + bucketsse "github.com/minio/minio/pkg/bucket/encryption" "github.com/minio/minio/pkg/bucket/lifecycle" "github.com/minio/minio/pkg/bucket/object/tagging" "github.com/minio/minio/pkg/bucket/policy" + "github.com/minio/minio/pkg/madmin" ) @@ -128,6 +131,21 @@ func (a GatewayUnsupported) DeleteBucketLifecycle(ctx context.Context, bucket st return NotImplemented{} } +// GetBucketSSEConfig returns bucket encryption config on given bucket +func (a GatewayUnsupported) GetBucketSSEConfig(ctx context.Context, bucket string) (*bucketsse.BucketSSEConfig, error) { + return nil, NotImplemented{} +} + +// SetBucketSSEConfig sets bucket encryption config on given bucket +func (a GatewayUnsupported) SetBucketSSEConfig(ctx context.Context, bucket string, config *bucketsse.BucketSSEConfig) error { + return NotImplemented{} +} + +// DeleteBucketSSEConfig deletes bucket encryption config on given bucket +func (a GatewayUnsupported) DeleteBucketSSEConfig(ctx context.Context, bucket string) error { + return NotImplemented{} +} + // ReloadFormat - Not implemented stub. func (a GatewayUnsupported) ReloadFormat(ctx context.Context, dryRun bool) error { return NotImplemented{} diff --git a/cmd/globals.go b/cmd/globals.go index 6ba6d5cdd..a535258a7 100644 --- a/cmd/globals.go +++ b/cmd/globals.go @@ -95,6 +95,9 @@ const ( // Limit of location constraint XML for unauthenticted PUT bucket operations. maxLocationConstraintSize = 3 * humanize.MiByte + + // Maximum size of default bucket encryption configuration allowed + maxBucketSSEConfigSize = 1 * humanize.MiByte ) var globalCLIContext = struct { @@ -144,7 +147,8 @@ var ( globalPolicySys *PolicySys globalIAMSys *IAMSys - globalLifecycleSys *LifecycleSys + globalLifecycleSys *LifecycleSys + globalBucketSSEConfigSys *BucketSSEConfigSys globalStorageClass storageclass.Config globalLDAPConfig xldap.Config diff --git a/cmd/lifecycle.go b/cmd/lifecycle.go index b4423ef89..03cb081bc 100644 --- a/cmd/lifecycle.go +++ b/cmd/lifecycle.go @@ -52,7 +52,7 @@ func (sys *LifecycleSys) Set(bucketName string, lifecycle lifecycle.Lifecycle) { } // Get - gets lifecycle config associated to a given bucket name. -func (sys *LifecycleSys) Get(bucketName string) (lifecycle lifecycle.Lifecycle, ok bool) { +func (sys *LifecycleSys) Get(bucketName string) (lc lifecycle.Lifecycle, ok bool) { if globalIsGateway { // When gateway is enabled, no cached value // is used to validate life cycle policies. @@ -60,14 +60,18 @@ func (sys *LifecycleSys) Get(bucketName string) (lifecycle lifecycle.Lifecycle, if objAPI == nil { return } + l, err := objAPI.GetBucketLifecycle(context.Background(), bucketName) - return *l, err == nil + if err != nil { + return + } + return *l, true } + sys.Lock() defer sys.Unlock() - - l, ok := sys.bucketLifecycleMap[bucketName] - return l, ok + lc, ok = sys.bucketLifecycleMap[bucketName] + return } func saveLifecycleConfig(ctx context.Context, objAPI ObjectLayer, bucketName string, bucketLifecycle *lifecycle.Lifecycle) error { @@ -149,7 +153,7 @@ func (sys *LifecycleSys) load(buckets []BucketInfo, objAPI ObjectLayer) error { return nil } -// Remove - removes policy for given bucket name. +// Remove - removes lifecycle config for given bucket name. func (sys *LifecycleSys) Remove(bucketName string) { sys.Lock() defer sys.Unlock() diff --git a/cmd/notification.go b/cmd/notification.go index ffacd8ce3..8abbc1892 100644 --- a/cmd/notification.go +++ b/cmd/notification.go @@ -32,9 +32,12 @@ import ( "github.com/klauspost/compress/zip" "github.com/minio/minio/cmd/crypto" "github.com/minio/minio/cmd/logger" + + bucketsse "github.com/minio/minio/pkg/bucket/encryption" "github.com/minio/minio/pkg/bucket/lifecycle" objectlock "github.com/minio/minio/pkg/bucket/object/lock" "github.com/minio/minio/pkg/bucket/policy" + "github.com/minio/minio/pkg/event" "github.com/minio/minio/pkg/madmin" xnet "github.com/minio/minio/pkg/net" @@ -576,6 +579,41 @@ func (sys *NotificationSys) RemoveBucketLifecycle(ctx context.Context, bucketNam }() } +// SetBucketSSEConfig - calls SetBucketSSEConfig on all peers. +func (sys *NotificationSys) SetBucketSSEConfig(ctx context.Context, bucketName string, + encConfig *bucketsse.BucketSSEConfig) { + go func() { + ng := WithNPeers(len(sys.peerClients)) + for idx, client := range sys.peerClients { + if client == nil { + continue + } + client := client + ng.Go(ctx, func() error { + return client.SetBucketSSEConfig(bucketName, encConfig) + }, idx, *client.host) + } + ng.Wait() + }() +} + +// RemoveBucketSSEConfig - calls RemoveBucketSSEConfig on all peers. +func (sys *NotificationSys) RemoveBucketSSEConfig(ctx context.Context, bucketName string) { + go func() { + ng := WithNPeers(len(sys.peerClients)) + for idx, client := range sys.peerClients { + if client == nil { + continue + } + client := client + ng.Go(ctx, func() error { + return client.RemoveBucketSSEConfig(bucketName) + }, idx, *client.host) + } + ng.Wait() + }() +} + // PutBucketNotification - calls PutBucketNotification RPC call on all peers. func (sys *NotificationSys) PutBucketNotification(ctx context.Context, bucketName string, rulesMap event.RulesMap) { go func() { diff --git a/cmd/object-api-errors.go b/cmd/object-api-errors.go index c9d6d7a06..bd9a99855 100644 --- a/cmd/object-api-errors.go +++ b/cmd/object-api-errors.go @@ -262,6 +262,13 @@ func (e BucketLifecycleNotFound) Error() string { return "No bucket life cycle found for bucket : " + e.Bucket } +// BucketSSEConfigNotFound - no bucket encryption config found +type BucketSSEConfigNotFound GenericError + +func (e BucketSSEConfigNotFound) Error() string { + return "No bucket encryption found for bucket: " + e.Bucket +} + /// Bucket related errors. // BucketNameInvalid - bucketname provided is invalid. diff --git a/cmd/object-api-interface.go b/cmd/object-api-interface.go index bb0bcdab3..67db9027d 100644 --- a/cmd/object-api-interface.go +++ b/cmd/object-api-interface.go @@ -22,9 +22,11 @@ import ( "net/http" "github.com/minio/minio-go/v6/pkg/encrypt" + bucketsse "github.com/minio/minio/pkg/bucket/encryption" "github.com/minio/minio/pkg/bucket/lifecycle" "github.com/minio/minio/pkg/bucket/object/tagging" "github.com/minio/minio/pkg/bucket/policy" + "github.com/minio/minio/pkg/madmin" ) @@ -121,6 +123,11 @@ type ObjectLayer interface { GetBucketLifecycle(context.Context, string) (*lifecycle.Lifecycle, error) DeleteBucketLifecycle(context.Context, string) error + // Bucket Encryption operations + SetBucketSSEConfig(context.Context, string, *bucketsse.BucketSSEConfig) error + GetBucketSSEConfig(context.Context, string) (*bucketsse.BucketSSEConfig, error) + DeleteBucketSSEConfig(context.Context, string) error + // Backend related metrics GetMetrics(ctx context.Context) (*Metrics, error) diff --git a/cmd/object-handlers.go b/cmd/object-handlers.go index aefe1cd59..6a5a300bb 100644 --- a/cmd/object-handlers.go +++ b/cmd/object-handlers.go @@ -766,8 +766,10 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re return } + // Check if bucket encryption is enabled + _, encEnabled := globalBucketSSEConfigSys.Get(dstBucket) // This request header needs to be set prior to setting ObjectOptions - if globalAutoEncryption && !crypto.SSEC.IsRequested(r.Header) { + if (globalAutoEncryption || encEnabled) && !crypto.SSEC.IsRequested(r.Header) { r.Header.Add(crypto.SSEHeader, crypto.SSEAlgorithmAES256) } @@ -1259,8 +1261,10 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req } } + // Check if bucket encryption is enabled + _, encEnabled := globalBucketSSEConfigSys.Get(bucket) // This request header needs to be set prior to setting ObjectOptions - if globalAutoEncryption && !crypto.SSEC.IsRequested(r.Header) && !crypto.S3KMS.IsRequested(r.Header) { + if (globalAutoEncryption || encEnabled) && !crypto.SSEC.IsRequested(r.Header) && !crypto.S3KMS.IsRequested(r.Header) { r.Header.Add(crypto.SSEHeader, crypto.SSEAlgorithmAES256) } @@ -1426,9 +1430,10 @@ func (api objectAPIHandlers) NewMultipartUploadHandler(w http.ResponseWriter, r writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r)) return } - + // Check if bucket encryption is enabled + _, encEnabled := globalBucketSSEConfigSys.Get(bucket) // This request header needs to be set prior to setting ObjectOptions - if globalAutoEncryption && !crypto.SSEC.IsRequested(r.Header) && !crypto.S3KMS.IsRequested(r.Header) { + if (globalAutoEncryption || encEnabled) && !crypto.SSEC.IsRequested(r.Header) && !crypto.S3KMS.IsRequested(r.Header) { r.Header.Add(crypto.SSEHeader, crypto.SSEAlgorithmAES256) } diff --git a/cmd/peer-rest-client.go b/cmd/peer-rest-client.go index 26a04a4cb..4f874c9ca 100644 --- a/cmd/peer-rest-client.go +++ b/cmd/peer-rest-client.go @@ -31,6 +31,7 @@ import ( "github.com/minio/minio/cmd/http" "github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/rest" + bucketsse "github.com/minio/minio/pkg/bucket/encryption" "github.com/minio/minio/pkg/bucket/lifecycle" objectlock "github.com/minio/minio/pkg/bucket/object/lock" "github.com/minio/minio/pkg/bucket/policy" @@ -411,6 +412,37 @@ func (client *peerRESTClient) SetBucketLifecycle(bucket string, bucketLifecycle return nil } +// RemoveBucketSSEConfig - Remove bucket encryption configuration on the peer node +func (client *peerRESTClient) RemoveBucketSSEConfig(bucket string) error { + values := make(url.Values) + values.Set(peerRESTBucket, bucket) + respBody, err := client.call(peerRESTMethodBucketEncryptionRemove, values, nil, -1) + if err != nil { + return err + } + defer http.DrainBody(respBody) + return nil +} + +// SetBucketSSEConfig - Set bucket encryption configuration on the peer node +func (client *peerRESTClient) SetBucketSSEConfig(bucket string, encConfig *bucketsse.BucketSSEConfig) error { + values := make(url.Values) + values.Set(peerRESTBucket, bucket) + + var reader bytes.Buffer + err := gob.NewEncoder(&reader).Encode(encConfig) + if err != nil { + return err + } + + respBody, err := client.call(peerRESTMethodBucketEncryptionSet, values, &reader, -1) + if err != nil { + return err + } + defer http.DrainBody(respBody) + return nil +} + // PutBucketNotification - Put bucket notification on the peer node. func (client *peerRESTClient) PutBucketNotification(bucket string, rulesMap event.RulesMap) error { values := make(url.Values) diff --git a/cmd/peer-rest-common.go b/cmd/peer-rest-common.go index 2b62a5125..80a270305 100644 --- a/cmd/peer-rest-common.go +++ b/cmd/peer-rest-common.go @@ -56,6 +56,8 @@ const ( peerRESTMethodListen = "/listen" peerRESTMethodBucketLifecycleSet = "/setbucketlifecycle" peerRESTMethodBucketLifecycleRemove = "/removebucketlifecycle" + peerRESTMethodBucketEncryptionSet = "/setbucketencryption" + peerRESTMethodBucketEncryptionRemove = "/removebucketencryption" peerRESTMethodLog = "/log" peerRESTMethodHardwareCPUInfo = "/cpuhardwareinfo" peerRESTMethodHardwareNetworkInfo = "/networkhardwareinfo" diff --git a/cmd/peer-rest-server.go b/cmd/peer-rest-server.go index ddc34f73b..94a64a673 100644 --- a/cmd/peer-rest-server.go +++ b/cmd/peer-rest-server.go @@ -31,6 +31,7 @@ import ( "github.com/gorilla/mux" "github.com/minio/minio/cmd/logger" + bucketsse "github.com/minio/minio/pkg/bucket/encryption" "github.com/minio/minio/pkg/bucket/lifecycle" objectlock "github.com/minio/minio/pkg/bucket/object/lock" "github.com/minio/minio/pkg/bucket/policy" @@ -693,6 +694,49 @@ func (s *peerRESTServer) SetBucketLifecycleHandler(w http.ResponseWriter, r *htt w.(http.Flusher).Flush() } +// RemoveBucketSSEConfigHandler - Remove bucket encryption. +func (s *peerRESTServer) RemoveBucketSSEConfigHandler(w http.ResponseWriter, r *http.Request) { + if !s.IsValid(w, r) { + s.writeErrorResponse(w, errors.New("Invalid request")) + return + } + + vars := mux.Vars(r) + bucketName := vars[peerRESTBucket] + if bucketName == "" { + s.writeErrorResponse(w, errors.New("Bucket name is missing")) + return + } + + globalBucketSSEConfigSys.Remove(bucketName) + w.(http.Flusher).Flush() +} + +// SetBucketSSEConfigHandler - Set bucket encryption. +func (s *peerRESTServer) SetBucketSSEConfigHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + bucketName := vars[peerRESTBucket] + if bucketName == "" { + s.writeErrorResponse(w, errors.New("Bucket name is missing")) + return + } + + var encConfig bucketsse.BucketSSEConfig + if r.ContentLength < 0 { + s.writeErrorResponse(w, errInvalidArgument) + return + } + + err := gob.NewDecoder(r.Body).Decode(&encConfig) + if err != nil { + s.writeErrorResponse(w, err) + return + } + + globalBucketSSEConfigSys.Set(bucketName, encConfig) + w.(http.Flusher).Flush() +} + type remoteTargetExistsResp struct { Exists bool } @@ -1179,6 +1223,8 @@ func registerPeerRESTHandlers(router *mux.Router) { subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodReloadFormat).HandlerFunc(httpTraceHdrs(server.ReloadFormatHandler)).Queries(restQueries(peerRESTDryRun)...) subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodBucketLifecycleSet).HandlerFunc(httpTraceHdrs(server.SetBucketLifecycleHandler)).Queries(restQueries(peerRESTBucket)...) subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodBucketLifecycleRemove).HandlerFunc(httpTraceHdrs(server.RemoveBucketLifecycleHandler)).Queries(restQueries(peerRESTBucket)...) + subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodBucketEncryptionSet).HandlerFunc(httpTraceHdrs(server.SetBucketSSEConfigHandler)).Queries(restQueries(peerRESTBucket)...) + subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodBucketEncryptionRemove).HandlerFunc(httpTraceHdrs(server.RemoveBucketSSEConfigHandler)).Queries(restQueries(peerRESTBucket)...) subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodBackgroundOpsStatus).HandlerFunc(server.BackgroundOpsStatusHandler) subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodTrace).HandlerFunc(server.TraceHandler) diff --git a/cmd/server-main.go b/cmd/server-main.go index 85349e424..62a8995b7 100644 --- a/cmd/server-main.go +++ b/cmd/server-main.go @@ -159,6 +159,9 @@ func newAllSubsystems() { // Create new lifecycle system. globalLifecycleSys = NewLifecycleSys() + + // Create new bucket encryption subsystem + globalBucketSSEConfigSys = NewBucketSSEConfigSys() } func initSafeMode(buckets []BucketInfo) (err error) { @@ -280,6 +283,10 @@ func initAllSubsystems(buckets []BucketInfo, newObject ObjectLayer) (err error) return fmt.Errorf("Unable to initialize lifecycle system: %w", err) } + // Initialize bucket encryption subsystem. + if err = globalBucketSSEConfigSys.Init(buckets, newObject); err != nil { + return fmt.Errorf("Unable to initialize bucket encryption subsystem: %w", err) + } return nil } diff --git a/cmd/test-utils_test.go b/cmd/test-utils_test.go index 80e61149d..c8461e07a 100644 --- a/cmd/test-utils_test.go +++ b/cmd/test-utils_test.go @@ -374,6 +374,9 @@ func UnstartedTestServer(t TestErrHandler, instanceType string) TestServer { globalLifecycleSys = NewLifecycleSys() globalLifecycleSys.Init(buckets, objLayer) + globalBucketSSEConfigSys = NewBucketSSEConfigSys() + globalBucketSSEConfigSys.Init(buckets, objLayer) + return testServer } @@ -1977,6 +1980,9 @@ func ExecObjectLayerTest(t TestErrHandler, objTest objTestType) { globalPolicySys = NewPolicySys() globalPolicySys.Init(buckets, objLayer) + globalBucketSSEConfigSys = NewBucketSSEConfigSys() + globalBucketSSEConfigSys.Init(buckets, objLayer) + // Executing the object layer tests for single node setup. objTest(objLayer, FSTestStr, t) diff --git a/cmd/web-handlers.go b/cmd/web-handlers.go index 9275bffc1..bb3a739be 100644 --- a/cmd/web-handlers.go +++ b/cmd/web-handlers.go @@ -987,7 +987,9 @@ func (web *webAPIHandlers) Upload(w http.ResponseWriter, r *http.Request) { return } - if globalAutoEncryption && !crypto.SSEC.IsRequested(r.Header) { + // Check if bucket encryption is enabled + _, encEnabled := globalBucketSSEConfigSys.Get(bucket) + if (globalAutoEncryption || encEnabled) && !crypto.SSEC.IsRequested(r.Header) { r.Header.Add(crypto.SSEHeader, crypto.SSEAlgorithmAES256) } diff --git a/cmd/xl-sets.go b/cmd/xl-sets.go index 2a6b84d1e..fd4ceba3f 100644 --- a/cmd/xl-sets.go +++ b/cmd/xl-sets.go @@ -30,6 +30,7 @@ import ( xhttp "github.com/minio/minio/cmd/http" "github.com/minio/minio/cmd/logger" "github.com/minio/minio/pkg/bpool" + bucketsse "github.com/minio/minio/pkg/bucket/encryption" "github.com/minio/minio/pkg/bucket/lifecycle" "github.com/minio/minio/pkg/bucket/object/tagging" "github.com/minio/minio/pkg/bucket/policy" @@ -597,6 +598,21 @@ func (s *xlSets) DeleteBucketLifecycle(ctx context.Context, bucket string) error return removeLifecycleConfig(ctx, s, bucket) } +// GetBucketSSEConfig returns bucket encryption config on given bucket +func (s *xlSets) GetBucketSSEConfig(ctx context.Context, bucket string) (*bucketsse.BucketSSEConfig, error) { + return getBucketSSEConfig(s, bucket) +} + +// SetBucketSSEConfig sets bucket encryption config on given bucket +func (s *xlSets) SetBucketSSEConfig(ctx context.Context, bucket string, config *bucketsse.BucketSSEConfig) error { + return saveBucketSSEConfig(ctx, s, bucket, config) +} + +// DeleteBucketSSEConfig deletes bucket encryption config on given bucket +func (s *xlSets) DeleteBucketSSEConfig(ctx context.Context, bucket string) error { + return removeBucketSSEConfig(ctx, s, bucket) +} + // IsNotificationSupported returns whether bucket notification is applicable for this layer. func (s *xlSets) IsNotificationSupported() bool { return s.getHashedSet("").IsNotificationSupported() diff --git a/cmd/xl-v1-bucket.go b/cmd/xl-v1-bucket.go index 0c599dc04..0adde25b2 100644 --- a/cmd/xl-v1-bucket.go +++ b/cmd/xl-v1-bucket.go @@ -22,8 +22,10 @@ import ( "github.com/minio/minio-go/v6/pkg/s3utils" "github.com/minio/minio/cmd/logger" + bucketsse "github.com/minio/minio/pkg/bucket/encryption" "github.com/minio/minio/pkg/bucket/lifecycle" "github.com/minio/minio/pkg/bucket/policy" + "github.com/minio/minio/pkg/sync/errgroup" ) @@ -293,6 +295,21 @@ func (xl xlObjects) DeleteBucketLifecycle(ctx context.Context, bucket string) er return removeLifecycleConfig(ctx, xl, bucket) } +// GetBucketSSEConfig returns bucket encryption config on given bucket +func (xl xlObjects) GetBucketSSEConfig(ctx context.Context, bucket string) (*bucketsse.BucketSSEConfig, error) { + return getBucketSSEConfig(xl, bucket) +} + +// SetBucketSSEConfig sets bucket encryption config on given bucket +func (xl xlObjects) SetBucketSSEConfig(ctx context.Context, bucket string, config *bucketsse.BucketSSEConfig) error { + return saveBucketSSEConfig(ctx, xl, bucket, config) +} + +// DeleteBucketSSEConfig deletes bucket encryption config on given bucket +func (xl xlObjects) DeleteBucketSSEConfig(ctx context.Context, bucket string) error { + return removeBucketSSEConfig(ctx, xl, bucket) +} + // IsNotificationSupported returns whether bucket notification is applicable for this layer. func (xl xlObjects) IsNotificationSupported() bool { return true diff --git a/cmd/xl-zones.go b/cmd/xl-zones.go index 6d52c92cf..a3ab71f7b 100644 --- a/cmd/xl-zones.go +++ b/cmd/xl-zones.go @@ -28,6 +28,7 @@ import ( xhttp "github.com/minio/minio/cmd/http" "github.com/minio/minio/cmd/logger" + bucketsse "github.com/minio/minio/pkg/bucket/encryption" "github.com/minio/minio/pkg/bucket/lifecycle" "github.com/minio/minio/pkg/bucket/object/tagging" "github.com/minio/minio/pkg/bucket/policy" @@ -1147,6 +1148,21 @@ func (z *xlZones) DeleteBucketLifecycle(ctx context.Context, bucket string) erro return removeLifecycleConfig(ctx, z, bucket) } +// GetBucketSSEConfig returns bucket encryption config on given bucket +func (z *xlZones) GetBucketSSEConfig(ctx context.Context, bucket string) (*bucketsse.BucketSSEConfig, error) { + return getBucketSSEConfig(z, bucket) +} + +// SetBucketSSEConfig sets bucket encryption config on given bucket +func (z *xlZones) SetBucketSSEConfig(ctx context.Context, bucket string, config *bucketsse.BucketSSEConfig) error { + return saveBucketSSEConfig(ctx, z, bucket, config) +} + +// DeleteBucketSSEConfig deletes bucket encryption config on given bucket +func (z *xlZones) DeleteBucketSSEConfig(ctx context.Context, bucket string) error { + return removeBucketSSEConfig(ctx, z, bucket) +} + // IsNotificationSupported returns whether bucket notification is applicable for this layer. func (z *xlZones) IsNotificationSupported() bool { return true diff --git a/pkg/bucket/encryption/bucket-sse-config.go b/pkg/bucket/encryption/bucket-sse-config.go new file mode 100644 index 000000000..646eb4f25 --- /dev/null +++ b/pkg/bucket/encryption/bucket-sse-config.go @@ -0,0 +1,103 @@ +/* + * MinIO Cloud Storage, (C) 2020 MinIO, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cmd + +import ( + "encoding/xml" + "errors" + "io" +) + +const ( + // AES256 is used with SSE-S3 + AES256 SSEAlgorithm = "AES256" + // AWSKms is used with SSE-KMS + AWSKms SSEAlgorithm = "aws:kms" +) + +// SSEAlgorithm - represents valid SSE algorithms supported; currently only AES256 is supported +type SSEAlgorithm string + +// UnmarshalXML - Unmarshals XML tag to valid SSE algorithm +func (alg *SSEAlgorithm) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + var s string + if err := d.DecodeElement(&s, &start); err != nil { + return err + } + + switch s { + case string(AES256): + *alg = AES256 + case string(AWSKms): + *alg = AWSKms + default: + return errors.New("Unknown SSE algorithm") + } + + return nil +} + +// MarshalXML - Marshals given SSE algorithm to valid XML +func (alg *SSEAlgorithm) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + return e.EncodeElement(string(*alg), start) +} + +// EncryptionAction - for ApplyServerSideEncryptionByDefault XML tag +type EncryptionAction struct { + Algorithm SSEAlgorithm `xml:"SSEAlgorithm"` + MasterKeyID string `xml:"KMSMasterKeyID"` +} + +// SSERule - for ServerSideEncryptionConfiguration XML tag +type SSERule struct { + DefaultEncryptionAction EncryptionAction `xml:"ApplyServerSideEncryptionByDefault"` +} + +// BucketSSEConfig - represents default bucket encryption configuration +type BucketSSEConfig struct { + XMLName xml.Name `xml:"ServerSideEncryptionConfiguration"` + Rules []SSERule `xml:"Rule"` +} + +// ParseBucketSSEConfig - Decodes given XML to a valid default bucket encryption config +func ParseBucketSSEConfig(r io.Reader) (*BucketSSEConfig, error) { + var config BucketSSEConfig + err := xml.NewDecoder(r).Decode(&config) + if err != nil { + return nil, err + } + + // Validates server-side encryption config rules + // Only one rule is allowed on AWS S3 + if len(config.Rules) != 1 { + return nil, errors.New("Only one server-side encryption rule is allowed") + } + + for _, rule := range config.Rules { + switch rule.DefaultEncryptionAction.Algorithm { + case AES256: + if rule.DefaultEncryptionAction.MasterKeyID != "" { + return nil, errors.New("MasterKeyID is allowed with aws:kms only") + } + case AWSKms: + if rule.DefaultEncryptionAction.MasterKeyID == "" { + return nil, errors.New("MasterKeyID is missing") + } + } + } + return &config, nil +} diff --git a/pkg/bucket/encryption/bucket-sse-config_test.go b/pkg/bucket/encryption/bucket-sse-config_test.go new file mode 100644 index 000000000..35a8c4fba --- /dev/null +++ b/pkg/bucket/encryption/bucket-sse-config_test.go @@ -0,0 +1,159 @@ +/* + * MinIO Cloud Storage, (C) 2020 MinIO, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cmd + +import ( + "bytes" + "encoding/xml" + "errors" + "testing" +) + +// TestParseBucketSSEConfig performs basic sanity tests on ParseBucketSSEConfig +func TestParseBucketSSEConfig(t *testing.T) { + testCases := []struct { + inputXML string + expectedErr error + shouldPass bool + }{ + // 1. Valid XML SSE-S3 + { + inputXML: ` + + + AES256 + + + `, + expectedErr: nil, + shouldPass: true, + }, + // 2. Valid XML SSE-KMS + { + inputXML: ` + + + aws:kms + arn:aws:kms:us-east-1:1234/5678example + + + `, + expectedErr: nil, + shouldPass: true, + }, + // 3. Invalid - more than one rule + { + inputXML: ` + + + AES256 + + + + + AES256 + + + `, + expectedErr: errors.New("Only one server-side encryption rule is allowed"), + shouldPass: false, + }, + // 4. Invalid XML - master key ID present in AES256 + { + inputXML: ` + + + AES256 + arn:aws:kms:us-east-1:1234/5678example + + + `, + expectedErr: errors.New("MasterKeyID is allowed with aws:kms only"), + shouldPass: false, + }, + // 5. Invalid XML - master key ID not found in aws:kms algorithm + { + inputXML: ` + + + aws:kms + + + `, + expectedErr: errors.New("MasterKeyID is missing"), + shouldPass: false, + }, + // 6. Invalid Algorithm + { + inputXML: ` + + + InvalidAlgorithm + + + `, + expectedErr: errors.New("Unknown SSE algorithm"), + shouldPass: false, + }, + // 7. Allow missing namespace + { + inputXML: ` + + + AES256 + + + `, + expectedErr: nil, + shouldPass: true, + }, + } + + actualConfig := &BucketSSEConfig{ + XMLName: xml.Name{ + Local: "ServerSideEncryptionConfiguration", + }, + Rules: []SSERule{ + { + DefaultEncryptionAction: EncryptionAction{ + Algorithm: AES256, + }, + }, + }, + } + + for i, tc := range testCases { + _, err := ParseBucketSSEConfig(bytes.NewReader([]byte(tc.inputXML))) + if tc.shouldPass && err != nil { + t.Fatalf("Test case %d: Expected to succeed but got %s", i+1, err) + } + + if !tc.shouldPass { + if err == nil || err != nil && err.Error() != tc.expectedErr.Error() { + t.Fatalf("Test case %d: Expected %s but got %s", i+1, tc.expectedErr, err) + } + } + + if !tc.shouldPass { + continue + } + + if actualXML, err := xml.Marshal(actualConfig); err != nil && bytes.Equal(actualXML, []byte(tc.inputXML)) { + t.Fatalf("Test case %d: Expected config %s but got %s", i+1, string(actualXML), tc.inputXML) + } + } +} diff --git a/pkg/bucket/policy/action.go b/pkg/bucket/policy/action.go index 5e51e280d..33933a9d7 100644 --- a/pkg/bucket/policy/action.go +++ b/pkg/bucket/policy/action.go @@ -113,6 +113,11 @@ const ( PutObjectTaggingAction = "s3:PutObjectTagging" // DeleteObjectTaggingAction - Delete Object Tags API action DeleteObjectTaggingAction = "s3:DeleteObjectTagging" + + // PutBucketEncryptionAction - PutBucketEncryption REST API action + PutBucketEncryptionAction = "s3:PutEncryptionConfiguration" + // GetBucketEncryptionAction - GetBucketEncryption REST API action + GetBucketEncryptionAction = "s3:GetEncryptionConfiguration" ) // isObjectAction - returns whether action is object type or not.