mirror of https://github.com/minio/minio.git
Add support for object locking with legal hold. (#8634)
This commit is contained in:
parent
ba758361b3
commit
60e60f68dd
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
12
cmd/utils.go
12
cmd/utils.go
|
@ -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())))
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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 $?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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":
|
||||||
|
|
|
@ -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, ""},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"})
|
||||||
|
|
|
@ -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...),
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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...),
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue