Add support for object locking with legal hold. (#8634)

This commit is contained in:
poornas 2020-01-16 15:41:56 -08:00 committed by kannappanr
parent ba758361b3
commit 60e60f68dd
21 changed files with 1559 additions and 517 deletions

View file

@ -34,6 +34,7 @@ import (
"github.com/minio/minio/pkg/auth" "github.com/minio/minio/pkg/auth"
"github.com/minio/minio/pkg/event" "github.com/minio/minio/pkg/event"
"github.com/minio/minio/pkg/hash" "github.com/minio/minio/pkg/hash"
"github.com/minio/minio/pkg/objectlock"
"github.com/minio/minio/pkg/policy" "github.com/minio/minio/pkg/policy"
) )
@ -1611,14 +1612,16 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
apiErr = ErrOperationTimedOut apiErr = ErrOperationTimedOut
case errDiskNotFound: case errDiskNotFound:
apiErr = ErrSlowDown apiErr = ErrSlowDown
case errInvalidRetentionDate: case objectlock.ErrInvalidRetentionDate:
apiErr = ErrInvalidRetentionDate apiErr = ErrInvalidRetentionDate
case errPastObjectLockRetainDate: case objectlock.ErrPastObjectLockRetainDate:
apiErr = ErrPastObjectLockRetainDate apiErr = ErrPastObjectLockRetainDate
case errUnknownWORMModeDirective: case objectlock.ErrUnknownWORMModeDirective:
apiErr = ErrUnknownWORMModeDirective apiErr = ErrUnknownWORMModeDirective
case errObjectLockInvalidHeaders: case objectlock.ErrObjectLockInvalidHeaders:
apiErr = ErrObjectLockInvalidHeaders apiErr = ErrObjectLockInvalidHeaders
case objectlock.ErrMalformedXML:
apiErr = ErrMalformedXML
} }
// Compression errors // Compression errors

View file

@ -111,20 +111,21 @@ func registerAPIRouter(router *mux.Router, encryptionEnabled, allowSSEKMS bool)
bucket.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc(collectAPIStats("selectobjectcontent", httpTraceHdrs(api.SelectObjectContentHandler))).Queries("select", "").Queries("select-type", "2") bucket.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc(collectAPIStats("selectobjectcontent", httpTraceHdrs(api.SelectObjectContentHandler))).Queries("select", "").Queries("select-type", "2")
// GetObjectRetention // GetObjectRetention
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(collectAPIStats("getobjectretention", httpTraceHdrs(api.GetObjectRetentionHandler))).Queries("retention", "") bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(collectAPIStats("getobjectretention", httpTraceHdrs(api.GetObjectRetentionHandler))).Queries("retention", "")
// GetObjectLegalHold
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(collectAPIStats("getobjectlegalhold", httpTraceHdrs(api.GetObjectLegalHoldHandler))).Queries("legal-hold", "")
// GetObject // GetObject
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(collectAPIStats("getobject", httpTraceHdrs(api.GetObjectHandler))) bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(collectAPIStats("getobject", httpTraceHdrs(api.GetObjectHandler)))
// CopyObject // CopyObject
bucket.Methods(http.MethodPut).Path("/{object:.+}").HeadersRegexp(xhttp.AmzCopySource, ".*?(\\/|%2F).*?").HandlerFunc(collectAPIStats("copyobject", httpTraceAll(api.CopyObjectHandler))) bucket.Methods(http.MethodPut).Path("/{object:.+}").HeadersRegexp(xhttp.AmzCopySource, ".*?(\\/|%2F).*?").HandlerFunc(collectAPIStats("copyobject", httpTraceAll(api.CopyObjectHandler)))
// PutObjectRetention // PutObjectRetention
bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(collectAPIStats("putobjectretention", httpTraceHdrs(api.PutObjectRetentionHandler))).Queries("retention", "") bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(collectAPIStats("putobjectretention", httpTraceHdrs(api.PutObjectRetentionHandler))).Queries("retention", "")
// PutObjectLegalHold
bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(collectAPIStats("putobjectlegalhold", httpTraceHdrs(api.PutObjectLegalHoldHandler))).Queries("legal-hold", "")
// PutObject // PutObject
bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(collectAPIStats("putobject", httpTraceHdrs(api.PutObjectHandler))) bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(collectAPIStats("putobject", httpTraceHdrs(api.PutObjectHandler)))
// DeleteObject // DeleteObject
bucket.Methods(http.MethodDelete).Path("/{object:.+}").HandlerFunc(collectAPIStats("deleteobject", httpTraceAll(api.DeleteObjectHandler))) bucket.Methods(http.MethodDelete).Path("/{object:.+}").HandlerFunc(collectAPIStats("deleteobject", httpTraceAll(api.DeleteObjectHandler)))
// PutObjectLegalHold
bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(collectAPIStats("putobjectlegalhold", httpTraceHdrs(api.PutObjectLegalHoldHandler))).Queries("legal-hold", "")
// GetObjectLegalHold
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(collectAPIStats("getobjectlegalhold", httpTraceHdrs(api.GetObjectLegalHoldHandler))).Queries("legal-hold", "")
/// Bucket operations /// Bucket operations
// GetBucketLocation // GetBucketLocation

View file

@ -40,6 +40,7 @@ import (
"github.com/minio/minio/pkg/handlers" "github.com/minio/minio/pkg/handlers"
"github.com/minio/minio/pkg/hash" "github.com/minio/minio/pkg/hash"
iampolicy "github.com/minio/minio/pkg/iam/policy" iampolicy "github.com/minio/minio/pkg/iam/policy"
"github.com/minio/minio/pkg/objectlock"
"github.com/minio/minio/pkg/policy" "github.com/minio/minio/pkg/policy"
"github.com/minio/minio/pkg/sync/errgroup" "github.com/minio/minio/pkg/sync/errgroup"
) )
@ -577,14 +578,14 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
return return
} }
if objectLockEnabled { if objectLockEnabled && !globalIsGateway {
configFile := path.Join(bucketConfigPrefix, bucket, bucketObjectLockEnabledConfigFile) configFile := path.Join(bucketConfigPrefix, bucket, bucketObjectLockEnabledConfigFile)
if err = saveConfig(ctx, objectAPI, configFile, []byte(bucketObjectLockEnabledConfig)); err != nil { if err = saveConfig(ctx, objectAPI, configFile, []byte(bucketObjectLockEnabledConfig)); err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return return
} }
globalBucketObjectLockConfig.Set(bucket, Retention{}) globalBucketObjectLockConfig.Set(bucket, objectlock.Retention{})
globalNotificationSys.PutBucketObjectLockConfig(ctx, bucket, Retention{}) globalNotificationSys.PutBucketObjectLockConfig(ctx, bucket, objectlock.Retention{})
} }
// Make sure to add Location information here only for bucket // Make sure to add Location information here only for bucket
@ -1005,7 +1006,7 @@ func (api objectAPIHandlers) PutBucketObjectLockConfigHandler(w http.ResponseWri
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
return return
} }
config, err := parseObjectLockConfig(r.Body) config, err := objectlock.ParseObjectLockConfig(r.Body)
if err != nil { if err != nil {
apiErr := errorCodes.ToAPIErr(ErrMalformedXML) apiErr := errorCodes.ToAPIErr(ErrMalformedXML)
apiErr.Description = err.Error() apiErr.Description = err.Error()
@ -1099,7 +1100,7 @@ func (api objectAPIHandlers) GetBucketObjectLockConfigHandler(w http.ResponseWri
return return
} }
if configData, err = xml.Marshal(newObjectLockConfig()); err != nil { if configData, err = xml.Marshal(objectlock.NewObjectLockConfig()); err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return return
} }

View file

@ -32,6 +32,7 @@ import (
"github.com/minio/minio/cmd/config/cache" "github.com/minio/minio/cmd/config/cache"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/color" "github.com/minio/minio/pkg/color"
"github.com/minio/minio/pkg/objectlock"
"github.com/minio/minio/pkg/sync/errgroup" "github.com/minio/minio/pkg/sync/errgroup"
"github.com/minio/minio/pkg/wildcard" "github.com/minio/minio/pkg/wildcard"
) )
@ -251,7 +252,13 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
c.cacheStats.incMiss() c.cacheStats.incMiss()
return c.GetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts) return c.GetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts)
} }
// skip cache for objects with locks
objRetention := objectlock.GetObjectRetentionMeta(objInfo.UserDefined)
legalHold := objectlock.GetObjectLegalHoldMeta(objInfo.UserDefined)
if objRetention.Mode != objectlock.Invalid || legalHold.Status != "" {
c.cacheStats.incMiss()
return c.GetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts)
}
if cacheErr == nil { if cacheErr == nil {
// if ETag matches for stale cache entry, serve from cache // if ETag matches for stale cache entry, serve from cache
if cacheReader.ObjInfo.ETag == objInfo.ETag { if cacheReader.ObjInfo.ETag == objInfo.ETag {
@ -596,8 +603,9 @@ func (c *cacheObjects) PutObject(ctx context.Context, bucket, object string, r *
} }
// skip cache for objects with locks // skip cache for objects with locks
objRetention := getObjectRetentionMeta(opts.UserDefined) objRetention := objectlock.GetObjectRetentionMeta(opts.UserDefined)
if objRetention.Mode == Governance || objRetention.Mode == Compliance { legalHold := objectlock.GetObjectLegalHoldMeta(opts.UserDefined)
if objRetention.Mode != objectlock.Invalid || legalHold.Status != "" {
dcache.Delete(ctx, bucket, object) dcache.Delete(ctx, bucket, object)
return putObjectFn(ctx, bucket, object, r, opts) return putObjectFn(ctx, bucket, object, r, opts)
} }

View file

@ -37,6 +37,7 @@ import (
"github.com/minio/minio/pkg/auth" "github.com/minio/minio/pkg/auth"
"github.com/minio/minio/pkg/certs" "github.com/minio/minio/pkg/certs"
"github.com/minio/minio/pkg/event" "github.com/minio/minio/pkg/event"
"github.com/minio/minio/pkg/objectlock"
"github.com/minio/minio/pkg/pubsub" "github.com/minio/minio/pkg/pubsub"
) )
@ -201,7 +202,7 @@ var (
// Is worm enabled // Is worm enabled
globalWORMEnabled bool globalWORMEnabled bool
globalBucketObjectLockConfig = newBucketObjectLockConfig() globalBucketObjectLockConfig = objectlock.NewBucketObjectLockConfig()
// Disk cache drives // Disk cache drives
globalCacheConfig cache.Config globalCacheConfig cache.Config

View file

@ -36,6 +36,7 @@ import (
"github.com/minio/minio/pkg/lifecycle" "github.com/minio/minio/pkg/lifecycle"
"github.com/minio/minio/pkg/madmin" "github.com/minio/minio/pkg/madmin"
xnet "github.com/minio/minio/pkg/net" xnet "github.com/minio/minio/pkg/net"
"github.com/minio/minio/pkg/objectlock"
"github.com/minio/minio/pkg/policy" "github.com/minio/minio/pkg/policy"
"github.com/minio/minio/pkg/sync/errgroup" "github.com/minio/minio/pkg/sync/errgroup"
) )
@ -668,7 +669,7 @@ func (sys *NotificationSys) initBucketObjectLockConfig(objAPI ObjectLayer) error
if string(bucketObjLockData) != bucketObjectLockEnabledConfig { if string(bucketObjLockData) != bucketObjectLockEnabledConfig {
// this should never happen // this should never happen
logger.LogIf(ctx, errMalformedBucketObjectConfig) logger.LogIf(ctx, objectlock.ErrMalformedBucketObjectConfig)
continue continue
} }
@ -677,17 +678,17 @@ func (sys *NotificationSys) initBucketObjectLockConfig(objAPI ObjectLayer) error
if err != nil { if err != nil {
if err == errConfigNotFound { if err == errConfigNotFound {
globalBucketObjectLockConfig.Set(bucket.Name, Retention{}) globalBucketObjectLockConfig.Set(bucket.Name, objectlock.Retention{})
continue continue
} }
return err return err
} }
config, err := parseObjectLockConfig(bytes.NewReader(configData)) config, err := objectlock.ParseObjectLockConfig(bytes.NewReader(configData))
if err != nil { if err != nil {
return err return err
} }
retention := Retention{} retention := objectlock.Retention{}
if config.Rule != nil { if config.Rule != nil {
retention = config.ToRetention() retention = config.ToRetention()
} }
@ -874,7 +875,7 @@ func (sys *NotificationSys) Send(args eventArgs) []event.TargetIDErr {
} }
// PutBucketObjectLockConfig - put bucket object lock configuration to all peers. // PutBucketObjectLockConfig - put bucket object lock configuration to all peers.
func (sys *NotificationSys) PutBucketObjectLockConfig(ctx context.Context, bucketName string, retention Retention) { func (sys *NotificationSys) PutBucketObjectLockConfig(ctx context.Context, bucketName string, retention objectlock.Retention) {
g := errgroup.WithNErrs(len(sys.peerClients)) g := errgroup.WithNErrs(len(sys.peerClients))
for index, client := range sys.peerClients { for index, client := range sys.peerClients {
if client == nil { if client == nil {

View file

@ -44,6 +44,8 @@ import (
"github.com/minio/minio/pkg/event" "github.com/minio/minio/pkg/event"
"github.com/minio/minio/pkg/handlers" "github.com/minio/minio/pkg/handlers"
"github.com/minio/minio/pkg/hash" "github.com/minio/minio/pkg/hash"
"github.com/minio/minio/pkg/objectlock"
iampolicy "github.com/minio/minio/pkg/iam/policy" iampolicy "github.com/minio/minio/pkg/iam/policy"
"github.com/minio/minio/pkg/ioutil" "github.com/minio/minio/pkg/ioutil"
"github.com/minio/minio/pkg/policy" "github.com/minio/minio/pkg/policy"
@ -209,8 +211,10 @@ func (api objectAPIHandlers) SelectObjectContentHandler(w http.ResponseWriter, r
return return
} }
getRetPerms := checkRequestAuthType(ctx, r, policy.GetObjectRetentionAction, bucket, object) getRetPerms := checkRequestAuthType(ctx, r, policy.GetObjectRetentionAction, bucket, object)
legalHoldPerms := checkRequestAuthType(ctx, r, policy.GetObjectLegalHoldAction, bucket, object)
// filter object lock metadata if permission does not permit // filter object lock metadata if permission does not permit
objInfo.UserDefined = filterObjectLockMetadata(ctx, r, bucket, object, objInfo.UserDefined, false, getRetPerms) objInfo.UserDefined = objectlock.FilterObjectLockMetadata(objInfo.UserDefined, getRetPerms != ErrNone, legalHoldPerms != ErrNone)
if err = s3Select.Open(getObject); err != nil { if err = s3Select.Open(getObject); err != nil {
if serr, ok := err.(s3select.SelectError); ok { if serr, ok := err.(s3select.SelectError); ok {
@ -353,7 +357,10 @@ func (api objectAPIHandlers) GetObjectHandler(w http.ResponseWriter, r *http.Req
// filter object lock metadata if permission does not permit // filter object lock metadata if permission does not permit
getRetPerms := checkRequestAuthType(ctx, r, policy.GetObjectRetentionAction, bucket, object) getRetPerms := checkRequestAuthType(ctx, r, policy.GetObjectRetentionAction, bucket, object)
objInfo.UserDefined = filterObjectLockMetadata(ctx, r, bucket, object, objInfo.UserDefined, false, getRetPerms) legalHoldPerms := checkRequestAuthType(ctx, r, policy.GetObjectLegalHoldAction, bucket, object)
// filter object lock metadata if permission does not permit
objInfo.UserDefined = objectlock.FilterObjectLockMetadata(objInfo.UserDefined, getRetPerms != ErrNone, legalHoldPerms != ErrNone)
if objectAPI.IsEncryptionSupported() { if objectAPI.IsEncryptionSupported() {
objInfo.UserDefined = CleanMinioInternalMetadataKeys(objInfo.UserDefined) objInfo.UserDefined = CleanMinioInternalMetadataKeys(objInfo.UserDefined)
@ -518,7 +525,10 @@ func (api objectAPIHandlers) HeadObjectHandler(w http.ResponseWriter, r *http.Re
// filter object lock metadata if permission does not permit // filter object lock metadata if permission does not permit
getRetPerms := checkRequestAuthType(ctx, r, policy.GetObjectRetentionAction, bucket, object) getRetPerms := checkRequestAuthType(ctx, r, policy.GetObjectRetentionAction, bucket, object)
objInfo.UserDefined = filterObjectLockMetadata(ctx, r, bucket, object, objInfo.UserDefined, false, getRetPerms) legalHoldPerms := checkRequestAuthType(ctx, r, policy.GetObjectLegalHoldAction, bucket, object)
// filter object lock metadata if permission does not permit
objInfo.UserDefined = objectlock.FilterObjectLockMetadata(objInfo.UserDefined, getRetPerms != ErrNone, legalHoldPerms != ErrNone)
if objectAPI.IsEncryptionSupported() { if objectAPI.IsEncryptionSupported() {
if _, err = DecryptObjectInfo(&objInfo, r.Header); err != nil { if _, err = DecryptObjectInfo(&objInfo, r.Header); err != nil {
@ -962,16 +972,19 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
if api.CacheAPI() != nil { if api.CacheAPI() != nil {
getObjectInfo = api.CacheAPI().GetObjectInfo getObjectInfo = api.CacheAPI().GetObjectInfo
} }
isCpy := true srcInfo.UserDefined = objectlock.FilterObjectLockMetadata(srcInfo.UserDefined, true, true)
getRetPerms := checkRequestAuthType(ctx, r, policy.GetObjectRetentionAction, srcBucket, srcObject)
srcInfo.UserDefined = filterObjectLockMetadata(ctx, r, srcBucket, srcObject, srcInfo.UserDefined, isCpy, getRetPerms)
retPerms := isPutActionAllowed(getRequestAuthType(r), dstBucket, dstObject, r, iampolicy.PutObjectRetentionAction) retPerms := isPutActionAllowed(getRequestAuthType(r), dstBucket, dstObject, r, iampolicy.PutObjectRetentionAction)
holdPerms := isPutActionAllowed(getRequestAuthType(r), dstBucket, dstObject, r, iampolicy.PutObjectLegalHoldAction)
// apply default bucket configuration/governance headers for dest side. // apply default bucket configuration/governance headers for dest side.
retentionMode, retentionDate, s3Err := checkPutObjectRetentionAllowed(ctx, r, dstBucket, dstObject, getObjectInfo, retPerms) retentionMode, retentionDate, legalHold, s3Err := checkPutObjectLockAllowed(ctx, r, dstBucket, dstObject, getObjectInfo, retPerms, holdPerms)
if s3Err == ErrNone && retentionMode != "" { if s3Err == ErrNone && retentionMode != "" {
srcInfo.UserDefined[xhttp.AmzObjectLockMode] = string(retentionMode) srcInfo.UserDefined[xhttp.AmzObjectLockMode] = string(retentionMode)
srcInfo.UserDefined[xhttp.AmzObjectLockRetainUntilDate] = retentionDate.UTC().Format(time.RFC3339) srcInfo.UserDefined[xhttp.AmzObjectLockRetainUntilDate] = retentionDate.UTC().Format(time.RFC3339)
} }
if s3Err == ErrNone && legalHold.Status != "" {
srcInfo.UserDefined[xhttp.AmzObjectLockLegalHold] = string(legalHold.Status)
}
if s3Err != ErrNone { if s3Err != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r))
return return
@ -1251,11 +1264,16 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
putObject = api.CacheAPI().PutObject putObject = api.CacheAPI().PutObject
} }
retPerms := isPutActionAllowed(rAuthType, bucket, object, r, iampolicy.PutObjectRetentionAction) retPerms := isPutActionAllowed(rAuthType, bucket, object, r, iampolicy.PutObjectRetentionAction)
retentionMode, retentionDate, s3Err := checkPutObjectRetentionAllowed(ctx, r, bucket, object, getObjectInfo, retPerms) holdPerms := isPutActionAllowed(getRequestAuthType(r), bucket, object, r, iampolicy.PutObjectLegalHoldAction)
retentionMode, retentionDate, legalHold, s3Err := checkPutObjectLockAllowed(ctx, r, bucket, object, getObjectInfo, retPerms, holdPerms)
if s3Err == ErrNone && retentionMode != "" { if s3Err == ErrNone && retentionMode != "" {
metadata[strings.ToLower(xhttp.AmzObjectLockMode)] = string(retentionMode) metadata[strings.ToLower(xhttp.AmzObjectLockMode)] = string(retentionMode)
metadata[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)] = retentionDate.UTC().Format(time.RFC3339) metadata[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)] = retentionDate.UTC().Format(time.RFC3339)
} }
if s3Err == ErrNone && legalHold.Status != "" {
metadata[strings.ToLower(xhttp.AmzObjectLockLegalHold)] = string(legalHold.Status)
}
if s3Err != ErrNone { if s3Err != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r))
return return
@ -1409,11 +1427,16 @@ func (api objectAPIHandlers) NewMultipartUploadHandler(w http.ResponseWriter, r
return return
} }
retPerms := isPutActionAllowed(getRequestAuthType(r), bucket, object, r, iampolicy.PutObjectRetentionAction) retPerms := isPutActionAllowed(getRequestAuthType(r), bucket, object, r, iampolicy.PutObjectRetentionAction)
retentionMode, retentionDate, s3Err := checkPutObjectRetentionAllowed(ctx, r, bucket, object, objectAPI.GetObjectInfo, retPerms) holdPerms := isPutActionAllowed(getRequestAuthType(r), bucket, object, r, iampolicy.PutObjectLegalHoldAction)
retentionMode, retentionDate, legalHold, s3Err := checkPutObjectLockAllowed(ctx, r, bucket, object, objectAPI.GetObjectInfo, retPerms, holdPerms)
if s3Err == ErrNone && retentionMode != "" { if s3Err == ErrNone && retentionMode != "" {
metadata[strings.ToLower(xhttp.AmzObjectLockMode)] = string(retentionMode) metadata[strings.ToLower(xhttp.AmzObjectLockMode)] = string(retentionMode)
metadata[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)] = retentionDate.UTC().Format(time.RFC3339) metadata[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)] = retentionDate.UTC().Format(time.RFC3339)
} }
if s3Err == ErrNone && legalHold.Status != "" {
metadata[strings.ToLower(xhttp.AmzObjectLockLegalHold)] = string(legalHold.Status)
}
if s3Err != ErrNone { if s3Err != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r))
return return
@ -2215,14 +2238,16 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
} }
// Reject retention or governance headers if set, CompleteMultipartUpload spec // Reject retention or governance headers if set, CompleteMultipartUpload spec
// does not use these headers, and should not be passed down to checkPutObjectRetentionAllowed // does not use these headers, and should not be passed down to checkPutObjectLockAllowed
if isObjectLockRequested(r.Header) || isObjectLockGovernanceBypassSet(r.Header) { if objectlock.IsObjectLockRequested(r.Header) || objectlock.IsObjectLockGovernanceBypassSet(r.Header) {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL, guessIsBrowserReq(r))
return return
} }
// Enforce object lock governance in case a competing upload finalized first. // Enforce object lock governance in case a competing upload finalized first.
retPerms := isPutActionAllowed(getRequestAuthType(r), bucket, object, r, iampolicy.PutObjectRetentionAction) retPerms := isPutActionAllowed(getRequestAuthType(r), bucket, object, r, iampolicy.PutObjectRetentionAction)
if _, _, s3Err := checkPutObjectRetentionAllowed(ctx, r, bucket, object, objectAPI.GetObjectInfo, retPerms); s3Err != ErrNone { holdPerms := isPutActionAllowed(getRequestAuthType(r), bucket, object, r, iampolicy.PutObjectLegalHoldAction)
if _, _, _, s3Err := checkPutObjectLockAllowed(ctx, r, bucket, object, objectAPI.GetObjectInfo, retPerms, holdPerms); s3Err != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r))
return return
} }
@ -2467,23 +2492,81 @@ func (api objectAPIHandlers) PutObjectLegalHoldHandler(w http.ResponseWriter, r
return return
} }
// Check permissions to perform this legal hold operation
if s3Err := isPutActionAllowed(getRequestAuthType(r), bucket, object, r, policy.PutObjectLegalHoldAction); s3Err != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r))
return
}
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
// Get Content-Md5 sent by client and verify if valid
md5Bytes, err := checkValidMD5(r.Header)
if err != nil {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidDigest), r.URL, guessIsBrowserReq(r))
return
}
if _, ok := globalBucketObjectLockConfig.Get(bucket); !ok {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidBucketObjectLockConfiguration), r.URL, guessIsBrowserReq(r))
return
}
legalHold, err := objectlock.ParseObjectLegalHold(r.Body)
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
// verify Content-MD5 sum of request body if this header set
if len(md5Bytes) > 0 {
data, err := xml.Marshal(legalHold)
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
if hex.EncodeToString(md5Bytes) != getMD5Hash(data) {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidDigest), r.URL, guessIsBrowserReq(r))
return
}
}
getObjectInfo := objectAPI.GetObjectInfo getObjectInfo := objectAPI.GetObjectInfo
if api.CacheAPI() != nil { if api.CacheAPI() != nil {
getObjectInfo = api.CacheAPI().GetObjectInfo getObjectInfo = api.CacheAPI().GetObjectInfo
} }
opts, err := getOpts(ctx, r, bucket, object) opts, err := getOpts(ctx, r, bucket, object)
if err != nil { if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return return
} }
if _, err = getObjectInfo(ctx, bucket, object, opts); err != nil { objInfo, err := getObjectInfo(ctx, bucket, object, opts)
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
objInfo.UserDefined[strings.ToLower(xhttp.AmzObjectLockLegalHold)] = strings.ToUpper(string(legalHold.Status))
objInfo.metadataOnly = true
if _, err = objectAPI.CopyObject(ctx, bucket, object, bucket, object, objInfo, ObjectOptions{}, ObjectOptions{}); err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return return
} }
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL, guessIsBrowserReq(r)) writeSuccessNoContent(w)
// Notify object event.
sendEvent(eventArgs{
EventName: event.ObjectCreatedPutLegalHold,
BucketName: bucket,
Object: objInfo,
ReqParams: extractReqParams(r),
RespElements: extractRespElements(w),
UserAgent: r.UserAgent(),
Host: handlers.GetSourceIP(r),
})
} }
// GetObjectLegalHoldHandler - get legal hold configuration to object, // GetObjectLegalHoldHandler - get legal hold configuration to object,
@ -2506,6 +2589,10 @@ func (api objectAPIHandlers) GetObjectLegalHoldHandler(w http.ResponseWriter, r
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r))
return return
} }
if s3Error := checkRequestAuthType(ctx, r, policy.GetObjectLegalHoldAction, bucket, object); s3Error != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
return
}
getObjectInfo := objectAPI.GetObjectInfo getObjectInfo := objectAPI.GetObjectInfo
if api.CacheAPI() != nil { if api.CacheAPI() != nil {
@ -2518,12 +2605,25 @@ func (api objectAPIHandlers) GetObjectLegalHoldHandler(w http.ResponseWriter, r
return return
} }
if _, err = getObjectInfo(ctx, bucket, object, opts); err != nil { objInfo, err := getObjectInfo(ctx, bucket, object, opts)
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return return
} }
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL, guessIsBrowserReq(r)) legalHold := objectlock.GetObjectLegalHoldMeta(objInfo.UserDefined)
writeSuccessResponseXML(w, encodeResponse(legalHold))
// Notify object legal hold accessed via a GET request.
sendEvent(eventArgs{
EventName: event.ObjectAccessedGetLegalHold,
BucketName: bucket,
Object: objInfo,
ReqParams: extractReqParams(r),
RespElements: extractRespElements(w),
UserAgent: r.UserAgent(),
Host: handlers.GetSourceIP(r),
})
} }
// PutObjectRetentionHandler - set object hold configuration to object, // PutObjectRetentionHandler - set object hold configuration to object,
@ -2568,7 +2668,7 @@ func (api objectAPIHandlers) PutObjectRetentionHandler(w http.ResponseWriter, r
return return
} }
objRetention, err := parseObjectRetention(r.Body) objRetention, err := objectlock.ParseObjectRetention(r.Body)
if err != nil { if err != nil {
apiErr := errorCodes.ToAPIErr(ErrMalformedXML) apiErr := errorCodes.ToAPIErr(ErrMalformedXML)
apiErr.Description = err.Error() apiErr.Description = err.Error()
@ -2660,7 +2760,7 @@ func (api objectAPIHandlers) GetObjectRetentionHandler(w http.ResponseWriter, r
return return
} }
retention := getObjectRetentionMeta(objInfo.UserDefined) retention := objectlock.GetObjectRetentionMeta(objInfo.UserDefined)
writeSuccessResponseXML(w, encodeResponse(retention)) writeSuccessResponseXML(w, encodeResponse(retention))
// Notify object retention accessed via a GET request. // Notify object retention accessed via a GET request.

View file

@ -18,387 +18,12 @@ package cmd
import ( import (
"context" "context"
"encoding/xml"
"errors"
"fmt"
"io"
"net/http" "net/http"
"strings"
"sync"
"time"
xhttp "github.com/minio/minio/cmd/http"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/env" "github.com/minio/minio/pkg/objectlock"
) )
// RetentionMode - object retention mode.
type RetentionMode string
const (
// Governance - governance mode.
Governance RetentionMode = "GOVERNANCE"
// Compliance - compliance mode.
Compliance RetentionMode = "COMPLIANCE"
// Invalid - invalid retention mode.
Invalid RetentionMode = ""
)
func parseRetentionMode(modeStr string) (mode RetentionMode) {
switch strings.ToUpper(modeStr) {
case "GOVERNANCE":
mode = Governance
case "COMPLIANCE":
mode = Compliance
default:
mode = Invalid
}
return mode
}
var (
errMalformedBucketObjectConfig = errors.New("Invalid bucket object lock config")
errInvalidRetentionDate = errors.New("Date must be provided in ISO 8601 format")
errPastObjectLockRetainDate = errors.New("the retain until date must be in the future")
errUnknownWORMModeDirective = errors.New("unknown WORM mode directive")
errObjectLockMissingContentMD5 = errors.New("Content-MD5 HTTP header is required for Put Object requests with Object Lock parameters")
errObjectLockInvalidHeaders = errors.New("x-amz-object-lock-retain-until-date and x-amz-object-lock-mode must both be supplied")
)
const (
ntpServerEnv = "MINIO_NTP_SERVER"
)
var (
ntpServer = env.Get(ntpServerEnv, "")
)
// Retention - bucket level retention configuration.
type Retention struct {
Mode RetentionMode
Validity time.Duration
}
// IsEmpty - returns whether retention is empty or not.
func (r Retention) IsEmpty() bool {
return r.Mode == "" || r.Validity == 0
}
// Retain - check whether given date is retainable by validity time.
func (r Retention) Retain(created time.Time) bool {
t, err := UTCNowNTP()
if err != nil {
logger.LogIf(context.Background(), err)
// Retain
return true
}
return globalWORMEnabled || created.Add(r.Validity).After(t)
}
// BucketObjectLockConfig - map of bucket and retention configuration.
type BucketObjectLockConfig struct {
sync.RWMutex
retentionMap map[string]Retention
}
// Set - set retention configuration.
func (config *BucketObjectLockConfig) Set(bucketName string, retention Retention) {
config.Lock()
config.retentionMap[bucketName] = retention
config.Unlock()
}
// Get - Get retention configuration.
func (config *BucketObjectLockConfig) Get(bucketName string) (r Retention, ok bool) {
config.RLock()
defer config.RUnlock()
r, ok = config.retentionMap[bucketName]
return r, ok
}
// Remove - removes retention configuration.
func (config *BucketObjectLockConfig) Remove(bucketName string) {
config.Lock()
delete(config.retentionMap, bucketName)
config.Unlock()
}
func newBucketObjectLockConfig() *BucketObjectLockConfig {
return &BucketObjectLockConfig{
retentionMap: map[string]Retention{},
}
}
// DefaultRetention - default retention configuration.
type DefaultRetention struct {
XMLName xml.Name `xml:"DefaultRetention"`
Mode RetentionMode `xml:"Mode"`
Days *uint64 `xml:"Days"`
Years *uint64 `xml:"Years"`
}
// Maximum support retention days and years supported by AWS S3.
const (
// This tested by using `mc lock` command
maximumRetentionDays = 36500
maximumRetentionYears = 100
)
// UnmarshalXML - decodes XML data.
func (dr *DefaultRetention) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
// Make subtype to avoid recursive UnmarshalXML().
type defaultRetention DefaultRetention
retention := defaultRetention{}
if err := d.DecodeElement(&retention, &start); err != nil {
return err
}
switch string(retention.Mode) {
case "GOVERNANCE", "COMPLIANCE":
default:
return fmt.Errorf("unknown retention mode %v", retention.Mode)
}
if retention.Days == nil && retention.Years == nil {
return fmt.Errorf("either Days or Years must be specified")
}
if retention.Days != nil && retention.Years != nil {
return fmt.Errorf("either Days or Years must be specified, not both")
}
if retention.Days != nil {
if *retention.Days == 0 {
return fmt.Errorf("Default retention period must be a positive integer value for 'Days'")
}
if *retention.Days > maximumRetentionDays {
return fmt.Errorf("Default retention period too large for 'Days' %d", *retention.Days)
}
} else if *retention.Years == 0 {
return fmt.Errorf("Default retention period must be a positive integer value for 'Years'")
} else if *retention.Years > maximumRetentionYears {
return fmt.Errorf("Default retention period too large for 'Years' %d", *retention.Years)
}
*dr = DefaultRetention(retention)
return nil
}
// ObjectLockConfig - object lock configuration specified in
// https://docs.aws.amazon.com/AmazonS3/latest/API/Type_API_ObjectLockConfiguration.html
type ObjectLockConfig struct {
XMLNS string `xml:"xmlns,attr,omitempty"`
XMLName xml.Name `xml:"ObjectLockConfiguration"`
ObjectLockEnabled string `xml:"ObjectLockEnabled"`
Rule *struct {
DefaultRetention DefaultRetention `xml:"DefaultRetention"`
} `xml:"Rule,omitempty"`
}
// UnmarshalXML - decodes XML data.
func (config *ObjectLockConfig) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
// Make subtype to avoid recursive UnmarshalXML().
type objectLockConfig ObjectLockConfig
parsedConfig := objectLockConfig{}
if err := d.DecodeElement(&parsedConfig, &start); err != nil {
return err
}
if parsedConfig.ObjectLockEnabled != "Enabled" {
return fmt.Errorf("only 'Enabled' value is allowd to ObjectLockEnabled element")
}
*config = ObjectLockConfig(parsedConfig)
return nil
}
// ToRetention - convert to Retention type.
func (config *ObjectLockConfig) ToRetention() (r Retention) {
if config.Rule != nil {
r.Mode = config.Rule.DefaultRetention.Mode
t, err := UTCNowNTP()
if err != nil {
logger.LogIf(context.Background(), err)
// Do not change any configuration
// upon NTP failure.
return r
}
if config.Rule.DefaultRetention.Days != nil {
r.Validity = t.AddDate(0, 0, int(*config.Rule.DefaultRetention.Days)).Sub(t)
} else {
r.Validity = t.AddDate(int(*config.Rule.DefaultRetention.Years), 0, 0).Sub(t)
}
}
return r
}
func parseObjectLockConfig(reader io.Reader) (*ObjectLockConfig, error) {
config := ObjectLockConfig{}
if err := xml.NewDecoder(reader).Decode(&config); err != nil {
return nil, err
}
return &config, nil
}
func newObjectLockConfig() *ObjectLockConfig {
return &ObjectLockConfig{
ObjectLockEnabled: "Enabled",
}
}
// RetentionDate is a embedded type containing time.Time to unmarshal
// Date in Retention
type RetentionDate struct {
time.Time
}
// UnmarshalXML parses date from Expiration and validates date format
func (rDate *RetentionDate) UnmarshalXML(d *xml.Decoder, startElement xml.StartElement) error {
var dateStr string
err := d.DecodeElement(&dateStr, &startElement)
if err != nil {
return err
}
// While AWS documentation mentions that the date specified
// must be present in ISO 8601 format, in reality they allow
// users to provide RFC 3339 compliant dates.
retDate, err := time.Parse(time.RFC3339, dateStr)
if err != nil {
return errInvalidRetentionDate
}
*rDate = RetentionDate{retDate}
return nil
}
// MarshalXML encodes expiration date if it is non-zero and encodes
// empty string otherwise
func (rDate *RetentionDate) MarshalXML(e *xml.Encoder, startElement xml.StartElement) error {
if *rDate == (RetentionDate{time.Time{}}) {
return nil
}
return e.EncodeElement(rDate.Format(time.RFC3339), startElement)
}
// ObjectRetention specified in
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectRetention.html
type ObjectRetention struct {
XMLNS string `xml:"xmlns,attr,omitempty"`
XMLName xml.Name `xml:"Retention"`
Mode RetentionMode `xml:"Mode,omitempty"`
RetainUntilDate RetentionDate `xml:"RetainUntilDate,omitempty"`
}
func parseObjectRetention(reader io.Reader) (*ObjectRetention, error) {
ret := ObjectRetention{}
if err := xml.NewDecoder(reader).Decode(&ret); err != nil {
return nil, err
}
if ret.Mode != Compliance && ret.Mode != Governance {
return &ret, errUnknownWORMModeDirective
}
t, err := UTCNowNTP()
if err != nil {
logger.LogIf(context.Background(), err)
return &ret, errPastObjectLockRetainDate
}
if ret.RetainUntilDate.Before(t) {
return &ret, errPastObjectLockRetainDate
}
return &ret, nil
}
func isObjectLockRetentionRequested(h http.Header) bool {
if _, ok := h[xhttp.AmzObjectLockMode]; ok {
return true
}
if _, ok := h[xhttp.AmzObjectLockRetainUntilDate]; ok {
return true
}
return false
}
func isObjectLockLegalHoldRequested(h http.Header) bool {
_, ok := h[xhttp.AmzObjectLockLegalHold]
return ok
}
func isObjectLockGovernanceBypassSet(h http.Header) bool {
v, ok := h[xhttp.AmzObjectLockBypassGovernance]
if !ok {
return false
}
val := strings.Join(v, "")
return strings.ToLower(val) == "true"
}
func isObjectLockRequested(h http.Header) bool {
return isObjectLockLegalHoldRequested(h) || isObjectLockRetentionRequested(h)
}
func parseObjectLockRetentionHeaders(h http.Header) (rmode RetentionMode, r RetentionDate, err error) {
retMode, ok := h[xhttp.AmzObjectLockMode]
if ok {
rmode = parseRetentionMode(strings.Join(retMode, ""))
if rmode == Invalid {
return rmode, r, errUnknownWORMModeDirective
}
}
var retDate time.Time
dateStr, ok := h[xhttp.AmzObjectLockRetainUntilDate]
if ok {
// While AWS documentation mentions that the date specified
// must be present in ISO 8601 format, in reality they allow
// users to provide RFC 3339 compliant dates.
retDate, err = time.Parse(time.RFC3339, strings.Join(dateStr, ""))
if err != nil {
return rmode, r, errInvalidRetentionDate
}
t, err := UTCNowNTP()
if err != nil {
logger.LogIf(context.Background(), err)
return rmode, r, errPastObjectLockRetainDate
}
if retDate.Before(t) {
return rmode, r, errPastObjectLockRetainDate
}
}
if len(retMode) == 0 || len(dateStr) == 0 {
return rmode, r, errObjectLockInvalidHeaders
}
return rmode, RetentionDate{retDate}, nil
}
func getObjectRetentionMeta(meta map[string]string) ObjectRetention {
var mode RetentionMode
var retainTill RetentionDate
if modeStr, ok := meta[strings.ToLower(xhttp.AmzObjectLockMode)]; ok {
mode = parseRetentionMode(modeStr)
}
if tillStr, ok := meta[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)]; ok {
if t, e := time.Parse(time.RFC3339, tillStr); e == nil {
retainTill = RetentionDate{t.UTC()}
}
}
return ObjectRetention{Mode: mode, RetainUntilDate: retainTill}
}
// enforceRetentionBypassForDelete enforces whether an existing object under governance can be deleted // enforceRetentionBypassForDelete enforces whether an existing object under governance can be deleted
// with governance bypass headers set in the request. // with governance bypass headers set in the request.
// Objects under site wide WORM can never be overwritten. // Objects under site wide WORM can never be overwritten.
@ -424,16 +49,19 @@ func enforceRetentionBypassForDelete(ctx context.Context, r *http.Request, bucke
} }
return oi, toAPIErrorCode(ctx, err) return oi, toAPIErrorCode(ctx, err)
} }
ret := getObjectRetentionMeta(oi.UserDefined) ret := objectlock.GetObjectRetentionMeta(oi.UserDefined)
lhold := objectlock.GetObjectLegalHoldMeta(oi.UserDefined)
if lhold.Status == objectlock.ON {
return oi, ErrObjectLocked
}
// Here bucket does not support object lock // Here bucket does not support object lock
if ret.Mode == Invalid { if ret.Mode == objectlock.Invalid {
return oi, ErrNone return oi, ErrNone
} }
if ret.Mode != Compliance && ret.Mode != Governance { if ret.Mode != objectlock.Compliance && ret.Mode != objectlock.Governance {
return oi, ErrUnknownWORMModeDirective return oi, ErrUnknownWORMModeDirective
} }
t, err := UTCNowNTP() t, err := objectlock.UTCNowNTP()
if err != nil { if err != nil {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
return oi, ErrObjectLocked return oi, ErrObjectLocked
@ -441,7 +69,7 @@ func enforceRetentionBypassForDelete(ctx context.Context, r *http.Request, bucke
if ret.RetainUntilDate.Before(t) { if ret.RetainUntilDate.Before(t) {
return oi, ErrNone return oi, ErrNone
} }
if isObjectLockGovernanceBypassSet(r.Header) && ret.Mode == Governance && govBypassPerm == ErrNone { if objectlock.IsObjectLockGovernanceBypassSet(r.Header) && ret.Mode == objectlock.Governance && govBypassPerm == ErrNone {
return oi, ErrNone return oi, ErrNone
} }
return oi, ErrObjectLocked return oi, ErrObjectLocked
@ -453,7 +81,7 @@ func enforceRetentionBypassForDelete(ctx context.Context, r *http.Request, bucke
// For objects in "Governance" mode, overwrite is allowed if a) object retention date is past OR // For objects in "Governance" mode, overwrite is allowed if a) object retention date is past OR
// governance bypass headers are set and user has governance bypass permissions. // governance bypass headers are set and user has governance bypass permissions.
// Objects in compliance mode can be overwritten only if retention date is being extended. No mode change is permitted. // Objects in compliance mode can be overwritten only if retention date is being extended. No mode change is permitted.
func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, bucket, object string, getObjectInfoFn GetObjectInfoFn, govBypassPerm APIErrorCode, objRetention *ObjectRetention) (oi ObjectInfo, s3Err APIErrorCode) { func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, bucket, object string, getObjectInfoFn GetObjectInfoFn, govBypassPerm APIErrorCode, objRetention *objectlock.ObjectRetention) (oi ObjectInfo, s3Err APIErrorCode) {
if globalWORMEnabled { if globalWORMEnabled {
return oi, ErrObjectLocked return oi, ErrObjectLocked
} }
@ -474,24 +102,24 @@ func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, bucket,
return oi, toAPIErrorCode(ctx, err) return oi, toAPIErrorCode(ctx, err)
} }
ret := getObjectRetentionMeta(oi.UserDefined) ret := objectlock.GetObjectRetentionMeta(oi.UserDefined)
// no retention metadata on object // no retention metadata on object
if ret.Mode == Invalid { if ret.Mode == objectlock.Invalid {
if _, isWORMBucket := globalBucketObjectLockConfig.Get(bucket); !isWORMBucket { if _, isWORMBucket := globalBucketObjectLockConfig.Get(bucket); !isWORMBucket {
return oi, ErrInvalidBucketObjectLockConfiguration return oi, ErrInvalidBucketObjectLockConfiguration
} }
return oi, ErrNone return oi, ErrNone
} }
t, err := UTCNowNTP() t, err := objectlock.UTCNowNTP()
if err != nil { if err != nil {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
return oi, ErrObjectLocked return oi, ErrObjectLocked
} }
if ret.Mode == Compliance { if ret.Mode == objectlock.Compliance {
// Compliance retention mode cannot be changed and retention period cannot be shortened as per // Compliance retention mode cannot be changed and retention period cannot be shortened as per
// https://docs.aws.amazon.com/AmazonS3/latest/dev/object-lock-overview.html#object-lock-retention-modes // https://docs.aws.amazon.com/AmazonS3/latest/dev/object-lock-overview.html#object-lock-retention-modes
if objRetention.Mode != Compliance || objRetention.RetainUntilDate.Before(ret.RetainUntilDate.Time) { if objRetention.Mode != objectlock.Compliance || objRetention.RetainUntilDate.Before(ret.RetainUntilDate.Time) {
return oi, ErrObjectLocked return oi, ErrObjectLocked
} }
if objRetention.RetainUntilDate.Before(t) { if objRetention.RetainUntilDate.Before(t) {
@ -500,8 +128,8 @@ func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, bucket,
return oi, ErrNone return oi, ErrNone
} }
if ret.Mode == Governance { if ret.Mode == objectlock.Governance {
if !isObjectLockGovernanceBypassSet(r.Header) { if !objectlock.IsObjectLockGovernanceBypassSet(r.Header) {
if objRetention.RetainUntilDate.Before(t) { if objRetention.RetainUntilDate.Before(t) {
return oi, ErrInvalidRetentionDate return oi, ErrInvalidRetentionDate
} }
@ -515,111 +143,104 @@ func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, bucket,
return oi, ErrNone return oi, ErrNone
} }
// checkPutObjectRetentionAllowed enforces object retention policy for requests with WORM headers // checkPutObjectLockAllowed enforces object retention policy and legal hold policy
// for requests with WORM headers
// See https://docs.aws.amazon.com/AmazonS3/latest/dev/object-lock-managing.html for the spec. // See https://docs.aws.amazon.com/AmazonS3/latest/dev/object-lock-managing.html for the spec.
// For non-existing objects with object retention headers set, this method returns ErrNone if bucket has // For non-existing objects with object retention headers set, this method returns ErrNone if bucket has
// locking enabled and user has requisite permissions (s3:PutObjectRetention) // locking enabled and user has requisite permissions (s3:PutObjectRetention)
// If object exists on object store and site wide WORM enabled - this method // If object exists on object store and site wide WORM enabled - this method
// returns an error. For objects in "Governance" mode, overwrite is allowed if the retention date has expired. // returns an error. For objects in "Governance" mode, overwrite is allowed if the retention date has expired.
// For objects in "Compliance" mode, retention date cannot be shortened, and mode cannot be altered. // For objects in "Compliance" mode, retention date cannot be shortened, and mode cannot be altered.
func checkPutObjectRetentionAllowed(ctx context.Context, r *http.Request, bucket, object string, getObjectInfoFn GetObjectInfoFn, retentionPermErr APIErrorCode) (RetentionMode, RetentionDate, APIErrorCode) { // For objects with legal hold header set, the s3:PutObjectLegalHold permission is expected to be set
var mode RetentionMode // Both legal hold and retention can be applied independently on an object
var retainDate RetentionDate func checkPutObjectLockAllowed(ctx context.Context, r *http.Request, bucket, object string, getObjectInfoFn GetObjectInfoFn, retentionPermErr, legalHoldPermErr APIErrorCode) (objectlock.Mode, objectlock.RetentionDate, objectlock.ObjectLegalHold, APIErrorCode) {
var mode objectlock.Mode
var retainDate objectlock.RetentionDate
var legalHold objectlock.ObjectLegalHold
retention, isWORMBucket := globalBucketObjectLockConfig.Get(bucket) retention, isWORMBucket := globalBucketObjectLockConfig.Get(bucket)
retentionRequested := isObjectLockRequested(r.Header) retentionRequested := objectlock.IsObjectLockRetentionRequested(r.Header)
legalHoldRequested := objectlock.IsObjectLockLegalHoldRequested(r.Header)
var objExists bool var objExists bool
opts, err := getOpts(ctx, r, bucket, object) opts, err := getOpts(ctx, r, bucket, object)
if err != nil { if err != nil {
return mode, retainDate, toAPIErrorCode(ctx, err) return mode, retainDate, legalHold, toAPIErrorCode(ctx, err)
} }
if objInfo, err := getObjectInfoFn(ctx, bucket, object, opts); err == nil { if objInfo, err := getObjectInfoFn(ctx, bucket, object, opts); err == nil {
objExists = true objExists = true
r := getObjectRetentionMeta(objInfo.UserDefined) r := objectlock.GetObjectRetentionMeta(objInfo.UserDefined)
if globalWORMEnabled || r.Mode == Compliance { if globalWORMEnabled || r.Mode == objectlock.Compliance {
return mode, retainDate, ErrObjectLocked return mode, retainDate, legalHold, ErrObjectLocked
} }
mode = r.Mode mode = r.Mode
retainDate = r.RetainUntilDate retainDate = r.RetainUntilDate
legalHold = objectlock.GetObjectLegalHoldMeta(objInfo.UserDefined)
// Disallow overwriting an object on legal hold
if legalHold.Status == "ON" {
return mode, retainDate, legalHold, ErrObjectLocked
}
}
if legalHoldRequested {
if !isWORMBucket {
return mode, retainDate, legalHold, ErrInvalidBucketObjectLockConfiguration
}
var lerr error
if legalHold, lerr = objectlock.ParseObjectLockLegalHoldHeaders(r.Header); lerr != nil {
return mode, retainDate, legalHold, toAPIErrorCode(ctx, err)
}
} }
if retentionRequested { if retentionRequested {
if !isWORMBucket { if !isWORMBucket {
return mode, retainDate, ErrInvalidBucketObjectLockConfiguration return mode, retainDate, legalHold, ErrInvalidBucketObjectLockConfiguration
} }
rMode, rDate, err := parseObjectLockRetentionHeaders(r.Header) legalHold, err := objectlock.ParseObjectLockLegalHoldHeaders(r.Header)
if err != nil { if err != nil {
return mode, retainDate, toAPIErrorCode(ctx, err) return mode, retainDate, legalHold, toAPIErrorCode(ctx, err)
}
rMode, rDate, err := objectlock.ParseObjectLockRetentionHeaders(r.Header)
if err != nil {
return mode, retainDate, legalHold, toAPIErrorCode(ctx, err)
} }
// AWS S3 just creates a new version of object when an object is being overwritten. // AWS S3 just creates a new version of object when an object is being overwritten.
t, err := UTCNowNTP() t, err := objectlock.UTCNowNTP()
if err != nil { if err != nil {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
return mode, retainDate, ErrObjectLocked return mode, retainDate, legalHold, ErrObjectLocked
} }
if objExists && retainDate.After(t) { if objExists && retainDate.After(t) {
return mode, retainDate, ErrObjectLocked return mode, retainDate, legalHold, ErrObjectLocked
} }
if rMode == Invalid { if rMode == objectlock.Invalid {
return mode, retainDate, toAPIErrorCode(ctx, errObjectLockInvalidHeaders) return mode, retainDate, legalHold, toAPIErrorCode(ctx, objectlock.ErrObjectLockInvalidHeaders)
} }
if retentionPermErr != ErrNone { if retentionPermErr != ErrNone {
return mode, retainDate, retentionPermErr return mode, retainDate, legalHold, retentionPermErr
} }
return rMode, rDate, ErrNone return rMode, rDate, legalHold, ErrNone
} }
if !retentionRequested && isWORMBucket { if !retentionRequested && isWORMBucket {
if retention.IsEmpty() && (mode == Compliance || mode == Governance) { if retention.IsEmpty() && (mode == objectlock.Compliance || mode == objectlock.Governance) {
return mode, retainDate, ErrObjectLocked return mode, retainDate, legalHold, ErrObjectLocked
} }
if retentionPermErr != ErrNone { if retentionPermErr != ErrNone {
return mode, retainDate, retentionPermErr return mode, retainDate, legalHold, retentionPermErr
} }
t, err := UTCNowNTP() t, err := objectlock.UTCNowNTP()
if err != nil { if err != nil {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
return mode, retainDate, ErrObjectLocked return mode, retainDate, legalHold, ErrObjectLocked
} }
// AWS S3 just creates a new version of object when an object is being overwritten. // AWS S3 just creates a new version of object when an object is being overwritten.
if objExists && retainDate.After(t) { if objExists && retainDate.After(t) {
return mode, retainDate, ErrObjectLocked return mode, retainDate, legalHold, ErrObjectLocked
}
if !legalHoldRequested {
// inherit retention from bucket configuration
return retention.Mode, objectlock.RetentionDate{Time: t.Add(retention.Validity)}, legalHold, ErrNone
} }
// inherit retention from bucket configuration
return retention.Mode, RetentionDate{t.Add(retention.Validity)}, ErrNone
} }
return mode, retainDate, ErrNone return mode, retainDate, legalHold, ErrNone
}
// filter object lock metadata if s3:GetObjectRetention permission is denied or if isCopy flag set.
func filterObjectLockMetadata(ctx context.Context, r *http.Request, bucket, object string, metadata map[string]string, isCopy bool, getRetPerms APIErrorCode) map[string]string {
// Copy on write
dst := metadata
var copied bool
delKey := func(key string) {
if _, ok := metadata[key]; !ok {
return
}
if !copied {
dst = make(map[string]string, len(metadata))
for k, v := range metadata {
dst[k] = v
}
copied = true
}
delete(dst, key)
}
ret := getObjectRetentionMeta(metadata)
if ret.Mode == Invalid || isCopy {
delKey(xhttp.AmzObjectLockMode)
delKey(xhttp.AmzObjectLockRetainUntilDate)
return metadata
}
if getRetPerms == ErrNone {
return dst
}
delKey(xhttp.AmzObjectLockMode)
delKey(xhttp.AmzObjectLockRetainUntilDate)
return dst
} }

View file

@ -35,6 +35,7 @@ import (
"github.com/minio/minio/pkg/lifecycle" "github.com/minio/minio/pkg/lifecycle"
"github.com/minio/minio/pkg/madmin" "github.com/minio/minio/pkg/madmin"
xnet "github.com/minio/minio/pkg/net" xnet "github.com/minio/minio/pkg/net"
"github.com/minio/minio/pkg/objectlock"
"github.com/minio/minio/pkg/policy" "github.com/minio/minio/pkg/policy"
trace "github.com/minio/minio/pkg/trace" trace "github.com/minio/minio/pkg/trace"
) )
@ -430,7 +431,7 @@ func (client *peerRESTClient) PutBucketNotification(bucket string, rulesMap even
} }
// PutBucketObjectLockConfig - PUT bucket object lock configuration. // PutBucketObjectLockConfig - PUT bucket object lock configuration.
func (client *peerRESTClient) PutBucketObjectLockConfig(bucket string, retention Retention) error { func (client *peerRESTClient) PutBucketObjectLockConfig(bucket string, retention objectlock.Retention) error {
values := make(url.Values) values := make(url.Values)
values.Set(peerRESTBucket, bucket) values.Set(peerRESTBucket, bucket)

View file

@ -33,6 +33,7 @@ import (
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/event" "github.com/minio/minio/pkg/event"
"github.com/minio/minio/pkg/lifecycle" "github.com/minio/minio/pkg/lifecycle"
objectlock "github.com/minio/minio/pkg/objectlock"
"github.com/minio/minio/pkg/policy" "github.com/minio/minio/pkg/policy"
trace "github.com/minio/minio/pkg/trace" trace "github.com/minio/minio/pkg/trace"
) )
@ -845,7 +846,7 @@ func (s *peerRESTServer) PutBucketObjectLockConfigHandler(w http.ResponseWriter,
return return
} }
var retention Retention var retention objectlock.Retention
if r.ContentLength < 0 { if r.ContentLength < 0 {
s.writeErrorResponse(w, errInvalidArgument) s.writeErrorResponse(w, errInvalidArgument)
return return

View file

@ -39,7 +39,6 @@ import (
"sync" "sync"
"time" "time"
"github.com/beevik/ntp"
xhttp "github.com/minio/minio/cmd/http" xhttp "github.com/minio/minio/cmd/http"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/handlers" "github.com/minio/minio/pkg/handlers"
@ -354,17 +353,6 @@ func UTCNow() time.Time {
return time.Now().UTC() return time.Now().UTC()
} }
// UTCNowNTP - is similar in functionality to UTCNow()
// but only used when we do not wish to rely on system
// time.
func UTCNowNTP() (time.Time, error) {
// ntp server is disabled
if ntpServer == "" {
return UTCNow(), nil
}
return ntp.Time(ntpServer)
}
// GenETag - generate UUID based ETag // GenETag - generate UUID based ETag
func GenETag() string { func GenETag() string {
return ToS3ETag(getMD5Hash([]byte(mustGetUUID()))) return ToS3ETag(getMD5Hash([]byte(mustGetUUID())))

View file

@ -50,6 +50,7 @@ import (
"github.com/minio/minio/pkg/hash" "github.com/minio/minio/pkg/hash"
iampolicy "github.com/minio/minio/pkg/iam/policy" iampolicy "github.com/minio/minio/pkg/iam/policy"
"github.com/minio/minio/pkg/ioutil" "github.com/minio/minio/pkg/ioutil"
"github.com/minio/minio/pkg/objectlock"
"github.com/minio/minio/pkg/policy" "github.com/minio/minio/pkg/policy"
) )
@ -928,6 +929,7 @@ func (web *webAPIHandlers) Upload(w http.ResponseWriter, r *http.Request) {
object := vars["object"] object := vars["object"]
retPerms := ErrAccessDenied retPerms := ErrAccessDenied
holdPerms := ErrAccessDenied
claims, owner, authErr := webRequestAuthenticate(r) claims, owner, authErr := webRequestAuthenticate(r)
if authErr != nil { if authErr != nil {
@ -974,7 +976,17 @@ func (web *webAPIHandlers) Upload(w http.ResponseWriter, r *http.Request) {
}) { }) {
retPerms = ErrNone retPerms = ErrNone
} }
if globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: claims.AccessKey(),
Action: iampolicy.PutObjectLegalHoldAction,
BucketName: bucket,
ConditionValues: getConditionValues(r, "", claims.AccessKey(), claims.Map()),
IsOwner: owner,
ObjectName: object,
Claims: claims.Map(),
}) {
holdPerms = ErrNone
}
} }
// Check if bucket is a reserved bucket name or invalid. // Check if bucket is a reserved bucket name or invalid.
@ -1068,11 +1080,14 @@ func (web *webAPIHandlers) Upload(w http.ResponseWriter, r *http.Request) {
getObjectInfo = web.CacheAPI().GetObjectInfo getObjectInfo = web.CacheAPI().GetObjectInfo
} }
// enforce object retention rules // enforce object retention rules
retentionMode, retentionDate, s3Err := checkPutObjectRetentionAllowed(ctx, r, bucket, object, getObjectInfo, retPerms) retentionMode, retentionDate, legalHold, s3Err := checkPutObjectLockAllowed(ctx, r, bucket, object, getObjectInfo, retPerms, holdPerms)
if s3Err == ErrNone && retentionMode != "" { if s3Err == ErrNone && retentionMode != "" {
opts.UserDefined[xhttp.AmzObjectLockMode] = string(retentionMode) opts.UserDefined[xhttp.AmzObjectLockMode] = string(retentionMode)
opts.UserDefined[xhttp.AmzObjectLockRetainUntilDate] = retentionDate.UTC().Format(time.RFC3339) opts.UserDefined[xhttp.AmzObjectLockRetainUntilDate] = retentionDate.UTC().Format(time.RFC3339)
} }
if s3Err == ErrNone && legalHold.Status != "" {
opts.UserDefined[xhttp.AmzObjectLockLegalHold] = string(legalHold.Status)
}
if s3Err != ErrNone { if s3Err != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r))
return return
@ -1130,6 +1145,7 @@ func (web *webAPIHandlers) Download(w http.ResponseWriter, r *http.Request) {
token := r.URL.Query().Get("token") token := r.URL.Query().Get("token")
getRetPerms := ErrAccessDenied getRetPerms := ErrAccessDenied
legalHoldPerms := ErrAccessDenied
claims, owner, authErr := webTokenAuthenticate(token) claims, owner, authErr := webTokenAuthenticate(token)
if authErr != nil { if authErr != nil {
@ -1154,6 +1170,15 @@ func (web *webAPIHandlers) Download(w http.ResponseWriter, r *http.Request) {
}) { }) {
getRetPerms = ErrNone getRetPerms = ErrNone
} }
if globalPolicySys.IsAllowed(policy.Args{
Action: policy.GetObjectLegalHoldAction,
BucketName: bucket,
ConditionValues: getConditionValues(r, "", "", nil),
IsOwner: false,
ObjectName: object,
}) {
legalHoldPerms = ErrNone
}
} else { } else {
writeWebErrorResponse(w, authErr) writeWebErrorResponse(w, authErr)
return return
@ -1185,6 +1210,17 @@ func (web *webAPIHandlers) Download(w http.ResponseWriter, r *http.Request) {
}) { }) {
getRetPerms = ErrNone getRetPerms = ErrNone
} }
if globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: claims.AccessKey(),
Action: iampolicy.GetObjectLegalHoldAction,
BucketName: bucket,
ConditionValues: getConditionValues(r, "", claims.AccessKey(), claims.Map()),
IsOwner: owner,
ObjectName: object,
Claims: claims.Map(),
}) {
legalHoldPerms = ErrNone
}
} }
// Check if bucket is a reserved bucket name or invalid. // Check if bucket is a reserved bucket name or invalid.
@ -1209,7 +1245,7 @@ func (web *webAPIHandlers) Download(w http.ResponseWriter, r *http.Request) {
objInfo := gr.ObjInfo objInfo := gr.ObjInfo
// filter object lock metadata if permission does not permit // filter object lock metadata if permission does not permit
objInfo.UserDefined = filterObjectLockMetadata(ctx, r, bucket, object, objInfo.UserDefined, false, getRetPerms) objInfo.UserDefined = objectlock.FilterObjectLockMetadata(objInfo.UserDefined, getRetPerms != ErrNone, legalHoldPerms != ErrNone)
if objectAPI.IsEncryptionSupported() { if objectAPI.IsEncryptionSupported() {
if _, err = DecryptObjectInfo(&objInfo, r.Header); err != nil { if _, err = DecryptObjectInfo(&objInfo, r.Header); err != nil {
@ -1304,6 +1340,8 @@ func (web *webAPIHandlers) DownloadZip(w http.ResponseWriter, r *http.Request) {
token := r.URL.Query().Get("token") token := r.URL.Query().Get("token")
claims, owner, authErr := webTokenAuthenticate(token) claims, owner, authErr := webTokenAuthenticate(token)
var getRetPerms []APIErrorCode var getRetPerms []APIErrorCode
var legalHoldPerms []APIErrorCode
if authErr != nil { if authErr != nil {
if authErr == errNoAuthToken { if authErr == errNoAuthToken {
for _, object := range args.Objects { for _, object := range args.Objects {
@ -1329,6 +1367,18 @@ func (web *webAPIHandlers) DownloadZip(w http.ResponseWriter, r *http.Request) {
retentionPerm = ErrNone retentionPerm = ErrNone
} }
getRetPerms = append(getRetPerms, retentionPerm) getRetPerms = append(getRetPerms, retentionPerm)
legalHoldPerm := ErrAccessDenied
if globalPolicySys.IsAllowed(policy.Args{
Action: policy.GetObjectLegalHoldAction,
BucketName: args.BucketName,
ConditionValues: getConditionValues(r, "", "", nil),
IsOwner: false,
ObjectName: pathJoin(args.Prefix, object),
}) {
legalHoldPerm = ErrNone
}
legalHoldPerms = append(legalHoldPerms, legalHoldPerm)
} }
} else { } else {
writeWebErrorResponse(w, authErr) writeWebErrorResponse(w, authErr)
@ -1354,7 +1404,7 @@ func (web *webAPIHandlers) DownloadZip(w http.ResponseWriter, r *http.Request) {
retentionPerm := ErrAccessDenied retentionPerm := ErrAccessDenied
if globalIAMSys.IsAllowed(iampolicy.Args{ if globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: claims.AccessKey(), AccountName: claims.AccessKey(),
Action: iampolicy.GetObjectAction, Action: iampolicy.GetObjectRetentionAction,
BucketName: args.BucketName, BucketName: args.BucketName,
ConditionValues: getConditionValues(r, "", claims.AccessKey(), claims.Map()), ConditionValues: getConditionValues(r, "", claims.AccessKey(), claims.Map()),
IsOwner: owner, IsOwner: owner,
@ -1364,6 +1414,20 @@ func (web *webAPIHandlers) DownloadZip(w http.ResponseWriter, r *http.Request) {
retentionPerm = ErrNone retentionPerm = ErrNone
} }
getRetPerms = append(getRetPerms, retentionPerm) getRetPerms = append(getRetPerms, retentionPerm)
legalHoldPerm := ErrAccessDenied
if globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: claims.AccessKey(),
Action: iampolicy.GetObjectLegalHoldAction,
BucketName: args.BucketName,
ConditionValues: getConditionValues(r, "", claims.AccessKey(), claims.Map()),
IsOwner: owner,
ObjectName: pathJoin(args.Prefix, object),
Claims: claims.Map(),
}) {
legalHoldPerm = ErrNone
}
legalHoldPerms = append(legalHoldPerms, legalHoldPerm)
} }
} }
@ -1394,7 +1458,7 @@ func (web *webAPIHandlers) DownloadZip(w http.ResponseWriter, r *http.Request) {
info := gr.ObjInfo info := gr.ObjInfo
// filter object lock metadata if permission does not permit // filter object lock metadata if permission does not permit
info.UserDefined = filterObjectLockMetadata(ctx, r, args.BucketName, objectName, info.UserDefined, false, getRetPerms[i]) info.UserDefined = objectlock.FilterObjectLockMetadata(info.UserDefined, getRetPerms[i] != ErrNone, legalHoldPerms[i] != ErrNone)
if info.IsCompressed() { if info.IsCompressed() {
// For reporting, set the file size to the uncompressed size. // For reporting, set the file size to the uncompressed size.

View file

@ -5,6 +5,9 @@ MinIO server allows selectively specify WORM for specific objects or configuring
Object locking requires locking to be enabled on a bucket at the time of bucket creation. In addition, a default retention period and retention mode can be configured on a bucket to be Object locking requires locking to be enabled on a bucket at the time of bucket creation. In addition, a default retention period and retention mode can be configured on a bucket to be
applied to objects created in that bucket. applied to objects created in that bucket.
Independently of retention, an object can also be under legal hold. This effectively disallows
all deletes and overwrites of an object under legal hold until the hold is lifted.
## Get Started ## Get Started
### 1. Prerequisites ### 1. Prerequisites
@ -29,10 +32,22 @@ aws s3api put-object --bucket testbucket --key lockme --object-lock-mode GOVERNA
See https://docs.aws.amazon.com/AmazonS3/latest/dev/object-lock-overview.html for AWS S3 spec on See https://docs.aws.amazon.com/AmazonS3/latest/dev/object-lock-overview.html for AWS S3 spec on
object locking and permissions required for object retention and governance bypass overrides. object locking and permissions required for object retention and governance bypass overrides.
### Set legal hold on an object
PutObject API allows setting legal hold using `x-amz-object-lock-legal-hold` header.
```sh
aws s3api put-object --bucket testbucket --key legalhold --object-lock-legal-hold-status ON --body /etc/issue
```
See https://docs.aws.amazon.com/AmazonS3/latest/dev/object-lock-overview.html for AWS S3 spec on
object locking and permissions required for specifying legal hold.
### 3. Note ### 3. Note
- When global WORM is enabled by `MINIO_WORM` environment variable or `worm` field in configuration file supersedes bucket level WORM and `PUT object lock configuration` REST API is disabled. - When global WORM is enabled by `MINIO_WORM` environment variable or `worm` field in configuration file supersedes bucket level WORM and `PUT object lock configuration` REST API is disabled.
- In global WORM mode objects can never be overwritten - In global WORM mode objects can never be overwritten
- If an object is under legal hold, it cannot be overwritten unless the legal hold is explicitly removed.
- In `Compliance` mode, objects cannot be overwritten or deleted by anyone until retention period - In `Compliance` mode, objects cannot be overwritten or deleted by anyone until retention period
is expired. If user has requisite governance bypass permissions, an object's retention date can is expired. If user has requisite governance bypass permissions, an object's retention date can
be extended in `Compliance` mode. be extended in `Compliance` mode.

View file

@ -69,6 +69,24 @@ function make_bucket() {
return $rv return $rv
} }
function make_bucket_with_lock() {
# Make bucket
bucket_name="awscli-mint-test-bucket-$RANDOM"
function="${AWS} s3api create-bucket --bucket ${bucket_name} --object-lock-enabled-for-bucket"
# execute the test
out=$($function 2>&1)
rv=$?
# if command is successful print bucket_name or print error
if [ $rv -eq 0 ]; then
echo "${bucket_name}"
else
echo "${out}"
fi
return $rv
}
function delete_bucket() { function delete_bucket() {
# Delete bucket # Delete bucket
function="${AWS} s3 rb s3://${1} --force" function="${AWS} s3 rb s3://${1} --force"
@ -1430,6 +1448,98 @@ function test_worm_bucket() {
return $rv return $rv
} }
# Tests creating and deleting an object with legal hold.
function test_legal_hold() {
# log start time
start_time=$(get_time)
function="make_bucket_with_lock"
bucket_name=$(make_bucket_with_lock)
rv=$?
# if make bucket succeeds upload a file
if [ $rv -eq 0 ]; then
function="${AWS} s3api put-object --body ${MINT_DATA_DIR}/datafile-1-kB --bucket ${bucket_name} --key datafile-1-kB --object-lock-legal-hold-status ON"
out=$($function 2>&1)
errcnt=$(echo "$out" | sed -n '/Bucket is missing ObjectLockConfiguration/p' | wc -l)
# skip test for gateways
if [ "$errcnt" -eq 1 ]; then
return 0
fi
rv=$?
else
# if make bucket fails, $bucket_name has the error output
out="${bucket_name}"
fi
# if upload succeeds download the file
if [ $rv -eq 0 ]; then
function="${AWS} s3api head-object --bucket ${bucket_name} --key datafile-1-kB"
# save the ref to function being tested, so it can be logged
test_function=${function}
out=$($function 2>&1)
lhold=$(echo "$out" | jq -r .ObjectLockLegalHoldStatus)
rv=$?
fi
# if head-object succeeds, verify metadata has legal hold status
if [ $rv -eq 0 ]; then
if [ "${lhold}" == "" ]; then
rv=1
out="Legal hold was not applied"
fi
if [ "${lhold}" == "OFF" ]; then
rv=1
out="Legal hold was not applied"
fi
fi
if [ $rv -eq 0 ]; then
function="${AWS} s3api put-object-legal-hold --bucket ${bucket_name} --key datafile-1-kB --legal-hold Status=OFF"
out=$($function 2>&1)
rv=$?
else
# if make bucket fails, $bucket_name has the error output
out="${bucket_name}"
fi
# if upload succeeds download the file
if [ $rv -eq 0 ]; then
function="${AWS} s3api get-object-legal-hold --bucket ${bucket_name} --key datafile-1-kB"
# save the ref to function being tested, so it can be logged
test_function=${function}
out=$($function 2>&1)
lhold=$(echo "$out" | jq -r .LegalHold.Status)
rv=$?
fi
# if head-object succeeds, verify metadata has legal hold status
if [ $rv -eq 0 ]; then
if [ "${lhold}" == "" ]; then
rv=1
out="Legal hold was not applied"
fi
if [ "${lhold}" == "ON" ]; then
rv=1
out="Legal hold status not turned off"
fi
fi
# Attempt a delete on prefix shouldn't delete the directory since we have an object inside it.
if [ $rv -eq 0 ]; then
function="${AWS} s3api delete-object --bucket ${bucket_name} --key datafile-1-kB"
# save the ref to function being tested, so it can be logged
test_function=${function}
out=$($function 2>&1)
rv=$?
fi
if [ $rv -eq 0 ]; then
log_success "$(get_duration "$start_time")" "${test_function}"
else
# clean up and log error
${AWS} s3 rb s3://"${bucket_name}" --force > /dev/null 2>&1
log_failure "$(get_duration "$start_time")" "${function}" "${out}"
fi
return $rv
}
# main handler for all the tests. # main handler for all the tests.
main() { main() {
# Success tests # Success tests
@ -1455,7 +1565,9 @@ main() {
test_list_objects_error && \ test_list_objects_error && \
test_put_object_error && \ test_put_object_error && \
test_serverside_encryption_error && \ test_serverside_encryption_error && \
test_worm_bucket test_worm_bucket && \
test_legal_hold
return $? return $?
} }

View file

@ -30,6 +30,7 @@ const (
ObjectAccessedAll Name = 1 + iota ObjectAccessedAll Name = 1 + iota
ObjectAccessedGet ObjectAccessedGet
ObjectAccessedGetRetention ObjectAccessedGetRetention
ObjectAccessedGetLegalHold
ObjectAccessedHead ObjectAccessedHead
ObjectCreatedAll ObjectCreatedAll
ObjectCreatedCompleteMultipartUpload ObjectCreatedCompleteMultipartUpload
@ -37,6 +38,7 @@ const (
ObjectCreatedPost ObjectCreatedPost
ObjectCreatedPut ObjectCreatedPut
ObjectCreatedPutRetention ObjectCreatedPutRetention
ObjectCreatedPutLegalHold
ObjectRemovedAll ObjectRemovedAll
ObjectRemovedDelete ObjectRemovedDelete
) )
@ -45,9 +47,9 @@ const (
func (name Name) Expand() []Name { func (name Name) Expand() []Name {
switch name { switch name {
case ObjectAccessedAll: case ObjectAccessedAll:
return []Name{ObjectAccessedGet, ObjectAccessedHead, ObjectAccessedGetRetention} return []Name{ObjectAccessedGet, ObjectAccessedHead, ObjectAccessedGetRetention, ObjectAccessedGetLegalHold}
case ObjectCreatedAll: case ObjectCreatedAll:
return []Name{ObjectCreatedCompleteMultipartUpload, ObjectCreatedCopy, ObjectCreatedPost, ObjectCreatedPut, ObjectCreatedPutRetention} return []Name{ObjectCreatedCompleteMultipartUpload, ObjectCreatedCopy, ObjectCreatedPost, ObjectCreatedPut, ObjectCreatedPutRetention, ObjectCreatedPutLegalHold}
case ObjectRemovedAll: case ObjectRemovedAll:
return []Name{ObjectRemovedDelete} return []Name{ObjectRemovedDelete}
default: default:
@ -64,6 +66,8 @@ func (name Name) String() string {
return "s3:ObjectAccessed:Get" return "s3:ObjectAccessed:Get"
case ObjectAccessedGetRetention: case ObjectAccessedGetRetention:
return "s3:ObjectAccessed:GetRetention" return "s3:ObjectAccessed:GetRetention"
case ObjectAccessedGetLegalHold:
return "s3:ObjectAccessed:GetLegalHold"
case ObjectAccessedHead: case ObjectAccessedHead:
return "s3:ObjectAccessed:Head" return "s3:ObjectAccessed:Head"
case ObjectCreatedAll: case ObjectCreatedAll:
@ -78,6 +82,8 @@ func (name Name) String() string {
return "s3:ObjectCreated:Put" return "s3:ObjectCreated:Put"
case ObjectCreatedPutRetention: case ObjectCreatedPutRetention:
return "s3:ObjectCreated:PutRetention" return "s3:ObjectCreated:PutRetention"
case ObjectCreatedPutLegalHold:
return "s3:ObjectCreated:PutLegalHold"
case ObjectRemovedAll: case ObjectRemovedAll:
return "s3:ObjectRemoved:*" return "s3:ObjectRemoved:*"
case ObjectRemovedDelete: case ObjectRemovedDelete:
@ -138,6 +144,8 @@ func ParseName(s string) (Name, error) {
return ObjectAccessedGet, nil return ObjectAccessedGet, nil
case "s3:ObjectAccessed:GetRetention": case "s3:ObjectAccessed:GetRetention":
return ObjectAccessedGetRetention, nil return ObjectAccessedGetRetention, nil
case "s3:ObjectAccessed:GetLegalHold":
return ObjectAccessedGetLegalHold, nil
case "s3:ObjectAccessed:Head": case "s3:ObjectAccessed:Head":
return ObjectAccessedHead, nil return ObjectAccessedHead, nil
case "s3:ObjectCreated:*": case "s3:ObjectCreated:*":
@ -152,6 +160,8 @@ func ParseName(s string) (Name, error) {
return ObjectCreatedPut, nil return ObjectCreatedPut, nil
case "s3:ObjectCreated:PutRetention": case "s3:ObjectCreated:PutRetention":
return ObjectCreatedPutRetention, nil return ObjectCreatedPutRetention, nil
case "s3:ObjectCreated:PutLegalHold":
return ObjectCreatedPutLegalHold, nil
case "s3:ObjectRemoved:*": case "s3:ObjectRemoved:*":
return ObjectRemovedAll, nil return ObjectRemovedAll, nil
case "s3:ObjectRemoved:Delete": case "s3:ObjectRemoved:Delete":

View file

@ -28,8 +28,8 @@ func TestNameExpand(t *testing.T) {
name Name name Name
expectedResult []Name expectedResult []Name
}{ }{
{ObjectAccessedAll, []Name{ObjectAccessedGet, ObjectAccessedHead, ObjectAccessedGetRetention}}, {ObjectAccessedAll, []Name{ObjectAccessedGet, ObjectAccessedHead, ObjectAccessedGetRetention, ObjectAccessedGetLegalHold}},
{ObjectCreatedAll, []Name{ObjectCreatedCompleteMultipartUpload, ObjectCreatedCopy, ObjectCreatedPost, ObjectCreatedPut, ObjectCreatedPutRetention}}, {ObjectCreatedAll, []Name{ObjectCreatedCompleteMultipartUpload, ObjectCreatedCopy, ObjectCreatedPost, ObjectCreatedPut, ObjectCreatedPutRetention, ObjectCreatedPutLegalHold}},
{ObjectRemovedAll, []Name{ObjectRemovedDelete}}, {ObjectRemovedAll, []Name{ObjectRemovedDelete}},
{ObjectAccessedHead, []Name{ObjectAccessedHead}}, {ObjectAccessedHead, []Name{ObjectAccessedHead}},
} }
@ -60,6 +60,11 @@ func TestNameString(t *testing.T) {
{ObjectCreatedPut, "s3:ObjectCreated:Put"}, {ObjectCreatedPut, "s3:ObjectCreated:Put"},
{ObjectRemovedAll, "s3:ObjectRemoved:*"}, {ObjectRemovedAll, "s3:ObjectRemoved:*"},
{ObjectRemovedDelete, "s3:ObjectRemoved:Delete"}, {ObjectRemovedDelete, "s3:ObjectRemoved:Delete"},
{ObjectCreatedPutRetention, "s3:ObjectCreated:PutRetention"},
{ObjectCreatedPutLegalHold, "s3:ObjectCreated:PutLegalHold"},
{ObjectAccessedGetRetention, "s3:ObjectAccessed:GetRetention"},
{ObjectAccessedGetLegalHold, "s3:ObjectAccessed:GetLegalHold"},
{blankName, ""}, {blankName, ""},
} }

View file

@ -153,12 +153,12 @@ func TestRulesMapMatch(t *testing.T) {
func TestNewRulesMap(t *testing.T) { func TestNewRulesMap(t *testing.T) {
rulesMapCase1 := make(RulesMap) rulesMapCase1 := make(RulesMap)
rulesMapCase1.add([]Name{ObjectAccessedGet, ObjectAccessedHead, ObjectAccessedGetRetention}, rulesMapCase1.add([]Name{ObjectAccessedGet, ObjectAccessedHead, ObjectAccessedGetRetention, ObjectAccessedGetLegalHold},
"*", TargetID{"1", "webhook"}) "*", TargetID{"1", "webhook"})
rulesMapCase2 := make(RulesMap) rulesMapCase2 := make(RulesMap)
rulesMapCase2.add([]Name{ObjectAccessedGet, ObjectAccessedHead, rulesMapCase2.add([]Name{ObjectAccessedGet, ObjectAccessedHead,
ObjectCreatedPut, ObjectAccessedGetRetention}, "*", TargetID{"1", "webhook"}) ObjectCreatedPut, ObjectAccessedGetRetention, ObjectAccessedGetLegalHold}, "*", TargetID{"1", "webhook"})
rulesMapCase3 := make(RulesMap) rulesMapCase3 := make(RulesMap)
rulesMapCase3.add([]Name{ObjectRemovedDelete}, "2010*.jpg", TargetID{"1", "webhook"}) rulesMapCase3.add([]Name{ObjectRemovedDelete}, "2010*.jpg", TargetID{"1", "webhook"})

View file

@ -102,6 +102,12 @@ const (
// GetObjectRetentionAction - GetObjectRetention, GetObject, HeadObject Rest API action. // GetObjectRetentionAction - GetObjectRetention, GetObject, HeadObject Rest API action.
GetObjectRetentionAction = "s3:GetObjectRetention" GetObjectRetentionAction = "s3:GetObjectRetention"
// GetObjectLegalHoldAction - GetObjectLegalHold, GetObject Rest API action.
GetObjectLegalHoldAction = "s3:GetObjectLegalHold"
// PutObjectLegalHoldAction - PutObjectLegalHold, PutObject Rest API action.
PutObjectLegalHoldAction = "s3:PutObjectLegalHold"
// GetBucketObjectLockConfigurationAction - GetBucketObjectLockConfiguration Rest API action // GetBucketObjectLockConfigurationAction - GetBucketObjectLockConfiguration Rest API action
GetBucketObjectLockConfigurationAction = "s3:GetBucketObjectLockConfiguration" GetBucketObjectLockConfigurationAction = "s3:GetBucketObjectLockConfiguration"
@ -137,6 +143,8 @@ var supportedActions = map[Action]struct{}{
PutBucketLifecycleAction: {}, PutBucketLifecycleAction: {},
PutObjectRetentionAction: {}, PutObjectRetentionAction: {},
GetObjectRetentionAction: {}, GetObjectRetentionAction: {},
GetObjectLegalHoldAction: {},
PutObjectLegalHoldAction: {},
PutBucketObjectLockConfigurationAction: {}, PutBucketObjectLockConfigurationAction: {},
GetBucketObjectLockConfigurationAction: {}, GetBucketObjectLockConfigurationAction: {},
BypassGovernanceModeAction: {}, BypassGovernanceModeAction: {},
@ -154,6 +162,8 @@ func (action Action) isObjectAction() bool {
return true return true
case PutObjectRetentionAction, GetObjectRetentionAction: case PutObjectRetentionAction, GetObjectRetentionAction:
return true return true
case PutObjectLegalHoldAction, GetObjectLegalHoldAction:
return true
} }
return false return false
@ -267,6 +277,8 @@ var actionConditionKeyMap = map[Action]condition.KeySet{
}, condition.CommonKeys...)...), }, condition.CommonKeys...)...),
PutObjectRetentionAction: condition.NewKeySet(condition.CommonKeys...), PutObjectRetentionAction: condition.NewKeySet(condition.CommonKeys...),
GetObjectRetentionAction: condition.NewKeySet(condition.CommonKeys...), GetObjectRetentionAction: condition.NewKeySet(condition.CommonKeys...),
PutObjectLegalHoldAction: condition.NewKeySet(condition.CommonKeys...),
GetObjectLegalHoldAction: condition.NewKeySet(condition.CommonKeys...),
BypassGovernanceModeAction: condition.NewKeySet(condition.CommonKeys...), BypassGovernanceModeAction: condition.NewKeySet(condition.CommonKeys...),
BypassGovernanceRetentionAction: condition.NewKeySet(condition.CommonKeys...), BypassGovernanceRetentionAction: condition.NewKeySet(condition.CommonKeys...),
GetBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...), GetBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...),

View file

@ -0,0 +1,521 @@
/*
* 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 objectlock
import (
"context"
"encoding/xml"
"errors"
"fmt"
"io"
"net/http"
"strings"
"sync"
"time"
"github.com/beevik/ntp"
xhttp "github.com/minio/minio/cmd/http"
"github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/env"
)
// Mode - object retention mode.
type Mode string
const (
// Governance - governance mode.
Governance Mode = "GOVERNANCE"
// Compliance - compliance mode.
Compliance Mode = "COMPLIANCE"
// Invalid - invalid retention mode.
Invalid Mode = ""
)
func parseMode(modeStr string) (mode Mode) {
switch strings.ToUpper(modeStr) {
case "GOVERNANCE":
mode = Governance
case "COMPLIANCE":
mode = Compliance
default:
mode = Invalid
}
return mode
}
// LegalHoldStatus - object legal hold status.
type LegalHoldStatus string
const (
// ON -legal hold is on.
ON LegalHoldStatus = "ON"
// OFF -legal hold is off.
OFF LegalHoldStatus = "OFF"
)
func parseLegalHoldStatus(holdStr string) LegalHoldStatus {
switch strings.ToUpper(holdStr) {
case "ON":
return ON
case "OFF":
return OFF
}
return LegalHoldStatus("")
}
var (
// ErrMalformedBucketObjectConfig -indicates that the bucket object lock config is malformed
ErrMalformedBucketObjectConfig = errors.New("invalid bucket object lock config")
// ErrInvalidRetentionDate - indicates that retention date needs to be in ISO 8601 format
ErrInvalidRetentionDate = errors.New("date must be provided in ISO 8601 format")
// ErrPastObjectLockRetainDate - indicates that retention date must be in the future
ErrPastObjectLockRetainDate = errors.New("the retain until date must be in the future")
// ErrUnknownWORMModeDirective - indicates that the retention mode is invalid
ErrUnknownWORMModeDirective = errors.New("unknown WORM mode directive")
// ErrObjectLockMissingContentMD5 - indicates missing Content-MD5 header for put object requests with locking
ErrObjectLockMissingContentMD5 = errors.New("content-MD5 HTTP header is required for Put Object requests with Object Lock parameters")
// ErrObjectLockInvalidHeaders indicates that object lock headers are missing
ErrObjectLockInvalidHeaders = errors.New("x-amz-object-lock-retain-until-date and x-amz-object-lock-mode must both be supplied")
// ErrMalformedXML - generic error indicating malformed XML
ErrMalformedXML = errors.New("the XML you provided was not well-formed or did not validate against our published schema")
)
const (
ntpServerEnv = "MINIO_NTP_SERVER"
)
var (
ntpServer = env.Get(ntpServerEnv, "")
)
// UTCNowNTP - is similar in functionality to UTCNow()
// but only used when we do not wish to rely on system
// time.
func UTCNowNTP() (time.Time, error) {
// ntp server is disabled
if ntpServer == "" {
return time.Now().UTC(), nil
}
return ntp.Time(ntpServer)
}
// Retention - bucket level retention configuration.
type Retention struct {
Mode Mode
Validity time.Duration
}
// IsEmpty - returns whether retention is empty or not.
func (r Retention) IsEmpty() bool {
return r.Mode == "" || r.Validity == 0
}
// Retain - check whether given date is retainable by validity time.
func (r Retention) Retain(created time.Time) bool {
t, err := UTCNowNTP()
if err != nil {
logger.LogIf(context.Background(), err)
// Retain
return true
}
return created.Add(r.Validity).After(t)
}
// BucketObjectLockConfig - map of bucket and retention configuration.
type BucketObjectLockConfig struct {
sync.RWMutex
retentionMap map[string]Retention
}
// Set - set retention configuration.
func (config *BucketObjectLockConfig) Set(bucketName string, retention Retention) {
config.Lock()
config.retentionMap[bucketName] = retention
config.Unlock()
}
// Get - Get retention configuration.
func (config *BucketObjectLockConfig) Get(bucketName string) (r Retention, ok bool) {
config.RLock()
defer config.RUnlock()
r, ok = config.retentionMap[bucketName]
return r, ok
}
// Remove - removes retention configuration.
func (config *BucketObjectLockConfig) Remove(bucketName string) {
config.Lock()
delete(config.retentionMap, bucketName)
config.Unlock()
}
// NewBucketObjectLockConfig returns initialized BucketObjectLockConfig
func NewBucketObjectLockConfig() *BucketObjectLockConfig {
return &BucketObjectLockConfig{
retentionMap: map[string]Retention{},
}
}
// DefaultRetention - default retention configuration.
type DefaultRetention struct {
XMLName xml.Name `xml:"DefaultRetention"`
Mode Mode `xml:"Mode"`
Days *uint64 `xml:"Days"`
Years *uint64 `xml:"Years"`
}
// Maximum support retention days and years supported by AWS S3.
const (
// This tested by using `mc lock` command
maximumRetentionDays = 36500
maximumRetentionYears = 100
)
// UnmarshalXML - decodes XML data.
func (dr *DefaultRetention) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
// Make subtype to avoid recursive UnmarshalXML().
type defaultRetention DefaultRetention
retention := defaultRetention{}
if err := d.DecodeElement(&retention, &start); err != nil {
return err
}
switch string(retention.Mode) {
case "GOVERNANCE", "COMPLIANCE":
default:
return fmt.Errorf("unknown retention mode %v", retention.Mode)
}
if retention.Days == nil && retention.Years == nil {
return fmt.Errorf("either Days or Years must be specified")
}
if retention.Days != nil && retention.Years != nil {
return fmt.Errorf("either Days or Years must be specified, not both")
}
if retention.Days != nil {
if *retention.Days == 0 {
return fmt.Errorf("Default retention period must be a positive integer value for 'Days'")
}
if *retention.Days > maximumRetentionDays {
return fmt.Errorf("Default retention period too large for 'Days' %d", *retention.Days)
}
} else if *retention.Years == 0 {
return fmt.Errorf("Default retention period must be a positive integer value for 'Years'")
} else if *retention.Years > maximumRetentionYears {
return fmt.Errorf("Default retention period too large for 'Years' %d", *retention.Years)
}
*dr = DefaultRetention(retention)
return nil
}
// Config - object lock configuration specified in
// https://docs.aws.amazon.com/AmazonS3/latest/API/Type_API_ObjectLockConfiguration.html
type Config struct {
XMLNS string `xml:"xmlns,attr,omitempty"`
XMLName xml.Name `xml:"ObjectLockConfiguration"`
ObjectLockEnabled string `xml:"ObjectLockEnabled"`
Rule *struct {
DefaultRetention DefaultRetention `xml:"DefaultRetention"`
} `xml:"Rule,omitempty"`
}
// UnmarshalXML - decodes XML data.
func (config *Config) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
// Make subtype to avoid recursive UnmarshalXML().
type objectLockConfig Config
parsedConfig := objectLockConfig{}
if err := d.DecodeElement(&parsedConfig, &start); err != nil {
return err
}
if parsedConfig.ObjectLockEnabled != "Enabled" {
return fmt.Errorf("only 'Enabled' value is allowd to ObjectLockEnabled element")
}
*config = Config(parsedConfig)
return nil
}
// ToRetention - convert to Retention type.
func (config *Config) ToRetention() (r Retention) {
if config.Rule != nil {
r.Mode = config.Rule.DefaultRetention.Mode
t, err := UTCNowNTP()
if err != nil {
logger.LogIf(context.Background(), err)
// Do not change any configuration
// upon NTP failure.
return r
}
if config.Rule.DefaultRetention.Days != nil {
r.Validity = t.AddDate(0, 0, int(*config.Rule.DefaultRetention.Days)).Sub(t)
} else {
r.Validity = t.AddDate(int(*config.Rule.DefaultRetention.Years), 0, 0).Sub(t)
}
}
return r
}
// ParseObjectLockConfig parses ObjectLockConfig from xml
func ParseObjectLockConfig(reader io.Reader) (*Config, error) {
config := Config{}
if err := xml.NewDecoder(reader).Decode(&config); err != nil {
return nil, err
}
return &config, nil
}
// NewObjectLockConfig returns a initialized objectlock.Config struct
func NewObjectLockConfig() *Config {
return &Config{
ObjectLockEnabled: "Enabled",
}
}
// RetentionDate is a embedded type containing time.Time to unmarshal
// Date in Retention
type RetentionDate struct {
time.Time
}
// UnmarshalXML parses date from Retention and validates date format
func (rDate *RetentionDate) UnmarshalXML(d *xml.Decoder, startElement xml.StartElement) error {
var dateStr string
err := d.DecodeElement(&dateStr, &startElement)
if err != nil {
return err
}
// While AWS documentation mentions that the date specified
// must be present in ISO 8601 format, in reality they allow
// users to provide RFC 3339 compliant dates.
retDate, err := time.Parse(time.RFC3339, dateStr)
if err != nil {
return ErrInvalidRetentionDate
}
*rDate = RetentionDate{retDate}
return nil
}
// MarshalXML encodes expiration date if it is non-zero and encodes
// empty string otherwise
func (rDate *RetentionDate) MarshalXML(e *xml.Encoder, startElement xml.StartElement) error {
if *rDate == (RetentionDate{time.Time{}}) {
return nil
}
return e.EncodeElement(rDate.Format(time.RFC3339), startElement)
}
// ObjectRetention specified in
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectRetention.html
type ObjectRetention struct {
XMLNS string `xml:"xmlns,attr,omitempty"`
XMLName xml.Name `xml:"Retention"`
Mode Mode `xml:"Mode,omitempty"`
RetainUntilDate RetentionDate `xml:"RetainUntilDate,omitempty"`
}
// ParseObjectRetention constructs ObjectRetention struct from xml input
func ParseObjectRetention(reader io.Reader) (*ObjectRetention, error) {
ret := ObjectRetention{}
if err := xml.NewDecoder(reader).Decode(&ret); err != nil {
return nil, err
}
if ret.Mode != Compliance && ret.Mode != Governance {
return &ret, ErrUnknownWORMModeDirective
}
t, err := UTCNowNTP()
if err != nil {
logger.LogIf(context.Background(), err)
return &ret, ErrPastObjectLockRetainDate
}
if ret.RetainUntilDate.Before(t) {
return &ret, ErrPastObjectLockRetainDate
}
return &ret, nil
}
// IsObjectLockRetentionRequested returns true if object lock retention headers are set.
func IsObjectLockRetentionRequested(h http.Header) bool {
if _, ok := h[xhttp.AmzObjectLockMode]; ok {
return true
}
if _, ok := h[xhttp.AmzObjectLockRetainUntilDate]; ok {
return true
}
return false
}
// IsObjectLockLegalHoldRequested returns true if object lock legal hold header is set.
func IsObjectLockLegalHoldRequested(h http.Header) bool {
_, ok := h[xhttp.AmzObjectLockLegalHold]
return ok
}
// IsObjectLockGovernanceBypassSet returns true if object lock governance bypass header is set.
func IsObjectLockGovernanceBypassSet(h http.Header) bool {
return strings.ToLower(h.Get(xhttp.AmzObjectLockBypassGovernance)) == "true"
}
// IsObjectLockRequested returns true if legal hold or object lock retention headers are requested.
func IsObjectLockRequested(h http.Header) bool {
return IsObjectLockLegalHoldRequested(h) || IsObjectLockRetentionRequested(h)
}
// ParseObjectLockRetentionHeaders parses http headers to extract retention mode and retention date
func ParseObjectLockRetentionHeaders(h http.Header) (rmode Mode, r RetentionDate, err error) {
retMode := h.Get(xhttp.AmzObjectLockMode)
dateStr := h.Get(xhttp.AmzObjectLockRetainUntilDate)
if len(retMode) == 0 || len(dateStr) == 0 {
return rmode, r, ErrObjectLockInvalidHeaders
}
rmode = parseMode(retMode)
if rmode == Invalid {
return rmode, r, ErrUnknownWORMModeDirective
}
var retDate time.Time
// While AWS documentation mentions that the date specified
// must be present in ISO 8601 format, in reality they allow
// users to provide RFC 3339 compliant dates.
retDate, err = time.Parse(time.RFC3339, dateStr)
if err != nil {
return rmode, r, ErrInvalidRetentionDate
}
t, err := UTCNowNTP()
if err != nil {
logger.LogIf(context.Background(), err)
return rmode, r, ErrPastObjectLockRetainDate
}
if retDate.Before(t) {
return rmode, r, ErrPastObjectLockRetainDate
}
return rmode, RetentionDate{retDate}, nil
}
// GetObjectRetentionMeta constructs ObjectRetention from metadata
func GetObjectRetentionMeta(meta map[string]string) ObjectRetention {
var mode Mode
var retainTill RetentionDate
if modeStr, ok := meta[strings.ToLower(xhttp.AmzObjectLockMode)]; ok {
mode = parseMode(modeStr)
}
if tillStr, ok := meta[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)]; ok {
if t, e := time.Parse(time.RFC3339, tillStr); e == nil {
retainTill = RetentionDate{t.UTC()}
}
}
return ObjectRetention{Mode: mode, RetainUntilDate: retainTill}
}
// GetObjectLegalHoldMeta constructs ObjectLegalHold from metadata
func GetObjectLegalHoldMeta(meta map[string]string) ObjectLegalHold {
holdStr, ok := meta[strings.ToLower(xhttp.AmzObjectLockLegalHold)]
if ok {
return ObjectLegalHold{Status: parseLegalHoldStatus(holdStr)}
}
return ObjectLegalHold{}
}
// ParseObjectLockLegalHoldHeaders parses request headers to construct ObjectLegalHold
func ParseObjectLockLegalHoldHeaders(h http.Header) (lhold ObjectLegalHold, err error) {
holdStatus, ok := h[xhttp.AmzObjectLockLegalHold]
if ok {
lh := parseLegalHoldStatus(strings.Join(holdStatus, ""))
if lh != ON && lh != OFF {
return lhold, ErrUnknownWORMModeDirective
}
lhold = ObjectLegalHold{Status: lh}
}
return lhold, nil
}
// ObjectLegalHold specified in
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectLegalHold.html
type ObjectLegalHold struct {
XMLNS string `xml:"xmlns,attr,omitempty"`
XMLName xml.Name `xml:"LegalHold"`
Status LegalHoldStatus `xml:"Status,omitempty"`
}
// ParseObjectLegalHold decodes the XML into ObjectLegalHold
func ParseObjectLegalHold(reader io.Reader) (hold *ObjectLegalHold, err error) {
if err = xml.NewDecoder(reader).Decode(&hold); err != nil {
return
}
if hold.Status != ON && hold.Status != OFF {
return nil, ErrMalformedXML
}
return
}
// FilterObjectLockMetadata filters object lock metadata if s3:GetObjectRetention permission is denied or if isCopy flag set.
func FilterObjectLockMetadata(metadata map[string]string, filterRetention, filterLegalHold bool) map[string]string {
// Copy on write
dst := metadata
var copied bool
delKey := func(key string) {
if _, ok := metadata[key]; !ok {
return
}
if !copied {
dst = make(map[string]string, len(metadata))
for k, v := range metadata {
dst[k] = v
}
copied = true
}
delete(dst, key)
}
legalHold := GetObjectLegalHoldMeta(metadata)
if legalHold.Status == "" || filterLegalHold {
delKey(xhttp.AmzObjectLockLegalHold)
}
ret := GetObjectRetentionMeta(metadata)
if ret.Mode == Invalid || filterRetention {
delKey(xhttp.AmzObjectLockMode)
delKey(xhttp.AmzObjectLockRetainUntilDate)
return dst
}
return dst
}

View file

@ -0,0 +1,567 @@
/*
* 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 objectlock
import (
"encoding/xml"
"fmt"
"net/http"
"reflect"
"strings"
"testing"
"time"
xhttp "github.com/minio/minio/cmd/http"
)
func TestParseMode(t *testing.T) {
testCases := []struct {
value string
expectedMode Mode
}{
{
value: "governance",
expectedMode: Governance,
},
{
value: "complIAnce",
expectedMode: Compliance,
},
{
value: "gce",
expectedMode: Invalid,
},
}
for _, tc := range testCases {
if parseMode(tc.value) != tc.expectedMode {
t.Errorf("Expected Mode %s, got %s", tc.expectedMode, parseMode(tc.value))
}
}
}
func TestParseLegalHoldStatus(t *testing.T) {
tests := []struct {
value string
expectedStatus LegalHoldStatus
}{
{
value: "ON",
expectedStatus: ON,
},
{
value: "Off",
expectedStatus: OFF,
},
{
value: "x",
expectedStatus: "",
},
}
for _, tt := range tests {
actualStatus := parseLegalHoldStatus(tt.value)
if actualStatus != tt.expectedStatus {
t.Errorf("Expected legal hold status %s, got %s", tt.expectedStatus, actualStatus)
}
}
}
// TestUnmarshalDefaultRetention checks if default retention
// marshaling and unmarshaling work as expected
func TestUnmarshalDefaultRetention(t *testing.T) {
days := uint64(4)
years := uint64(1)
zerodays := uint64(0)
invalidDays := uint64(maximumRetentionDays + 1)
tests := []struct {
value DefaultRetention
expectedErr error
expectErr bool
}{
{
value: DefaultRetention{Mode: "retain"},
expectedErr: fmt.Errorf("unknown retention mode retain"),
expectErr: true,
},
{
value: DefaultRetention{Mode: "GOVERNANCE"},
expectedErr: fmt.Errorf("either Days or Years must be specified"),
expectErr: true,
},
{
value: DefaultRetention{Mode: "GOVERNANCE", Days: &days},
expectedErr: nil,
expectErr: false,
},
{
value: DefaultRetention{Mode: "GOVERNANCE", Years: &years},
expectedErr: nil,
expectErr: false,
},
{
value: DefaultRetention{Mode: "GOVERNANCE", Days: &days, Years: &years},
expectedErr: fmt.Errorf("either Days or Years must be specified, not both"),
expectErr: true,
},
{
value: DefaultRetention{Mode: "GOVERNANCE", Days: &zerodays},
expectedErr: fmt.Errorf("Default retention period must be a positive integer value for 'Days'"),
expectErr: true,
},
{
value: DefaultRetention{Mode: "GOVERNANCE", Days: &invalidDays},
expectedErr: fmt.Errorf("Default retention period too large for 'Days' %d", invalidDays),
expectErr: true,
},
}
for _, tt := range tests {
d, err := xml.MarshalIndent(&tt.value, "", "\t")
if err != nil {
t.Fatal(err)
}
var dr DefaultRetention
err = xml.Unmarshal(d, &dr)
if tt.expectedErr == nil {
if err != nil {
t.Fatalf("error: expected = <nil>, got = %v", err)
}
} else if err == nil {
t.Fatalf("error: expected = %v, got = <nil>", tt.expectedErr)
} else if tt.expectedErr.Error() != err.Error() {
t.Fatalf("error: expected = %v, got = %v", tt.expectedErr, err)
}
}
}
func TestParseObjectLockConfig(t *testing.T) {
tests := []struct {
value string
expectedErr error
expectErr bool
}{
{
value: "<ObjectLockConfiguration><ObjectLockEnabled>yes</ObjectLockEnabled></ObjectLockConfiguration>",
expectedErr: fmt.Errorf("only 'Enabled' value is allowd to ObjectLockEnabled element"),
expectErr: true,
},
{
value: "<ObjectLockConfiguration><ObjectLockEnabled>Enabled</ObjectLockEnabled><Rule><DefaultRetention><Mode>COMPLIANCE</Mode><Days>0</Days></DefaultRetention></Rule></ObjectLockConfiguration>",
expectedErr: fmt.Errorf("Default retention period must be a positive integer value for 'Days'"),
expectErr: true,
},
{
value: "<ObjectLockConfiguration><ObjectLockEnabled>Enabled</ObjectLockEnabled><Rule><DefaultRetention><Mode>COMPLIANCE</Mode><Days>30</Days></DefaultRetention></Rule></ObjectLockConfiguration>",
expectedErr: nil,
expectErr: false,
},
}
for _, tt := range tests {
_, err := ParseObjectLockConfig(strings.NewReader(tt.value))
if tt.expectedErr == nil {
if err != nil {
t.Fatalf("error: expected = <nil>, got = %v", err)
}
} else if err == nil {
t.Fatalf("error: expected = %v, got = <nil>", tt.expectedErr)
} else if tt.expectedErr.Error() != err.Error() {
t.Fatalf("error: expected = %v, got = %v", tt.expectedErr, err)
}
}
}
func TestParseObjectRetention(t *testing.T) {
tests := []struct {
value string
expectedErr error
expectErr bool
}{
{
value: "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Retention xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Mode>string</Mode><RetainUntilDate>2020-01-02T15:04:05Z</RetainUntilDate></Retention>",
expectedErr: ErrUnknownWORMModeDirective,
expectErr: true,
},
{
value: "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Retention xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Mode>COMPLIANCE</Mode><RetainUntilDate>2017-01-02T15:04:05Z</RetainUntilDate></Retention>",
expectedErr: ErrPastObjectLockRetainDate,
expectErr: true,
},
{
value: "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Retention xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Mode>GOVERNANCE</Mode><RetainUntilDate>2057-01-02T15:04:05Z</RetainUntilDate></Retention>",
expectedErr: nil,
expectErr: false,
},
}
for _, tt := range tests {
_, err := ParseObjectRetention(strings.NewReader(tt.value))
if tt.expectedErr == nil {
if err != nil {
t.Fatalf("error: expected = <nil>, got = %v", err)
}
} else if err == nil {
t.Fatalf("error: expected = %v, got = <nil>", tt.expectedErr)
} else if tt.expectedErr.Error() != err.Error() {
t.Fatalf("error: expected = %v, got = %v", tt.expectedErr, err)
}
}
}
func TestIsObjectLockRequested(t *testing.T) {
tests := []struct {
header http.Header
expectedVal bool
}{
{
header: http.Header{
"Authorization": []string{"AWS4-HMAC-SHA256 <cred_string>"},
"X-Amz-Content-Sha256": []string{""},
"Content-Encoding": []string{""},
},
expectedVal: false,
},
{
header: http.Header{
xhttp.AmzObjectLockLegalHold: []string{""},
},
expectedVal: true,
},
{
header: http.Header{
xhttp.AmzObjectLockRetainUntilDate: []string{""},
xhttp.AmzObjectLockMode: []string{""},
},
expectedVal: true,
},
{
header: http.Header{
xhttp.AmzObjectLockBypassGovernance: []string{""},
},
expectedVal: false,
},
}
for _, tt := range tests {
actualVal := IsObjectLockRequested(tt.header)
if actualVal != tt.expectedVal {
t.Fatalf("error: expected %v, actual %v", tt.expectedVal, actualVal)
}
}
}
func TestIsObjectLockGovernanceBypassSet(t *testing.T) {
tests := []struct {
header http.Header
expectedVal bool
}{
{
header: http.Header{
"Authorization": []string{"AWS4-HMAC-SHA256 <cred_string>"},
"X-Amz-Content-Sha256": []string{""},
"Content-Encoding": []string{""},
},
expectedVal: false,
},
{
header: http.Header{
xhttp.AmzObjectLockLegalHold: []string{""},
},
expectedVal: false,
},
{
header: http.Header{
xhttp.AmzObjectLockRetainUntilDate: []string{""},
xhttp.AmzObjectLockMode: []string{""},
},
expectedVal: false,
},
{
header: http.Header{
xhttp.AmzObjectLockBypassGovernance: []string{""},
},
expectedVal: false,
},
{
header: http.Header{
xhttp.AmzObjectLockBypassGovernance: []string{"true"},
},
expectedVal: true,
},
}
for _, tt := range tests {
actualVal := IsObjectLockGovernanceBypassSet(tt.header)
if actualVal != tt.expectedVal {
t.Fatalf("error: expected %v, actual %v", tt.expectedVal, actualVal)
}
}
}
func TestParseObjectLockRetentionHeaders(t *testing.T) {
tests := []struct {
header http.Header
expectedErr error
}{
{
header: http.Header{
"Authorization": []string{"AWS4-HMAC-SHA256 <cred_string>"},
"X-Amz-Content-Sha256": []string{""},
"Content-Encoding": []string{""},
},
expectedErr: ErrObjectLockInvalidHeaders,
},
{
header: http.Header{
xhttp.AmzObjectLockMode: []string{"lock"},
xhttp.AmzObjectLockRetainUntilDate: []string{"2017-01-02"},
},
expectedErr: ErrUnknownWORMModeDirective,
},
{
header: http.Header{
xhttp.AmzObjectLockMode: []string{"governance"},
},
expectedErr: ErrObjectLockInvalidHeaders,
},
{
header: http.Header{
xhttp.AmzObjectLockRetainUntilDate: []string{"2017-01-02"},
xhttp.AmzObjectLockMode: []string{"governance"},
},
expectedErr: ErrInvalidRetentionDate,
},
{
header: http.Header{
xhttp.AmzObjectLockRetainUntilDate: []string{"2017-01-02T15:04:05Z"},
xhttp.AmzObjectLockMode: []string{"governance"},
},
expectedErr: ErrPastObjectLockRetainDate,
},
{
header: http.Header{
xhttp.AmzObjectLockMode: []string{"governance"},
xhttp.AmzObjectLockRetainUntilDate: []string{"2017-01-02T15:04:05Z"},
},
expectedErr: ErrPastObjectLockRetainDate,
},
{
header: http.Header{
xhttp.AmzObjectLockMode: []string{"governance"},
xhttp.AmzObjectLockRetainUntilDate: []string{"2087-01-02T15:04:05Z"},
},
expectedErr: nil,
},
}
for i, tt := range tests {
_, _, err := ParseObjectLockRetentionHeaders(tt.header)
if tt.expectedErr == nil {
if err != nil {
t.Fatalf("Case %d error: expected = <nil>, got = %v", i, err)
}
} else if err == nil {
t.Fatalf("Case %d error: expected = %v, got = <nil>", i, tt.expectedErr)
} else if tt.expectedErr.Error() != err.Error() {
t.Fatalf("Case %d error: expected = %v, got = %v", i, tt.expectedErr, err)
}
}
}
func TestGetObjectRetentionMeta(t *testing.T) {
tests := []struct {
metadata map[string]string
expected ObjectRetention
}{
{
metadata: map[string]string{
"Authorization": "AWS4-HMAC-SHA256 <cred_string>",
"X-Amz-Content-Sha256": "",
"Content-Encoding": "",
},
expected: ObjectRetention{},
},
{
metadata: map[string]string{
"x-amz-object-lock-mode": "governance",
},
expected: ObjectRetention{Mode: Governance},
},
{
metadata: map[string]string{
"x-amz-object-lock-retain-until-date": "2020-02-01",
},
expected: ObjectRetention{RetainUntilDate: RetentionDate{time.Date(2020, 2, 1, 12, 0, 0, 0, time.UTC)}},
},
}
for i, tt := range tests {
o := GetObjectRetentionMeta(tt.metadata)
if o.Mode != tt.expected.Mode {
t.Fatalf("Case %d expected %v, got %v", i, tt.expected.Mode, o.Mode)
}
}
}
func TestGetObjectLegalHoldMeta(t *testing.T) {
tests := []struct {
metadata map[string]string
expected ObjectLegalHold
}{
{
metadata: map[string]string{
"x-amz-object-lock-mode": "governance",
},
expected: ObjectLegalHold{},
},
{
metadata: map[string]string{
"x-amz-object-lock-legal-hold": "on",
},
expected: ObjectLegalHold{Status: ON},
},
{
metadata: map[string]string{
"x-amz-object-lock-legal-hold": "off",
},
expected: ObjectLegalHold{Status: OFF},
},
{
metadata: map[string]string{
"x-amz-object-lock-legal-hold": "X",
},
expected: ObjectLegalHold{Status: ""},
},
}
for i, tt := range tests {
o := GetObjectLegalHoldMeta(tt.metadata)
if o.Status != tt.expected.Status {
t.Fatalf("Case %d expected %v, got %v", i, tt.expected.Status, o.Status)
}
}
}
func TestParseObjectLegalHold(t *testing.T) {
tests := []struct {
value string
expectedErr error
expectErr bool
}{
{
value: "<?xml version=\"1.0\" encoding=\"UTF-8\"?><LegalHold xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Status>string</Status></LegalHold>",
expectedErr: ErrMalformedXML,
expectErr: true,
},
{
value: "<?xml version=\"1.0\" encoding=\"UTF-8\"?><LegalHold xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Status>ON</Status></LegalHold>",
expectedErr: nil,
expectErr: false,
},
{
value: "<?xml version=\"1.0\" encoding=\"UTF-8\"?><LegalHold xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Status>On</Status></LegalHold>",
expectedErr: ErrMalformedXML,
expectErr: true,
},
}
for i, tt := range tests {
_, err := ParseObjectLegalHold(strings.NewReader(tt.value))
if tt.expectedErr == nil {
if err != nil {
t.Fatalf("Case %d error: expected = <nil>, got = %v", i, err)
}
} else if err == nil {
t.Fatalf("Case %d error: expected = %v, got = <nil>", i, tt.expectedErr)
} else if tt.expectedErr.Error() != err.Error() {
t.Fatalf("Case %d error: expected = %v, got = %v", i, tt.expectedErr, err)
}
}
}
func TestFilterObjectLockMetadata(t *testing.T) {
tests := []struct {
metadata map[string]string
filterRetention bool
filterLegalHold bool
expected map[string]string
}{
{
metadata: map[string]string{
"Authorization": "AWS4-HMAC-SHA256 <cred_string>",
"X-Amz-Content-Sha256": "",
"Content-Encoding": "",
},
expected: map[string]string{
"Authorization": "AWS4-HMAC-SHA256 <cred_string>",
"X-Amz-Content-Sha256": "",
"Content-Encoding": "",
},
},
{
metadata: map[string]string{
"x-amz-object-lock-mode": "governance",
},
expected: map[string]string{
"x-amz-object-lock-mode": "governance",
},
filterRetention: false,
},
{
metadata: map[string]string{
"x-amz-object-lock-mode": "governance",
"x-amz-object-lock-retain-until-date": "2020-02-01",
},
expected: map[string]string{},
filterRetention: true,
},
{
metadata: map[string]string{
"x-amz-object-lock-legal-hold": "off",
},
expected: map[string]string{},
filterLegalHold: true,
},
{
metadata: map[string]string{
"x-amz-object-lock-legal-hold": "on",
},
expected: map[string]string{"x-amz-object-lock-legal-hold": "on"},
filterLegalHold: false,
},
{
metadata: map[string]string{
"x-amz-object-lock-legal-hold": "on",
"x-amz-object-lock-mode": "governance",
"x-amz-object-lock-retain-until-date": "2020-02-01",
},
expected: map[string]string{},
filterRetention: true,
filterLegalHold: true,
},
{
metadata: map[string]string{
"x-amz-object-lock-legal-hold": "on",
"x-amz-object-lock-mode": "governance",
"x-amz-object-lock-retain-until-date": "2020-02-01",
},
expected: map[string]string{"x-amz-object-lock-legal-hold": "on",
"x-amz-object-lock-mode": "governance",
"x-amz-object-lock-retain-until-date": "2020-02-01"},
},
}
for i, tt := range tests {
o := FilterObjectLockMetadata(tt.metadata, tt.filterRetention, tt.filterLegalHold)
if !reflect.DeepEqual(o, tt.metadata) {
t.Fatalf("Case %d expected %v, got %v", i, tt.metadata, o)
}
}
}

View file

@ -98,6 +98,10 @@ const (
// GetObjectRetentionAction - GetObjectRetention, GetObject, HeadObject Rest API action. // GetObjectRetentionAction - GetObjectRetention, GetObject, HeadObject Rest API action.
GetObjectRetentionAction = "s3:GetObjectRetention" GetObjectRetentionAction = "s3:GetObjectRetention"
// GetObjectLegalHoldAction - GetObjectLegalHold, GetObject Rest API action.
GetObjectLegalHoldAction = "s3:GetObjectLegalHold"
// PutObjectLegalHoldAction - PutObjectLegalHold, PutObject Rest API action.
PutObjectLegalHoldAction = "s3:PutObjectLegalHold"
// GetBucketObjectLockConfigurationAction - GetObjectLockConfiguration Rest API action // GetBucketObjectLockConfigurationAction - GetObjectLockConfiguration Rest API action
GetBucketObjectLockConfigurationAction = "s3:GetBucketObjectLockConfiguration" GetBucketObjectLockConfigurationAction = "s3:GetBucketObjectLockConfiguration"
// PutBucketObjectLockConfigurationAction - PutObjectLockConfiguration Rest API action // PutBucketObjectLockConfigurationAction - PutObjectLockConfiguration Rest API action
@ -113,6 +117,8 @@ func (action Action) isObjectAction() bool {
return true return true
case PutObjectRetentionAction, GetObjectRetentionAction: case PutObjectRetentionAction, GetObjectRetentionAction:
return true return true
case PutObjectLegalHoldAction, GetObjectLegalHoldAction:
return true
case BypassGovernanceModeAction, BypassGovernanceRetentionAction: case BypassGovernanceModeAction, BypassGovernanceRetentionAction:
return true return true
} }
@ -143,6 +149,8 @@ func (action Action) IsValid() bool {
return true return true
case PutObjectRetentionAction, GetObjectRetentionAction: case PutObjectRetentionAction, GetObjectRetentionAction:
return true return true
case PutObjectLegalHoldAction, GetObjectLegalHoldAction:
return true
case PutBucketObjectLockConfigurationAction, GetBucketObjectLockConfigurationAction: case PutBucketObjectLockConfigurationAction, GetBucketObjectLockConfigurationAction:
return true return true
} }
@ -231,6 +239,8 @@ var actionConditionKeyMap = map[Action]condition.KeySet{
GetObjectRetentionAction: condition.NewKeySet(condition.CommonKeys...), GetObjectRetentionAction: condition.NewKeySet(condition.CommonKeys...),
BypassGovernanceModeAction: condition.NewKeySet(condition.CommonKeys...), BypassGovernanceModeAction: condition.NewKeySet(condition.CommonKeys...),
BypassGovernanceRetentionAction: condition.NewKeySet(condition.CommonKeys...), BypassGovernanceRetentionAction: condition.NewKeySet(condition.CommonKeys...),
PutObjectLegalHoldAction: condition.NewKeySet(condition.CommonKeys...),
GetObjectLegalHoldAction: condition.NewKeySet(condition.CommonKeys...),
GetBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...), GetBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...),
PutBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...), PutBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...),
} }