fix: support object-remaining-retention-days policy condition (#9259)

This PR also tries to simplify the approach taken in
object-locking implementation by preferential treatment
given towards full validation.

This in-turn has fixed couple of bugs related to
how policy should have been honored when ByPassGovernance
is provided.

Simplifies code a bit, but also duplicates code intentionally
for clarity due to complex nature of object locking
implementation.
master
Harshavardhana 5 years ago committed by GitHub
parent b9b1bfefe7
commit 43a3778b45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      buildscripts/gateway-tests.sh
  2. 103
      cmd/auth-handler.go
  3. 11
      cmd/bucket-handlers.go
  4. 4
      cmd/disk-cache.go
  5. 164
      cmd/object-handlers.go
  6. 293
      cmd/object-lock.go
  7. 23
      cmd/policy.go
  8. 165
      cmd/web-handlers.go
  9. 146
      pkg/bucket/object/lock/lock.go
  10. 52
      pkg/bucket/object/lock/lock_test.go
  11. 7
      pkg/bucket/policy/action.go
  12. 1
      pkg/bucket/policy/condition/stringequalsfunc.go
  13. 6
      pkg/iam/policy/action.go

@ -46,7 +46,10 @@ function main()
gw_pid="$(start_minio_gateway_s3)" gw_pid="$(start_minio_gateway_s3)"
SERVER_ENDPOINT=127.0.0.1:24240 ENABLE_HTTPS=0 ACCESS_KEY=minio \ SERVER_ENDPOINT=127.0.0.1:24240 ENABLE_HTTPS=0 ACCESS_KEY=minio \
SECRET_KEY=minio123 MINT_MODE="full" /mint/entrypoint.sh SECRET_KEY=minio123 MINT_MODE="full" /mint/entrypoint.sh \
aws-sdk-go aws-sdk-java aws-sdk-php aws-sdk-ruby awscli \
healthcheck mc minio-dotnet minio-java minio-js \
minio-py s3cmd s3select security
rv=$? rv=$?
kill "$sr_pid" kill "$sr_pid"

@ -26,12 +26,15 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"strconv"
"strings" "strings"
"time"
xhttp "github.com/minio/minio/cmd/http" xhttp "github.com/minio/minio/cmd/http"
xjwt "github.com/minio/minio/cmd/jwt" xjwt "github.com/minio/minio/cmd/jwt"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/auth" "github.com/minio/minio/pkg/auth"
objectlock "github.com/minio/minio/pkg/bucket/object/lock"
"github.com/minio/minio/pkg/bucket/policy" "github.com/minio/minio/pkg/bucket/policy"
"github.com/minio/minio/pkg/hash" "github.com/minio/minio/pkg/hash"
iampolicy "github.com/minio/minio/pkg/iam/policy" iampolicy "github.com/minio/minio/pkg/iam/policy"
@ -464,6 +467,106 @@ func (a authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrSignatureVersionNotSupported), r.URL, guessIsBrowserReq(r)) writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrSignatureVersionNotSupported), r.URL, guessIsBrowserReq(r))
} }
func validateSignature(atype authType, r *http.Request) (auth.Credentials, bool, map[string]interface{}, APIErrorCode) {
var cred auth.Credentials
var owner bool
var s3Err APIErrorCode
switch atype {
case authTypeUnknown, authTypeStreamingSigned:
return cred, owner, nil, ErrSignatureVersionNotSupported
case authTypeSignedV2, authTypePresignedV2:
if s3Err = isReqAuthenticatedV2(r); s3Err != ErrNone {
return cred, owner, nil, s3Err
}
cred, owner, s3Err = getReqAccessKeyV2(r)
case authTypePresigned, authTypeSigned:
region := globalServerRegion
if s3Err = isReqAuthenticated(GlobalContext, r, region, serviceS3); s3Err != ErrNone {
return cred, owner, nil, s3Err
}
cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3)
}
if s3Err != ErrNone {
return cred, owner, nil, s3Err
}
claims, s3Err := checkClaimsFromToken(r, cred)
if s3Err != ErrNone {
return cred, owner, nil, s3Err
}
return cred, owner, claims, ErrNone
}
func isPutRetentionAllowed(bucketName, objectName string, retDays int, retDate time.Time, retMode objectlock.RetMode, byPassSet bool, r *http.Request, cred auth.Credentials, owner bool, claims map[string]interface{}) (s3Err APIErrorCode) {
var retSet bool
if cred.AccessKey == "" {
conditions := getConditionValues(r, "", "", nil)
conditions["object-lock-mode"] = []string{string(retMode)}
conditions["object-lock-retain-until-date"] = []string{retDate.Format(time.RFC3339)}
if retDays > 0 {
conditions["object-lock-remaining-retention-days"] = []string{strconv.Itoa(retDays)}
}
if retMode == objectlock.RetGovernance && byPassSet {
byPassSet = globalPolicySys.IsAllowed(policy.Args{
AccountName: cred.AccessKey,
Action: policy.Action(policy.BypassGovernanceRetentionAction),
BucketName: bucketName,
ConditionValues: conditions,
IsOwner: false,
ObjectName: objectName,
})
}
if globalPolicySys.IsAllowed(policy.Args{
AccountName: cred.AccessKey,
Action: policy.Action(policy.PutObjectRetentionAction),
BucketName: bucketName,
ConditionValues: conditions,
IsOwner: false,
ObjectName: objectName,
}) {
retSet = true
}
if byPassSet || retSet {
return ErrNone
}
return ErrAccessDenied
}
conditions := getConditionValues(r, "", cred.AccessKey, claims)
conditions["object-lock-mode"] = []string{string(retMode)}
conditions["object-lock-retain-until-date"] = []string{retDate.Format(time.RFC3339)}
if retDays > 0 {
conditions["object-lock-remaining-retention-days"] = []string{strconv.Itoa(retDays)}
}
if retMode == objectlock.RetGovernance && byPassSet {
byPassSet = globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: cred.AccessKey,
Action: policy.BypassGovernanceRetentionAction,
BucketName: bucketName,
ObjectName: objectName,
ConditionValues: conditions,
IsOwner: owner,
Claims: claims,
})
}
if globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: cred.AccessKey,
Action: policy.PutObjectRetentionAction,
BucketName: bucketName,
ConditionValues: conditions,
ObjectName: objectName,
IsOwner: owner,
Claims: claims,
}) {
retSet = true
}
if byPassSet || retSet {
return ErrNone
}
return ErrAccessDenied
}
// isPutActionAllowed - check if PUT operation is allowed on the resource, this // isPutActionAllowed - check if PUT operation is allowed on the resource, this
// call verifies bucket policies and IAM policies, supports multi user // call verifies bucket policies and IAM policies, supports multi user
// checks etc. // checks etc.

