Add object retention at the per object (#8528)

level - this PR builds on #8120 which
added PutBucketObjectLockConfiguration and
GetBucketObjectLockConfiguration APIS

This PR implements PutObjectRetention,
GetObjectRetention API and enhances
PUT and GET API operations to display
governance metadata if permissions allow.
master
poornas 5 years ago committed by kannappanr
parent cc1a84b62e
commit ca96560d56
  1. 2
      cmd/api-datatypes.go
  2. 45
      cmd/api-errors.go
  3. 12
      cmd/api-router.go
  4. 7
      cmd/auth-handler.go
  5. 86
      cmd/bucket-handlers.go
  6. 7
      cmd/disk-cache.go
  7. 4
      cmd/fs-v1-multipart.go
  8. 4
      cmd/fs-v1.go
  9. 2
      cmd/globals.go
  10. 1
      cmd/handler-utils.go
  11. 12
      cmd/http/headers.go
  12. 30
      cmd/notification.go
  13. 3
      cmd/object-api-interface.go
  14. 213
      cmd/object-handlers.go
  15. 299
      cmd/object-lock.go
  16. 4
      cmd/peer-rest-server.go
  17. 3
      cmd/utils.go
  18. 128
      cmd/web-handlers.go
  19. 4
      cmd/xl-v1-multipart.go
  20. 4
      cmd/xl-v1-object.go
  21. 25
      docs/retention/README.md
  22. 14
      pkg/event/name.go
  23. 1
      pkg/event/target/elasticsearch.go
  24. 76
      pkg/iam/policy/action.go
  25. 30
      pkg/policy/action.go

@ -26,7 +26,7 @@ type ObjectIdentifier struct {
} }
// createBucketConfiguration container for bucket configuration request from client. // createBucketConfiguration container for bucket configuration request from client.
// Used for parsing the location from the request body for MakeBucketbucket. // Used for parsing the location from the request body for Makebucket.
type createBucketLocationConfiguration struct { type createBucketLocationConfiguration struct {
XMLName xml.Name `xml:"CreateBucketConfiguration" json:"-"` XMLName xml.Name `xml:"CreateBucketConfiguration" json:"-"`
Location string `xml:"LocationConstraint"` Location string `xml:"LocationConstraint"`

@ -141,6 +141,12 @@ const (
ErrInvalidPrefixMarker ErrInvalidPrefixMarker
ErrBadRequest ErrBadRequest
ErrKeyTooLongError ErrKeyTooLongError
ErrInvalidBucketObjectLockConfiguration
ErrObjectLocked
ErrInvalidRetentionDate
ErrPastObjectLockRetainDate
ErrUnknownWORMModeDirective
ErrObjectLockInvalidHeaders
// Add new error codes here. // Add new error codes here.
// SSE-S3 related API errors // SSE-S3 related API errors
@ -720,7 +726,36 @@ var errorCodes = errorCodeMap{
Description: "Duration provided in the request is invalid.", Description: "Duration provided in the request is invalid.",
HTTPStatusCode: http.StatusBadRequest, HTTPStatusCode: http.StatusBadRequest,
}, },
ErrInvalidBucketObjectLockConfiguration: {
Code: "InvalidRequest",
Description: "Bucket is missing ObjectLockConfiguration",
HTTPStatusCode: http.StatusBadRequest,
},
ErrObjectLocked: {
Code: "InvalidRequest",
Description: "Object is WORM protected and cannot be overwritten",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidRetentionDate: {
Code: "InvalidRequest",
Description: "Date must be provided in ISO 8601 format",
HTTPStatusCode: http.StatusBadRequest,
},
ErrPastObjectLockRetainDate: {
Code: "InvalidRequest",
Description: "the retain until date must be in the future",
HTTPStatusCode: http.StatusBadRequest,
},
ErrUnknownWORMModeDirective: {
Code: "InvalidRequest",
Description: "unknown wormMode directive",
HTTPStatusCode: http.StatusBadRequest,
},
ErrObjectLockInvalidHeaders: {
Code: "InvalidRequest",
Description: "x-amz-object-lock-retain-until-date and x-amz-object-lock-mode must both be supplied",
HTTPStatusCode: http.StatusBadRequest,
},
/// Bucket notification related errors. /// Bucket notification related errors.
ErrEventNotification: { ErrEventNotification: {
Code: "InvalidArgument", Code: "InvalidArgument",
@ -1569,6 +1604,14 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
apiErr = ErrOperationTimedOut apiErr = ErrOperationTimedOut
case errDiskNotFound: case errDiskNotFound:
apiErr = ErrSlowDown apiErr = ErrSlowDown
case errInvalidRetentionDate:
apiErr = ErrInvalidRetentionDate
case errPastObjectLockRetainDate:
apiErr = ErrPastObjectLockRetainDate
case errUnknownWORMModeDirective:
apiErr = ErrUnknownWORMModeDirective
case errObjectLockInvalidHeaders:
apiErr = ErrObjectLockInvalidHeaders
} }
// Compression errors // Compression errors

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

@ -357,7 +357,6 @@ func checkRequestAuthTypeToAccessKey(ctx context.Context, r *http.Request, actio
} }
return accessKey, owner, ErrAccessDenied return accessKey, owner, ErrAccessDenied
} }
if globalIAMSys.IsAllowed(iampolicy.Args{ if globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: cred.AccessKey, AccountName: cred.AccessKey,
Action: iampolicy.Action(action), Action: iampolicy.Action(action),
@ -487,10 +486,10 @@ func (a authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
writeErrorResponse(context.Background(), w, errorCodes.ToAPIErr(ErrSignatureVersionNotSupported), r.URL, guessIsBrowserReq(r)) writeErrorResponse(context.Background(), w, errorCodes.ToAPIErr(ErrSignatureVersionNotSupported), r.URL, guessIsBrowserReq(r))
} }
// isPutAllowed - 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.
func isPutAllowed(atype authType, bucketName, objectName string, r *http.Request) (s3Err APIErrorCode) { func isPutActionAllowed(atype authType, bucketName, objectName string, r *http.Request, action iampolicy.Action) (s3Err APIErrorCode) {
var cred auth.Credentials var cred auth.Credentials
var owner bool var owner bool
switch atype { switch atype {
@ -527,7 +526,7 @@ func isPutAllowed(atype authType, bucketName, objectName string, r *http.Request
if globalIAMSys.IsAllowed(iampolicy.Args{ if globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: cred.AccessKey, AccountName: cred.AccessKey,
Action: policy.PutObjectAction, Action: action,
BucketName: bucketName, BucketName: bucketName,
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims), ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
ObjectName: objectName, ObjectName: objectName,

@ -45,10 +45,10 @@ import (
) )
const ( const (
getBucketVersioningResponse = `<VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"/>` getBucketVersioningResponse = `<VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"/>`
objectLockConfig = "object-lock.xml" objectLockConfig = "object-lock.xml"
objectLockEnabledConfigFile = "object-lock-enabled.json" bucketObjectLockEnabledConfigFile = "object-lock-enabled.json"
objectLockEnabledConfig = `{"x-amz-bucket-object-lock-enabled":true}` bucketObjectLockEnabledConfig = `{"x-amz-bucket-object-lock-enabled":true}`
) )
// Check if there are buckets on server without corresponding entry in etcd backend and // Check if there are buckets on server without corresponding entry in etcd backend and
@ -370,20 +370,17 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
return return
} }
// Deny if WORM is enabled
if _, isWORMBucket := isWORMEnabled(bucket); isWORMBucket {
// Not required to check whether given objects exist or not, because
// DeleteMultipleObject is always successful irrespective of object existence.
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL, guessIsBrowserReq(r))
return
}
deleteObjectsFn := objectAPI.DeleteObjects deleteObjectsFn := objectAPI.DeleteObjects
if api.CacheAPI() != nil { if api.CacheAPI() != nil {
deleteObjectsFn = api.CacheAPI().DeleteObjects deleteObjectsFn = api.CacheAPI().DeleteObjects
} }
var objectsToDelete = map[string]int{} var objectsToDelete = map[string]int{}
getObjectInfoFn := objectAPI.GetObjectInfo
if api.CacheAPI() != nil {
getObjectInfoFn = api.CacheAPI().GetObjectInfo
}
var dErrs = make([]APIErrorCode, len(deleteObjects.Objects)) var dErrs = make([]APIErrorCode, len(deleteObjects.Objects))
for index, object := range deleteObjects.Objects { for index, object := range deleteObjects.Objects {
@ -394,7 +391,11 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
} }
continue continue
} }
govBypassPerms := checkRequestAuthType(ctx, r, policy.BypassGovernanceRetentionAction, bucket, object.ObjectName)
if _, err := checkGovernanceBypassAllowed(ctx, r, bucket, object.ObjectName, getObjectInfoFn, govBypassPerms); err != ErrNone {
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
@ -517,8 +518,8 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
} }
if objectLockEnabled { if objectLockEnabled {
configFile := path.Join(bucketConfigPrefix, bucket, objectLockEnabledConfigFile) configFile := path.Join(bucketConfigPrefix, bucket, bucketObjectLockEnabledConfigFile)
if err = saveConfig(ctx, objectAPI, configFile, []byte(objectLockEnabledConfig)); err != nil { if err = saveConfig(ctx, objectAPI, configFile, []byte(bucketObjectLockEnabledConfig)); err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return return
} }
@ -553,11 +554,12 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
} }
if objectLockEnabled { if objectLockEnabled {
configFile := path.Join(bucketConfigPrefix, bucket, objectLockEnabledConfigFile) configFile := path.Join(bucketConfigPrefix, bucket, bucketObjectLockEnabledConfigFile)
if err = saveConfig(ctx, objectAPI, configFile, []byte(objectLockEnabledConfig)); err != nil { if err = saveConfig(ctx, objectAPI, configFile, []byte(bucketObjectLockEnabledConfig)); err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return return
} }
globalBucketObjectLockConfig.Set(bucket, Retention{})
} }
// Make sure to add Location information here only for bucket // Make sure to add Location information here only for bucket
@ -890,7 +892,7 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
} }
} }
globalBucketRetentionConfig.Delete(bucket) globalBucketObjectLockConfig.Delete(bucket)
globalNotificationSys.RemoveNotification(bucket) globalNotificationSys.RemoveNotification(bucket)
globalPolicySys.Remove(bucket) globalPolicySys.Remove(bucket)
globalNotificationSys.DeleteBucket(ctx, bucket) globalNotificationSys.DeleteBucket(ctx, bucket)
@ -979,7 +981,10 @@ func (api objectAPIHandlers) PutBucketObjectLockConfigHandler(w http.ResponseWri
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL, guessIsBrowserReq(r))
return return
} }
if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketObjectLockConfigurationAction, bucket, ""); s3Error != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
return
}
config, err := parseObjectLockConfig(r.Body) config, err := parseObjectLockConfig(r.Body)
if err != nil { if err != nil {
apiErr := errorCodes.ToAPIErr(ErrMalformedXML) apiErr := errorCodes.ToAPIErr(ErrMalformedXML)
@ -987,8 +992,7 @@ func (api objectAPIHandlers) PutBucketObjectLockConfigHandler(w http.ResponseWri
writeErrorResponse(ctx, w, apiErr, r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, apiErr, r.URL, guessIsBrowserReq(r))
return return
} }
configFile := path.Join(bucketConfigPrefix, bucket, bucketObjectLockEnabledConfigFile)
configFile := path.Join(bucketConfigPrefix, bucket, objectLockEnabledConfigFile)
configData, err := readConfig(ctx, objectAPI, configFile) configData, err := readConfig(ctx, objectAPI, configFile)
if err != nil { if err != nil {
aerr := toAPIError(ctx, err) aerr := toAPIError(ctx, err)
@ -999,29 +1003,27 @@ func (api objectAPIHandlers) PutBucketObjectLockConfigHandler(w http.ResponseWri
return return
} }
if string(configData) != objectLockEnabledConfig { if string(configData) != bucketObjectLockEnabledConfig {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInternalError), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInternalError), r.URL, guessIsBrowserReq(r))
return return
} }
data, err := xml.Marshal(config)
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
configFile = path.Join(bucketConfigPrefix, bucket, objectLockConfig)
if err = saveConfig(ctx, objectAPI, configFile, data); err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
if config.Rule != nil { if config.Rule != nil {
data, err := xml.Marshal(config)
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
configFile := path.Join(bucketConfigPrefix, bucket, objectLockConfig)
if err = saveConfig(ctx, objectAPI, configFile, data); err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
retention := config.ToRetention() retention := config.ToRetention()
globalBucketRetentionConfig.Set(bucket, retention) globalBucketObjectLockConfig.Set(bucket, retention)
globalNotificationSys.PutBucketObjectLockConfig(ctx, bucket, retention) globalNotificationSys.PutBucketObjectLockConfig(ctx, bucket, retention)
} else { } else {
globalBucketRetentionConfig.Delete(bucket) globalBucketObjectLockConfig.Set(bucket, Retention{})
globalBucketObjectLockConfig.Delete(bucket)
} }
// Write success response. // Write success response.
@ -1046,8 +1048,12 @@ func (api objectAPIHandlers) GetBucketObjectLockConfigHandler(w http.ResponseWri
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r))
return return
} }
// check if user has permissions to perform this operation
configFile := path.Join(bucketConfigPrefix, bucket, objectLockEnabledConfigFile) if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketObjectLockConfigurationAction, bucket, ""); s3Error != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
return
}
configFile := path.Join(bucketConfigPrefix, bucket, bucketObjectLockEnabledConfigFile)
configData, err := readConfig(ctx, objectAPI, configFile) configData, err := readConfig(ctx, objectAPI, configFile)
if err != nil { if err != nil {
aerr := toAPIError(ctx, err) aerr := toAPIError(ctx, err)
@ -1058,7 +1064,7 @@ func (api objectAPIHandlers) GetBucketObjectLockConfigHandler(w http.ResponseWri
return return
} }
if string(configData) != objectLockEnabledConfig { if string(configData) != bucketObjectLockEnabledConfig {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInternalError), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInternalError), r.URL, guessIsBrowserReq(r))
return return
} }

