mirror of
https://github.com/minio/minio.git
synced 2024-12-24 06:05:55 -05:00
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/event"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
"github.com/minio/minio/pkg/objectlock"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
)
|
||||
|
||||
@ -1611,14 +1612,16 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
||||
apiErr = ErrOperationTimedOut
|
||||
case errDiskNotFound:
|
||||
apiErr = ErrSlowDown
|
||||
case errInvalidRetentionDate:
|
||||
case objectlock.ErrInvalidRetentionDate:
|
||||
apiErr = ErrInvalidRetentionDate
|
||||
case errPastObjectLockRetainDate:
|
||||
case objectlock.ErrPastObjectLockRetainDate:
|
||||
apiErr = ErrPastObjectLockRetainDate
|
||||
case errUnknownWORMModeDirective:
|
||||
case objectlock.ErrUnknownWORMModeDirective:
|
||||
apiErr = ErrUnknownWORMModeDirective
|
||||
case errObjectLockInvalidHeaders:
|
||||
case objectlock.ErrObjectLockInvalidHeaders:
|
||||
apiErr = ErrObjectLockInvalidHeaders
|
||||
case objectlock.ErrMalformedXML:
|
||||
apiErr = ErrMalformedXML
|
||||
}
|
||||
|
||||
// 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")
|
||||
// GetObjectRetention
|
||||
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
|
||||
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(collectAPIStats("getobject", httpTraceHdrs(api.GetObjectHandler)))
|
||||
// CopyObject
|
||||
bucket.Methods(http.MethodPut).Path("/{object:.+}").HeadersRegexp(xhttp.AmzCopySource, ".*?(\\/|%2F).*?").HandlerFunc(collectAPIStats("copyobject", httpTraceAll(api.CopyObjectHandler)))
|
||||
// PutObjectRetention
|
||||
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
|
||||
bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(collectAPIStats("putobject", httpTraceHdrs(api.PutObjectHandler)))
|
||||
// DeleteObject
|
||||
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
|
||||
// GetBucketLocation
|
||||
|
@ -40,6 +40,7 @@ import (
|
||||
"github.com/minio/minio/pkg/handlers"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
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/sync/errgroup"
|
||||
)
|
||||
@ -577,14 +578,14 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
||||
return
|
||||
}
|
||||
|
||||
if objectLockEnabled {
|
||||
if objectLockEnabled && !globalIsGateway {
|
||||
configFile := path.Join(bucketConfigPrefix, bucket, bucketObjectLockEnabledConfigFile)
|
||||
if err = saveConfig(ctx, objectAPI, configFile, []byte(bucketObjectLockEnabledConfig)); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
globalBucketObjectLockConfig.Set(bucket, Retention{})
|
||||
globalNotificationSys.PutBucketObjectLockConfig(ctx, bucket, Retention{})
|
||||
globalBucketObjectLockConfig.Set(bucket, objectlock.Retention{})
|
||||
globalNotificationSys.PutBucketObjectLockConfig(ctx, bucket, objectlock.Retention{})
|
||||
}
|
||||
|
||||
// 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))
|
||||
return
|
||||
}
|
||||
config, err := parseObjectLockConfig(r.Body)
|
||||
config, err := objectlock.ParseObjectLockConfig(r.Body)
|
||||
if err != nil {
|
||||
apiErr := errorCodes.ToAPIErr(ErrMalformedXML)
|
||||
apiErr.Description = err.Error()
|
||||
@ -1099,7 +1100,7 @@ func (api objectAPIHandlers) GetBucketObjectLockConfigHandler(w http.ResponseWri
|
||||
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))
|
||||
return
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ import (
|
||||
"github.com/minio/minio/cmd/config/cache"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/color"
|
||||
"github.com/minio/minio/pkg/objectlock"
|
||||
"github.com/minio/minio/pkg/sync/errgroup"
|
||||
"github.com/minio/minio/pkg/wildcard"
|
||||
)
|
||||
@ -251,7 +252,13 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
|
||||
c.cacheStats.incMiss()
|
||||
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 ETag matches for stale cache entry, serve from cache
|
||||
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
|
||||
objRetention := getObjectRetentionMeta(opts.UserDefined)
|
||||
if objRetention.Mode == Governance || objRetention.Mode == Compliance {
|
||||
objRetention := objectlock.GetObjectRetentionMeta(opts.UserDefined)
|
||||
legalHold := objectlock.GetObjectLegalHoldMeta(opts.UserDefined)
|
||||
if objRetention.Mode != objectlock.Invalid || legalHold.Status != "" {
|
||||
dcache.Delete(ctx, bucket, object)
|
||||
return putObjectFn(ctx, bucket, object, r, opts)
|
||||
}
|
||||
|
@ -37,6 +37,7 @@ import (
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/certs"
|
||||
"github.com/minio/minio/pkg/event"
|
||||
"github.com/minio/minio/pkg/objectlock"
|
||||
"github.com/minio/minio/pkg/pubsub"
|
||||
)
|
||||
|
||||
@ -201,7 +202,7 @@ var (
|
||||
// Is worm enabled
|
||||
globalWORMEnabled bool
|
||||
|
||||
globalBucketObjectLockConfig = newBucketObjectLockConfig()
|
||||
globalBucketObjectLockConfig = objectlock.NewBucketObjectLockConfig()
|
||||
|
||||
// Disk cache drives
|
||||
globalCacheConfig cache.Config
|
||||
|
@ -36,6 +36,7 @@ import (
|
||||
"github.com/minio/minio/pkg/lifecycle"
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
xnet "github.com/minio/minio/pkg/net"
|
||||
"github.com/minio/minio/pkg/objectlock"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
"github.com/minio/minio/pkg/sync/errgroup"
|
||||
)
|
||||
@ -668,7 +669,7 @@ func (sys *NotificationSys) initBucketObjectLockConfig(objAPI ObjectLayer) error
|
||||
|
||||
if string(bucketObjLockData) != bucketObjectLockEnabledConfig {
|
||||
// this should never happen
|
||||
logger.LogIf(ctx, errMalformedBucketObjectConfig)
|
||||
logger.LogIf(ctx, objectlock.ErrMalformedBucketObjectConfig)
|
||||
continue
|
||||
}
|
||||
|
||||
@ -677,17 +678,17 @@ func (sys *NotificationSys) initBucketObjectLockConfig(objAPI ObjectLayer) error
|
||||
|
||||
if err != nil {
|
||||
if err == errConfigNotFound {
|
||||
globalBucketObjectLockConfig.Set(bucket.Name, Retention{})
|
||||
globalBucketObjectLockConfig.Set(bucket.Name, objectlock.Retention{})
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
config, err := parseObjectLockConfig(bytes.NewReader(configData))
|
||||
config, err := objectlock.ParseObjectLockConfig(bytes.NewReader(configData))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
retention := Retention{}
|
||||
retention := objectlock.Retention{}
|
||||
if config.Rule != nil {
|
||||
retention = config.ToRetention()
|
||||
}
|
||||
@ -874,7 +875,7 @@ func (sys *NotificationSys) Send(args eventArgs) []event.TargetIDErr {
|
||||
}
|
||||
|
||||
// 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))
|
||||
for index, client := range sys.peerClients {
|
||||
if client == nil {
|
||||
|
@ -44,6 +44,8 @@ import (
|
||||
"github.com/minio/minio/pkg/event"
|
||||
"github.com/minio/minio/pkg/handlers"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
"github.com/minio/minio/pkg/objectlock"
|
||||
|
||||
iampolicy "github.com/minio/minio/pkg/iam/policy"
|
||||
"github.com/minio/minio/pkg/ioutil"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
@ -209,8 +211,10 @@ func (api objectAPIHandlers) SelectObjectContentHandler(w http.ResponseWriter, r
|
||||
return
|
||||
}
|
||||
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
|
||||
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 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
|
||||
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() {
|
||||
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
|
||||
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 _, 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 {
|
||||
getObjectInfo = api.CacheAPI().GetObjectInfo
|
||||
}
|
||||
isCpy := true
|
||||
getRetPerms := checkRequestAuthType(ctx, r, policy.GetObjectRetentionAction, srcBucket, srcObject)
|
||||
srcInfo.UserDefined = filterObjectLockMetadata(ctx, r, srcBucket, srcObject, srcInfo.UserDefined, isCpy, getRetPerms)
|
||||
srcInfo.UserDefined = objectlock.FilterObjectLockMetadata(srcInfo.UserDefined, true, true)
|
||||
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.
|
||||
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 != "" {
|
||||
srcInfo.UserDefined[xhttp.AmzObjectLockMode] = string(retentionMode)
|
||||
srcInfo.UserDefined[xhttp.AmzObjectLockRetainUntilDate] = retentionDate.UTC().Format(time.RFC3339)
|
||||
}
|
||||
if s3Err == ErrNone && legalHold.Status != "" {
|
||||
srcInfo.UserDefined[xhttp.AmzObjectLockLegalHold] = string(legalHold.Status)
|
||||
}
|
||||
if s3Err != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@ -1251,11 +1264,16 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
putObject = api.CacheAPI().PutObject
|
||||
}
|
||||
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 != "" {
|
||||
metadata[strings.ToLower(xhttp.AmzObjectLockMode)] = string(retentionMode)
|
||||
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 {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@ -1409,11 +1427,16 @@ func (api objectAPIHandlers) NewMultipartUploadHandler(w http.ResponseWriter, r
|
||||
return
|
||||
}
|
||||
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 != "" {
|
||||
metadata[strings.ToLower(xhttp.AmzObjectLockMode)] = string(retentionMode)
|
||||
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 {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@ -2215,14 +2238,16 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
|
||||
}
|
||||
|
||||
// Reject retention or governance headers if set, CompleteMultipartUpload spec
|
||||
// does not use these headers, and should not be passed down to checkPutObjectRetentionAllowed
|
||||
if isObjectLockRequested(r.Header) || isObjectLockGovernanceBypassSet(r.Header) {
|
||||
// does not use these headers, and should not be passed down to checkPutObjectLockAllowed
|
||||
if objectlock.IsObjectLockRequested(r.Header) || objectlock.IsObjectLockGovernanceBypassSet(r.Header) {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
// Enforce object lock governance in case a competing upload finalized first.
|
||||
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))
|
||||
return
|
||||
}
|
||||
@ -2467,23 +2492,81 @@ func (api objectAPIHandlers) PutObjectLegalHoldHandler(w http.ResponseWriter, r
|
||||
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
|
||||
if api.CacheAPI() != nil {
|
||||
getObjectInfo = api.CacheAPI().GetObjectInfo
|
||||
}
|
||||
|
||||
opts, err := getOpts(ctx, r, bucket, object)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
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))
|
||||
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,
|
||||
@ -2506,6 +2589,10 @@ func (api objectAPIHandlers) GetObjectLegalHoldHandler(w http.ResponseWriter, r
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r))
|
||||
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
|
||||
if api.CacheAPI() != nil {
|
||||
@ -2518,12 +2605,25 @@ func (api objectAPIHandlers) GetObjectLegalHoldHandler(w http.ResponseWriter, r
|
||||
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
|
||||
}
|
||||
|
||||
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,
|
||||
@ -2568,7 +2668,7 @@ func (api objectAPIHandlers) PutObjectRetentionHandler(w http.ResponseWriter, r
|
||||
return
|
||||
}
|
||||
|
||||
objRetention, err := parseObjectRetention(r.Body)
|
||||
objRetention, err := objectlock.ParseObjectRetention(r.Body)
|
||||
if err != nil {
|
||||
apiErr := errorCodes.ToAPIErr(ErrMalformedXML)
|
||||
apiErr.Description = err.Error()
|
||||
@ -2660,7 +2760,7 @@ func (api objectAPIHandlers) GetObjectRetentionHandler(w http.ResponseWriter, r
|
||||
return
|
||||
}
|
||||
|
||||
retention := getObjectRetentionMeta(objInfo.UserDefined)
|
||||
retention := objectlock.GetObjectRetentionMeta(objInfo.UserDefined)
|
||||
|
||||
writeSuccessResponseXML(w, encodeResponse(retention))
|
||||
// Notify object retention accessed via a GET request.
|
||||
|
@ -18,387 +18,12 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
xhttp "github.com/minio/minio/cmd/http"
|
||||
"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
|
||||
// with governance bypass headers set in the request.
|
||||
// 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)
|
||||
}
|
||||
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
|
||||
if ret.Mode == Invalid {
|
||||
if ret.Mode == objectlock.Invalid {
|
||||
return oi, ErrNone
|
||||
}
|
||||
if ret.Mode != Compliance && ret.Mode != Governance {
|
||||
if ret.Mode != objectlock.Compliance && ret.Mode != objectlock.Governance {
|
||||
return oi, ErrUnknownWORMModeDirective
|
||||
}
|
||||
t, err := UTCNowNTP()
|
||||
t, err := objectlock.UTCNowNTP()
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return oi, ErrObjectLocked
|
||||
@ -441,7 +69,7 @@ func enforceRetentionBypassForDelete(ctx context.Context, r *http.Request, bucke
|
||||
if ret.RetainUntilDate.Before(t) {
|
||||
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, 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
|
||||
// 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.
|
||||
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 {
|
||||
return oi, ErrObjectLocked
|
||||
}
|
||||
@ -474,24 +102,24 @@ func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, bucket,
|
||||
return oi, toAPIErrorCode(ctx, err)
|
||||
}
|
||||
|
||||
ret := getObjectRetentionMeta(oi.UserDefined)
|
||||
ret := objectlock.GetObjectRetentionMeta(oi.UserDefined)
|
||||
// no retention metadata on object
|
||||
if ret.Mode == Invalid {
|
||||
if ret.Mode == objectlock.Invalid {
|
||||
if _, isWORMBucket := globalBucketObjectLockConfig.Get(bucket); !isWORMBucket {
|
||||
return oi, ErrInvalidBucketObjectLockConfiguration
|
||||
}
|
||||
return oi, ErrNone
|
||||
}
|
||||
t, err := UTCNowNTP()
|
||||
t, err := objectlock.UTCNowNTP()
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
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
|
||||
// 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
|
||||
}
|
||||
if objRetention.RetainUntilDate.Before(t) {
|
||||
@ -500,8 +128,8 @@ func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, bucket,
|
||||
return oi, ErrNone
|
||||
}
|
||||
|
||||
if ret.Mode == Governance {
|
||||
if !isObjectLockGovernanceBypassSet(r.Header) {
|
||||
if ret.Mode == objectlock.Governance {
|
||||
if !objectlock.IsObjectLockGovernanceBypassSet(r.Header) {
|
||||
if objRetention.RetainUntilDate.Before(t) {
|
||||
return oi, ErrInvalidRetentionDate
|
||||
}
|
||||
@ -515,111 +143,104 @@ func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, bucket,
|
||||
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.
|
||||
// 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)
|
||||
// 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.
|
||||
// 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) {
|
||||
var mode RetentionMode
|
||||
var retainDate RetentionDate
|
||||
// For objects with legal hold header set, the s3:PutObjectLegalHold permission is expected to be set
|
||||
// Both legal hold and retention can be applied independently on an object
|
||||
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)
|
||||
|
||||
retentionRequested := isObjectLockRequested(r.Header)
|
||||
retentionRequested := objectlock.IsObjectLockRetentionRequested(r.Header)
|
||||
legalHoldRequested := objectlock.IsObjectLockLegalHoldRequested(r.Header)
|
||||
|
||||
var objExists bool
|
||||
opts, err := getOpts(ctx, r, bucket, object)
|
||||
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 {
|
||||
objExists = true
|
||||
r := getObjectRetentionMeta(objInfo.UserDefined)
|
||||
if globalWORMEnabled || r.Mode == Compliance {
|
||||
return mode, retainDate, ErrObjectLocked
|
||||
r := objectlock.GetObjectRetentionMeta(objInfo.UserDefined)
|
||||
if globalWORMEnabled || r.Mode == objectlock.Compliance {
|
||||
return mode, retainDate, legalHold, ErrObjectLocked
|
||||
}
|
||||
mode = r.Mode
|
||||
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 !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 {
|
||||
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.
|
||||
t, err := UTCNowNTP()
|
||||
t, err := objectlock.UTCNowNTP()
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return mode, retainDate, ErrObjectLocked
|
||||
return mode, retainDate, legalHold, ErrObjectLocked
|
||||
}
|
||||
if objExists && retainDate.After(t) {
|
||||
return mode, retainDate, ErrObjectLocked
|
||||
return mode, retainDate, legalHold, ErrObjectLocked
|
||||
}
|
||||
if rMode == Invalid {
|
||||
return mode, retainDate, toAPIErrorCode(ctx, errObjectLockInvalidHeaders)
|
||||
if rMode == objectlock.Invalid {
|
||||
return mode, retainDate, legalHold, toAPIErrorCode(ctx, objectlock.ErrObjectLockInvalidHeaders)
|
||||
}
|
||||
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 retention.IsEmpty() && (mode == Compliance || mode == Governance) {
|
||||
return mode, retainDate, ErrObjectLocked
|
||||
if retention.IsEmpty() && (mode == objectlock.Compliance || mode == objectlock.Governance) {
|
||||
return mode, retainDate, legalHold, ErrObjectLocked
|
||||
}
|
||||
if retentionPermErr != ErrNone {
|
||||
return mode, retainDate, retentionPermErr
|
||||
return mode, retainDate, legalHold, retentionPermErr
|
||||
}
|
||||
t, err := UTCNowNTP()
|
||||
t, err := objectlock.UTCNowNTP()
|
||||
if err != nil {
|
||||
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.
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
return mode, retainDate, legalHold, ErrNone
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ import (
|
||||
"github.com/minio/minio/pkg/lifecycle"
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
xnet "github.com/minio/minio/pkg/net"
|
||||
"github.com/minio/minio/pkg/objectlock"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
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.
|
||||
func (client *peerRESTClient) PutBucketObjectLockConfig(bucket string, retention Retention) error {
|
||||
func (client *peerRESTClient) PutBucketObjectLockConfig(bucket string, retention objectlock.Retention) error {
|
||||
values := make(url.Values)
|
||||
values.Set(peerRESTBucket, bucket)
|
||||
|
||||
|
@ -33,6 +33,7 @@ import (
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/event"
|
||||
"github.com/minio/minio/pkg/lifecycle"
|
||||
objectlock "github.com/minio/minio/pkg/objectlock"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
trace "github.com/minio/minio/pkg/trace"
|
||||
)
|
||||
@ -845,7 +846,7 @@ func (s *peerRESTServer) PutBucketObjectLockConfigHandler(w http.ResponseWriter,
|
||||
return
|
||||
}
|
||||
|
||||
var retention Retention
|
||||
var retention objectlock.Retention
|
||||
if r.ContentLength < 0 {
|
||||
s.writeErrorResponse(w, errInvalidArgument)
|
||||
return
|
||||
|
12
cmd/utils.go
12
cmd/utils.go
@ -39,7 +39,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/beevik/ntp"
|
||||
xhttp "github.com/minio/minio/cmd/http"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/handlers"
|
||||
@ -354,17 +353,6 @@ func UTCNow() time.Time {
|
||||
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
|
||||
func GenETag() string {
|
||||
return ToS3ETag(getMD5Hash([]byte(mustGetUUID())))
|
||||
|
@ -50,6 +50,7 @@ import (
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
iampolicy "github.com/minio/minio/pkg/iam/policy"
|
||||
"github.com/minio/minio/pkg/ioutil"
|
||||
"github.com/minio/minio/pkg/objectlock"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
)
|
||||
|
||||
@ -928,6 +929,7 @@ func (web *webAPIHandlers) Upload(w http.ResponseWriter, r *http.Request) {
|
||||
object := vars["object"]
|
||||
|
||||
retPerms := ErrAccessDenied
|
||||
holdPerms := ErrAccessDenied
|
||||
|
||||
claims, owner, authErr := webRequestAuthenticate(r)
|
||||
if authErr != nil {
|
||||
@ -974,7 +976,17 @@ func (web *webAPIHandlers) Upload(w http.ResponseWriter, r *http.Request) {
|
||||
}) {
|
||||
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.
|
||||
@ -1068,11 +1080,14 @@ func (web *webAPIHandlers) Upload(w http.ResponseWriter, r *http.Request) {
|
||||
getObjectInfo = web.CacheAPI().GetObjectInfo
|
||||
}
|
||||
// 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 != "" {
|
||||
opts.UserDefined[xhttp.AmzObjectLockMode] = string(retentionMode)
|
||||
opts.UserDefined[xhttp.AmzObjectLockRetainUntilDate] = retentionDate.UTC().Format(time.RFC3339)
|
||||
}
|
||||
if s3Err == ErrNone && legalHold.Status != "" {
|
||||
opts.UserDefined[xhttp.AmzObjectLockLegalHold] = string(legalHold.Status)
|
||||
}
|
||||
if s3Err != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@ -1130,6 +1145,7 @@ func (web *webAPIHandlers) Download(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.URL.Query().Get("token")
|
||||
|
||||
getRetPerms := ErrAccessDenied
|
||||
legalHoldPerms := ErrAccessDenied
|
||||
|
||||
claims, owner, authErr := webTokenAuthenticate(token)
|
||||
if authErr != nil {
|
||||
@ -1154,6 +1170,15 @@ func (web *webAPIHandlers) Download(w http.ResponseWriter, r *http.Request) {
|
||||
}) {
|
||||
getRetPerms = ErrNone
|
||||
}
|
||||
if globalPolicySys.IsAllowed(policy.Args{
|
||||
Action: policy.GetObjectLegalHoldAction,
|
||||
BucketName: bucket,
|
||||
ConditionValues: getConditionValues(r, "", "", nil),
|
||||
IsOwner: false,
|
||||
ObjectName: object,
|
||||
}) {
|
||||
legalHoldPerms = ErrNone
|
||||
}
|
||||
} else {
|
||||
writeWebErrorResponse(w, authErr)
|
||||
return
|
||||
@ -1185,6 +1210,17 @@ func (web *webAPIHandlers) Download(w http.ResponseWriter, r *http.Request) {
|
||||
}) {
|
||||
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.
|
||||
@ -1209,7 +1245,7 @@ func (web *webAPIHandlers) Download(w http.ResponseWriter, r *http.Request) {
|
||||
objInfo := gr.ObjInfo
|
||||
|
||||
// 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 _, 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")
|
||||
claims, owner, authErr := webTokenAuthenticate(token)
|
||||
var getRetPerms []APIErrorCode
|
||||
var legalHoldPerms []APIErrorCode
|
||||
|
||||
if authErr != nil {
|
||||
if authErr == errNoAuthToken {
|
||||
for _, object := range args.Objects {
|
||||
@ -1329,6 +1367,18 @@ func (web *webAPIHandlers) DownloadZip(w http.ResponseWriter, r *http.Request) {
|
||||
retentionPerm = ErrNone
|
||||
}
|
||||
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 {
|
||||
writeWebErrorResponse(w, authErr)
|
||||
@ -1354,7 +1404,7 @@ func (web *webAPIHandlers) DownloadZip(w http.ResponseWriter, r *http.Request) {
|
||||
retentionPerm := ErrAccessDenied
|
||||
if globalIAMSys.IsAllowed(iampolicy.Args{
|
||||
AccountName: claims.AccessKey(),
|
||||
Action: iampolicy.GetObjectAction,
|
||||
Action: iampolicy.GetObjectRetentionAction,
|
||||
BucketName: args.BucketName,
|
||||
ConditionValues: getConditionValues(r, "", claims.AccessKey(), claims.Map()),
|
||||
IsOwner: owner,
|
||||
@ -1364,6 +1414,20 @@ func (web *webAPIHandlers) DownloadZip(w http.ResponseWriter, r *http.Request) {
|
||||
retentionPerm = ErrNone
|
||||
}
|
||||
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
|
||||
// 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() {
|
||||
// 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
|
||||
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
|
||||
|
||||
### 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
|
||||
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
|
||||
|
||||
- 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
|
||||
- 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
|
||||
is expired. If user has requisite governance bypass permissions, an object's retention date can
|
||||
be extended in `Compliance` mode.
|
||||
|
@ -69,6 +69,24 @@ function make_bucket() {
|
||||
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() {
|
||||
# Delete bucket
|
||||
function="${AWS} s3 rb s3://${1} --force"
|
||||
@ -1430,6 +1448,98 @@ function test_worm_bucket() {
|
||||
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() {
|
||||
# Success tests
|
||||
@ -1455,7 +1565,9 @@ main() {
|
||||
test_list_objects_error && \
|
||||
test_put_object_error && \
|
||||
test_serverside_encryption_error && \
|
||||
test_worm_bucket
|
||||
test_worm_bucket && \
|
||||
test_legal_hold
|
||||
|
||||
return $?
|
||||
}
|
||||
|
||||
|
@ -30,6 +30,7 @@ const (
|
||||
ObjectAccessedAll Name = 1 + iota
|
||||
ObjectAccessedGet
|
||||
ObjectAccessedGetRetention
|
||||
ObjectAccessedGetLegalHold
|
||||
ObjectAccessedHead
|
||||
ObjectCreatedAll
|
||||
ObjectCreatedCompleteMultipartUpload
|
||||
@ -37,6 +38,7 @@ const (
|
||||
ObjectCreatedPost
|
||||
ObjectCreatedPut
|
||||
ObjectCreatedPutRetention
|
||||
ObjectCreatedPutLegalHold
|
||||
ObjectRemovedAll
|
||||
ObjectRemovedDelete
|
||||
)
|
||||
@ -45,9 +47,9 @@ const (
|
||||
func (name Name) Expand() []Name {
|
||||
switch name {
|
||||
case ObjectAccessedAll:
|
||||
return []Name{ObjectAccessedGet, ObjectAccessedHead, ObjectAccessedGetRetention}
|
||||
return []Name{ObjectAccessedGet, ObjectAccessedHead, ObjectAccessedGetRetention, ObjectAccessedGetLegalHold}
|
||||
case ObjectCreatedAll:
|
||||
return []Name{ObjectCreatedCompleteMultipartUpload, ObjectCreatedCopy, ObjectCreatedPost, ObjectCreatedPut, ObjectCreatedPutRetention}
|
||||
return []Name{ObjectCreatedCompleteMultipartUpload, ObjectCreatedCopy, ObjectCreatedPost, ObjectCreatedPut, ObjectCreatedPutRetention, ObjectCreatedPutLegalHold}
|
||||
case ObjectRemovedAll:
|
||||
return []Name{ObjectRemovedDelete}
|
||||
default:
|
||||
@ -64,6 +66,8 @@ func (name Name) String() string {
|
||||
return "s3:ObjectAccessed:Get"
|
||||
case ObjectAccessedGetRetention:
|
||||
return "s3:ObjectAccessed:GetRetention"
|
||||
case ObjectAccessedGetLegalHold:
|
||||
return "s3:ObjectAccessed:GetLegalHold"
|
||||
case ObjectAccessedHead:
|
||||
return "s3:ObjectAccessed:Head"
|
||||
case ObjectCreatedAll:
|
||||
@ -78,6 +82,8 @@ func (name Name) String() string {
|
||||
return "s3:ObjectCreated:Put"
|
||||
case ObjectCreatedPutRetention:
|
||||
return "s3:ObjectCreated:PutRetention"
|
||||
case ObjectCreatedPutLegalHold:
|
||||
return "s3:ObjectCreated:PutLegalHold"
|
||||
case ObjectRemovedAll:
|
||||
return "s3:ObjectRemoved:*"
|
||||
case ObjectRemovedDelete:
|
||||
@ -138,6 +144,8 @@ func ParseName(s string) (Name, error) {
|
||||
return ObjectAccessedGet, nil
|
||||
case "s3:ObjectAccessed:GetRetention":
|
||||
return ObjectAccessedGetRetention, nil
|
||||
case "s3:ObjectAccessed:GetLegalHold":
|
||||
return ObjectAccessedGetLegalHold, nil
|
||||
case "s3:ObjectAccessed:Head":
|
||||
return ObjectAccessedHead, nil
|
||||
case "s3:ObjectCreated:*":
|
||||
@ -152,6 +160,8 @@ func ParseName(s string) (Name, error) {
|
||||
return ObjectCreatedPut, nil
|
||||
case "s3:ObjectCreated:PutRetention":
|
||||
return ObjectCreatedPutRetention, nil
|
||||
case "s3:ObjectCreated:PutLegalHold":
|
||||
return ObjectCreatedPutLegalHold, nil
|
||||
case "s3:ObjectRemoved:*":
|
||||
return ObjectRemovedAll, nil
|
||||
case "s3:ObjectRemoved:Delete":
|
||||
|
@ -28,8 +28,8 @@ func TestNameExpand(t *testing.T) {
|
||||
name Name
|
||||
expectedResult []Name
|
||||
}{
|
||||
{ObjectAccessedAll, []Name{ObjectAccessedGet, ObjectAccessedHead, ObjectAccessedGetRetention}},
|
||||
{ObjectCreatedAll, []Name{ObjectCreatedCompleteMultipartUpload, ObjectCreatedCopy, ObjectCreatedPost, ObjectCreatedPut, ObjectCreatedPutRetention}},
|
||||
{ObjectAccessedAll, []Name{ObjectAccessedGet, ObjectAccessedHead, ObjectAccessedGetRetention, ObjectAccessedGetLegalHold}},
|
||||
{ObjectCreatedAll, []Name{ObjectCreatedCompleteMultipartUpload, ObjectCreatedCopy, ObjectCreatedPost, ObjectCreatedPut, ObjectCreatedPutRetention, ObjectCreatedPutLegalHold}},
|
||||
{ObjectRemovedAll, []Name{ObjectRemovedDelete}},
|
||||
{ObjectAccessedHead, []Name{ObjectAccessedHead}},
|
||||
}
|
||||
@ -60,6 +60,11 @@ func TestNameString(t *testing.T) {
|
||||
{ObjectCreatedPut, "s3:ObjectCreated:Put"},
|
||||
{ObjectRemovedAll, "s3:ObjectRemoved:*"},
|
||||
{ObjectRemovedDelete, "s3:ObjectRemoved:Delete"},
|
||||
{ObjectCreatedPutRetention, "s3:ObjectCreated:PutRetention"},
|
||||
{ObjectCreatedPutLegalHold, "s3:ObjectCreated:PutLegalHold"},
|
||||
{ObjectAccessedGetRetention, "s3:ObjectAccessed:GetRetention"},
|
||||
{ObjectAccessedGetLegalHold, "s3:ObjectAccessed:GetLegalHold"},
|
||||
|
||||
{blankName, ""},
|
||||
}
|
||||
|
||||
|
@ -153,12 +153,12 @@ func TestRulesMapMatch(t *testing.T) {
|
||||
|
||||
func TestNewRulesMap(t *testing.T) {
|
||||
rulesMapCase1 := make(RulesMap)
|
||||
rulesMapCase1.add([]Name{ObjectAccessedGet, ObjectAccessedHead, ObjectAccessedGetRetention},
|
||||
rulesMapCase1.add([]Name{ObjectAccessedGet, ObjectAccessedHead, ObjectAccessedGetRetention, ObjectAccessedGetLegalHold},
|
||||
"*", TargetID{"1", "webhook"})
|
||||
|
||||
rulesMapCase2 := make(RulesMap)
|
||||
rulesMapCase2.add([]Name{ObjectAccessedGet, ObjectAccessedHead,
|
||||
ObjectCreatedPut, ObjectAccessedGetRetention}, "*", TargetID{"1", "webhook"})
|
||||
ObjectCreatedPut, ObjectAccessedGetRetention, ObjectAccessedGetLegalHold}, "*", TargetID{"1", "webhook"})
|
||||
|
||||
rulesMapCase3 := make(RulesMap)
|
||||
rulesMapCase3.add([]Name{ObjectRemovedDelete}, "2010*.jpg", TargetID{"1", "webhook"})
|
||||
|
@ -102,6 +102,12 @@ const (
|
||||
// GetObjectRetentionAction - GetObjectRetention, GetObject, HeadObject Rest API action.
|
||||
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 = "s3:GetBucketObjectLockConfiguration"
|
||||
|
||||
@ -137,6 +143,8 @@ var supportedActions = map[Action]struct{}{
|
||||
PutBucketLifecycleAction: {},
|
||||
PutObjectRetentionAction: {},
|
||||
GetObjectRetentionAction: {},
|
||||
GetObjectLegalHoldAction: {},
|
||||
PutObjectLegalHoldAction: {},
|
||||
PutBucketObjectLockConfigurationAction: {},
|
||||
GetBucketObjectLockConfigurationAction: {},
|
||||
BypassGovernanceModeAction: {},
|
||||
@ -154,6 +162,8 @@ func (action Action) isObjectAction() bool {
|
||||
return true
|
||||
case PutObjectRetentionAction, GetObjectRetentionAction:
|
||||
return true
|
||||
case PutObjectLegalHoldAction, GetObjectLegalHoldAction:
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
@ -267,6 +277,8 @@ var actionConditionKeyMap = map[Action]condition.KeySet{
|
||||
}, condition.CommonKeys...)...),
|
||||
PutObjectRetentionAction: condition.NewKeySet(condition.CommonKeys...),
|
||||
GetObjectRetentionAction: condition.NewKeySet(condition.CommonKeys...),
|
||||
PutObjectLegalHoldAction: condition.NewKeySet(condition.CommonKeys...),
|
||||
GetObjectLegalHoldAction: condition.NewKeySet(condition.CommonKeys...),
|
||||
BypassGovernanceModeAction: condition.NewKeySet(condition.CommonKeys...),
|
||||
BypassGovernanceRetentionAction: condition.NewKeySet(condition.CommonKeys...),
|
||||
GetBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...),
|
||||
|
521
pkg/objectlock/objectlock.go
Normal file
521
pkg/objectlock/objectlock.go
Normal file
@ -0,0 +1,521 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2020 MinIO, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package objectlock
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/beevik/ntp"
|
||||
xhttp "github.com/minio/minio/cmd/http"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/env"
|
||||
)
|
||||
|
||||
// Mode - object retention mode.
|
||||
type Mode string
|
||||
|
||||
const (
|
||||
// Governance - governance mode.
|
||||
Governance Mode = "GOVERNANCE"
|
||||
|
||||
// Compliance - compliance mode.
|
||||
Compliance Mode = "COMPLIANCE"
|
||||
|
||||
// Invalid - invalid retention mode.
|
||||
Invalid Mode = ""
|
||||
)
|
||||
|
||||
func parseMode(modeStr string) (mode Mode) {
|
||||
switch strings.ToUpper(modeStr) {
|
||||
case "GOVERNANCE":
|
||||
mode = Governance
|
||||
case "COMPLIANCE":
|
||||
mode = Compliance
|
||||
default:
|
||||
mode = Invalid
|
||||
}
|
||||
return mode
|
||||
}
|
||||
|
||||
// LegalHoldStatus - object legal hold status.
|
||||
type LegalHoldStatus string
|
||||
|
||||
const (
|
||||
// ON -legal hold is on.
|
||||
ON LegalHoldStatus = "ON"
|
||||
|
||||
// OFF -legal hold is off.
|
||||
OFF LegalHoldStatus = "OFF"
|
||||
)
|
||||
|
||||
func parseLegalHoldStatus(holdStr string) LegalHoldStatus {
|
||||
switch strings.ToUpper(holdStr) {
|
||||
case "ON":
|
||||
return ON
|
||||
case "OFF":
|
||||
return OFF
|
||||
}
|
||||
return LegalHoldStatus("")
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrMalformedBucketObjectConfig -indicates that the bucket object lock config is malformed
|
||||
ErrMalformedBucketObjectConfig = errors.New("invalid bucket object lock config")
|
||||
// ErrInvalidRetentionDate - indicates that retention date needs to be in ISO 8601 format
|
||||
ErrInvalidRetentionDate = errors.New("date must be provided in ISO 8601 format")
|
||||
// ErrPastObjectLockRetainDate - indicates that retention date must be in the future
|
||||
ErrPastObjectLockRetainDate = errors.New("the retain until date must be in the future")
|
||||
// ErrUnknownWORMModeDirective - indicates that the retention mode is invalid
|
||||
ErrUnknownWORMModeDirective = errors.New("unknown WORM mode directive")
|
||||
// ErrObjectLockMissingContentMD5 - indicates missing Content-MD5 header for put object requests with locking
|
||||
ErrObjectLockMissingContentMD5 = errors.New("content-MD5 HTTP header is required for Put Object requests with Object Lock parameters")
|
||||
// ErrObjectLockInvalidHeaders indicates that object lock headers are missing
|
||||
ErrObjectLockInvalidHeaders = errors.New("x-amz-object-lock-retain-until-date and x-amz-object-lock-mode must both be supplied")
|
||||
// ErrMalformedXML - generic error indicating malformed XML
|
||||
ErrMalformedXML = errors.New("the XML you provided was not well-formed or did not validate against our published schema")
|
||||
)
|
||||
|
||||
const (
|
||||
ntpServerEnv = "MINIO_NTP_SERVER"
|
||||
)
|
||||
|
||||
var (
|
||||
ntpServer = env.Get(ntpServerEnv, "")
|
||||
)
|
||||
|
||||
// UTCNowNTP - is similar in functionality to UTCNow()
|
||||
// but only used when we do not wish to rely on system
|
||||
// time.
|
||||
func UTCNowNTP() (time.Time, error) {
|
||||
// ntp server is disabled
|
||||
if ntpServer == "" {
|
||||
return time.Now().UTC(), nil
|
||||
}
|
||||
return ntp.Time(ntpServer)
|
||||
}
|
||||
|
||||
// Retention - bucket level retention configuration.
|
||||
type Retention struct {
|
||||
Mode Mode
|
||||
Validity time.Duration
|
||||
}
|
||||
|
||||
// IsEmpty - returns whether retention is empty or not.
|
||||
func (r Retention) IsEmpty() bool {
|
||||
return r.Mode == "" || r.Validity == 0
|
||||
}
|
||||
|
||||
// Retain - check whether given date is retainable by validity time.
|
||||
func (r Retention) Retain(created time.Time) bool {
|
||||
t, err := UTCNowNTP()
|
||||
if err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
// Retain
|
||||
return true
|
||||
}
|
||||
return created.Add(r.Validity).After(t)
|
||||
}
|
||||
|
||||
// BucketObjectLockConfig - map of bucket and retention configuration.
|
||||
type BucketObjectLockConfig struct {
|
||||
sync.RWMutex
|
||||
retentionMap map[string]Retention
|
||||
}
|
||||
|
||||
// Set - set retention configuration.
|
||||
func (config *BucketObjectLockConfig) Set(bucketName string, retention Retention) {
|
||||
config.Lock()
|
||||
config.retentionMap[bucketName] = retention
|
||||
config.Unlock()
|
||||
}
|
||||
|
||||
// Get - Get retention configuration.
|
||||
func (config *BucketObjectLockConfig) Get(bucketName string) (r Retention, ok bool) {
|
||||
config.RLock()
|
||||
defer config.RUnlock()
|
||||
r, ok = config.retentionMap[bucketName]
|
||||
return r, ok
|
||||
}
|
||||
|
||||
// Remove - removes retention configuration.
|
||||
func (config *BucketObjectLockConfig) Remove(bucketName string) {
|
||||
config.Lock()
|
||||
delete(config.retentionMap, bucketName)
|
||||
config.Unlock()
|
||||
}
|
||||
|
||||
// NewBucketObjectLockConfig returns initialized BucketObjectLockConfig
|
||||
func NewBucketObjectLockConfig() *BucketObjectLockConfig {
|
||||
return &BucketObjectLockConfig{
|
||||
retentionMap: map[string]Retention{},
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultRetention - default retention configuration.
|
||||
type DefaultRetention struct {
|
||||
XMLName xml.Name `xml:"DefaultRetention"`
|
||||
Mode Mode `xml:"Mode"`
|
||||
Days *uint64 `xml:"Days"`
|
||||
Years *uint64 `xml:"Years"`
|
||||
}
|
||||
|
||||
// Maximum support retention days and years supported by AWS S3.
|
||||
const (
|
||||
// This tested by using `mc lock` command
|
||||
maximumRetentionDays = 36500
|
||||
maximumRetentionYears = 100
|
||||
)
|
||||
|
||||
// UnmarshalXML - decodes XML data.
|
||||
func (dr *DefaultRetention) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
// Make subtype to avoid recursive UnmarshalXML().
|
||||
type defaultRetention DefaultRetention
|
||||
retention := defaultRetention{}
|
||||
|
||||
if err := d.DecodeElement(&retention, &start); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch string(retention.Mode) {
|
||||
case "GOVERNANCE", "COMPLIANCE":
|
||||
default:
|
||||
return fmt.Errorf("unknown retention mode %v", retention.Mode)
|
||||
}
|
||||
|
||||
if retention.Days == nil && retention.Years == nil {
|
||||
return fmt.Errorf("either Days or Years must be specified")
|
||||
}
|
||||
|
||||
if retention.Days != nil && retention.Years != nil {
|
||||
return fmt.Errorf("either Days or Years must be specified, not both")
|
||||
}
|
||||
|
||||
if retention.Days != nil {
|
||||
if *retention.Days == 0 {
|
||||
return fmt.Errorf("Default retention period must be a positive integer value for 'Days'")
|
||||
}
|
||||
if *retention.Days > maximumRetentionDays {
|
||||
return fmt.Errorf("Default retention period too large for 'Days' %d", *retention.Days)
|
||||
}
|
||||
} else if *retention.Years == 0 {
|
||||
return fmt.Errorf("Default retention period must be a positive integer value for 'Years'")
|
||||
} else if *retention.Years > maximumRetentionYears {
|
||||
return fmt.Errorf("Default retention period too large for 'Years' %d", *retention.Years)
|
||||
}
|
||||
|
||||
*dr = DefaultRetention(retention)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Config - object lock configuration specified in
|
||||
// https://docs.aws.amazon.com/AmazonS3/latest/API/Type_API_ObjectLockConfiguration.html
|
||||
type Config struct {
|
||||
XMLNS string `xml:"xmlns,attr,omitempty"`
|
||||
XMLName xml.Name `xml:"ObjectLockConfiguration"`
|
||||
ObjectLockEnabled string `xml:"ObjectLockEnabled"`
|
||||
Rule *struct {
|
||||
DefaultRetention DefaultRetention `xml:"DefaultRetention"`
|
||||
} `xml:"Rule,omitempty"`
|
||||
}
|
||||
|
||||
// UnmarshalXML - decodes XML data.
|
||||
func (config *Config) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
// Make subtype to avoid recursive UnmarshalXML().
|
||||
type objectLockConfig Config
|
||||
parsedConfig := objectLockConfig{}
|
||||
|
||||
if err := d.DecodeElement(&parsedConfig, &start); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if parsedConfig.ObjectLockEnabled != "Enabled" {
|
||||
return fmt.Errorf("only 'Enabled' value is allowd to ObjectLockEnabled element")
|
||||
}
|
||||
|
||||
*config = Config(parsedConfig)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToRetention - convert to Retention type.
|
||||
func (config *Config) ToRetention() (r Retention) {
|
||||
if config.Rule != nil {
|
||||
r.Mode = config.Rule.DefaultRetention.Mode
|
||||
|
||||
t, err := UTCNowNTP()
|
||||
if err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
// Do not change any configuration
|
||||
// upon NTP failure.
|
||||
return r
|
||||
}
|
||||
|
||||
if config.Rule.DefaultRetention.Days != nil {
|
||||
r.Validity = t.AddDate(0, 0, int(*config.Rule.DefaultRetention.Days)).Sub(t)
|
||||
} else {
|
||||
r.Validity = t.AddDate(int(*config.Rule.DefaultRetention.Years), 0, 0).Sub(t)
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// ParseObjectLockConfig parses ObjectLockConfig from xml
|
||||
func ParseObjectLockConfig(reader io.Reader) (*Config, error) {
|
||||
config := Config{}
|
||||
if err := xml.NewDecoder(reader).Decode(&config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// NewObjectLockConfig returns a initialized objectlock.Config struct
|
||||
func NewObjectLockConfig() *Config {
|
||||
return &Config{
|
||||
ObjectLockEnabled: "Enabled",
|
||||
}
|
||||
}
|
||||
|
||||
// RetentionDate is a embedded type containing time.Time to unmarshal
|
||||
// Date in Retention
|
||||
type RetentionDate struct {
|
||||
time.Time
|
||||
}
|
||||
|
||||
// UnmarshalXML parses date from Retention and validates date format
|
||||
func (rDate *RetentionDate) UnmarshalXML(d *xml.Decoder, startElement xml.StartElement) error {
|
||||
var dateStr string
|
||||
err := d.DecodeElement(&dateStr, &startElement)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// While AWS documentation mentions that the date specified
|
||||
// must be present in ISO 8601 format, in reality they allow
|
||||
// users to provide RFC 3339 compliant dates.
|
||||
retDate, err := time.Parse(time.RFC3339, dateStr)
|
||||
if err != nil {
|
||||
return ErrInvalidRetentionDate
|
||||
}
|
||||
|
||||
*rDate = RetentionDate{retDate}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalXML encodes expiration date if it is non-zero and encodes
|
||||
// empty string otherwise
|
||||
func (rDate *RetentionDate) MarshalXML(e *xml.Encoder, startElement xml.StartElement) error {
|
||||
if *rDate == (RetentionDate{time.Time{}}) {
|
||||
return nil
|
||||
}
|
||||
return e.EncodeElement(rDate.Format(time.RFC3339), startElement)
|
||||
}
|
||||
|
||||
// ObjectRetention specified in
|
||||
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectRetention.html
|
||||
type ObjectRetention struct {
|
||||
XMLNS string `xml:"xmlns,attr,omitempty"`
|
||||
XMLName xml.Name `xml:"Retention"`
|
||||
Mode Mode `xml:"Mode,omitempty"`
|
||||
RetainUntilDate RetentionDate `xml:"RetainUntilDate,omitempty"`
|
||||
}
|
||||
|
||||
// ParseObjectRetention constructs ObjectRetention struct from xml input
|
||||
func ParseObjectRetention(reader io.Reader) (*ObjectRetention, error) {
|
||||
ret := ObjectRetention{}
|
||||
if err := xml.NewDecoder(reader).Decode(&ret); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ret.Mode != Compliance && ret.Mode != Governance {
|
||||
return &ret, ErrUnknownWORMModeDirective
|
||||
}
|
||||
|
||||
t, err := UTCNowNTP()
|
||||
if err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
return &ret, ErrPastObjectLockRetainDate
|
||||
}
|
||||
|
||||
if ret.RetainUntilDate.Before(t) {
|
||||
return &ret, ErrPastObjectLockRetainDate
|
||||
}
|
||||
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
// IsObjectLockRetentionRequested returns true if object lock retention headers are set.
|
||||
func IsObjectLockRetentionRequested(h http.Header) bool {
|
||||
if _, ok := h[xhttp.AmzObjectLockMode]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := h[xhttp.AmzObjectLockRetainUntilDate]; ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsObjectLockLegalHoldRequested returns true if object lock legal hold header is set.
|
||||
func IsObjectLockLegalHoldRequested(h http.Header) bool {
|
||||
_, ok := h[xhttp.AmzObjectLockLegalHold]
|
||||
return ok
|
||||
}
|
||||
|
||||
// IsObjectLockGovernanceBypassSet returns true if object lock governance bypass header is set.
|
||||
func IsObjectLockGovernanceBypassSet(h http.Header) bool {
|
||||
return strings.ToLower(h.Get(xhttp.AmzObjectLockBypassGovernance)) == "true"
|
||||
}
|
||||
|
||||
// IsObjectLockRequested returns true if legal hold or object lock retention headers are requested.
|
||||
func IsObjectLockRequested(h http.Header) bool {
|
||||
return IsObjectLockLegalHoldRequested(h) || IsObjectLockRetentionRequested(h)
|
||||
}
|
||||
|
||||
// ParseObjectLockRetentionHeaders parses http headers to extract retention mode and retention date
|
||||
func ParseObjectLockRetentionHeaders(h http.Header) (rmode Mode, r RetentionDate, err error) {
|
||||
retMode := h.Get(xhttp.AmzObjectLockMode)
|
||||
dateStr := h.Get(xhttp.AmzObjectLockRetainUntilDate)
|
||||
if len(retMode) == 0 || len(dateStr) == 0 {
|
||||
return rmode, r, ErrObjectLockInvalidHeaders
|
||||
}
|
||||
rmode = parseMode(retMode)
|
||||
if rmode == Invalid {
|
||||
return rmode, r, ErrUnknownWORMModeDirective
|
||||
}
|
||||
|
||||
var retDate time.Time
|
||||
// While AWS documentation mentions that the date specified
|
||||
// must be present in ISO 8601 format, in reality they allow
|
||||
// users to provide RFC 3339 compliant dates.
|
||||
retDate, err = time.Parse(time.RFC3339, dateStr)
|
||||
if err != nil {
|
||||
return rmode, r, ErrInvalidRetentionDate
|
||||
}
|
||||
|
||||
t, err := UTCNowNTP()
|
||||
if err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
return rmode, r, ErrPastObjectLockRetainDate
|
||||
}
|
||||
|
||||
if retDate.Before(t) {
|
||||
return rmode, r, ErrPastObjectLockRetainDate
|
||||
}
|
||||
|
||||
return rmode, RetentionDate{retDate}, nil
|
||||
|
||||
}
|
||||
|
||||
// GetObjectRetentionMeta constructs ObjectRetention from metadata
|
||||
func GetObjectRetentionMeta(meta map[string]string) ObjectRetention {
|
||||
var mode Mode
|
||||
var retainTill RetentionDate
|
||||
|
||||
if modeStr, ok := meta[strings.ToLower(xhttp.AmzObjectLockMode)]; ok {
|
||||
mode = parseMode(modeStr)
|
||||
}
|
||||
if tillStr, ok := meta[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)]; ok {
|
||||
if t, e := time.Parse(time.RFC3339, tillStr); e == nil {
|
||||
retainTill = RetentionDate{t.UTC()}
|
||||
}
|
||||
}
|
||||
return ObjectRetention{Mode: mode, RetainUntilDate: retainTill}
|
||||
}
|
||||
|
||||
// GetObjectLegalHoldMeta constructs ObjectLegalHold from metadata
|
||||
func GetObjectLegalHoldMeta(meta map[string]string) ObjectLegalHold {
|
||||
|
||||
holdStr, ok := meta[strings.ToLower(xhttp.AmzObjectLockLegalHold)]
|
||||
if ok {
|
||||
return ObjectLegalHold{Status: parseLegalHoldStatus(holdStr)}
|
||||
}
|
||||
return ObjectLegalHold{}
|
||||
}
|
||||
|
||||
// ParseObjectLockLegalHoldHeaders parses request headers to construct ObjectLegalHold
|
||||
func ParseObjectLockLegalHoldHeaders(h http.Header) (lhold ObjectLegalHold, err error) {
|
||||
holdStatus, ok := h[xhttp.AmzObjectLockLegalHold]
|
||||
if ok {
|
||||
lh := parseLegalHoldStatus(strings.Join(holdStatus, ""))
|
||||
if lh != ON && lh != OFF {
|
||||
return lhold, ErrUnknownWORMModeDirective
|
||||
}
|
||||
lhold = ObjectLegalHold{Status: lh}
|
||||
}
|
||||
return lhold, nil
|
||||
|
||||
}
|
||||
|
||||
// ObjectLegalHold specified in
|
||||
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectLegalHold.html
|
||||
type ObjectLegalHold struct {
|
||||
XMLNS string `xml:"xmlns,attr,omitempty"`
|
||||
XMLName xml.Name `xml:"LegalHold"`
|
||||
Status LegalHoldStatus `xml:"Status,omitempty"`
|
||||
}
|
||||
|
||||
// ParseObjectLegalHold decodes the XML into ObjectLegalHold
|
||||
func ParseObjectLegalHold(reader io.Reader) (hold *ObjectLegalHold, err error) {
|
||||
if err = xml.NewDecoder(reader).Decode(&hold); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if hold.Status != ON && hold.Status != OFF {
|
||||
return nil, ErrMalformedXML
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// FilterObjectLockMetadata filters object lock metadata if s3:GetObjectRetention permission is denied or if isCopy flag set.
|
||||
func FilterObjectLockMetadata(metadata map[string]string, filterRetention, filterLegalHold bool) map[string]string {
|
||||
// Copy on write
|
||||
dst := metadata
|
||||
var copied bool
|
||||
delKey := func(key string) {
|
||||
if _, ok := metadata[key]; !ok {
|
||||
return
|
||||
}
|
||||
if !copied {
|
||||
dst = make(map[string]string, len(metadata))
|
||||
for k, v := range metadata {
|
||||
dst[k] = v
|
||||
}
|
||||
copied = true
|
||||
}
|
||||
delete(dst, key)
|
||||
}
|
||||
legalHold := GetObjectLegalHoldMeta(metadata)
|
||||
if legalHold.Status == "" || filterLegalHold {
|
||||
delKey(xhttp.AmzObjectLockLegalHold)
|
||||
}
|
||||
|
||||
ret := GetObjectRetentionMeta(metadata)
|
||||
|
||||
if ret.Mode == Invalid || filterRetention {
|
||||
delKey(xhttp.AmzObjectLockMode)
|
||||
delKey(xhttp.AmzObjectLockRetainUntilDate)
|
||||
return dst
|
||||
}
|
||||
return dst
|
||||
}
|
567
pkg/objectlock/objectlock_test.go
Normal file
567
pkg/objectlock/objectlock_test.go
Normal file
@ -0,0 +1,567 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2020 MinIO, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package objectlock
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
xhttp "github.com/minio/minio/cmd/http"
|
||||
)
|
||||
|
||||
func TestParseMode(t *testing.T) {
|
||||
testCases := []struct {
|
||||
value string
|
||||
expectedMode Mode
|
||||
}{
|
||||
{
|
||||
value: "governance",
|
||||
expectedMode: Governance,
|
||||
},
|
||||
{
|
||||
value: "complIAnce",
|
||||
expectedMode: Compliance,
|
||||
},
|
||||
{
|
||||
value: "gce",
|
||||
expectedMode: Invalid,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
if parseMode(tc.value) != tc.expectedMode {
|
||||
t.Errorf("Expected Mode %s, got %s", tc.expectedMode, parseMode(tc.value))
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestParseLegalHoldStatus(t *testing.T) {
|
||||
tests := []struct {
|
||||
value string
|
||||
expectedStatus LegalHoldStatus
|
||||
}{
|
||||
{
|
||||
value: "ON",
|
||||
expectedStatus: ON,
|
||||
},
|
||||
{
|
||||
value: "Off",
|
||||
expectedStatus: OFF,
|
||||
},
|
||||
{
|
||||
value: "x",
|
||||
expectedStatus: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
actualStatus := parseLegalHoldStatus(tt.value)
|
||||
if actualStatus != tt.expectedStatus {
|
||||
t.Errorf("Expected legal hold status %s, got %s", tt.expectedStatus, actualStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestUnmarshalDefaultRetention checks if default retention
|
||||
// marshaling and unmarshaling work as expected
|
||||
func TestUnmarshalDefaultRetention(t *testing.T) {
|
||||
days := uint64(4)
|
||||
years := uint64(1)
|
||||
zerodays := uint64(0)
|
||||
invalidDays := uint64(maximumRetentionDays + 1)
|
||||
tests := []struct {
|
||||
value DefaultRetention
|
||||
expectedErr error
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
value: DefaultRetention{Mode: "retain"},
|
||||
expectedErr: fmt.Errorf("unknown retention mode retain"),
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
value: DefaultRetention{Mode: "GOVERNANCE"},
|
||||
expectedErr: fmt.Errorf("either Days or Years must be specified"),
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
value: DefaultRetention{Mode: "GOVERNANCE", Days: &days},
|
||||
expectedErr: nil,
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
value: DefaultRetention{Mode: "GOVERNANCE", Years: &years},
|
||||
expectedErr: nil,
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
value: DefaultRetention{Mode: "GOVERNANCE", Days: &days, Years: &years},
|
||||
expectedErr: fmt.Errorf("either Days or Years must be specified, not both"),
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
value: DefaultRetention{Mode: "GOVERNANCE", Days: &zerodays},
|
||||
expectedErr: fmt.Errorf("Default retention period must be a positive integer value for 'Days'"),
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
value: DefaultRetention{Mode: "GOVERNANCE", Days: &invalidDays},
|
||||
expectedErr: fmt.Errorf("Default retention period too large for 'Days' %d", invalidDays),
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
d, err := xml.MarshalIndent(&tt.value, "", "\t")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var dr DefaultRetention
|
||||
err = xml.Unmarshal(d, &dr)
|
||||
if tt.expectedErr == nil {
|
||||
if err != nil {
|
||||
t.Fatalf("error: expected = <nil>, got = %v", err)
|
||||
}
|
||||
} else if err == nil {
|
||||
t.Fatalf("error: expected = %v, got = <nil>", tt.expectedErr)
|
||||
} else if tt.expectedErr.Error() != err.Error() {
|
||||
t.Fatalf("error: expected = %v, got = %v", tt.expectedErr, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseObjectLockConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
value string
|
||||
expectedErr error
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
value: "<ObjectLockConfiguration><ObjectLockEnabled>yes</ObjectLockEnabled></ObjectLockConfiguration>",
|
||||
expectedErr: fmt.Errorf("only 'Enabled' value is allowd to ObjectLockEnabled element"),
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
value: "<ObjectLockConfiguration><ObjectLockEnabled>Enabled</ObjectLockEnabled><Rule><DefaultRetention><Mode>COMPLIANCE</Mode><Days>0</Days></DefaultRetention></Rule></ObjectLockConfiguration>",
|
||||
expectedErr: fmt.Errorf("Default retention period must be a positive integer value for 'Days'"),
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
value: "<ObjectLockConfiguration><ObjectLockEnabled>Enabled</ObjectLockEnabled><Rule><DefaultRetention><Mode>COMPLIANCE</Mode><Days>30</Days></DefaultRetention></Rule></ObjectLockConfiguration>",
|
||||
expectedErr: nil,
|
||||
expectErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
_, err := ParseObjectLockConfig(strings.NewReader(tt.value))
|
||||
if tt.expectedErr == nil {
|
||||
if err != nil {
|
||||
t.Fatalf("error: expected = <nil>, got = %v", err)
|
||||
}
|
||||
} else if err == nil {
|
||||
t.Fatalf("error: expected = %v, got = <nil>", tt.expectedErr)
|
||||
} else if tt.expectedErr.Error() != err.Error() {
|
||||
t.Fatalf("error: expected = %v, got = %v", tt.expectedErr, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseObjectRetention(t *testing.T) {
|
||||
tests := []struct {
|
||||
value string
|
||||
expectedErr error
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
value: "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Retention xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Mode>string</Mode><RetainUntilDate>2020-01-02T15:04:05Z</RetainUntilDate></Retention>",
|
||||
expectedErr: ErrUnknownWORMModeDirective,
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
value: "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Retention xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Mode>COMPLIANCE</Mode><RetainUntilDate>2017-01-02T15:04:05Z</RetainUntilDate></Retention>",
|
||||
expectedErr: ErrPastObjectLockRetainDate,
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
value: "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Retention xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Mode>GOVERNANCE</Mode><RetainUntilDate>2057-01-02T15:04:05Z</RetainUntilDate></Retention>",
|
||||
expectedErr: nil,
|
||||
expectErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
_, err := ParseObjectRetention(strings.NewReader(tt.value))
|
||||
if tt.expectedErr == nil {
|
||||
if err != nil {
|
||||
t.Fatalf("error: expected = <nil>, got = %v", err)
|
||||
}
|
||||
} else if err == nil {
|
||||
t.Fatalf("error: expected = %v, got = <nil>", tt.expectedErr)
|
||||
} else if tt.expectedErr.Error() != err.Error() {
|
||||
t.Fatalf("error: expected = %v, got = %v", tt.expectedErr, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsObjectLockRequested(t *testing.T) {
|
||||
tests := []struct {
|
||||
header http.Header
|
||||
expectedVal bool
|
||||
}{
|
||||
{
|
||||
header: http.Header{
|
||||
"Authorization": []string{"AWS4-HMAC-SHA256 <cred_string>"},
|
||||
"X-Amz-Content-Sha256": []string{""},
|
||||
"Content-Encoding": []string{""},
|
||||
},
|
||||
expectedVal: false,
|
||||
},
|
||||
{
|
||||
header: http.Header{
|
||||
xhttp.AmzObjectLockLegalHold: []string{""},
|
||||
},
|
||||
expectedVal: true,
|
||||
},
|
||||
{
|
||||
header: http.Header{
|
||||
xhttp.AmzObjectLockRetainUntilDate: []string{""},
|
||||
xhttp.AmzObjectLockMode: []string{""},
|
||||
},
|
||||
expectedVal: true,
|
||||
},
|
||||
{
|
||||
header: http.Header{
|
||||
xhttp.AmzObjectLockBypassGovernance: []string{""},
|
||||
},
|
||||
expectedVal: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
actualVal := IsObjectLockRequested(tt.header)
|
||||
if actualVal != tt.expectedVal {
|
||||
t.Fatalf("error: expected %v, actual %v", tt.expectedVal, actualVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsObjectLockGovernanceBypassSet(t *testing.T) {
|
||||
tests := []struct {
|
||||
header http.Header
|
||||
expectedVal bool
|
||||
}{
|
||||
{
|
||||
header: http.Header{
|
||||
"Authorization": []string{"AWS4-HMAC-SHA256 <cred_string>"},
|
||||
"X-Amz-Content-Sha256": []string{""},
|
||||
"Content-Encoding": []string{""},
|
||||
},
|
||||
expectedVal: false,
|
||||
},
|
||||
{
|
||||
header: http.Header{
|
||||
xhttp.AmzObjectLockLegalHold: []string{""},
|
||||
},
|
||||
expectedVal: false,
|
||||
},
|
||||
{
|
||||
header: http.Header{
|
||||
xhttp.AmzObjectLockRetainUntilDate: []string{""},
|
||||
xhttp.AmzObjectLockMode: []string{""},
|
||||
},
|
||||
expectedVal: false,
|
||||
},
|
||||
{
|
||||
header: http.Header{
|
||||
xhttp.AmzObjectLockBypassGovernance: []string{""},
|
||||
},
|
||||
expectedVal: false,
|
||||
},
|
||||
{
|
||||
header: http.Header{
|
||||
xhttp.AmzObjectLockBypassGovernance: []string{"true"},
|
||||
},
|
||||
expectedVal: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
actualVal := IsObjectLockGovernanceBypassSet(tt.header)
|
||||
if actualVal != tt.expectedVal {
|
||||
t.Fatalf("error: expected %v, actual %v", tt.expectedVal, actualVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseObjectLockRetentionHeaders(t *testing.T) {
|
||||
tests := []struct {
|
||||
header http.Header
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
header: http.Header{
|
||||
"Authorization": []string{"AWS4-HMAC-SHA256 <cred_string>"},
|
||||
"X-Amz-Content-Sha256": []string{""},
|
||||
"Content-Encoding": []string{""},
|
||||
},
|
||||
expectedErr: ErrObjectLockInvalidHeaders,
|
||||
},
|
||||
{
|
||||
header: http.Header{
|
||||
xhttp.AmzObjectLockMode: []string{"lock"},
|
||||
xhttp.AmzObjectLockRetainUntilDate: []string{"2017-01-02"},
|
||||
},
|
||||
expectedErr: ErrUnknownWORMModeDirective,
|
||||
},
|
||||
{
|
||||
header: http.Header{
|
||||
xhttp.AmzObjectLockMode: []string{"governance"},
|
||||
},
|
||||
expectedErr: ErrObjectLockInvalidHeaders,
|
||||
},
|
||||
{
|
||||
header: http.Header{
|
||||
xhttp.AmzObjectLockRetainUntilDate: []string{"2017-01-02"},
|
||||
xhttp.AmzObjectLockMode: []string{"governance"},
|
||||
},
|
||||
expectedErr: ErrInvalidRetentionDate,
|
||||
},
|
||||
{
|
||||
header: http.Header{
|
||||
xhttp.AmzObjectLockRetainUntilDate: []string{"2017-01-02T15:04:05Z"},
|
||||
xhttp.AmzObjectLockMode: []string{"governance"},
|
||||
},
|
||||
expectedErr: ErrPastObjectLockRetainDate,
|
||||
},
|
||||
{
|
||||
header: http.Header{
|
||||
xhttp.AmzObjectLockMode: []string{"governance"},
|
||||
xhttp.AmzObjectLockRetainUntilDate: []string{"2017-01-02T15:04:05Z"},
|
||||
},
|
||||
expectedErr: ErrPastObjectLockRetainDate,
|
||||
},
|
||||
{
|
||||
header: http.Header{
|
||||
xhttp.AmzObjectLockMode: []string{"governance"},
|
||||
xhttp.AmzObjectLockRetainUntilDate: []string{"2087-01-02T15:04:05Z"},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
_, _, err := ParseObjectLockRetentionHeaders(tt.header)
|
||||
if tt.expectedErr == nil {
|
||||
if err != nil {
|
||||
t.Fatalf("Case %d error: expected = <nil>, got = %v", i, err)
|
||||
}
|
||||
} else if err == nil {
|
||||
t.Fatalf("Case %d error: expected = %v, got = <nil>", i, tt.expectedErr)
|
||||
} else if tt.expectedErr.Error() != err.Error() {
|
||||
t.Fatalf("Case %d error: expected = %v, got = %v", i, tt.expectedErr, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetObjectRetentionMeta(t *testing.T) {
|
||||
tests := []struct {
|
||||
metadata map[string]string
|
||||
expected ObjectRetention
|
||||
}{
|
||||
{
|
||||
metadata: map[string]string{
|
||||
"Authorization": "AWS4-HMAC-SHA256 <cred_string>",
|
||||
"X-Amz-Content-Sha256": "",
|
||||
"Content-Encoding": "",
|
||||
},
|
||||
expected: ObjectRetention{},
|
||||
},
|
||||
{
|
||||
metadata: map[string]string{
|
||||
"x-amz-object-lock-mode": "governance",
|
||||
},
|
||||
expected: ObjectRetention{Mode: Governance},
|
||||
},
|
||||
{
|
||||
metadata: map[string]string{
|
||||
"x-amz-object-lock-retain-until-date": "2020-02-01",
|
||||
},
|
||||
expected: ObjectRetention{RetainUntilDate: RetentionDate{time.Date(2020, 2, 1, 12, 0, 0, 0, time.UTC)}},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
o := GetObjectRetentionMeta(tt.metadata)
|
||||
if o.Mode != tt.expected.Mode {
|
||||
t.Fatalf("Case %d expected %v, got %v", i, tt.expected.Mode, o.Mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetObjectLegalHoldMeta(t *testing.T) {
|
||||
tests := []struct {
|
||||
metadata map[string]string
|
||||
expected ObjectLegalHold
|
||||
}{
|
||||
{
|
||||
metadata: map[string]string{
|
||||
"x-amz-object-lock-mode": "governance",
|
||||
},
|
||||
expected: ObjectLegalHold{},
|
||||
},
|
||||
{
|
||||
metadata: map[string]string{
|
||||
"x-amz-object-lock-legal-hold": "on",
|
||||
},
|
||||
expected: ObjectLegalHold{Status: ON},
|
||||
},
|
||||
{
|
||||
metadata: map[string]string{
|
||||
"x-amz-object-lock-legal-hold": "off",
|
||||
},
|
||||
expected: ObjectLegalHold{Status: OFF},
|
||||
},
|
||||
{
|
||||
metadata: map[string]string{
|
||||
"x-amz-object-lock-legal-hold": "X",
|
||||
},
|
||||
expected: ObjectLegalHold{Status: ""},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
o := GetObjectLegalHoldMeta(tt.metadata)
|
||||
if o.Status != tt.expected.Status {
|
||||
t.Fatalf("Case %d expected %v, got %v", i, tt.expected.Status, o.Status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseObjectLegalHold(t *testing.T) {
|
||||
tests := []struct {
|
||||
value string
|
||||
expectedErr error
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
value: "<?xml version=\"1.0\" encoding=\"UTF-8\"?><LegalHold xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Status>string</Status></LegalHold>",
|
||||
expectedErr: ErrMalformedXML,
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
value: "<?xml version=\"1.0\" encoding=\"UTF-8\"?><LegalHold xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Status>ON</Status></LegalHold>",
|
||||
expectedErr: nil,
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
value: "<?xml version=\"1.0\" encoding=\"UTF-8\"?><LegalHold xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Status>On</Status></LegalHold>",
|
||||
expectedErr: ErrMalformedXML,
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
_, err := ParseObjectLegalHold(strings.NewReader(tt.value))
|
||||
if tt.expectedErr == nil {
|
||||
if err != nil {
|
||||
t.Fatalf("Case %d error: expected = <nil>, got = %v", i, err)
|
||||
}
|
||||
} else if err == nil {
|
||||
t.Fatalf("Case %d error: expected = %v, got = <nil>", i, tt.expectedErr)
|
||||
} else if tt.expectedErr.Error() != err.Error() {
|
||||
t.Fatalf("Case %d error: expected = %v, got = %v", i, tt.expectedErr, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestFilterObjectLockMetadata(t *testing.T) {
|
||||
tests := []struct {
|
||||
metadata map[string]string
|
||||
filterRetention bool
|
||||
filterLegalHold bool
|
||||
expected map[string]string
|
||||
}{
|
||||
{
|
||||
metadata: map[string]string{
|
||||
"Authorization": "AWS4-HMAC-SHA256 <cred_string>",
|
||||
"X-Amz-Content-Sha256": "",
|
||||
"Content-Encoding": "",
|
||||
},
|
||||
expected: map[string]string{
|
||||
"Authorization": "AWS4-HMAC-SHA256 <cred_string>",
|
||||
"X-Amz-Content-Sha256": "",
|
||||
"Content-Encoding": "",
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: map[string]string{
|
||||
"x-amz-object-lock-mode": "governance",
|
||||
},
|
||||
expected: map[string]string{
|
||||
"x-amz-object-lock-mode": "governance",
|
||||
},
|
||||
filterRetention: false,
|
||||
},
|
||||
{
|
||||
metadata: map[string]string{
|
||||
"x-amz-object-lock-mode": "governance",
|
||||
"x-amz-object-lock-retain-until-date": "2020-02-01",
|
||||
},
|
||||
expected: map[string]string{},
|
||||
filterRetention: true,
|
||||
},
|
||||
{
|
||||
metadata: map[string]string{
|
||||
"x-amz-object-lock-legal-hold": "off",
|
||||
},
|
||||
expected: map[string]string{},
|
||||
filterLegalHold: true,
|
||||
},
|
||||
{
|
||||
metadata: map[string]string{
|
||||
"x-amz-object-lock-legal-hold": "on",
|
||||
},
|
||||
expected: map[string]string{"x-amz-object-lock-legal-hold": "on"},
|
||||
filterLegalHold: false,
|
||||
},
|
||||
{
|
||||
metadata: map[string]string{
|
||||
"x-amz-object-lock-legal-hold": "on",
|
||||
"x-amz-object-lock-mode": "governance",
|
||||
"x-amz-object-lock-retain-until-date": "2020-02-01",
|
||||
},
|
||||
expected: map[string]string{},
|
||||
filterRetention: true,
|
||||
filterLegalHold: true,
|
||||
},
|
||||
{
|
||||
metadata: map[string]string{
|
||||
"x-amz-object-lock-legal-hold": "on",
|
||||
"x-amz-object-lock-mode": "governance",
|
||||
"x-amz-object-lock-retain-until-date": "2020-02-01",
|
||||
},
|
||||
expected: map[string]string{"x-amz-object-lock-legal-hold": "on",
|
||||
"x-amz-object-lock-mode": "governance",
|
||||
"x-amz-object-lock-retain-until-date": "2020-02-01"},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
o := FilterObjectLockMetadata(tt.metadata, tt.filterRetention, tt.filterLegalHold)
|
||||
if !reflect.DeepEqual(o, tt.metadata) {
|
||||
t.Fatalf("Case %d expected %v, got %v", i, tt.metadata, o)
|
||||
}
|
||||
}
|
||||
}
|
@ -98,6 +98,10 @@ const (
|
||||
|
||||
// GetObjectRetentionAction - GetObjectRetention, GetObject, HeadObject Rest API action.
|
||||
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 = "s3:GetBucketObjectLockConfiguration"
|
||||
// PutBucketObjectLockConfigurationAction - PutObjectLockConfiguration Rest API action
|
||||
@ -113,6 +117,8 @@ func (action Action) isObjectAction() bool {
|
||||
return true
|
||||
case PutObjectRetentionAction, GetObjectRetentionAction:
|
||||
return true
|
||||
case PutObjectLegalHoldAction, GetObjectLegalHoldAction:
|
||||
return true
|
||||
case BypassGovernanceModeAction, BypassGovernanceRetentionAction:
|
||||
return true
|
||||
}
|
||||
@ -143,6 +149,8 @@ func (action Action) IsValid() bool {
|
||||
return true
|
||||
case PutObjectRetentionAction, GetObjectRetentionAction:
|
||||
return true
|
||||
case PutObjectLegalHoldAction, GetObjectLegalHoldAction:
|
||||
return true
|
||||
case PutBucketObjectLockConfigurationAction, GetBucketObjectLockConfigurationAction:
|
||||
return true
|
||||
}
|
||||
@ -231,6 +239,8 @@ var actionConditionKeyMap = map[Action]condition.KeySet{
|
||||
GetObjectRetentionAction: condition.NewKeySet(condition.CommonKeys...),
|
||||
BypassGovernanceModeAction: condition.NewKeySet(condition.CommonKeys...),
|
||||
BypassGovernanceRetentionAction: condition.NewKeySet(condition.CommonKeys...),
|
||||
PutObjectLegalHoldAction: condition.NewKeySet(condition.CommonKeys...),
|
||||
GetObjectLegalHoldAction: condition.NewKeySet(condition.CommonKeys...),
|
||||
GetBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...),
|
||||
PutBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...),
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user