@ -399,11 +399,14 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
} }
continue continue
} }
govBypassPerms := checkRequestAuthType(ctx, r, policy.BypassGovernanceRetentionAction, bucket, object.ObjectName)
if _, err := enforceRetentionBypassForDelete(ctx, r, bucket, object.ObjectName, getObjectInfoFn, govBypassPerms); err != ErrNone { if _, ok := globalBucketObjectLockConfig.Get(bucket); ok {
dErrs[index] = err if err := enforceRetentionBypassForDelete(ctx, r, bucket, object.ObjectName, getObjectInfoFn); err != ErrNone {
continue dErrs[index] = err
continue
}
} }
// Avoid duplicate objects, we use map to filter them out. // Avoid duplicate objects, we use map to filter them out.
if _, ok := objectsToDelete[object.ObjectName]; !ok { if _, ok := objectsToDelete[object.ObjectName]; !ok {
objectsToDelete[object.ObjectName] = index objectsToDelete[object.ObjectName] = index

@ -239,7 +239,7 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
// skip cache for objects with locks // skip cache for objects with locks
objRetention := objectlock.GetObjectRetentionMeta(objInfo.UserDefined) objRetention := objectlock.GetObjectRetentionMeta(objInfo.UserDefined)
legalHold := objectlock.GetObjectLegalHoldMeta(objInfo.UserDefined) legalHold := objectlock.GetObjectLegalHoldMeta(objInfo.UserDefined)
if objRetention.Mode != objectlock.Invalid || legalHold.Status != "" { if objRetention.Mode.Valid() || legalHold.Status.Valid() {
c.cacheStats.incMiss() c.cacheStats.incMiss()
return c.GetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts) return c.GetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts)
} }
@ -614,7 +614,7 @@ func (c *cacheObjects) PutObject(ctx context.Context, bucket, object string, r *
// skip cache for objects with locks // skip cache for objects with locks
objRetention := objectlock.GetObjectRetentionMeta(opts.UserDefined) objRetention := objectlock.GetObjectRetentionMeta(opts.UserDefined)
legalHold := objectlock.GetObjectLegalHoldMeta(opts.UserDefined) legalHold := objectlock.GetObjectLegalHoldMeta(opts.UserDefined)
if objRetention.Mode != objectlock.Invalid || legalHold.Status != "" { if objRetention.Mode.Valid() || legalHold.Status.Valid() {
dcache.Delete(ctx, bucket, object) dcache.Delete(ctx, bucket, object)
return putObjectFn(ctx, bucket, object, r, opts) return putObjectFn(ctx, bucket, object, r, opts)
} }

@ -1017,27 +1017,29 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
srcInfo.UserDefined[xhttp.AmzObjectTagging] = tags srcInfo.UserDefined[xhttp.AmzObjectTagging] = tags
} }
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)
getObjectInfo := objectAPI.GetObjectInfo getObjectInfo := objectAPI.GetObjectInfo
if api.CacheAPI() != nil { if api.CacheAPI() != nil {
getObjectInfo = api.CacheAPI().GetObjectInfo getObjectInfo = api.CacheAPI().GetObjectInfo
} }
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. // apply default bucket configuration/governance headers for dest side.
retentionMode, retentionDate, legalHold, s3Err := checkPutObjectLockAllowed(ctx, r, dstBucket, dstObject, getObjectInfo, retPerms, holdPerms) retentionMode, retentionDate, legalHold, s3Err := checkPutObjectLockAllowed(ctx, r, dstBucket, dstObject, getObjectInfo, retPerms, holdPerms)
if s3Err == ErrNone && retentionMode != "" { if s3Err == ErrNone && retentionMode.Valid() {
srcInfo.UserDefined[xhttp.AmzObjectLockMode] = string(retentionMode) srcInfo.UserDefined[xhttp.AmzObjectLockMode] = string(retentionMode)
srcInfo.UserDefined[xhttp.AmzObjectLockRetainUntilDate] = retentionDate.UTC().Format(time.RFC3339) srcInfo.UserDefined[xhttp.AmzObjectLockRetainUntilDate] = retentionDate.UTC().Format(time.RFC3339)
} }
if s3Err == ErrNone && legalHold.Status != "" { if s3Err == ErrNone && legalHold.Status.Valid() {
srcInfo.UserDefined[xhttp.AmzObjectLockLegalHold] = string(legalHold.Status) srcInfo.UserDefined[xhttp.AmzObjectLockLegalHold] = string(legalHold.Status)
} }
if s3Err != ErrNone { if s3Err != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r))
return return
} }
// Store the preserved compression metadata. // Store the preserved compression metadata.
for k, v := range compressMetadata { for k, v := range compressMetadata {
srcInfo.UserDefined[k] = v srcInfo.UserDefined[k] = v
@ -1327,20 +1329,25 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
writeErrorResponseHeadersOnly(w, toAPIError(ctx, err)) writeErrorResponseHeadersOnly(w, toAPIError(ctx, err))
return return
} }
getObjectInfo := objectAPI.GetObjectInfo
if api.CacheAPI() != nil { if api.CacheAPI() != nil {
getObjectInfo = api.CacheAPI().GetObjectInfo
putObject = api.CacheAPI().PutObject putObject = api.CacheAPI().PutObject
} }
retPerms := isPutActionAllowed(rAuthType, bucket, object, r, iampolicy.PutObjectRetentionAction)
retPerms := isPutActionAllowed(getRequestAuthType(r), bucket, object, r, iampolicy.PutObjectRetentionAction)
holdPerms := isPutActionAllowed(getRequestAuthType(r), bucket, object, r, iampolicy.PutObjectLegalHoldAction) holdPerms := isPutActionAllowed(getRequestAuthType(r), bucket, object, r, iampolicy.PutObjectLegalHoldAction)
getObjectInfo := objectAPI.GetObjectInfo
if api.CacheAPI() != nil {
getObjectInfo = api.CacheAPI().GetObjectInfo
}
retentionMode, retentionDate, legalHold, s3Err := checkPutObjectLockAllowed(ctx, r, bucket, object, getObjectInfo, retPerms, holdPerms) retentionMode, retentionDate, legalHold, s3Err := checkPutObjectLockAllowed(ctx, r, bucket, object, getObjectInfo, retPerms, holdPerms)
if s3Err == ErrNone && retentionMode != "" { if s3Err == ErrNone && retentionMode.Valid() {
metadata[strings.ToLower(xhttp.AmzObjectLockMode)] = string(retentionMode) metadata[strings.ToLower(xhttp.AmzObjectLockMode)] = string(retentionMode)
metadata[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)] = retentionDate.UTC().Format(time.RFC3339) metadata[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)] = retentionDate.UTC().Format(time.RFC3339)
} }
if s3Err == ErrNone && legalHold.Status != "" { if s3Err == ErrNone && legalHold.Status.Valid() {
metadata[strings.ToLower(xhttp.AmzObjectLockLegalHold)] = string(legalHold.Status) metadata[strings.ToLower(xhttp.AmzObjectLockLegalHold)] = string(legalHold.Status)
} }
if s3Err != ErrNone { if s3Err != ErrNone {
@ -1471,6 +1478,14 @@ func (api objectAPIHandlers) NewMultipartUploadHandler(w http.ResponseWriter, r
return return
} }
// Deny if WORM is enabled
if globalWORMEnabled {
if _, err := objectAPI.GetObjectInfo(ctx, bucket, object, opts); err == nil {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL, guessIsBrowserReq(r))
return
}
}
// Validate storage class metadata if present // Validate storage class metadata if present
if sc := r.Header.Get(xhttp.AmzStorageClass); sc != "" { if sc := r.Header.Get(xhttp.AmzStorageClass); sc != "" {
if !storageclass.IsValid(sc) { if !storageclass.IsValid(sc) {
@ -1499,21 +1514,28 @@ func (api objectAPIHandlers) NewMultipartUploadHandler(w http.ResponseWriter, r
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return return
} }
retPerms := isPutActionAllowed(getRequestAuthType(r), bucket, object, r, iampolicy.PutObjectRetentionAction) retPerms := isPutActionAllowed(getRequestAuthType(r), bucket, object, r, iampolicy.PutObjectRetentionAction)
holdPerms := isPutActionAllowed(getRequestAuthType(r), bucket, object, r, iampolicy.PutObjectLegalHoldAction) holdPerms := isPutActionAllowed(getRequestAuthType(r), bucket, object, r, iampolicy.PutObjectLegalHoldAction)
retentionMode, retentionDate, legalHold, s3Err := checkPutObjectLockAllowed(ctx, r, bucket, object, objectAPI.GetObjectInfo, retPerms, holdPerms) getObjectInfo := objectAPI.GetObjectInfo
if s3Err == ErrNone && retentionMode != "" { if api.CacheAPI() != nil {
getObjectInfo = api.CacheAPI().GetObjectInfo
}
retentionMode, retentionDate, legalHold, s3Err := checkPutObjectLockAllowed(ctx, r, bucket, object, getObjectInfo, retPerms, holdPerms)
if s3Err == ErrNone && retentionMode.Valid() {
metadata[strings.ToLower(xhttp.AmzObjectLockMode)] = string(retentionMode) metadata[strings.ToLower(xhttp.AmzObjectLockMode)] = string(retentionMode)
metadata[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)] = retentionDate.UTC().Format(time.RFC3339) metadata[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)] = retentionDate.UTC().Format(time.RFC3339)
} }
if s3Err == ErrNone && legalHold.Status != "" { if s3Err == ErrNone && legalHold.Status.Valid() {
metadata[strings.ToLower(xhttp.AmzObjectLockLegalHold)] = string(legalHold.Status) metadata[strings.ToLower(xhttp.AmzObjectLockLegalHold)] = string(legalHold.Status)
} }
if s3Err != ErrNone { if s3Err != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r))
return return
} }
// We need to preserve the encryption headers set in EncryptRequest, // We need to preserve the encryption headers set in EncryptRequest,
// so we do not want to override them, copy them instead. // so we do not want to override them, copy them instead.
for k, v := range encMetadata { for k, v := range encMetadata {
@ -2339,22 +2361,6 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
return return
} }
// Reject retention or governance headers if set, CompleteMultipartUpload spec
// 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)
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
}
// Get upload id. // Get upload id.
uploadID, _, _, _, s3Error := getObjectResources(r.URL.Query()) uploadID, _, _, _, s3Error := getObjectResources(r.URL.Query())
if s3Error != ErrNone { if s3Error != ErrNone {
@ -2375,6 +2381,36 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidPartOrder), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidPartOrder), r.URL, guessIsBrowserReq(r))
return return
} }
// Reject retention or governance headers if set, CompleteMultipartUpload spec
// 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
}
// Deny if global WORM is enabled
if globalWORMEnabled {
opts, err := getOpts(ctx, r, bucket, object)
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
if _, err := objectAPI.GetObjectInfo(ctx, bucket, object, opts); err == nil {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), 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)
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
}
var objectEncryptionKey []byte var objectEncryptionKey []byte
var opts ObjectOptions var opts ObjectOptions
var isEncrypted, ssec bool var isEncrypted, ssec bool
@ -2546,12 +2582,6 @@ func (api objectAPIHandlers) DeleteObjectHandler(w http.ResponseWriter, r *http.
getObjectInfo = api.CacheAPI().GetObjectInfo getObjectInfo = api.CacheAPI().GetObjectInfo
} }
govBypassPerms := checkRequestAuthType(ctx, r, policy.BypassGovernanceRetentionAction, bucket, object)
if _, err := enforceRetentionBypassForDelete(ctx, r, bucket, object, getObjectInfo, govBypassPerms); err != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(err), r.URL, guessIsBrowserReq(r))
return
}
if globalDNSConfig != nil { if globalDNSConfig != nil {
_, err := globalDNSConfig.Get(bucket) _, err := globalDNSConfig.Get(bucket)
if err != nil { if err != nil {
@ -2560,16 +2590,41 @@ func (api objectAPIHandlers) DeleteObjectHandler(w http.ResponseWriter, r *http.
} }
} }
// http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectDELETE.html // Deny if global WORM is enabled
if err := deleteObject(ctx, objectAPI, api.CacheAPI(), bucket, object, r); err != nil { if globalWORMEnabled {
switch err.(type) { opts, err := getOpts(ctx, r, bucket, object)
case BucketNotFound: if err != nil {
// When bucket doesn't exist specially handle it.
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return return
} }
// Ignore delete object errors while replying to client, since we are suppposed to reply only 204. if _, err := objectAPI.GetObjectInfo(ctx, bucket, object, opts); err == nil {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL, guessIsBrowserReq(r))
return
}
} }
apiErr := ErrNone
if _, ok := globalBucketObjectLockConfig.Get(bucket); ok {
apiErr = enforceRetentionBypassForDelete(ctx, r, bucket, object, getObjectInfo)
if apiErr != ErrNone && apiErr != ErrNoSuchKey {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(apiErr), r.URL, guessIsBrowserReq(r))
return
}
}
if apiErr == ErrNone {
// http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectDELETE.html
if err := deleteObject(ctx, objectAPI, api.CacheAPI(), bucket, object, r); err != nil {
switch err.(type) {
case BucketNotFound:
// When bucket doesn't exist specially handle it.
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
// Ignore delete object errors while replying to client, since we are suppposed to reply only 204.
}
}
writeSuccessNoContent(w) writeSuccessNoContent(w)
} }
@ -2695,6 +2750,11 @@ func (api objectAPIHandlers) GetObjectLegalHoldHandler(w http.ResponseWriter, r
getObjectInfo = api.CacheAPI().GetObjectInfo getObjectInfo = api.CacheAPI().GetObjectInfo
} }
if _, ok := globalBucketObjectLockConfig.Get(bucket); !ok {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidBucketObjectLockConfiguration), r.URL, guessIsBrowserReq(r))
return
}
opts, err := getOpts(ctx, r, bucket, object) opts, err := getOpts(ctx, r, bucket, object)
if err != nil { if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
@ -2707,15 +2767,12 @@ func (api objectAPIHandlers) GetObjectLegalHoldHandler(w http.ResponseWriter, r
return return
} }
if _, ok := globalBucketObjectLockConfig.Get(bucket); !ok {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidBucketObjectLockConfiguration), r.URL, guessIsBrowserReq(r))
return
}
legalHold := objectlock.GetObjectLegalHoldMeta(objInfo.UserDefined) legalHold := objectlock.GetObjectLegalHoldMeta(objInfo.UserDefined)
if legalHold.IsEmpty() { if legalHold.IsEmpty() {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNoSuchObjectLockConfiguration), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNoSuchObjectLockConfiguration), r.URL, guessIsBrowserReq(r))
return return
} }
writeSuccessResponseXML(w, encodeResponse(legalHold)) writeSuccessResponseXML(w, encodeResponse(legalHold))
// Notify object legal hold accessed via a GET request. // Notify object legal hold accessed via a GET request.
sendEvent(eventArgs{ sendEvent(eventArgs{
@ -2753,8 +2810,9 @@ func (api objectAPIHandlers) PutObjectRetentionHandler(w http.ResponseWriter, r
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r))
return return
} }
// Check permissions to perform this governance operation
if s3Err := checkRequestAuthType(ctx, r, policy.PutObjectRetentionAction, bucket, object); s3Err != ErrNone { cred, owner, claims, s3Err := validateSignature(getRequestAuthType(r), r)
if s3Err != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r))
return return
} }
@ -2763,11 +2821,17 @@ func (api objectAPIHandlers) PutObjectRetentionHandler(w http.ResponseWriter, r
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return return
} }
if !hasContentMD5(r.Header) { if !hasContentMD5(r.Header) {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentMD5), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentMD5), r.URL, guessIsBrowserReq(r))
return return
} }
if globalWORMEnabled {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrObjectLocked), r.URL, guessIsBrowserReq(r))
return
}
if _, ok := globalBucketObjectLockConfig.Get(bucket); !ok { if _, ok := globalBucketObjectLockConfig.Get(bucket); !ok {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidBucketObjectLockConfiguration), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidBucketObjectLockConfiguration), r.URL, guessIsBrowserReq(r))
return return
@ -2780,13 +2844,13 @@ func (api objectAPIHandlers) PutObjectRetentionHandler(w http.ResponseWriter, r
writeErrorResponse(ctx, w, apiErr, r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, apiErr, r.URL, guessIsBrowserReq(r))
return return
} }
getObjectInfo := objectAPI.GetObjectInfo getObjectInfo := objectAPI.GetObjectInfo
if api.CacheAPI() != nil { if api.CacheAPI() != nil {
getObjectInfo = api.CacheAPI().GetObjectInfo getObjectInfo = api.CacheAPI().GetObjectInfo
} }
govBypassPerms := isPutActionAllowed(getRequestAuthType(r), bucket, object, r, policy.BypassGovernanceRetentionAction) objInfo, s3Err := enforceRetentionBypassForPut(ctx, r, bucket, object, getObjectInfo, objRetention, cred, owner, claims)
objInfo, s3Err := enforceRetentionBypassForPut(ctx, r, bucket, object, getObjectInfo, govBypassPerms, objRetention)
if s3Err != ErrNone { if s3Err != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r))
return return
@ -2794,7 +2858,7 @@ func (api objectAPIHandlers) PutObjectRetentionHandler(w http.ResponseWriter, r
objInfo.UserDefined[strings.ToLower(xhttp.AmzObjectLockMode)] = string(objRetention.Mode) objInfo.UserDefined[strings.ToLower(xhttp.AmzObjectLockMode)] = string(objRetention.Mode)
objInfo.UserDefined[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)] = objRetention.RetainUntilDate.UTC().Format(time.RFC3339) objInfo.UserDefined[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)] = objRetention.RetainUntilDate.UTC().Format(time.RFC3339)
objInfo.metadataOnly = true objInfo.metadataOnly = true // Perform only metadata updates.
if _, err = objectAPI.CopyObject(ctx, bucket, object, bucket, object, objInfo, ObjectOptions{}, ObjectOptions{}); err != nil { if _, err = objectAPI.CopyObject(ctx, bucket, object, bucket, object, objInfo, ObjectOptions{}, ObjectOptions{}); err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return return

@ -20,62 +20,164 @@ import (
"bytes" "bytes"
"context" "context"
"errors" "errors"
"math"
"net/http" "net/http"
"path" "path"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/auth"
objectlock "github.com/minio/minio/pkg/bucket/object/lock" objectlock "github.com/minio/minio/pkg/bucket/object/lock"
"github.com/minio/minio/pkg/bucket/policy"
) )
// Similar to enforceRetentionBypassForDelete but for WebUI
func enforceRetentionBypassForDeleteWeb(ctx context.Context, r *http.Request, bucket, object string, getObjectInfoFn GetObjectInfoFn) APIErrorCode {
opts, err := getOpts(ctx, r, bucket, object)
if err != nil {
return toAPIErrorCode(ctx, err)
}
oi, err := getObjectInfoFn(ctx, bucket, object, opts)
if err != nil {
return toAPIErrorCode(ctx, err)
}
lhold := objectlock.GetObjectLegalHoldMeta(oi.UserDefined)
if lhold.Status.Valid() && lhold.Status == objectlock.LegalHoldOn {
return ErrObjectLocked
}
ret := objectlock.GetObjectRetentionMeta(oi.UserDefined)
if ret.Mode.Valid() {
switch ret.Mode {
case objectlock.RetCompliance:
// In compliance mode, a protected object version can't be overwritten
// or deleted by any user, including the root user in your AWS account.
// When an object is locked in compliance mode, its retention mode can't
// be changed, and its retention period can't be shortened. Compliance mode
// ensures that an object version can't be overwritten or deleted for the
// duration of the retention period.
t, err := objectlock.UTCNowNTP()
if err != nil {
logger.LogIf(ctx, err)
return ErrObjectLocked
}
if !ret.RetainUntilDate.Before(t) {
return ErrObjectLocked
}
return ErrNone
case objectlock.RetGovernance:
// In governance mode, users can't overwrite or delete an object
// version or alter its lock settings unless they have special
// permissions. With governance mode, you protect objects against
// being deleted by most users, but you can still grant some users
// permission to alter the retention settings or delete the object
// if necessary. You can also use governance mode to test retention-period
// settings before creating a compliance-mode retention period.
// To override or remove governance-mode retention settings, a
// user must have the s3:BypassGovernanceRetention permission
// and must explicitly include x-amz-bypass-governance-retention:true
// as a request header with any request that requires overriding
// governance mode.
byPassSet := objectlock.IsObjectLockGovernanceBypassSet(r.Header)
if !byPassSet {
t, err := objectlock.UTCNowNTP()
if err != nil {
logger.LogIf(ctx, err)
return ErrObjectLocked
}
if !ret.RetainUntilDate.Before(t) {
return ErrObjectLocked
}
return ErrNone
}
}
}
return ErrNone
}
// enforceRetentionBypassForDelete enforces whether an existing object under governance can be deleted // enforceRetentionBypassForDelete enforces whether an existing object under governance can be deleted
// with governance bypass headers set in the request. // with governance bypass headers set in the request.
// Objects under site wide WORM can never be overwritten. // Objects under site wide WORM can never be overwritten.
// For objects in "Governance" mode, overwrite is allowed if a) object retention date is past OR // For objects in "Governance" mode, overwrite is allowed if a) object retention date is past OR
// governance bypass headers are set and user has governance bypass permissions. // governance bypass headers are set and user has governance bypass permissions.
// Objects in "Compliance" mode can be overwritten only if retention date is past. // Objects in "Compliance" mode can be overwritten only if retention date is past.
func enforceRetentionBypassForDelete(ctx context.Context, r *http.Request, bucket, object string, getObjectInfoFn GetObjectInfoFn, govBypassPerm APIErrorCode) (oi ObjectInfo, s3Err APIErrorCode) { func enforceRetentionBypassForDelete(ctx context.Context, r *http.Request, bucket, object string, getObjectInfoFn GetObjectInfoFn) APIErrorCode {
if globalWORMEnabled { opts, err := getOpts(ctx, r, bucket, object)
return oi, ErrObjectLocked
}
var err error
var opts ObjectOptions
opts, err = getOpts(ctx, r, bucket, object)
if err != nil { if err != nil {
return oi, toAPIErrorCode(ctx, err) return toAPIErrorCode(ctx, err)
} }
oi, err = getObjectInfoFn(ctx, bucket, object, opts)
oi, err := getObjectInfoFn(ctx, bucket, object, opts)
if err != nil { if err != nil {
// ignore case where object no longer exists return toAPIErrorCode(ctx, err)
if toAPIError(ctx, err).Code == "NoSuchKey" {
oi.UserDefined = map[string]string{}
return oi, ErrNone
}
return oi, toAPIErrorCode(ctx, err)
} }
ret := objectlock.GetObjectRetentionMeta(oi.UserDefined)
lhold := objectlock.GetObjectLegalHoldMeta(oi.UserDefined) lhold := objectlock.GetObjectLegalHoldMeta(oi.UserDefined)
if lhold.Status == objectlock.ON { if lhold.Status.Valid() && lhold.Status == objectlock.LegalHoldOn {
return oi, ErrObjectLocked return ErrObjectLocked
}
// Here bucket does not support object lock
if ret.Mode == objectlock.Invalid {
return oi, ErrNone
} }
if ret.Mode != objectlock.Compliance && ret.Mode != objectlock.Governance {
return oi, ErrUnknownWORMModeDirective ret := objectlock.GetObjectRetentionMeta(oi.UserDefined)
} if ret.Mode.Valid() {
t, err := objectlock.UTCNowNTP() switch ret.Mode {
if err != nil { case objectlock.RetCompliance:
logger.LogIf(ctx, err) // In compliance mode, a protected object version can't be overwritten
return oi, ErrObjectLocked // or deleted by any user, including the root user in your AWS account.
} // When an object is locked in compliance mode, its retention mode can't
if ret.RetainUntilDate.Before(t) { // be changed, and its retention period can't be shortened. Compliance mode
return oi, ErrNone // ensures that an object version can't be overwritten or deleted for the
} // duration of the retention period.
if objectlock.IsObjectLockGovernanceBypassSet(r.Header) && ret.Mode == objectlock.Governance && govBypassPerm == ErrNone { t, err := objectlock.UTCNowNTP()
return oi, ErrNone if err != nil {
logger.LogIf(ctx, err)
return ErrObjectLocked
}
if !ret.RetainUntilDate.Before(t) {
return ErrObjectLocked
}
return ErrNone
case objectlock.RetGovernance:
// In governance mode, users can't overwrite or delete an object
// version or alter its lock settings unless they have special
// permissions. With governance mode, you protect objects against
// being deleted by most users, but you can still grant some users
// permission to alter the retention settings or delete the object
// if necessary. You can also use governance mode to test retention-period
// settings before creating a compliance-mode retention period.
// To override or remove governance-mode retention settings, a
// user must have the s3:BypassGovernanceRetention permission
// and must explicitly include x-amz-bypass-governance-retention:true
// as a request header with any request that requires overriding
// governance mode.
//
byPassSet := objectlock.IsObjectLockGovernanceBypassSet(r.Header)
if !byPassSet {
t, err := objectlock.UTCNowNTP()
if err != nil {
logger.LogIf(ctx, err)
return ErrObjectLocked
}
if !ret.RetainUntilDate.Before(t) {
return ErrObjectLocked
}
return ErrNone
}
// https://docs.aws.amazon.com/AmazonS3/latest/dev/object-lock-overview.html#object-lock-retention-modes
// If you try to delete objects protected by governance mode and have s3:BypassGovernanceRetention
// or s3:GetBucketObjectLockConfiguration permissions, the operation will succeed.
govBypassPerms1 := checkRequestAuthType(ctx, r, policy.BypassGovernanceRetentionAction, bucket, object)
govBypassPerms2 := checkRequestAuthType(ctx, r, policy.GetBucketObjectLockConfigurationAction, bucket, object)
if govBypassPerms1 != ErrNone && govBypassPerms2 != ErrNone {
return ErrAccessDenied
}
}
} }
return oi, ErrObjectLocked return ErrNone
} }
// enforceRetentionBypassForPut enforces whether an existing object under governance can be overwritten // enforceRetentionBypassForPut enforces whether an existing object under governance can be overwritten
@ -84,66 +186,68 @@ func enforceRetentionBypassForDelete(ctx context.Context, r *http.Request, bucke
// For objects in "Governance" mode, overwrite is allowed if a) object retention date is past OR // For objects in "Governance" mode, overwrite is allowed if a) object retention date is past OR
// governance bypass headers are set and user has governance bypass permissions. // governance bypass headers are set and user has governance bypass permissions.
// Objects in compliance mode can be overwritten only if retention date is being extended. No mode change is permitted. // Objects in compliance mode can be overwritten only if retention date is being extended. No mode change is permitted.
func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, bucket, object string, getObjectInfoFn GetObjectInfoFn, govBypassPerm APIErrorCode, objRetention *objectlock.ObjectRetention) (oi ObjectInfo, s3Err APIErrorCode) { func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, bucket, object string, getObjectInfoFn GetObjectInfoFn, objRetention *objectlock.ObjectRetention, cred auth.Credentials, owner bool, claims map[string]interface{}) (ObjectInfo, APIErrorCode) {
if globalWORMEnabled { byPassSet := objectlock.IsObjectLockGovernanceBypassSet(r.Header)
return oi, ErrObjectLocked opts, err := getOpts(ctx, r, bucket, object)
}
var err error
var opts ObjectOptions
opts, err = getOpts(ctx, r, bucket, object)
if err != nil { if err != nil {
return oi, toAPIErrorCode(ctx, err) return ObjectInfo{}, toAPIErrorCode(ctx, err)
} }
oi, err = getObjectInfoFn(ctx, bucket, object, opts)
oi, err := getObjectInfoFn(ctx, bucket, object, opts)
if err != nil { if err != nil {
// ignore case where object no longer exists
if toAPIError(ctx, err).Code == "NoSuchKey" {
oi.UserDefined = map[string]string{}
return oi, ErrNone
}
return oi, toAPIErrorCode(ctx, err) return oi, toAPIErrorCode(ctx, err)
} }
ret := objectlock.GetObjectRetentionMeta(oi.UserDefined)
// no retention metadata on object
if ret.Mode == objectlock.Invalid {
if _, isWORMBucket := globalBucketObjectLockConfig.Get(bucket); !isWORMBucket {
return oi, ErrInvalidBucketObjectLockConfiguration
}
return oi, ErrNone
}
t, err := objectlock.UTCNowNTP() t, err := objectlock.UTCNowNTP()
if err != nil { if err != nil {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
return oi, ErrObjectLocked return oi, ErrObjectLocked
} }
if ret.Mode == objectlock.Compliance { // Pass in relative days from current time, to additionally to verify "object-lock-remaining-retention-days" policy if any.
// Compliance retention mode cannot be changed and retention period cannot be shortened as per days := int(math.Ceil(math.Abs(objRetention.RetainUntilDate.Sub(t).Hours()) / 24))
// https://docs.aws.amazon.com/AmazonS3/latest/dev/object-lock-overview.html#object-lock-retention-modes
if objRetention.Mode != objectlock.Compliance || objRetention.RetainUntilDate.Before(ret.RetainUntilDate.Time) { ret := objectlock.GetObjectRetentionMeta(oi.UserDefined)
return oi, ErrObjectLocked if ret.Mode.Valid() {
} // Retention has expired you may change whatever you like.
if objRetention.RetainUntilDate.Before(t) { if ret.RetainUntilDate.Before(t) {
return oi, ErrInvalidRetentionDate perm := isPutRetentionAllowed(bucket, object,
days, objRetention.RetainUntilDate.Time,
objRetention.Mode, byPassSet, r, cred,
owner, claims)
return oi, perm
} }
return oi, ErrNone
}
if ret.Mode == objectlock.Governance { switch ret.Mode {
if !objectlock.IsObjectLockGovernanceBypassSet(r.Header) { case objectlock.RetGovernance:
if objRetention.RetainUntilDate.Before(t) { govPerm := isPutRetentionAllowed(bucket, object, days,
return oi, ErrInvalidRetentionDate objRetention.RetainUntilDate.Time, objRetention.Mode,
byPassSet, r, cred, owner, claims)
// Governance mode retention period cannot be shortened, if x-amz-bypass-governance is not set.
if !byPassSet {
if objRetention.Mode != objectlock.RetGovernance || objRetention.RetainUntilDate.Before((ret.RetainUntilDate.Time)) {
return oi, ErrObjectLocked
}
} }
if objRetention.RetainUntilDate.Before((ret.RetainUntilDate.Time)) { return oi, govPerm
case objectlock.RetCompliance:
// Compliance retention mode cannot be changed or shortened.
// https://docs.aws.amazon.com/AmazonS3/latest/dev/object-lock-overview.html#object-lock-retention-modes
if objRetention.Mode != objectlock.RetCompliance || objRetention.RetainUntilDate.Before((ret.RetainUntilDate.Time)) {
return oi, ErrObjectLocked return oi, ErrObjectLocked
} }
return oi, ErrNone compliancePerm := isPutRetentionAllowed(bucket, object,
days, objRetention.RetainUntilDate.Time, objRetention.Mode,
false, r, cred, owner, claims)
return oi, compliancePerm
} }
return oi, govBypassPerm return oi, ErrNone
} } // No pre-existing retention metadata present.
return oi, ErrNone
perm := isPutRetentionAllowed(bucket, object,
days, objRetention.RetainUntilDate.Time,
objRetention.Mode, byPassSet, r, cred, owner, claims)
return oi, perm
} }
// checkPutObjectLockAllowed enforces object retention policy and legal hold policy // checkPutObjectLockAllowed enforces object retention policy and legal hold policy
@ -156,16 +260,23 @@ func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, bucket,
// For objects in "Compliance" mode, retention date cannot be shortened, and mode cannot be altered. // For objects in "Compliance" mode, retention date cannot be shortened, and mode cannot be altered.
// For objects with legal hold header set, the s3:PutObjectLegalHold permission is expected to be set // 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 // 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) { func checkPutObjectLockAllowed(ctx context.Context, r *http.Request, bucket, object string, getObjectInfoFn GetObjectInfoFn, retentionPermErr, legalHoldPermErr APIErrorCode) (objectlock.RetMode, objectlock.RetentionDate, objectlock.ObjectLegalHold, APIErrorCode) {
var mode objectlock.Mode var mode objectlock.RetMode
var retainDate objectlock.RetentionDate var retainDate objectlock.RetentionDate
var legalHold objectlock.ObjectLegalHold var legalHold objectlock.ObjectLegalHold
retentionCfg, isWORMBucket := globalBucketObjectLockConfig.Get(bucket)
retentionRequested := objectlock.IsObjectLockRetentionRequested(r.Header) retentionRequested := objectlock.IsObjectLockRetentionRequested(r.Header)
legalHoldRequested := objectlock.IsObjectLockLegalHoldRequested(r.Header) legalHoldRequested := objectlock.IsObjectLockLegalHoldRequested(r.Header)
retentionCfg, isWORMBucket := globalBucketObjectLockConfig.Get(bucket)
if !isWORMBucket {
if legalHoldRequested || retentionRequested {
return mode, retainDate, legalHold, ErrInvalidBucketObjectLockConfiguration
}
// If this not a WORM enabled bucket, we should return right here.
return mode, retainDate, legalHold, ErrNone
}
var objExists bool var objExists bool
opts, err := getOpts(ctx, r, bucket, object) opts, err := getOpts(ctx, r, bucket, object)
if err != nil { if err != nil {
@ -177,33 +288,30 @@ func checkPutObjectLockAllowed(ctx context.Context, r *http.Request, bucket, obj
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
return mode, retainDate, legalHold, ErrObjectLocked return mode, retainDate, legalHold, ErrObjectLocked
} }
if objInfo, err := getObjectInfoFn(ctx, bucket, object, opts); err == nil { if objInfo, err := getObjectInfoFn(ctx, bucket, object, opts); err == nil {
objExists = true objExists = true
r := objectlock.GetObjectRetentionMeta(objInfo.UserDefined) r := objectlock.GetObjectRetentionMeta(objInfo.UserDefined)
if globalWORMEnabled || ((r.Mode == objectlock.Compliance) && r.RetainUntilDate.After(t)) { if globalWORMEnabled || ((r.Mode == objectlock.RetCompliance) && r.RetainUntilDate.After(t)) {
return mode, retainDate, legalHold, ErrObjectLocked return mode, retainDate, legalHold, ErrObjectLocked
} }
mode = r.Mode mode = r.Mode
retainDate = r.RetainUntilDate retainDate = r.RetainUntilDate
legalHold = objectlock.GetObjectLegalHoldMeta(objInfo.UserDefined) legalHold = objectlock.GetObjectLegalHoldMeta(objInfo.UserDefined)
// Disallow overwriting an object on legal hold // Disallow overwriting an object on legal hold
if legalHold.Status == "ON" { if legalHold.Status == objectlock.LegalHoldOn {
return mode, retainDate, legalHold, ErrObjectLocked return mode, retainDate, legalHold, ErrObjectLocked
} }
} }
if legalHoldRequested { if legalHoldRequested {
if !isWORMBucket {
return mode, retainDate, legalHold, ErrInvalidBucketObjectLockConfiguration
}
var lerr error var lerr error
if legalHold, lerr = objectlock.ParseObjectLockLegalHoldHeaders(r.Header); lerr != nil { if legalHold, lerr = objectlock.ParseObjectLockLegalHoldHeaders(r.Header); lerr != nil {
return mode, retainDate, legalHold, toAPIErrorCode(ctx, err) return mode, retainDate, legalHold, toAPIErrorCode(ctx, err)
} }
} }
if retentionRequested { if retentionRequested {
if !isWORMBucket {
return mode, retainDate, legalHold, ErrInvalidBucketObjectLockConfiguration
}
legalHold, err := objectlock.ParseObjectLockLegalHoldHeaders(r.Header) legalHold, err := objectlock.ParseObjectLockLegalHoldHeaders(r.Header)
if err != nil { if err != nil {
return mode, retainDate, legalHold, toAPIErrorCode(ctx, err) return mode, retainDate, legalHold, toAPIErrorCode(ctx, err)
@ -215,9 +323,6 @@ func checkPutObjectLockAllowed(ctx context.Context, r *http.Request, bucket, obj
if objExists && retainDate.After(t) { if objExists && retainDate.After(t) {
return mode, retainDate, legalHold, ErrObjectLocked return mode, retainDate, legalHold, ErrObjectLocked
} }
if rMode == objectlock.Invalid {
return mode, retainDate, legalHold, toAPIErrorCode(ctx, objectlock.ErrObjectLockInvalidHeaders)
}
if retentionPermErr != ErrNone { if retentionPermErr != ErrNone {
return mode, retainDate, legalHold, retentionPermErr return mode, retainDate, legalHold, retentionPermErr
} }
@ -241,7 +346,7 @@ func checkPutObjectLockAllowed(ctx context.Context, r *http.Request, bucket, obj
// inherit retention from bucket configuration // inherit retention from bucket configuration
return retentionCfg.Mode, objectlock.RetentionDate{Time: t.Add(retentionCfg.Validity)}, legalHold, ErrNone return retentionCfg.Mode, objectlock.RetentionDate{Time: t.Add(retentionCfg.Validity)}, legalHold, ErrNone
} }
return objectlock.Mode(""), objectlock.RetentionDate{}, legalHold, ErrNone return "", objectlock.RetentionDate{}, legalHold, ErrNone
} }
return mode, retainDate, legalHold, ErrNone return mode, retainDate, legalHold, ErrNone
} }

@ -20,10 +20,10 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"net/url" "net/url"
"path" "path"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -146,7 +146,7 @@ func NewPolicySys() *PolicySys {
} }
} }
func getConditionValues(request *http.Request, locationConstraint string, username string, claims map[string]interface{}) map[string][]string { func getConditionValues(r *http.Request, lc string, username string, claims map[string]interface{}) map[string][]string {
currTime := UTCNow() currTime := UTCNow()
principalType := "Anonymous" principalType := "Anonymous"
@ -156,22 +156,21 @@ func getConditionValues(request *http.Request, locationConstraint string, userna
args := map[string][]string{ args := map[string][]string{
"CurrentTime": {currTime.Format(time.RFC3339)}, "CurrentTime": {currTime.Format(time.RFC3339)},
"EpochTime": {fmt.Sprintf("%d", currTime.Unix())}, "EpochTime": {strconv.FormatInt(currTime.Unix(), 10)},
"SecureTransport": {strconv.FormatBool(r.TLS != nil)},
"SourceIp": {handlers.GetSourceIP(r)},
"UserAgent": {r.UserAgent()},
"Referer": {r.Referer()},
"principaltype": {principalType}, "principaltype": {principalType},
"SecureTransport": {fmt.Sprintf("%t", request.TLS != nil)},
"SourceIp": {handlers.GetSourceIP(request)},
"UserAgent": {request.UserAgent()},
"Referer": {request.Referer()},
"userid": {username}, "userid": {username},
"username": {username}, "username": {username},
} }
if locationConstraint != "" { if lc != "" {
args["LocationConstraint"] = []string{locationConstraint} args["LocationConstraint"] = []string{lc}
} }
// TODO: support object-lock-remaining-retention-days cloneHeader := r.Header.Clone()
cloneHeader := request.Header.Clone()
for _, objLock := range []string{ for _, objLock := range []string{
xhttp.AmzObjectLockMode, xhttp.AmzObjectLockMode,
@ -193,7 +192,7 @@ func getConditionValues(request *http.Request, locationConstraint string, userna
} }
var cloneURLValues = url.Values{} var cloneURLValues = url.Values{}
for k, v := range request.URL.Query() { for k, v := range r.URL.Query() {
cloneURLValues[k] = v cloneURLValues[k] = v
} }

@ -588,11 +588,17 @@ func (web *webAPIHandlers) RemoveObject(r *http.Request, args *RemoveObjectArgs,
if objectAPI == nil { if objectAPI == nil {
return toJSONError(ctx, errServerNotInitialized) return toJSONError(ctx, errServerNotInitialized)
} }
listObjects := objectAPI.ListObjects
getObjectInfo := objectAPI.GetObjectInfo getObjectInfo := objectAPI.GetObjectInfo
if web.CacheAPI() != nil { if web.CacheAPI() != nil {
getObjectInfo = web.CacheAPI().GetObjectInfo getObjectInfo = web.CacheAPI().GetObjectInfo
} }
deleteObjects := objectAPI.DeleteObjects
if web.CacheAPI() != nil {
deleteObjects = web.CacheAPI().DeleteObjects
}
claims, owner, authErr := webRequestAuthenticate(r) claims, owner, authErr := webRequestAuthenticate(r)
if authErr != nil { if authErr != nil {
if authErr == errNoAuthToken { if authErr == errNoAuthToken {
@ -688,6 +694,17 @@ next:
}) { }) {
govBypassPerms = ErrNone govBypassPerms = ErrNone
} }
if globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: claims.AccessKey,
Action: iampolicy.GetBucketObjectLockConfigurationAction,
BucketName: args.BucketName,
ConditionValues: getConditionValues(r, "", claims.AccessKey, claims.Map()),
IsOwner: owner,
ObjectName: objectName,
Claims: claims.Map(),
}) {
govBypassPerms = ErrNone
}
} }
if authErr == errNoAuthToken { if authErr == errNoAuthToken {
// Check if object is allowed to be deleted anonymously // Check if object is allowed to be deleted anonymously
@ -711,12 +728,44 @@ next:
}) { }) {
govBypassPerms = ErrNone govBypassPerms = ErrNone
} }
// Check if object is allowed to be deleted anonymously
if globalPolicySys.IsAllowed(policy.Args{
Action: policy.GetBucketObjectLockConfigurationAction,
BucketName: args.BucketName,
ConditionValues: getConditionValues(r, "", "", nil),
IsOwner: false,
ObjectName: objectName,
}) {
govBypassPerms = ErrNone
}
} }
if _, err := enforceRetentionBypassForDelete(ctx, r, args.BucketName, objectName, getObjectInfo, govBypassPerms); err != ErrNone { if govBypassPerms != ErrNone {
return toJSONError(ctx, errAccessDenied) return toJSONError(ctx, errAccessDenied)
} }
if err = deleteObject(ctx, objectAPI, web.CacheAPI(), args.BucketName, objectName, r); err != nil {
break next apiErr := ErrNone
// Deny if global WORM is enabled
if globalWORMEnabled {
opts, err := getOpts(ctx, r, args.BucketName, objectName)
if err != nil {
apiErr = toAPIErrorCode(ctx, err)
} else {
if _, err := getObjectInfo(ctx, args.BucketName, objectName, opts); err == nil {
apiErr = ErrMethodNotAllowed
}
}
}
if _, ok := globalBucketObjectLockConfig.Get(args.BucketName); ok && (apiErr == ErrNone) {
apiErr = enforceRetentionBypassForDeleteWeb(ctx, r, args.BucketName, objectName, getObjectInfo)
if apiErr != ErrNone && apiErr != ErrNoSuchKey {
return toJSONError(ctx, errAccessDenied)
}
}
if apiErr == ErrNone {
if err = deleteObject(ctx, objectAPI, web.CacheAPI(), args.BucketName, objectName, r); err != nil {
break next
}
} }
continue continue
} }
@ -746,23 +795,34 @@ next:
} }
} }
// For directories, list the contents recursively and remove. // Allocate new results channel to receive ObjectInfo.
marker := "" objInfoCh := make(chan ObjectInfo)
// Walk through all objects
if err = objectAPI.Walk(ctx, args.BucketName, objectName, objInfoCh); err != nil {
break next
}
for { for {
var lo ListObjectsInfo var objects []string
lo, err = listObjects(ctx, args.BucketName, objectName, marker, "", maxObjectList) for obj := range objInfoCh {
if err != nil { if len(objects) == maxObjectList {
break next // Reached maximum delete requests, attempt a delete for now.
} break
marker = lo.NextMarker
for _, obj := range lo.Objects {
err = deleteObject(ctx, objectAPI, web.CacheAPI(), args.BucketName, obj.Name, r)
if err != nil {
break next
} }
objects = append(objects, obj.Name)
}
// Nothing to do.
if len(objects) == 0 {
break next
} }
if !lo.IsTruncated {
break // Deletes a list of objects.
_, err = deleteObjects(ctx, args.BucketName, objects)
if err != nil {
logger.LogIf(ctx, err)
break next
} }
} }
} }
@ -1097,27 +1157,30 @@ func (web *webAPIHandlers) Upload(w http.ResponseWriter, r *http.Request) {
// Ensure that metadata does not contain sensitive information // Ensure that metadata does not contain sensitive information
crypto.RemoveSensitiveEntries(metadata) crypto.RemoveSensitiveEntries(metadata)
retentionRequested := objectlock.IsObjectLockRetentionRequested(r.Header)
legalHoldRequested := objectlock.IsObjectLockLegalHoldRequested(r.Header)
putObject := objectAPI.PutObject
getObjectInfo := objectAPI.GetObjectInfo getObjectInfo := objectAPI.GetObjectInfo
if web.CacheAPI() != nil { if web.CacheAPI() != nil {
putObject = web.CacheAPI().PutObject
getObjectInfo = web.CacheAPI().GetObjectInfo getObjectInfo = web.CacheAPI().GetObjectInfo
} }
// enforce object retention rules
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
}
putObject := objectAPI.PutObject if retentionRequested || legalHoldRequested {
if web.CacheAPI() != nil { // enforce object retention rules
putObject = web.CacheAPI().PutObject 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
}
} }
objInfo, err := putObject(context.Background(), bucket, object, pReader, opts) objInfo, err := putObject(context.Background(), bucket, object, pReader, opts)
@ -1462,13 +1525,12 @@ func (web *webAPIHandlers) DownloadZip(w http.ResponseWriter, r *http.Request) {
writeWebErrorResponse(w, errInvalidBucketName) writeWebErrorResponse(w, errInvalidBucketName)
return return
} }
getObjectNInfo := objectAPI.GetObjectNInfo getObjectNInfo := objectAPI.GetObjectNInfo
if web.CacheAPI() != nil { if web.CacheAPI() != nil {
getObjectNInfo = web.CacheAPI().GetObjectNInfo getObjectNInfo = web.CacheAPI().GetObjectNInfo
} }
listObjects := objectAPI.ListObjects
archive := zip.NewWriter(w) archive := zip.NewWriter(w)
defer archive.Close() defer archive.Close()
@ -1541,29 +1603,24 @@ func (web *webAPIHandlers) DownloadZip(w http.ResponseWriter, r *http.Request) {
// If not a directory, compress the file and write it to response. // If not a directory, compress the file and write it to response.
err := zipit(pathJoin(args.Prefix, object)) err := zipit(pathJoin(args.Prefix, object))
if err != nil { if err != nil {
logger.LogIf(ctx, err)
return return
} }
continue continue
} }
// For directories, list the contents recursively and write the objects as compressed objInfoCh := make(chan ObjectInfo)
// date to the response writer.
marker := "" // Walk through all objects
for { if err := objectAPI.Walk(ctx, args.BucketName, pathJoin(args.Prefix, object), objInfoCh); err != nil {
lo, err := listObjects(ctx, args.BucketName, pathJoin(args.Prefix, object), marker, "", logger.LogIf(ctx, err)
maxObjectList) continue
if err != nil { }
return
} for obj := range objInfoCh {
marker = lo.NextMarker if err := zipit(obj.Name); err != nil {
for _, obj := range lo.Objects { logger.LogIf(ctx, err)
err = zipit(obj.Name) continue
if err != nil {
return
}
}
if !lo.IsTruncated {
break
} }
} }
} }