@ -527,6 +527,13 @@ func (c *cacheObjects) PutObject(ctx context.Context, bucket, object string, r *
if opts.ServerSideEncryption != nil { if opts.ServerSideEncryption != nil {
return putObjectFn(ctx, bucket, object, r, opts) return putObjectFn(ctx, bucket, object, r, opts)
} }
// skip cache for objects with locks
objRetention := getObjectRetentionMeta(opts.UserDefined)
if objRetention.Mode == Governance || objRetention.Mode == Compliance {
return putObjectFn(ctx, bucket, object, r, opts)
}
// fetch from backend if cache exclude pattern or cache-control // fetch from backend if cache exclude pattern or cache-control
// directive set to exclude // directive set to exclude
if c.isCacheExclude(bucket, object) { if c.isCacheExclude(bucket, object) {

@ -683,8 +683,8 @@ func (fs *FSObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
} }
// Deny if WORM is enabled // Deny if WORM is enabled
if retention, isWORMBucket := isWORMEnabled(bucket); isWORMBucket { if globalWORMEnabled {
if fi, err := fsStatFile(ctx, pathJoin(fs.fsPath, bucket, object)); err == nil && retention.Retain(fi.ModTime()) { if _, err := fsStatFile(ctx, pathJoin(fs.fsPath, bucket, object)); err == nil {
return ObjectInfo{}, ObjectAlreadyExists{Bucket: bucket, Object: object} return ObjectInfo{}, ObjectAlreadyExists{Bucket: bucket, Object: object}
} }
} }

@ -931,8 +931,8 @@ func (fs *FSObjects) putObject(ctx context.Context, bucket string, object string
// Entire object was written to the temp location, now it's safe to rename it to the actual location. // Entire object was written to the temp location, now it's safe to rename it to the actual location.
fsNSObjPath := pathJoin(fs.fsPath, bucket, object) fsNSObjPath := pathJoin(fs.fsPath, bucket, object)
// Deny if WORM is enabled // Deny if WORM is enabled
if retention, isWORMBucket := isWORMEnabled(bucket); isWORMBucket { if globalWORMEnabled {
if fi, err := fsStatFile(ctx, fsNSObjPath); err == nil && retention.Retain(fi.ModTime()) { if _, err := fsStatFile(ctx, fsNSObjPath); err == nil {
return ObjectInfo{}, ObjectAlreadyExists{Bucket: bucket, Object: object} return ObjectInfo{}, ObjectAlreadyExists{Bucket: bucket, Object: object}
} }
} }

@ -196,7 +196,7 @@ var (
// Is worm enabled // Is worm enabled
globalWORMEnabled bool globalWORMEnabled bool
globalBucketRetentionConfig = newBucketRetentionConfig() globalBucketObjectLockConfig = newBucketObjectLockConfig()
// Disk cache drives // Disk cache drives
globalCacheConfig cache.Config globalCacheConfig cache.Config

@ -141,7 +141,6 @@ func extractMetadata(ctx context.Context, r *http.Request) (metadata map[string]
if _, ok := metadata["content-type"]; !ok { if _, ok := metadata["content-type"]; !ok {
metadata["content-type"] = "application/octet-stream" metadata["content-type"] = "application/octet-stream"
} }
// Success. // Success.
return metadata, nil return metadata, nil
} }

@ -57,10 +57,14 @@ const (
AmzCopySourceIfNoneMatch = "x-amz-copy-source-if-none-match" AmzCopySourceIfNoneMatch = "x-amz-copy-source-if-none-match"
AmzCopySourceIfMatch = "x-amz-copy-source-if-match" AmzCopySourceIfMatch = "x-amz-copy-source-if-match"
AmzCopySource = "X-Amz-Copy-Source" AmzCopySource = "X-Amz-Copy-Source"
AmzCopySourceVersionID = "X-Amz-Copy-Source-Version-Id" AmzCopySourceVersionID = "X-Amz-Copy-Source-Version-Id"
AmzCopySourceRange = "X-Amz-Copy-Source-Range" AmzCopySourceRange = "X-Amz-Copy-Source-Range"
AmzMetadataDirective = "X-Amz-Metadata-Directive" AmzMetadataDirective = "X-Amz-Metadata-Directive"
AmzObjectLockMode = "X-Amz-Object-Lock-Mode"
AmzObjectLockRetainUntilDate = "X-Amz-Object-Lock-Retain-Until-Date"
AmzObjectLockLegalHold = "X-Amz-Object-Lock-Legal-Hold"
AmzObjectLockBypassGovernance = "X-Amz-Bypass-Governance-Retention"
// Signature V4 related contants. // Signature V4 related contants.
AmzContentSha256 = "X-Amz-Content-Sha256" AmzContentSha256 = "X-Amz-Content-Sha256"

@ -777,20 +777,37 @@ func (sys *NotificationSys) load(buckets []BucketInfo, objAPI ObjectLayer) error
return nil return nil
} }
func (sys *NotificationSys) initBucketRetentionConfig(objAPI ObjectLayer) error { func (sys *NotificationSys) initBucketObjectLockConfig(objAPI ObjectLayer) error {
buckets, err := objAPI.ListBuckets(context.Background()) buckets, err := objAPI.ListBuckets(context.Background())
if err != nil { if err != nil {
return err return err
} }
for _, bucket := range buckets { for _, bucket := range buckets {
ctx := logger.SetReqInfo(context.Background(), &logger.ReqInfo{BucketName: bucket.Name}) ctx := logger.SetReqInfo(context.Background(), &logger.ReqInfo{BucketName: bucket.Name})
configFile := path.Join(bucketConfigPrefix, bucket.Name, objectLockConfig) configFile := path.Join(bucketConfigPrefix, bucket.Name, bucketObjectLockEnabledConfigFile)
configData, err := readConfig(ctx, objAPI, configFile) bucketObjLockData, err := readConfig(ctx, objAPI, configFile)
if err != nil { if err != nil {
if err == errConfigNotFound { if err == errConfigNotFound {
continue continue
} }
return err
}
if string(bucketObjLockData) != bucketObjectLockEnabledConfig {
// this should never happen
logger.LogIf(ctx, errMalformedBucketObjectConfig)
continue
}
configFile = path.Join(bucketConfigPrefix, bucket.Name, objectLockConfig)
configData, err := readConfig(ctx, objAPI, configFile)
if err != nil {
if err == errConfigNotFound {
globalBucketObjectLockConfig.Set(bucket.Name, Retention{})
continue
}
return err return err
} }
@ -798,10 +815,11 @@ func (sys *NotificationSys) initBucketRetentionConfig(objAPI ObjectLayer) error
if err != nil { if err != nil {
return err return err
} }
retention := Retention{}
if config.Rule != nil { if config.Rule != nil {
globalBucketRetentionConfig.Set(bucket.Name, config.ToRetention()) retention = config.ToRetention()
} }
globalBucketObjectLockConfig.Set(bucket.Name, retention)
} }
return nil return nil
} }
@ -849,7 +867,7 @@ func (sys *NotificationSys) Init(buckets []BucketInfo, objAPI ObjectLayer) error
for { for {
select { select {
case <-retryTimerCh: case <-retryTimerCh:
if err := sys.initBucketRetentionConfig(objAPI); err != nil { if err := sys.initBucketObjectLockConfig(objAPI); err != nil {
if err == errDiskNotFound || if err == errDiskNotFound ||
strings.Contains(err.Error(), InsufficientReadQuorum{}.Error()) || strings.Contains(err.Error(), InsufficientReadQuorum{}.Error()) ||
strings.Contains(err.Error(), InsufficientWriteQuorum{}.Error()) { strings.Contains(err.Error(), InsufficientWriteQuorum{}.Error()) {

@ -30,6 +30,9 @@ import (
// CheckCopyPreconditionFn returns true if copy precondition check failed. // CheckCopyPreconditionFn returns true if copy precondition check failed.
type CheckCopyPreconditionFn func(o ObjectInfo, encETag string) bool type CheckCopyPreconditionFn func(o ObjectInfo, encETag string) bool
// GetObjectInfoFn is the signature of GetObjectInfo function.
type GetObjectInfoFn func(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error)
// ObjectOptions represents object options for ObjectLayer operations // ObjectOptions represents object options for ObjectLayer operations
type ObjectOptions struct { type ObjectOptions struct {
ServerSideEncryption encrypt.ServerSide ServerSideEncryption encrypt.ServerSide

@ -43,6 +43,7 @@ import (
"github.com/minio/minio/pkg/event" "github.com/minio/minio/pkg/event"
"github.com/minio/minio/pkg/handlers" "github.com/minio/minio/pkg/handlers"
"github.com/minio/minio/pkg/hash" "github.com/minio/minio/pkg/hash"
iampolicy "github.com/minio/minio/pkg/iam/policy"
"github.com/minio/minio/pkg/ioutil" "github.com/minio/minio/pkg/ioutil"
"github.com/minio/minio/pkg/policy" "github.com/minio/minio/pkg/policy"
"github.com/minio/minio/pkg/s3select" "github.com/minio/minio/pkg/s3select"
@ -201,6 +202,9 @@ func (api objectAPIHandlers) SelectObjectContentHandler(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
} }
getRetPerms := checkRequestAuthType(ctx, r, policy.GetObjectRetentionAction, bucket, object)
// filter object lock metadata if permission does not permit
objInfo.UserDefined = filterObjectLockMetadata(ctx, r, bucket, object, objInfo.UserDefined, false, getRetPerms)
if err = s3Select.Open(getObject); err != nil { if err = s3Select.Open(getObject); err != nil {
if serr, ok := err.(s3select.SelectError); ok { if serr, ok := err.(s3select.SelectError); ok {
@ -341,6 +345,10 @@ func (api objectAPIHandlers) GetObjectHandler(w http.ResponseWriter, r *http.Req
defer gr.Close() defer gr.Close()
objInfo := gr.ObjInfo objInfo := gr.ObjInfo
// 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)
if objectAPI.IsEncryptionSupported() { if objectAPI.IsEncryptionSupported() {
objInfo.UserDefined = CleanMinioInternalMetadataKeys(objInfo.UserDefined) objInfo.UserDefined = CleanMinioInternalMetadataKeys(objInfo.UserDefined)
if _, err = DecryptObjectInfo(&objInfo, r.Header); err != nil { if _, err = DecryptObjectInfo(&objInfo, r.Header); err != nil {
@ -501,6 +509,11 @@ func (api objectAPIHandlers) HeadObjectHandler(w http.ResponseWriter, r *http.Re
writeErrorResponseHeadersOnly(w, toAPIError(ctx, err)) writeErrorResponseHeadersOnly(w, toAPIError(ctx, err))
return return
} }
// 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)
if objectAPI.IsEncryptionSupported() { if objectAPI.IsEncryptionSupported() {
if _, err = DecryptObjectInfo(&objInfo, r.Header); err != nil { if _, err = DecryptObjectInfo(&objInfo, r.Header); err != nil {
writeErrorResponseHeadersOnly(w, toAPIError(ctx, err)) writeErrorResponseHeadersOnly(w, toAPIError(ctx, err))
@ -737,14 +750,6 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
cpSrcDstSame := isStringEqual(pathJoin(srcBucket, srcObject), pathJoin(dstBucket, dstObject)) cpSrcDstSame := isStringEqual(pathJoin(srcBucket, srcObject), pathJoin(dstBucket, dstObject))
// Deny if WORM is enabled.
if retention, isWORMBucket := isWORMEnabled(dstBucket); isWORMBucket {
if oi, err := objectAPI.GetObjectInfo(ctx, dstBucket, dstObject, dstOpts); err == nil && retention.Retain(oi.ModTime) {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL, guessIsBrowserReq(r))
return
}
}
getObjectNInfo := objectAPI.GetObjectNInfo getObjectNInfo := objectAPI.GetObjectNInfo
if api.CacheAPI() != nil { if api.CacheAPI() != nil {
getObjectNInfo = api.CacheAPI().GetObjectNInfo getObjectNInfo = api.CacheAPI().GetObjectNInfo
@ -944,7 +949,24 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return return
} }
getObjectInfo := objectAPI.GetObjectInfo
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)
retPerms := isPutActionAllowed(getRequestAuthType(r), dstBucket, dstObject, r, iampolicy.PutObjectRetentionAction)
// apply default bucket configuration/governance headers for dest side.
retentionMode, retentionDate, s3Err := checkPutObjectRetentionAllowed(ctx, r, dstBucket, dstObject, getObjectInfo, retPerms)
if s3Err == ErrNone && retentionMode != "" {
srcInfo.UserDefined[xhttp.AmzObjectLockMode] = string(retentionMode)
srcInfo.UserDefined[xhttp.AmzObjectLockRetainUntilDate] = retentionDate.UTC().Format(time.RFC3339)
}
if s3Err != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r))
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
@ -1036,7 +1058,6 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
// - X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key // - X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key
func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Request) { func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "PutObject") ctx := newContext(r, w, "PutObject")
defer logger.AuditLog(w, r, "PutObject", mustGetClaimsFromToken(r)) defer logger.AuditLog(w, r, "PutObject", mustGetClaimsFromToken(r))
objectAPI := api.ObjectAPI() objectAPI := api.ObjectAPI()
@ -1142,7 +1163,7 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
reader = r.Body reader = r.Body
// Check if put is allowed // Check if put is allowed
if s3Err = isPutAllowed(rAuthType, bucket, object, r); s3Err != ErrNone { if s3Err = isPutActionAllowed(rAuthType, bucket, object, r, iampolicy.PutObjectAction); s3Err != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r))
return return
} }
@ -1220,12 +1241,15 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
getObjectInfo = api.CacheAPI().GetObjectInfo getObjectInfo = api.CacheAPI().GetObjectInfo
putObject = api.CacheAPI().PutObject putObject = api.CacheAPI().PutObject
} }
// Deny if WORM is enabled retPerms := isPutActionAllowed(rAuthType, bucket, object, r, iampolicy.PutObjectRetentionAction)
if retention, isWORMBucket := isWORMEnabled(bucket); isWORMBucket { retentionMode, retentionDate, s3Err := checkPutObjectRetentionAllowed(ctx, r, bucket, object, getObjectInfo, retPerms)
if oi, err := getObjectInfo(ctx, bucket, object, opts); err == nil && retention.Retain(oi.ModTime) { if s3Err == ErrNone && retentionMode != "" {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL, guessIsBrowserReq(r)) metadata[strings.ToLower(xhttp.AmzObjectLockMode)] = string(retentionMode)
return metadata[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)] = retentionDate.UTC().Format(time.RFC3339)
} }
if s3Err != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r))
return
} }
var objectEncryptionKey []byte var objectEncryptionKey []byte
@ -1347,14 +1371,6 @@ func (api objectAPIHandlers) NewMultipartUploadHandler(w http.ResponseWriter, r
return return
} }
// Deny if WORM is enabled
if retention, isWORMBucket := isWORMEnabled(bucket); isWORMBucket {
if oi, err := objectAPI.GetObjectInfo(ctx, bucket, object, opts); err == nil && retention.Retain(oi.ModTime) {
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) {
@ -1383,7 +1399,16 @@ 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)
retentionMode, retentionDate, s3Err := checkPutObjectRetentionAllowed(ctx, r, bucket, object, objectAPI.GetObjectInfo, retPerms)
if s3Err == ErrNone && retentionMode != "" {
metadata[strings.ToLower(xhttp.AmzObjectLockMode)] = string(retentionMode)
metadata[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)] = retentionDate.UTC().Format(time.RFC3339)
}
if s3Err != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r))
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 {
@ -1520,9 +1545,9 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
return return
} }
// Deny if WORM is enabled // Deny if global WORM is enabled
if retention, isWORMBucket := isWORMEnabled(dstBucket); isWORMBucket { if globalWORMEnabled {
if oi, err := objectAPI.GetObjectInfo(ctx, dstBucket, dstObject, dstOpts); err == nil && retention.Retain(oi.ModTime) { if _, err := objectAPI.GetObjectInfo(ctx, dstBucket, dstObject, dstOpts); err == nil {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL, guessIsBrowserReq(r))
return return
} }
@ -1822,7 +1847,7 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
s3Error APIErrorCode s3Error APIErrorCode
) )
reader = r.Body reader = r.Body
if s3Error = isPutAllowed(rAuthType, bucket, object, r); s3Error != ErrNone { if s3Error = isPutActionAllowed(rAuthType, bucket, object, r, iampolicy.PutObjectAction); s3Error != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
return return
} }
@ -1898,8 +1923,8 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
pReader := NewPutObjReader(rawReader, nil, nil) pReader := NewPutObjReader(rawReader, nil, nil)
// Deny if WORM is enabled // Deny if WORM is enabled
if retention, isWORMBucket := isWORMEnabled(bucket); isWORMBucket { if globalWORMEnabled {
if oi, err := objectAPI.GetObjectInfo(ctx, bucket, object, opts); err == nil && retention.Retain(oi.ModTime) { if _, err := objectAPI.GetObjectInfo(ctx, bucket, object, opts); err == nil {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL, guessIsBrowserReq(r))
return return
} }
@ -2006,14 +2031,6 @@ func (api objectAPIHandlers) AbortMultipartUploadHandler(w http.ResponseWriter,
return return
} }
// Deny if WORM is enabled
if retention, isWORMBucket := isWORMEnabled(bucket); isWORMBucket {
if oi, err := objectAPI.GetObjectInfo(ctx, bucket, object, ObjectOptions{}); err == nil && retention.Retain(oi.ModTime) {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL, guessIsBrowserReq(r))
return
}
}
uploadID, _, _, _, s3Error := getObjectResources(r.URL.Query()) uploadID, _, _, _, s3Error := getObjectResources(r.URL.Query())
if s3Error != ErrNone { if s3Error != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
@ -2182,12 +2199,17 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
return return
} }
// Deny if WORM is enabled // Reject retention or governance headers if set, CompleteMultipartUpload spec
if retention, isWORMBucket := isWORMEnabled(bucket); isWORMBucket { // does not use these headers, and should not be passed down to checkPutObjectRetentionAllowed
if oi, err := objectAPI.GetObjectInfo(ctx, bucket, object, ObjectOptions{}); err == nil && retention.Retain(oi.ModTime) { if isObjectLockRequested(r.Header) || isObjectLockGovernanceBypassSet(r.Header) {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL, guessIsBrowserReq(r))
return return
} }
// Enforce object lock governance in case a competing upload finalized first.
retPerms := isPutActionAllowed(getRequestAuthType(r), bucket, object, r, iampolicy.PutObjectRetentionAction)
if _, _, s3Err := checkPutObjectRetentionAllowed(ctx, r, bucket, object, objectAPI.GetObjectInfo, retPerms); s3Err != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r))
return
} }
// Get upload id. // Get upload id.
@ -2377,14 +2399,16 @@ func (api objectAPIHandlers) DeleteObjectHandler(w http.ResponseWriter, r *http.
return return
} }
// Deny if WORM is enabled getObjectInfo := objectAPI.GetObjectInfo
if _, isWORMBucket := isWORMEnabled(bucket); isWORMBucket { if api.CacheAPI() != nil {
// Not required to check whether given object exists or not, because getObjectInfo = api.CacheAPI().GetObjectInfo
// DeleteObject is always successful irrespective of object existence.
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL, guessIsBrowserReq(r))
return
} }
govBypassPerms := checkRequestAuthType(ctx, r, policy.BypassGovernanceRetentionAction, bucket, object)
if _, err := checkGovernanceBypassAllowed(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 {
@ -2486,7 +2510,7 @@ func (api objectAPIHandlers) GetObjectLegalHoldHandler(w http.ResponseWriter, r
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL, guessIsBrowserReq(r))
} }
// PutObjectRetentionHandler - set legal hold configuration to object, // PutObjectRetentionHandler - set object hold configuration to object,
func (api objectAPIHandlers) PutObjectRetentionHandler(w http.ResponseWriter, r *http.Request) { func (api objectAPIHandlers) PutObjectRetentionHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "PutObjectRetention") ctx := newContext(r, w, "PutObjectRetention")
@ -2506,30 +2530,78 @@ 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 := isPutActionAllowed(getRequestAuthType(r), bucket, object, r, policy.PutObjectRetentionAction); s3Err != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), 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 _, isWORMBucket := isWORMEnabled(bucket); !isWORMBucket {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidBucketObjectLockConfiguration), r.URL, guessIsBrowserReq(r))
return
}
getObjectInfo := objectAPI.GetObjectInfo getObjectInfo := objectAPI.GetObjectInfo
if api.CacheAPI() != nil { if api.CacheAPI() != nil {
getObjectInfo = api.CacheAPI().GetObjectInfo getObjectInfo = api.CacheAPI().GetObjectInfo
} }
opts, err := getOpts(ctx, r, bucket, object) govBypassPerms := checkRequestAuthType(ctx, r, policy.BypassGovernanceRetentionAction, bucket, object)
if err != nil { objInfo, s3Err := checkGovernanceBypassAllowed(ctx, r, bucket, object, getObjectInfo, govBypassPerms)
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) if s3Err != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r))
return return
} }
if _, err = getObjectInfo(ctx, bucket, object, opts); err != nil { objRetention, err := parseObjectRetention(r.Body)
if err != nil {
apiErr := errorCodes.ToAPIErr(ErrMalformedXML)
apiErr.Description = err.Error()
writeErrorResponse(ctx, w, apiErr, r.URL, guessIsBrowserReq(r))
return
}
// verify Content-MD5 sum of request body if this header set
if len(md5Bytes) > 0 {
data, err := xml.Marshal(objRetention)
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
}
}
objInfo.UserDefined[strings.ToLower(xhttp.AmzObjectLockMode)] = string(objRetention.Mode)
objInfo.UserDefined[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)] = objRetention.RetainUntilDate.UTC().Format(time.RFC3339)
objInfo.metadataOnly = true
if _, err = objectAPI.CopyObject(ctx, bucket, object, bucket, object, objInfo, ObjectOptions{}, ObjectOptions{}); err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return return
} }
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL, guessIsBrowserReq(r)) writeSuccessNoContent(w)
// Notify object event.
sendEvent(eventArgs{
EventName: event.ObjectCreatedPutRetention,
BucketName: bucket,
Object: objInfo,
ReqParams: extractReqParams(r),
RespElements: extractRespElements(w),
UserAgent: r.UserAgent(),
Host: handlers.GetSourceIP(r),
})
} }
// GetObjectRetentionHandler - get legal hold configuration to object, // GetObjectRetentionHandler - get object retention configuration of object,
func (api objectAPIHandlers) GetObjectRetentionHandler(w http.ResponseWriter, r *http.Request) { func (api objectAPIHandlers) GetObjectRetentionHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "GetObjectRetention") ctx := newContext(r, w, "GetObjectRetention")
defer logger.AuditLog(w, r, "GetObjectRetention", mustGetClaimsFromToken(r)) defer logger.AuditLog(w, r, "GetObjectRetention", mustGetClaimsFromToken(r))
vars := mux.Vars(r) vars := mux.Vars(r)
@ -2546,6 +2618,10 @@ func (api objectAPIHandlers) GetObjectRetentionHandler(w http.ResponseWriter, r
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r))
return return
} }
if s3Error := checkRequestAuthType(ctx, r, policy.GetObjectRetentionAction, bucket, object); s3Error != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
return
}
getObjectInfo := objectAPI.GetObjectInfo getObjectInfo := objectAPI.GetObjectInfo
if api.CacheAPI() != nil { if api.CacheAPI() != nil {
@ -2558,10 +2634,23 @@ func (api objectAPIHandlers) GetObjectRetentionHandler(w http.ResponseWriter, r
return return
} }
if _, err = getObjectInfo(ctx, bucket, object, opts); err != nil { objInfo, err := getObjectInfo(ctx, bucket, object, opts)
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return return
} }
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL, guessIsBrowserReq(r)) retention := getObjectRetentionMeta(objInfo.UserDefined)
writeSuccessResponseXML(w, encodeResponse(retention))
// Notify object retention accessed via a GET request.
sendEvent(eventArgs{
EventName: event.ObjectAccessedGetRetention,
BucketName: bucket,
Object: objInfo,
ReqParams: extractReqParams(r),
RespElements: extractRespElements(w),
UserAgent: r.UserAgent(),
Host: handlers.GetSourceIP(r),
})
} }