@ -28,33 +28,36 @@ import (
"time" "time"
"github.com/beevik/ntp" "github.com/beevik/ntp"
xhttp "github.com/minio/minio/cmd/http"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/env" "github.com/minio/minio/pkg/env"
) )
// Mode - object retention mode. // RetMode - object retention mode.
type Mode string type RetMode string
const ( const (
// Governance - governance mode. // RetGovernance - governance mode.
Governance Mode = "GOVERNANCE" RetGovernance RetMode = "GOVERNANCE"
// Compliance - compliance mode. // RetCompliance - compliance mode.
Compliance Mode = "COMPLIANCE" RetCompliance RetMode = "COMPLIANCE"
// Invalid - invalid retention mode.
Invalid Mode = ""
) )
func parseMode(modeStr string) (mode Mode) { // Valid - returns if retention mode is valid
func (r RetMode) Valid() bool {
switch r {
case RetGovernance, RetCompliance:
return true
}
return false
}
func parseRetMode(modeStr string) (mode RetMode) {
switch strings.ToUpper(modeStr) { switch strings.ToUpper(modeStr) {
case "GOVERNANCE": case "GOVERNANCE":
mode = Governance mode = RetGovernance
case "COMPLIANCE": case "COMPLIANCE":
mode = Compliance mode = RetCompliance
default:
mode = Invalid
} }
return mode return mode
} }
@ -63,23 +66,40 @@ func parseMode(modeStr string) (mode Mode) {
type LegalHoldStatus string type LegalHoldStatus string
const ( const (
// ON -legal hold is on. // LegalHoldOn - legal hold is on.
ON LegalHoldStatus = "ON" LegalHoldOn LegalHoldStatus = "ON"
// OFF -legal hold is off. // LegalHoldOff - legal hold is off.
OFF LegalHoldStatus = "OFF" LegalHoldOff LegalHoldStatus = "OFF"
) )
func parseLegalHoldStatus(holdStr string) LegalHoldStatus { // Valid - returns true if legal hold status has valid values
func (l LegalHoldStatus) Valid() bool {
switch l {
case LegalHoldOn, LegalHoldOff:
return true
}
return false
}
func parseLegalHoldStatus(holdStr string) (st LegalHoldStatus) {
switch strings.ToUpper(holdStr) { switch strings.ToUpper(holdStr) {
case "ON": case "ON":
return ON st = LegalHoldOn
case "OFF": case "OFF":
return OFF st = LegalHoldOff
} }
return LegalHoldStatus("") return st
} }
// Bypass retention governance header.
const (
AmzObjectLockBypassRetGovernance = "X-Amz-Bypass-Governance-Retention"
AmzObjectLockRetainUntilDate = "X-Amz-Object-Lock-Retain-Until-Date"
AmzObjectLockMode = "X-Amz-Object-Lock-Mode"
AmzObjectLockLegalHold = "X-Amz-Object-Lock-Legal-Hold"
)
var ( var (
// ErrMalformedBucketObjectConfig -indicates that the bucket object lock config is malformed // ErrMalformedBucketObjectConfig -indicates that the bucket object lock config is malformed
ErrMalformedBucketObjectConfig = errors.New("invalid bucket object lock config") ErrMalformedBucketObjectConfig = errors.New("invalid bucket object lock config")
@ -118,13 +138,13 @@ func UTCNowNTP() (time.Time, error) {
// Retention - bucket level retention configuration. // Retention - bucket level retention configuration.
type Retention struct { type Retention struct {
Mode Mode Mode RetMode
Validity time.Duration Validity time.Duration
} }
// IsEmpty - returns whether retention is empty or not. // IsEmpty - returns whether retention is empty or not.
func (r Retention) IsEmpty() bool { func (r Retention) IsEmpty() bool {
return r.Mode == "" || r.Validity == 0 return !r.Mode.Valid() || r.Validity == 0
} }
// Retain - check whether given date is retainable by validity time. // Retain - check whether given date is retainable by validity time.
@ -176,7 +196,7 @@ func NewBucketObjectLockConfig() *BucketObjectLockConfig {
// DefaultRetention - default retention configuration. // DefaultRetention - default retention configuration.
type DefaultRetention struct { type DefaultRetention struct {
XMLName xml.Name `xml:"DefaultRetention"` XMLName xml.Name `xml:"DefaultRetention"`
Mode Mode `xml:"Mode"` Mode RetMode `xml:"Mode"`
Days *uint64 `xml:"Days"` Days *uint64 `xml:"Days"`
Years *uint64 `xml:"Years"` Years *uint64 `xml:"Years"`
} }
@ -198,8 +218,8 @@ func (dr *DefaultRetention) UnmarshalXML(d *xml.Decoder, start xml.StartElement)
return err return err
} }
switch string(retention.Mode) { switch retention.Mode {
case "GOVERNANCE", "COMPLIANCE": case RetGovernance, RetCompliance:
default: default:
return fmt.Errorf("unknown retention mode %v", retention.Mode) return fmt.Errorf("unknown retention mode %v", retention.Mode)
} }
@ -282,10 +302,13 @@ func (config *Config) ToRetention() (r Retention) {
return r return r
} }
// Maximum 4KiB size per object lock config.
const maxObjectLockConfigSize = 1 << 12
// ParseObjectLockConfig parses ObjectLockConfig from xml // ParseObjectLockConfig parses ObjectLockConfig from xml
func ParseObjectLockConfig(reader io.Reader) (*Config, error) { func ParseObjectLockConfig(reader io.Reader) (*Config, error) {
config := Config{} config := Config{}
if err := xml.NewDecoder(reader).Decode(&config); err != nil { if err := xml.NewDecoder(io.LimitReader(reader, maxObjectLockConfigSize)).Decode(&config); err != nil {
return nil, err return nil, err
} }
@ -338,17 +361,20 @@ func (rDate *RetentionDate) MarshalXML(e *xml.Encoder, startElement xml.StartEle
type ObjectRetention struct { type ObjectRetention struct {
XMLNS string `xml:"xmlns,attr,omitempty"` XMLNS string `xml:"xmlns,attr,omitempty"`
XMLName xml.Name `xml:"Retention"` XMLName xml.Name `xml:"Retention"`
Mode Mode `xml:"Mode,omitempty"` Mode RetMode `xml:"Mode,omitempty"`
RetainUntilDate RetentionDate `xml:"RetainUntilDate,omitempty"` RetainUntilDate RetentionDate `xml:"RetainUntilDate,omitempty"`
} }
// Maximum 4KiB size per object retention config.
const maxObjectRetentionSize = 1 << 12
// ParseObjectRetention constructs ObjectRetention struct from xml input // ParseObjectRetention constructs ObjectRetention struct from xml input
func ParseObjectRetention(reader io.Reader) (*ObjectRetention, error) { func ParseObjectRetention(reader io.Reader) (*ObjectRetention, error) {
ret := ObjectRetention{} ret := ObjectRetention{}
if err := xml.NewDecoder(reader).Decode(&ret); err != nil { if err := xml.NewDecoder(io.LimitReader(reader, maxObjectRetentionSize)).Decode(&ret); err != nil {
return nil, err return nil, err
} }
if ret.Mode != Compliance && ret.Mode != Governance { if !ret.Mode.Valid() {
return &ret, ErrUnknownWORMModeDirective return &ret, ErrUnknownWORMModeDirective
} }
@ -367,10 +393,10 @@ func ParseObjectRetention(reader io.Reader) (*ObjectRetention, error) {
// IsObjectLockRetentionRequested returns true if object lock retention headers are set. // IsObjectLockRetentionRequested returns true if object lock retention headers are set.
func IsObjectLockRetentionRequested(h http.Header) bool { func IsObjectLockRetentionRequested(h http.Header) bool {
if _, ok := h[xhttp.AmzObjectLockMode]; ok { if _, ok := h[AmzObjectLockMode]; ok {
return true return true
} }
if _, ok := h[xhttp.AmzObjectLockRetainUntilDate]; ok { if _, ok := h[AmzObjectLockRetainUntilDate]; ok {
return true return true
} }
return false return false
@ -378,13 +404,13 @@ func IsObjectLockRetentionRequested(h http.Header) bool {
// IsObjectLockLegalHoldRequested returns true if object lock legal hold header is set. // IsObjectLockLegalHoldRequested returns true if object lock legal hold header is set.
func IsObjectLockLegalHoldRequested(h http.Header) bool { func IsObjectLockLegalHoldRequested(h http.Header) bool {
_, ok := h[xhttp.AmzObjectLockLegalHold] _, ok := h[AmzObjectLockLegalHold]
return ok return ok
} }
// IsObjectLockGovernanceBypassSet returns true if object lock governance bypass header is set. // IsObjectLockGovernanceBypassSet returns true if object lock governance bypass header is set.
func IsObjectLockGovernanceBypassSet(h http.Header) bool { func IsObjectLockGovernanceBypassSet(h http.Header) bool {
return strings.ToLower(h.Get(xhttp.AmzObjectLockBypassGovernance)) == "true" return strings.ToLower(h.Get(AmzObjectLockBypassRetGovernance)) == "true"
} }
// IsObjectLockRequested returns true if legal hold or object lock retention headers are requested. // IsObjectLockRequested returns true if legal hold or object lock retention headers are requested.
@ -393,14 +419,15 @@ func IsObjectLockRequested(h http.Header) bool {
} }
// ParseObjectLockRetentionHeaders parses http headers to extract retention mode and retention date // ParseObjectLockRetentionHeaders parses http headers to extract retention mode and retention date
func ParseObjectLockRetentionHeaders(h http.Header) (rmode Mode, r RetentionDate, err error) { func ParseObjectLockRetentionHeaders(h http.Header) (rmode RetMode, r RetentionDate, err error) {
retMode := h.Get(xhttp.AmzObjectLockMode) retMode := h.Get(AmzObjectLockMode)
dateStr := h.Get(xhttp.AmzObjectLockRetainUntilDate) dateStr := h.Get(AmzObjectLockRetainUntilDate)
if len(retMode) == 0 || len(dateStr) == 0 { if len(retMode) == 0 || len(dateStr) == 0 {
return rmode, r, ErrObjectLockInvalidHeaders return rmode, r, ErrObjectLockInvalidHeaders
} }
rmode = parseMode(retMode)
if rmode == Invalid { rmode = parseRetMode(retMode)
if !rmode.Valid() {
return rmode, r, ErrUnknownWORMModeDirective return rmode, r, ErrUnknownWORMModeDirective
} }
@ -429,13 +456,13 @@ func ParseObjectLockRetentionHeaders(h http.Header) (rmode Mode, r RetentionDate
// GetObjectRetentionMeta constructs ObjectRetention from metadata // GetObjectRetentionMeta constructs ObjectRetention from metadata
func GetObjectRetentionMeta(meta map[string]string) ObjectRetention { func GetObjectRetentionMeta(meta map[string]string) ObjectRetention {
var mode Mode var mode RetMode
var retainTill RetentionDate var retainTill RetentionDate
if modeStr, ok := meta[strings.ToLower(xhttp.AmzObjectLockMode)]; ok { if modeStr, ok := meta[strings.ToLower(AmzObjectLockMode)]; ok {
mode = parseMode(modeStr) mode = parseRetMode(modeStr)
} }
if tillStr, ok := meta[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)]; ok { if tillStr, ok := meta[strings.ToLower(AmzObjectLockRetainUntilDate)]; ok {
if t, e := time.Parse(time.RFC3339, tillStr); e == nil { if t, e := time.Parse(time.RFC3339, tillStr); e == nil {
retainTill = RetentionDate{t.UTC()} retainTill = RetentionDate{t.UTC()}
} }
@ -445,8 +472,7 @@ func GetObjectRetentionMeta(meta map[string]string) ObjectRetention {
// GetObjectLegalHoldMeta constructs ObjectLegalHold from metadata // GetObjectLegalHoldMeta constructs ObjectLegalHold from metadata
func GetObjectLegalHoldMeta(meta map[string]string) ObjectLegalHold { func GetObjectLegalHoldMeta(meta map[string]string) ObjectLegalHold {
holdStr, ok := meta[strings.ToLower(AmzObjectLockLegalHold)]
holdStr, ok := meta[strings.ToLower(xhttp.AmzObjectLockLegalHold)]
if ok { if ok {
return ObjectLegalHold{XMLNS: "http://s3.amazonaws.com/doc/2006-03-01/", Status: parseLegalHoldStatus(holdStr)} return ObjectLegalHold{XMLNS: "http://s3.amazonaws.com/doc/2006-03-01/", Status: parseLegalHoldStatus(holdStr)}
} }
@ -455,13 +481,13 @@ func GetObjectLegalHoldMeta(meta map[string]string) ObjectLegalHold {
// ParseObjectLockLegalHoldHeaders parses request headers to construct ObjectLegalHold // ParseObjectLockLegalHoldHeaders parses request headers to construct ObjectLegalHold
func ParseObjectLockLegalHoldHeaders(h http.Header) (lhold ObjectLegalHold, err error) { func ParseObjectLockLegalHoldHeaders(h http.Header) (lhold ObjectLegalHold, err error) {
holdStatus, ok := h[xhttp.AmzObjectLockLegalHold] holdStatus, ok := h[AmzObjectLockLegalHold]
if ok { if ok {
lh := parseLegalHoldStatus(strings.Join(holdStatus, "")) lh := parseLegalHoldStatus(holdStatus[0])
if lh != ON && lh != OFF { if !lh.Valid() {
return lhold, ErrUnknownWORMModeDirective return lhold, ErrUnknownWORMModeDirective
} }
lhold = ObjectLegalHold{Status: lh} lhold = ObjectLegalHold{XMLNS: "http://s3.amazonaws.com/doc/2006-03-01/", Status: lh}
} }
return lhold, nil return lhold, nil
@ -477,16 +503,17 @@ type ObjectLegalHold struct {
// IsEmpty returns true if struct is empty // IsEmpty returns true if struct is empty
func (l *ObjectLegalHold) IsEmpty() bool { func (l *ObjectLegalHold) IsEmpty() bool {
return l.Status != ON && l.Status != OFF return !l.Status.Valid()
} }
// ParseObjectLegalHold decodes the XML into ObjectLegalHold // ParseObjectLegalHold decodes the XML into ObjectLegalHold
func ParseObjectLegalHold(reader io.Reader) (hold *ObjectLegalHold, err error) { func ParseObjectLegalHold(reader io.Reader) (hold *ObjectLegalHold, err error) {
if err = xml.NewDecoder(reader).Decode(&hold); err != nil { hold = &ObjectLegalHold{}
if err = xml.NewDecoder(reader).Decode(hold); err != nil {
return return
} }
if hold.Status != ON && hold.Status != OFF { if !hold.Status.Valid() {
return nil, ErrMalformedXML return nil, ErrMalformedXML
} }
return return
@ -511,15 +538,14 @@ func FilterObjectLockMetadata(metadata map[string]string, filterRetention, filte
delete(dst, key) delete(dst, key)
} }
legalHold := GetObjectLegalHoldMeta(metadata) legalHold := GetObjectLegalHoldMeta(metadata)
if legalHold.Status == "" || filterLegalHold { if !legalHold.Status.Valid() || filterLegalHold {
delKey(xhttp.AmzObjectLockLegalHold) delKey(AmzObjectLockLegalHold)
} }
ret := GetObjectRetentionMeta(metadata) ret := GetObjectRetentionMeta(metadata)
if !ret.Mode.Valid() || filterRetention {
if ret.Mode == Invalid || filterRetention { delKey(AmzObjectLockMode)
delKey(xhttp.AmzObjectLockMode) delKey(AmzObjectLockRetainUntilDate)
delKey(xhttp.AmzObjectLockRetainUntilDate)
return dst return dst
} }
return dst return dst

@ -31,25 +31,25 @@ import (
func TestParseMode(t *testing.T) { func TestParseMode(t *testing.T) {
testCases := []struct { testCases := []struct {
value string value string
expectedMode Mode expectedMode RetMode
}{ }{
{ {
value: "governance", value: "governance",
expectedMode: Governance, expectedMode: RetGovernance,
}, },
{ {
value: "complIAnce", value: "complIAnce",
expectedMode: Compliance, expectedMode: RetCompliance,
}, },
{ {
value: "gce", value: "gce",
expectedMode: Invalid, expectedMode: "",
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
if parseMode(tc.value) != tc.expectedMode { if parseRetMode(tc.value) != tc.expectedMode {
t.Errorf("Expected Mode %s, got %s", tc.expectedMode, parseMode(tc.value)) t.Errorf("Expected Mode %s, got %s", tc.expectedMode, parseRetMode(tc.value))
} }
} }
} }
@ -60,11 +60,11 @@ func TestParseLegalHoldStatus(t *testing.T) {
}{ }{
{ {
value: "ON", value: "ON",
expectedStatus: ON, expectedStatus: LegalHoldOn,
}, },
{ {
value: "Off", value: "Off",
expectedStatus: OFF, expectedStatus: LegalHoldOff,
}, },
{ {
value: "x", value: "x",
@ -98,32 +98,32 @@ func TestUnmarshalDefaultRetention(t *testing.T) {
expectErr: true, expectErr: true,
}, },
{ {
value: DefaultRetention{Mode: "GOVERNANCE"}, value: DefaultRetention{Mode: RetGovernance},
expectedErr: fmt.Errorf("either Days or Years must be specified"), expectedErr: fmt.Errorf("either Days or Years must be specified"),
expectErr: true, expectErr: true,
}, },
{ {
value: DefaultRetention{Mode: "GOVERNANCE", Days: &days}, value: DefaultRetention{Mode: RetGovernance, Days: &days},
expectedErr: nil, expectedErr: nil,
expectErr: false, expectErr: false,
}, },
{ {
value: DefaultRetention{Mode: "GOVERNANCE", Years: &years}, value: DefaultRetention{Mode: RetGovernance, Years: &years},
expectedErr: nil, expectedErr: nil,
expectErr: false, expectErr: false,
}, },
{ {
value: DefaultRetention{Mode: "GOVERNANCE", Days: &days, Years: &years}, value: DefaultRetention{Mode: RetGovernance, Days: &days, Years: &years},
expectedErr: fmt.Errorf("either Days or Years must be specified, not both"), expectedErr: fmt.Errorf("either Days or Years must be specified, not both"),
expectErr: true, expectErr: true,
}, },
{ {
value: DefaultRetention{Mode: "GOVERNANCE", Days: &zerodays}, value: DefaultRetention{Mode: RetGovernance, Days: &zerodays},
expectedErr: fmt.Errorf("Default retention period must be a positive integer value for 'Days'"), expectedErr: fmt.Errorf("Default retention period must be a positive integer value for 'Days'"),
expectErr: true, expectErr: true,
}, },
{ {
value: DefaultRetention{Mode: "GOVERNANCE", Days: &invalidDays}, value: DefaultRetention{Mode: RetGovernance, Days: &invalidDays},
expectedErr: fmt.Errorf("Default retention period too large for 'Days' %d", invalidDays), expectedErr: fmt.Errorf("Default retention period too large for 'Days' %d", invalidDays),
expectErr: true, expectErr: true,
}, },
@ -234,20 +234,20 @@ func TestIsObjectLockRequested(t *testing.T) {
}, },
{ {
header: http.Header{ header: http.Header{
xhttp.AmzObjectLockLegalHold: []string{""}, AmzObjectLockLegalHold: []string{""},
}, },
expectedVal: true, expectedVal: true,
}, },
{ {
header: http.Header{ header: http.Header{
xhttp.AmzObjectLockRetainUntilDate: []string{""}, AmzObjectLockRetainUntilDate: []string{""},
xhttp.AmzObjectLockMode: []string{""}, AmzObjectLockMode: []string{""},
}, },
expectedVal: true, expectedVal: true,
}, },
{ {
header: http.Header{ header: http.Header{
xhttp.AmzObjectLockBypassGovernance: []string{""}, AmzObjectLockBypassRetGovernance: []string{""},
}, },
expectedVal: false, expectedVal: false,
}, },
@ -275,26 +275,26 @@ func TestIsObjectLockGovernanceBypassSet(t *testing.T) {
}, },
{ {
header: http.Header{ header: http.Header{
xhttp.AmzObjectLockLegalHold: []string{""}, AmzObjectLockLegalHold: []string{""},
}, },
expectedVal: false, expectedVal: false,
}, },
{ {
header: http.Header{ header: http.Header{
xhttp.AmzObjectLockRetainUntilDate: []string{""}, AmzObjectLockRetainUntilDate: []string{""},
xhttp.AmzObjectLockMode: []string{""}, AmzObjectLockMode: []string{""},
}, },
expectedVal: false, expectedVal: false,
}, },
{ {
header: http.Header{ header: http.Header{
xhttp.AmzObjectLockBypassGovernance: []string{""}, AmzObjectLockBypassRetGovernance: []string{""},
}, },
expectedVal: false, expectedVal: false,
}, },
{ {
header: http.Header{ header: http.Header{
xhttp.AmzObjectLockBypassGovernance: []string{"true"}, AmzObjectLockBypassRetGovernance: []string{"true"},
}, },
expectedVal: true, expectedVal: true,
}, },
@ -394,7 +394,7 @@ func TestGetObjectRetentionMeta(t *testing.T) {
metadata: map[string]string{ metadata: map[string]string{
"x-amz-object-lock-mode": "governance", "x-amz-object-lock-mode": "governance",
}, },
expected: ObjectRetention{Mode: Governance}, expected: ObjectRetention{Mode: RetGovernance},
}, },
{ {
metadata: map[string]string{ metadata: map[string]string{
@ -427,13 +427,13 @@ func TestGetObjectLegalHoldMeta(t *testing.T) {
metadata: map[string]string{ metadata: map[string]string{
"x-amz-object-lock-legal-hold": "on", "x-amz-object-lock-legal-hold": "on",
}, },
expected: ObjectLegalHold{Status: ON}, expected: ObjectLegalHold{Status: LegalHoldOn},
}, },
{ {
metadata: map[string]string{ metadata: map[string]string{
"x-amz-object-lock-legal-hold": "off", "x-amz-object-lock-legal-hold": "off",
}, },
expected: ObjectLegalHold{Status: OFF}, expected: ObjectLegalHold{Status: LegalHoldOff},
}, },
{ {
metadata: map[string]string{ metadata: map[string]string{

@ -263,12 +263,14 @@ var actionConditionKeyMap = map[Action]condition.KeySet{
condition.S3ObjectLockMode, condition.S3ObjectLockMode,
condition.S3ObjectLockLegalHold, condition.S3ObjectLockLegalHold,
}, condition.CommonKeys...)...), }, condition.CommonKeys...)...),
// https://docs.aws.amazon.com/AmazonS3/latest/dev/list_amazons3.html
// LockLegalHold is not supported with PutObjectRetentionAction
PutObjectRetentionAction: condition.NewKeySet( PutObjectRetentionAction: condition.NewKeySet(
append([]condition.Key{ append([]condition.Key{
condition.S3ObjectLockRemainingRetentionDays, condition.S3ObjectLockRemainingRetentionDays,
condition.S3ObjectLockRetainUntilDate, condition.S3ObjectLockRetainUntilDate,
condition.S3ObjectLockMode, condition.S3ObjectLockMode,
condition.S3ObjectLockLegalHold,
}, condition.CommonKeys...)...), }, condition.CommonKeys...)...),
GetObjectRetentionAction: condition.NewKeySet(condition.CommonKeys...), GetObjectRetentionAction: condition.NewKeySet(condition.CommonKeys...),
@ -277,6 +279,8 @@ var actionConditionKeyMap = map[Action]condition.KeySet{
condition.S3ObjectLockLegalHold, condition.S3ObjectLockLegalHold,
}, condition.CommonKeys...)...), }, condition.CommonKeys...)...),
GetObjectLegalHoldAction: condition.NewKeySet(condition.CommonKeys...), GetObjectLegalHoldAction: condition.NewKeySet(condition.CommonKeys...),
// https://docs.aws.amazon.com/AmazonS3/latest/dev/list_amazons3.html
BypassGovernanceRetentionAction: condition.NewKeySet( BypassGovernanceRetentionAction: condition.NewKeySet(
append([]condition.Key{ append([]condition.Key{
condition.S3ObjectLockRemainingRetentionDays, condition.S3ObjectLockRemainingRetentionDays,
@ -284,6 +288,7 @@ var actionConditionKeyMap = map[Action]condition.KeySet{
condition.S3ObjectLockMode, condition.S3ObjectLockMode,
condition.S3ObjectLockLegalHold, condition.S3ObjectLockLegalHold,
}, condition.CommonKeys...)...), }, condition.CommonKeys...)...),
GetBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...), GetBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...),
PutBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...), PutBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...),
PutObjectTaggingAction: condition.NewKeySet(condition.CommonKeys...), PutObjectTaggingAction: condition.NewKeySet(condition.CommonKeys...),

@ -112,7 +112,6 @@ func valuesToStringSlice(n name, values ValueSet) ([]string, error) {
valueStrings := []string{} valueStrings := []string{}
for value := range values { for value := range values {
// FIXME: if AWS supports non-string values, we would need to support it.
s, err := value.GetString() s, err := value.GetString()
if err != nil { if err != nil {
return nil, fmt.Errorf("value must be a string for %v condition", n) return nil, fmt.Errorf("value must be a string for %v condition", n)

@ -301,12 +301,13 @@ var actionConditionKeyMap = map[Action]condition.KeySet{
condition.S3ObjectLockLegalHold, condition.S3ObjectLockLegalHold,
}, condition.CommonKeys...)...), }, condition.CommonKeys...)...),
// https://docs.aws.amazon.com/AmazonS3/latest/dev/list_amazons3.html
// LockLegalHold is not supported with PutObjectRetentionAction
PutObjectRetentionAction: condition.NewKeySet( PutObjectRetentionAction: condition.NewKeySet(
append([]condition.Key{ append([]condition.Key{
condition.S3ObjectLockRemainingRetentionDays, condition.S3ObjectLockRemainingRetentionDays,
condition.S3ObjectLockRetainUntilDate, condition.S3ObjectLockRetainUntilDate,
condition.S3ObjectLockMode, condition.S3ObjectLockMode,
condition.S3ObjectLockLegalHold,
}, condition.CommonKeys...)...), }, condition.CommonKeys...)...),
GetObjectRetentionAction: condition.NewKeySet(condition.CommonKeys...), GetObjectRetentionAction: condition.NewKeySet(condition.CommonKeys...),
@ -315,6 +316,8 @@ var actionConditionKeyMap = map[Action]condition.KeySet{
condition.S3ObjectLockLegalHold, condition.S3ObjectLockLegalHold,
}, condition.CommonKeys...)...), }, condition.CommonKeys...)...),
GetObjectLegalHoldAction: condition.NewKeySet(condition.CommonKeys...), GetObjectLegalHoldAction: condition.NewKeySet(condition.CommonKeys...),
// https://docs.aws.amazon.com/AmazonS3/latest/dev/list_amazons3.html
BypassGovernanceRetentionAction: condition.NewKeySet( BypassGovernanceRetentionAction: condition.NewKeySet(
append([]condition.Key{ append([]condition.Key{
condition.S3ObjectLockRemainingRetentionDays, condition.S3ObjectLockRemainingRetentionDays,
@ -322,6 +325,7 @@ var actionConditionKeyMap = map[Action]condition.KeySet{
condition.S3ObjectLockMode, condition.S3ObjectLockMode,
condition.S3ObjectLockLegalHold, condition.S3ObjectLockLegalHold,
}, condition.CommonKeys...)...), }, condition.CommonKeys...)...),
GetBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...), GetBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...),
PutBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...), PutBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...),
PutObjectTaggingAction: condition.NewKeySet(condition.CommonKeys...), PutObjectTaggingAction: condition.NewKeySet(condition.CommonKeys...),

Loading…
Cancel
Save