@ -17,11 +17,17 @@
package cmd package cmd
import ( import (
"context"
"encoding/xml" "encoding/xml"
"errors"
"fmt" "fmt"
"io" "io"
"net/http"
"strings"
"sync" "sync"
"time" "time"
xhttp "github.com/minio/minio/cmd/http"
) )
// RetentionMode - object retention mode. // RetentionMode - object retention mode.
@ -33,6 +39,30 @@ const (
// Compliance - compliance mode. // Compliance - compliance mode.
Compliance RetentionMode = "COMPLIANCE" Compliance RetentionMode = "COMPLIANCE"
// Invalid - invalid retention mode.
Invalid RetentionMode = "Invalid"
)
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")
) )
// Retention - bucket level retention configuration. // Retention - bucket level retention configuration.
@ -51,37 +81,36 @@ func (r Retention) Retain(created time.Time) bool {
return globalWORMEnabled || created.Add(r.Validity).After(time.Now()) return globalWORMEnabled || created.Add(r.Validity).After(time.Now())
} }
// BucketRetentionConfig - map of bucket and retention configuration. // BucketObjectLockConfig - map of bucket and retention configuration.
type BucketRetentionConfig struct { type BucketObjectLockConfig struct {
sync.RWMutex sync.RWMutex
retentionMap map[string]Retention retentionMap map[string]Retention
} }
// Set - set retention configuration. // Set - set retention configuration.
func (config *BucketRetentionConfig) Set(bucketName string, retention Retention) { func (config *BucketObjectLockConfig) Set(bucketName string, retention Retention) {
config.Lock() config.Lock()
config.retentionMap[bucketName] = retention config.retentionMap[bucketName] = retention
config.Unlock() config.Unlock()
} }
// Get - Get retention configuration. // Get - Get retention configuration.
func (config *BucketRetentionConfig) Get(bucketName string) (r Retention, ok bool) { func (config *BucketObjectLockConfig) Get(bucketName string) (r Retention, ok bool) {
config.RLock() config.RLock()
defer config.RUnlock() defer config.RUnlock()
r, ok = config.retentionMap[bucketName] r, ok = config.retentionMap[bucketName]
return r, ok return r, ok
} }
// Delete - delete retention configuration. // Delete - delete retention configuration.
func (config *BucketRetentionConfig) Delete(bucketName string) { func (config *BucketObjectLockConfig) Delete(bucketName string) {
config.Lock() config.Lock()
delete(config.retentionMap, bucketName) delete(config.retentionMap, bucketName)
config.Unlock() config.Unlock()
} }
func newBucketRetentionConfig() *BucketRetentionConfig { func newBucketObjectLockConfig() *BucketObjectLockConfig {
return &BucketRetentionConfig{ return &BucketObjectLockConfig{
retentionMap: map[string]Retention{}, retentionMap: map[string]Retention{},
} }
} }
@ -130,12 +159,12 @@ func (dr *DefaultRetention) UnmarshalXML(d *xml.Decoder, start xml.StartElement)
return fmt.Errorf("Default retention period must be a positive integer value for 'Days'") return fmt.Errorf("Default retention period must be a positive integer value for 'Days'")
} }
if *retention.Days > maximumRetentionDays { if *retention.Days > maximumRetentionDays {
return fmt.Errorf("Default retention period too large for 'Days' %w", *retention.Days) return fmt.Errorf("Default retention period too large for 'Days' %d", *retention.Days)
} }
} else if *retention.Years == 0 { } else if *retention.Years == 0 {
return fmt.Errorf("Default retention period must be a positive integer value for 'Years'") return fmt.Errorf("Default retention period must be a positive integer value for 'Years'")
} else if *retention.Years > maximumRetentionYears { } else if *retention.Years > maximumRetentionYears {
return fmt.Errorf("Default retention period too large for 'Years' %w", *retention.Years) return fmt.Errorf("Default retention period too large for 'Years' %d", *retention.Years)
} }
*dr = DefaultRetention(retention) *dr = DefaultRetention(retention)
@ -201,3 +230,253 @@ func newObjectLockConfig() *ObjectLockConfig {
ObjectLockEnabled: "Enabled", 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
}
if ret.RetainUntilDate.Before(time.Now()) {
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
}
if retDate.Before(time.Now()) {
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}
}
// checkGovernanceBypassAllowed enforces whether an existing object under governance can be overwritten
// with governance bypass headers set in the request.
// Objects under site wide WORM or those in "Compliance" mode can never be overwritten.
// 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.
func checkGovernanceBypassAllowed(ctx context.Context, r *http.Request, bucket, object string, getObjectInfoFn GetObjectInfoFn, govBypassPerm APIErrorCode) (oi ObjectInfo, s3Err APIErrorCode) {
var err error
var opts ObjectOptions
opts, err = getOpts(ctx, r, bucket, object)
if err != nil {
return oi, toAPIErrorCode(ctx, err)
}
oi, err = getObjectInfoFn(ctx, bucket, object, opts)
if err != nil {
// ignore case where object no longer exists
if toAPIError(ctx, err).Code == "NoSuchKey" {
return oi, ErrNone
}
return oi, toAPIErrorCode(ctx, err)
}
ret := getObjectRetentionMeta(oi.UserDefined)
if globalWORMEnabled || ret.Mode == Compliance {
return oi, ErrObjectLocked
}
// Here bucket does not support object lock
if ret.Mode == Invalid && isObjectLockGovernanceBypassSet(r.Header) {
return oi, ErrInvalidBucketObjectLockConfiguration
}
if ret.Mode == Compliance {
return oi, ErrObjectLocked
}
if ret.Mode == Governance {
if !isObjectLockGovernanceBypassSet(r.Header) {
if ret.RetainUntilDate.After(UTCNow()) {
return oi, ErrObjectLocked
}
return oi, ErrNone
}
if govBypassPerm != ErrNone {
return oi, ErrAccessDenied
}
}
return oi, ErrNone
}
// checkPutObjectRetentionAllowed enforces object retention 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, if retention mode is "Compliance" or site wide WORM enabled -this method
// returns an error. For objects in "Governance" mode, overwrite is allowed if the retention date has expired.
func checkPutObjectRetentionAllowed(ctx context.Context, r *http.Request, bucket, object string, getObjectInfoFn GetObjectInfoFn, retentionPermErr APIErrorCode) (RetentionMode, RetentionDate, APIErrorCode) {
var mode RetentionMode
var retainDate RetentionDate
retention, isWORMBucket := isWORMEnabled(bucket)
retentionRequested := isObjectLockRequested(r.Header)
var objExists bool
opts, err := getOpts(ctx, r, bucket, object)
if err != nil {
return mode, retainDate, 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
}
mode = r.Mode
retainDate = r.RetainUntilDate
}
if retentionRequested {
if !isWORMBucket {
return mode, retainDate, ErrInvalidBucketObjectLockConfiguration
}
rMode, rDate, err := parseObjectLockRetentionHeaders(r.Header)
if err != nil {
return mode, retainDate, toAPIErrorCode(ctx, err)
}
// AWS S3 just creates a new version of object when an object is being overwritten.
if objExists && retainDate.After(UTCNow()) {
return mode, retainDate, ErrObjectLocked
}
if rMode == Invalid {
return mode, retainDate, toAPIErrorCode(ctx, errObjectLockInvalidHeaders)
}
if retentionPermErr != ErrNone {
return mode, retainDate, retentionPermErr
}
return rMode, rDate, ErrNone
}
if !retentionRequested && isWORMBucket && !retention.IsEmpty() {
if retentionPermErr != ErrNone {
return mode, retainDate, retentionPermErr
}
// AWS S3 just creates a new version of object when an object is being overwritten.
if objExists && retainDate.After(UTCNow()) {
return mode, retainDate, ErrObjectLocked
}
// inherit retention from bucket configuration
return retention.Mode, RetentionDate{UTCNow().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 {
ret := getObjectRetentionMeta(metadata)
if ret.Mode == Invalid || isCopy {
delete(metadata, xhttp.AmzObjectLockMode)
delete(metadata, xhttp.AmzObjectLockRetainUntilDate)
return metadata
}
if getRetPerms == ErrNone {
return metadata
}
delete(metadata, xhttp.AmzObjectLockMode)
delete(metadata, xhttp.AmzObjectLockRetainUntilDate)
return metadata
}

@ -510,7 +510,7 @@ func (s *peerRESTServer) DeleteBucketHandler(w http.ResponseWriter, r *http.Requ
globalNotificationSys.RemoveNotification(bucketName) globalNotificationSys.RemoveNotification(bucketName)
globalPolicySys.Remove(bucketName) globalPolicySys.Remove(bucketName)
globalBucketRetentionConfig.Delete(bucketName) globalBucketObjectLockConfig.Delete(bucketName)
w.(http.Flusher).Flush() w.(http.Flusher).Flush()
} }
@ -787,7 +787,7 @@ func (s *peerRESTServer) PutBucketObjectLockConfigHandler(w http.ResponseWriter,
return return
} }
globalBucketRetentionConfig.Set(bucketName, retention) globalBucketObjectLockConfig.Set(bucketName, retention)
w.(http.Flusher).Flush() w.(http.Flusher).Flush()
} }

@ -521,6 +521,5 @@ func isWORMEnabled(bucket string) (Retention, bool) {
if globalWORMEnabled { if globalWORMEnabled {
return Retention{}, true return Retention{}, true
} }
return globalBucketObjectLockConfig.Get(bucket)
return globalBucketRetentionConfig.Get(bucket)
} }

@ -598,7 +598,10 @@ func (web *webAPIHandlers) RemoveObject(r *http.Request, args *RemoveObjectArgs,
return toJSONError(ctx, errServerNotInitialized) return toJSONError(ctx, errServerNotInitialized)
} }
listObjects := objectAPI.ListObjects listObjects := objectAPI.ListObjects
getObjectInfo := objectAPI.GetObjectInfo
if web.CacheAPI() != nil {
getObjectInfo = web.CacheAPI().GetObjectInfo
}
claims, owner, authErr := webRequestAuthenticate(r) claims, owner, authErr := webRequestAuthenticate(r)
if authErr != nil { if authErr != nil {
if authErr == errNoAuthToken { if authErr == errNoAuthToken {
@ -667,15 +670,10 @@ next:
for _, objectName := range args.Objects { for _, objectName := range args.Objects {
// If not a directory, remove the object. // If not a directory, remove the object.
if !hasSuffix(objectName, SlashSeparator) && objectName != "" { if !hasSuffix(objectName, SlashSeparator) && objectName != "" {
// Deny if WORM is enabled
if retention, isWORMBucket := isWORMEnabled(args.BucketName); isWORMBucket {
if oi, err := objectAPI.GetObjectInfo(ctx, args.BucketName, objectName, ObjectOptions{}); err == nil && retention.Retain(oi.ModTime) {
return toJSONError(ctx, errMethodNotAllowed)
}
}
// Check for permissions only in the case of // Check for permissions only in the case of
// non-anonymous login. For anonymous login, policy has already // non-anonymous login. For anonymous login, policy has already
// been checked. // been checked.
govBypassPerms := ErrAccessDenied
if authErr != errNoAuthToken { if authErr != errNoAuthToken {
if !globalIAMSys.IsAllowed(iampolicy.Args{ if !globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: claims.AccessKey(), AccountName: claims.AccessKey(),
@ -688,8 +686,33 @@ next:
}) { }) {
return toJSONError(ctx, errAccessDenied) return toJSONError(ctx, errAccessDenied)
} }
if globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: claims.AccessKey(),
Action: iampolicy.BypassGovernanceRetentionAction,
BucketName: args.BucketName,
ConditionValues: getConditionValues(r, "", claims.AccessKey(), claims.Map()),
IsOwner: owner,
ObjectName: objectName,
Claims: claims.Map(),
}) {
govBypassPerms = ErrNone
}
}
if authErr == errNoAuthToken {
// Check if object is allowed to be deleted anonymously
if globalPolicySys.IsAllowed(policy.Args{
Action: policy.BypassGovernanceRetentionAction,
BucketName: args.BucketName,
ConditionValues: getConditionValues(r, "", "", nil),
IsOwner: false,
ObjectName: objectName,
}) {
govBypassPerms = ErrNone
}
}
if _, err := checkGovernanceBypassAllowed(ctx, r, args.BucketName, objectName, getObjectInfo, govBypassPerms); err != ErrNone {
return toJSONError(ctx, errAccessDenied)
} }
if err = deleteObject(ctx, objectAPI, web.CacheAPI(), args.BucketName, objectName, r); err != nil { if err = deleteObject(ctx, objectAPI, web.CacheAPI(), args.BucketName, objectName, r); err != nil {
break next break next
} }
@ -906,6 +929,8 @@ func (web *webAPIHandlers) Upload(w http.ResponseWriter, r *http.Request) {
bucket := vars["bucket"] bucket := vars["bucket"]
object := vars["object"] object := vars["object"]
retPerms := ErrAccessDenied
claims, owner, authErr := webRequestAuthenticate(r) claims, owner, authErr := webRequestAuthenticate(r)
if authErr != nil { if authErr != nil {
if authErr == errNoAuthToken { if authErr == errNoAuthToken {
@ -940,6 +965,18 @@ func (web *webAPIHandlers) Upload(w http.ResponseWriter, r *http.Request) {
writeWebErrorResponse(w, errAuthentication) writeWebErrorResponse(w, errAuthentication)
return return
} }
if globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: claims.AccessKey(),
Action: iampolicy.PutObjectRetentionAction,
BucketName: bucket,
ConditionValues: getConditionValues(r, "", claims.AccessKey(), claims.Map()),
IsOwner: owner,
ObjectName: object,
Claims: claims.Map(),
}) {
retPerms = ErrNone
}
} }
// Check if bucket is a reserved bucket name or invalid. // Check if bucket is a reserved bucket name or invalid.
@ -1028,12 +1065,19 @@ 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)
// Deny if WORM is enabled getObjectInfo := objectAPI.GetObjectInfo
if retention, isWORMBucket := isWORMEnabled(bucket); isWORMBucket { if web.CacheAPI() != nil {
if oi, err := objectAPI.GetObjectInfo(ctx, bucket, object, opts); err == nil && retention.Retain(oi.ModTime) { getObjectInfo = web.CacheAPI().GetObjectInfo
writeWebErrorResponse(w, errMethodNotAllowed) }
return // enforce object retention rules
} retentionMode, retentionDate, s3Err := checkPutObjectRetentionAllowed(ctx, r, bucket, object, getObjectInfo, retPerms)
if s3Err == ErrNone && retentionMode != "" {
opts.UserDefined[xhttp.AmzObjectLockMode] = string(retentionMode)
opts.UserDefined[xhttp.AmzObjectLockRetainUntilDate] = retentionDate.UTC().Format(time.RFC3339)
}
if s3Err != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r))
return
} }
putObject := objectAPI.PutObject putObject := objectAPI.PutObject
@ -1087,6 +1131,8 @@ func (web *webAPIHandlers) Download(w http.ResponseWriter, r *http.Request) {
object := vars["object"] object := vars["object"]
token := r.URL.Query().Get("token") token := r.URL.Query().Get("token")
getRetPerms := ErrAccessDenied
claims, owner, authErr := webTokenAuthenticate(token) claims, owner, authErr := webTokenAuthenticate(token)
if authErr != nil { if authErr != nil {
if authErr == errNoAuthToken { if authErr == errNoAuthToken {
@ -1101,6 +1147,15 @@ func (web *webAPIHandlers) Download(w http.ResponseWriter, r *http.Request) {
writeWebErrorResponse(w, errAuthentication) writeWebErrorResponse(w, errAuthentication)
return return
} }
if globalPolicySys.IsAllowed(policy.Args{
Action: policy.GetObjectRetentionAction,
BucketName: bucket,
ConditionValues: getConditionValues(r, "", "", nil),
IsOwner: false,
ObjectName: object,
}) {
getRetPerms = ErrNone
}
} else { } else {
writeWebErrorResponse(w, authErr) writeWebErrorResponse(w, authErr)
return return
@ -1121,6 +1176,17 @@ func (web *webAPIHandlers) Download(w http.ResponseWriter, r *http.Request) {
writeWebErrorResponse(w, errAuthentication) writeWebErrorResponse(w, errAuthentication)
return return
} }
if globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: claims.AccessKey(),
Action: iampolicy.GetObjectRetentionAction,
BucketName: bucket,
ConditionValues: getConditionValues(r, "", claims.AccessKey(), claims.Map()),
IsOwner: owner,
ObjectName: object,
Claims: claims.Map(),
}) {
getRetPerms = ErrNone
}
} }
// Check if bucket is a reserved bucket name or invalid. // Check if bucket is a reserved bucket name or invalid.
@ -1144,6 +1210,9 @@ func (web *webAPIHandlers) Download(w http.ResponseWriter, r *http.Request) {
objInfo := gr.ObjInfo objInfo := gr.ObjInfo
// filter object lock metadata if permission does not permit
objInfo.UserDefined = filterObjectLockMetadata(ctx, r, bucket, object, objInfo.UserDefined, false, getRetPerms)
if objectAPI.IsEncryptionSupported() { if objectAPI.IsEncryptionSupported() {
if _, err = DecryptObjectInfo(&objInfo, r.Header); err != nil { if _, err = DecryptObjectInfo(&objInfo, r.Header); err != nil {
writeWebErrorResponse(w, err) writeWebErrorResponse(w, err)
@ -1234,9 +1303,9 @@ func (web *webAPIHandlers) DownloadZip(w http.ResponseWriter, r *http.Request) {
writeWebErrorResponse(w, decodeErr) writeWebErrorResponse(w, decodeErr)
return return
} }
token := r.URL.Query().Get("token") token := r.URL.Query().Get("token")
claims, owner, authErr := webTokenAuthenticate(token) claims, owner, authErr := webTokenAuthenticate(token)
var getRetPerms []APIErrorCode
if authErr != nil { if authErr != nil {
if authErr == errNoAuthToken { if authErr == errNoAuthToken {
for _, object := range args.Objects { for _, object := range args.Objects {
@ -1251,6 +1320,17 @@ func (web *webAPIHandlers) DownloadZip(w http.ResponseWriter, r *http.Request) {
writeWebErrorResponse(w, errAuthentication) writeWebErrorResponse(w, errAuthentication)
return return
} }
retentionPerm := ErrAccessDenied
if globalPolicySys.IsAllowed(policy.Args{
Action: policy.GetObjectRetentionAction,
BucketName: args.BucketName,
ConditionValues: getConditionValues(r, "", "", nil),
IsOwner: false,
ObjectName: pathJoin(args.Prefix, object),
}) {
retentionPerm = ErrNone
}
getRetPerms = append(getRetPerms, retentionPerm)
} }
} else { } else {
writeWebErrorResponse(w, authErr) writeWebErrorResponse(w, authErr)
@ -1273,6 +1353,19 @@ func (web *webAPIHandlers) DownloadZip(w http.ResponseWriter, r *http.Request) {
writeWebErrorResponse(w, errAuthentication) writeWebErrorResponse(w, errAuthentication)
return return
} }
retentionPerm := ErrAccessDenied
if globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: claims.AccessKey(),
Action: iampolicy.GetObjectAction,
BucketName: args.BucketName,
ConditionValues: getConditionValues(r, "", claims.AccessKey(), claims.Map()),
IsOwner: owner,
ObjectName: pathJoin(args.Prefix, object),
Claims: claims.Map(),
}) {
retentionPerm = ErrNone
}
getRetPerms = append(getRetPerms, retentionPerm)
} }
} }
@ -1291,7 +1384,7 @@ func (web *webAPIHandlers) DownloadZip(w http.ResponseWriter, r *http.Request) {
archive := zip.NewWriter(w) archive := zip.NewWriter(w)
defer archive.Close() defer archive.Close()
for _, object := range args.Objects { for i, object := range args.Objects {
// Writes compressed object file to the response. // Writes compressed object file to the response.
zipit := func(objectName string) error { zipit := func(objectName string) error {
var opts ObjectOptions var opts ObjectOptions
@ -1302,6 +1395,9 @@ func (web *webAPIHandlers) DownloadZip(w http.ResponseWriter, r *http.Request) {
defer gr.Close() defer gr.Close()
info := gr.ObjInfo 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])
if info.IsCompressed() { if info.IsCompressed() {
// For reporting, set the file size to the uncompressed size. // For reporting, set the file size to the uncompressed size.
info.Size = info.GetActualSize() info.Size = info.GetActualSize()

@ -708,8 +708,8 @@ func (xl xlObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
if xl.isObject(bucket, object) { if xl.isObject(bucket, object) {
// Deny if WORM is enabled // Deny if WORM is enabled
if retention, isWORMBucket := isWORMEnabled(bucket); isWORMBucket { if globalWORMEnabled {
if oi, err := xl.getObjectInfo(ctx, bucket, object); err == nil && retention.Retain(oi.ModTime) { if _, err := xl.getObjectInfo(ctx, bucket, object); err == nil {
return ObjectInfo{}, ObjectAlreadyExists{Bucket: bucket, Object: object} return ObjectInfo{}, ObjectAlreadyExists{Bucket: bucket, Object: object}
} }
} }

@ -611,8 +611,8 @@ func (xl xlObjects) putObject(ctx context.Context, bucket string, object string,
if xl.isObject(bucket, object) { if xl.isObject(bucket, object) {
// Deny if WORM is enabled // Deny if WORM is enabled
if retention, isWORMBucket := isWORMEnabled(bucket); isWORMBucket { if globalWORMEnabled {
if oi, err := xl.getObjectInfo(ctx, bucket, object); err == nil && retention.Retain(oi.ModTime) { if _, err := xl.getObjectInfo(ctx, bucket, object); err == nil {
return ObjectInfo{}, ObjectAlreadyExists{Bucket: bucket, Object: object} return ObjectInfo{}, ObjectAlreadyExists{Bucket: bucket, Object: object}
} }
} }

@ -1,6 +1,9 @@
# Object Lock and Immutablity [![Slack](https://slack.min.io/slack?type=svg)](https://slack.min.io) # Object Lock and Immutablity [![Slack](https://slack.min.io/slack?type=svg)](https://slack.min.io)
MinIO server allows to set bucket level WORM which makes objects in the bucket immutable i.e. delete and overwrite are not allowed till stipulated time specified in the bucket's object lock configuration. MinIO server allows selectively specify WORM for specific objects or configuring a bucket with default object lock configuration that applies default retention mode and retention duration to all incoming objects. Essentially, this makes objects in the bucket immutable i.e. delete and overwrite are not allowed till stipulated time specified in the bucket's object lock configuration or object retention.
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.
## Get Started ## Get Started
@ -8,19 +11,33 @@ MinIO server allows to set bucket level WORM which makes objects in the bucket i
Install MinIO - [MinIO Quickstart Guide](https://docs.min.io/docs/minio-quickstart-guide). Install MinIO - [MinIO Quickstart Guide](https://docs.min.io/docs/minio-quickstart-guide).
### 2. Set per bucket WORM ### 2. Set bucket WORM configuration
WORM on a bucket is enabled by setting object lock configuration. This configuration is applied to existing and new objects in the bucket. Below is an example sets `Governance` mode and one day retention time from object creation time of all objects in `mybucket`. WORM on a bucket is enabled by setting object lock configuration. This configuration is applied to existing and new objects in the bucket. Below is an example sets `Governance` mode and one day retention time from object creation time of all objects in `mybucket`.
```sh ```sh
$ awscli s3api put-object-lock-configuration --bucket mybucket --object-lock-configuration 'ObjectLockEnabled=\"Enabled\",Rule={DefaultRetention={Mode=\"GOVERNANCE\",Days=1}}' $ awscli s3api put-object-lock-configuration --bucket mybucket --object-lock-configuration 'ObjectLockEnabled=\"Enabled\",Rule={DefaultRetention={Mode=\"GOVERNANCE\",Days=1}}'
``` ```
### Set object lock
PutObject API allows setting per object retention mode and retention duration using `x-amz-object-lock-mode` and `x-amz-object-lock-retain-until-date` headers. This takes precedence over any bucket object lock configuration w.r.t retention.
```sh
aws s3api put-object --bucket testbucket --key lockme --object-lock-mode GOVERNANCE --object-lock-retain-until-date "2019-11-20" --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 object retention and governance bypass overrides.
### 3. Note ### 3. Note
- When global WORM is enabled by `MINIO_WORM` environment variable or `worm` field in configuration file supersedes bucket level WORM and `PUT object lock configuration` REST API is disabled. - When global WORM is enabled by `MINIO_WORM` environment variable or `worm` field in configuration file supersedes bucket level WORM and `PUT object lock configuration` REST API is disabled.
- Currently Governance mode is treated as Compliance mode. - global WORM and objects in `Compliance` mode can never be overwritten
- Once object lock configuration is set to a bucket, existing and new objects are put in WORM mode. - Currently `Governance` mode does not allow overwriting an existing object as versioning is not
available in MinIO. To that extent `Governance` mode is similar to `Compliance`. However,
if user has requisite `Governance` bypass permissions, an object in `Governance` mode can be overwritten.
- Once object lock configuration is set to a bucket, new objects inherit the retention settings of the bucket object lock configuration (if set) or the retention headers set in the PUT request
or set with PutObjectRetention API call
## Explore Further ## Explore Further

@ -29,12 +29,14 @@ type Name int
const ( const (
ObjectAccessedAll Name = 1 + iota ObjectAccessedAll Name = 1 + iota
ObjectAccessedGet ObjectAccessedGet
ObjectAccessedGetRetention
ObjectAccessedHead ObjectAccessedHead
ObjectCreatedAll ObjectCreatedAll
ObjectCreatedCompleteMultipartUpload ObjectCreatedCompleteMultipartUpload
ObjectCreatedCopy ObjectCreatedCopy
ObjectCreatedPost ObjectCreatedPost
ObjectCreatedPut ObjectCreatedPut
ObjectCreatedPutRetention
ObjectRemovedAll ObjectRemovedAll
ObjectRemovedDelete ObjectRemovedDelete
) )
@ -43,9 +45,9 @@ const (
func (name Name) Expand() []Name { func (name Name) Expand() []Name {
switch name { switch name {
case ObjectAccessedAll: case ObjectAccessedAll:
return []Name{ObjectAccessedGet, ObjectAccessedHead} return []Name{ObjectAccessedGet, ObjectAccessedHead, ObjectAccessedGetRetention}
case ObjectCreatedAll: case ObjectCreatedAll:
return []Name{ObjectCreatedCompleteMultipartUpload, ObjectCreatedCopy, ObjectCreatedPost, ObjectCreatedPut} return []Name{ObjectCreatedCompleteMultipartUpload, ObjectCreatedCopy, ObjectCreatedPost, ObjectCreatedPut, ObjectCreatedPutRetention}
case ObjectRemovedAll: case ObjectRemovedAll:
return []Name{ObjectRemovedDelete} return []Name{ObjectRemovedDelete}
default: default:
@ -60,6 +62,8 @@ func (name Name) String() string {
return "s3:ObjectAccessed:*" return "s3:ObjectAccessed:*"
case ObjectAccessedGet: case ObjectAccessedGet:
return "s3:ObjectAccessed:Get" return "s3:ObjectAccessed:Get"
case ObjectAccessedGetRetention:
return "s3:ObjectAccessed:GetRetention"
case ObjectAccessedHead: case ObjectAccessedHead:
return "s3:ObjectAccessed:Head" return "s3:ObjectAccessed:Head"
case ObjectCreatedAll: case ObjectCreatedAll:
@ -72,6 +76,8 @@ func (name Name) String() string {
return "s3:ObjectCreated:Post" return "s3:ObjectCreated:Post"
case ObjectCreatedPut: case ObjectCreatedPut:
return "s3:ObjectCreated:Put" return "s3:ObjectCreated:Put"
case ObjectCreatedPutRetention:
return "s3:ObjectAccessed:PutRetention"
case ObjectRemovedAll: case ObjectRemovedAll:
return "s3:ObjectRemoved:*" return "s3:ObjectRemoved:*"
case ObjectRemovedDelete: case ObjectRemovedDelete:
@ -130,6 +136,8 @@ func ParseName(s string) (Name, error) {
return ObjectAccessedAll, nil return ObjectAccessedAll, nil
case "s3:ObjectAccessed:Get": case "s3:ObjectAccessed:Get":
return ObjectAccessedGet, nil return ObjectAccessedGet, nil
case "s3:ObjectAccessed:GetRetention":
return ObjectAccessedGetRetention, nil
case "s3:ObjectAccessed:Head": case "s3:ObjectAccessed:Head":
return ObjectAccessedHead, nil return ObjectAccessedHead, nil
case "s3:ObjectCreated:*": case "s3:ObjectCreated:*":
@ -142,6 +150,8 @@ func ParseName(s string) (Name, error) {
return ObjectCreatedPost, nil return ObjectCreatedPost, nil
case "s3:ObjectCreated:Put": case "s3:ObjectCreated:Put":
return ObjectCreatedPut, nil return ObjectCreatedPut, nil
case "s3:ObjectCreated:PutRetention":
return ObjectCreatedPutRetention, nil
case "s3:ObjectRemoved:*": case "s3:ObjectRemoved:*":
return ObjectRemovedAll, nil return ObjectRemovedAll, nil
case "s3:ObjectRemoved:Delete": case "s3:ObjectRemoved:Delete":

@ -146,7 +146,6 @@ func (target *ElasticsearchTarget) send(eventData event.Event) error {
} else { } else {
err = update() err = update()
} }
return err return err
} }

@ -91,33 +91,57 @@ const (
// PutObjectAction - PutObject Rest API action. // PutObjectAction - PutObject Rest API action.
PutObjectAction = "s3:PutObject" PutObjectAction = "s3:PutObject"
// BypassGovernanceModeAction - bypass governance mode for DeleteObject Rest API action.
BypassGovernanceModeAction = "s3:BypassGovernanceMode"
// BypassGovernanceRetentionAction - bypass governance retention for PutObjectRetention, PutObject and DeleteObject Rest API action.
BypassGovernanceRetentionAction = "s3:BypassGovernanceRetention"
// PutObjectRetentionAction - PutObjectRetention Rest API action.
PutObjectRetentionAction = "s3:PutObjectRetention"
// GetObjectRetentionAction - GetObjectRetention, GetObject, HeadObject Rest API action.
GetObjectRetentionAction = "s3:GetObjectRetention"
// GetBucketObjectLockConfigurationAction - GetBucketObjectLockConfiguration Rest API action
GetBucketObjectLockConfigurationAction = "s3:GetBucketObjectLockConfiguration"
// PutBucketObjectLockConfigurationAction - PutBucketObjectLockConfiguration Rest API action
PutBucketObjectLockConfigurationAction = "s3:PutBucketObjectLockConfiguration"
// AllActions - all API actions // AllActions - all API actions
AllActions = "s3:*" AllActions = "s3:*"
) )
// List of all supported actions. // List of all supported actions.
var supportedActions = map[Action]struct{}{ var supportedActions = map[Action]struct{}{
AllActions: {}, AllActions: {},
AbortMultipartUploadAction: {}, AbortMultipartUploadAction: {},
CreateBucketAction: {}, CreateBucketAction: {},
DeleteBucketAction: {}, DeleteBucketAction: {},
DeleteBucketPolicyAction: {}, DeleteBucketPolicyAction: {},
DeleteObjectAction: {}, DeleteObjectAction: {},
GetBucketLocationAction: {}, GetBucketLocationAction: {},
GetBucketNotificationAction: {}, GetBucketNotificationAction: {},
GetBucketPolicyAction: {}, GetBucketPolicyAction: {},
GetObjectAction: {}, GetObjectAction: {},
HeadBucketAction: {}, HeadBucketAction: {},
ListAllMyBucketsAction: {}, ListAllMyBucketsAction: {},
ListBucketAction: {}, ListBucketAction: {},
ListBucketMultipartUploadsAction: {}, ListBucketMultipartUploadsAction: {},
ListenBucketNotificationAction: {}, ListenBucketNotificationAction: {},
ListMultipartUploadPartsAction: {}, ListMultipartUploadPartsAction: {},
PutBucketNotificationAction: {}, PutBucketNotificationAction: {},
PutBucketPolicyAction: {}, PutBucketPolicyAction: {},
PutObjectAction: {}, PutObjectAction: {},
GetBucketLifecycleAction: {}, GetBucketLifecycleAction: {},
PutBucketLifecycleAction: {}, PutBucketLifecycleAction: {},
PutObjectRetentionAction: {},
GetObjectRetentionAction: {},
PutBucketObjectLockConfigurationAction: {},
GetBucketObjectLockConfigurationAction: {},
BypassGovernanceModeAction: {},
BypassGovernanceRetentionAction: {},
} }
// isObjectAction - returns whether action is object type or not. // isObjectAction - returns whether action is object type or not.
@ -127,6 +151,10 @@ func (action Action) isObjectAction() bool {
fallthrough fallthrough
case ListMultipartUploadPartsAction, PutObjectAction, AllActions: case ListMultipartUploadPartsAction, PutObjectAction, AllActions:
return true return true
case BypassGovernanceModeAction, BypassGovernanceRetentionAction:
return true
case PutObjectRetentionAction, GetObjectRetentionAction:
return true
} }
return false return false
@ -238,4 +266,10 @@ var actionConditionKeyMap = map[Action]condition.KeySet{
condition.S3XAmzMetadataDirective, condition.S3XAmzMetadataDirective,
condition.S3XAmzStorageClass, condition.S3XAmzStorageClass,
}, condition.CommonKeys...)...), }, condition.CommonKeys...)...),
PutObjectRetentionAction: condition.NewKeySet(condition.CommonKeys...),
GetObjectRetentionAction: condition.NewKeySet(condition.CommonKeys...),
BypassGovernanceModeAction: condition.NewKeySet(condition.CommonKeys...),
BypassGovernanceRetentionAction: condition.NewKeySet(condition.CommonKeys...),
GetBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...),
PutBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...),
} }

@ -89,6 +89,20 @@ const (
// GetBucketLifecycleAction - GetBucketLifecycle Rest API action. // GetBucketLifecycleAction - GetBucketLifecycle Rest API action.
GetBucketLifecycleAction = "s3:GetBucketLifecycle" GetBucketLifecycleAction = "s3:GetBucketLifecycle"
// BypassGovernanceModeAction - bypass governance mode for DeleteObject Rest API action.
BypassGovernanceModeAction = "s3:BypassGovernanceMode"
// BypassGovernanceRetentionAction - bypass governance retention for PutObjectRetention, PutObject and DeleteObject Rest API action.
BypassGovernanceRetentionAction = "s3:BypassGovernanceRetention"
// PutObjectRetentionAction - PutObjectRetention Rest API action.
PutObjectRetentionAction = "s3:PutObjectRetention"
// GetObjectRetentionAction - GetObjectRetention, GetObject, HeadObject Rest API action.
GetObjectRetentionAction = "s3:GetObjectRetention"
// GetBucketObjectLockConfigurationAction - GetObjectLockConfiguration Rest API action
GetBucketObjectLockConfigurationAction = "s3:GetBucketObjectLockConfiguration"
// PutBucketObjectLockConfigurationAction - PutObjectLockConfiguration Rest API action
PutBucketObjectLockConfigurationAction = "s3:PutBucketObjectLockConfiguration"
) )
// isObjectAction - returns whether action is object type or not. // isObjectAction - returns whether action is object type or not.
@ -98,6 +112,10 @@ func (action Action) isObjectAction() bool {
fallthrough fallthrough
case ListMultipartUploadPartsAction, PutObjectAction: case ListMultipartUploadPartsAction, PutObjectAction:
return true return true
case PutObjectRetentionAction, GetObjectRetentionAction:
return true
case BypassGovernanceModeAction, BypassGovernanceRetentionAction:
return true
} }
return false return false
@ -122,6 +140,12 @@ func (action Action) IsValid() bool {
fallthrough fallthrough
case PutBucketLifecycleAction, GetBucketLifecycleAction: case PutBucketLifecycleAction, GetBucketLifecycleAction:
return true return true
case BypassGovernanceModeAction, BypassGovernanceRetentionAction:
return true
case PutObjectRetentionAction, GetObjectRetentionAction:
return true
case PutBucketObjectLockConfigurationAction, GetBucketObjectLockConfigurationAction:
return true
} }
return false return false
@ -204,4 +228,10 @@ var actionConditionKeyMap = map[Action]condition.KeySet{
condition.S3XAmzMetadataDirective, condition.S3XAmzMetadataDirective,
condition.S3XAmzStorageClass, condition.S3XAmzStorageClass,
}, condition.CommonKeys...)...), }, condition.CommonKeys...)...),
PutObjectRetentionAction: condition.NewKeySet(condition.CommonKeys...),
GetObjectRetentionAction: condition.NewKeySet(condition.CommonKeys...),
BypassGovernanceModeAction: condition.NewKeySet(condition.CommonKeys...),
BypassGovernanceRetentionAction: condition.NewKeySet(condition.CommonKeys...),
GetBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...),
PutBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...),
} }

Loading…
Cancel
Save