You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
415 lines
16 KiB
415 lines
16 KiB
/*
|
|
* MinIO Cloud Storage, (C) 2019-2020 MinIO, Inc.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"math"
|
|
"net/http"
|
|
"path"
|
|
|
|
"github.com/minio/minio/cmd/logger"
|
|
"github.com/minio/minio/pkg/auth"
|
|
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
|
|
}
|
|
|
|
// enforceRetentionForLifecycle checks if it is appropriate to remove an
|
|
// object according to locking configuration when this is lifecycle asking.
|
|
func enforceRetentionForLifecycle(ctx context.Context, objInfo ObjectInfo) (locked bool) {
|
|
lhold := objectlock.GetObjectLegalHoldMeta(objInfo.UserDefined)
|
|
if lhold.Status.Valid() && lhold.Status == objectlock.LegalHoldOn {
|
|
return true
|
|
}
|
|
|
|
ret := objectlock.GetObjectRetentionMeta(objInfo.UserDefined)
|
|
if ret.Mode.Valid() && (ret.Mode == objectlock.RetCompliance || ret.Mode == objectlock.RetGovernance) {
|
|
t, err := objectlock.UTCNowNTP()
|
|
if err != nil {
|
|
logger.LogIf(ctx, err)
|
|
return true
|
|
}
|
|
if ret.RetainUntilDate.After(t) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// enforceRetentionBypassForDelete enforces whether an existing object under governance can be deleted
|
|
// with governance bypass headers set in the request.
|
|
// Objects under site wide WORM can never be overwritten.
|
|
// For objects in "Governance" mode, overwrite is allowed if a) object retention date is past OR
|
|
// governance bypass headers are set and user has governance bypass permissions.
|
|
// Objects in "Compliance" mode can be overwritten only if retention date is past.
|
|
func enforceRetentionBypassForDelete(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
|
|
}
|
|
// 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 ErrNone
|
|
}
|
|
|
|
// enforceRetentionBypassForPut enforces whether an existing object under governance can be overwritten
|
|
// with governance bypass headers set in the request.
|
|
// Objects under site wide WORM cannot 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.
|
|
// 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, objRetention *objectlock.ObjectRetention, cred auth.Credentials, owner bool, claims map[string]interface{}) (ObjectInfo, APIErrorCode) {
|
|
byPassSet := objectlock.IsObjectLockGovernanceBypassSet(r.Header)
|
|
opts, err := getOpts(ctx, r, bucket, object)
|
|
if err != nil {
|
|
return ObjectInfo{}, toAPIErrorCode(ctx, err)
|
|
}
|
|
|
|
oi, err := getObjectInfoFn(ctx, bucket, object, opts)
|
|
if err != nil {
|
|
return oi, toAPIErrorCode(ctx, err)
|
|
}
|
|
|
|
t, err := objectlock.UTCNowNTP()
|
|
if err != nil {
|
|
logger.LogIf(ctx, err)
|
|
return oi, ErrObjectLocked
|
|
}
|
|
|
|
// Pass in relative days from current time, to additionally to verify "object-lock-remaining-retention-days" policy if any.
|
|
days := int(math.Ceil(math.Abs(objRetention.RetainUntilDate.Sub(t).Hours()) / 24))
|
|
|
|
ret := objectlock.GetObjectRetentionMeta(oi.UserDefined)
|
|
if ret.Mode.Valid() {
|
|
// Retention has expired you may change whatever you like.
|
|
if ret.RetainUntilDate.Before(t) {
|
|
perm := isPutRetentionAllowed(bucket, object,
|
|
days, objRetention.RetainUntilDate.Time,
|
|
objRetention.Mode, byPassSet, r, cred,
|
|
owner, claims)
|
|
return oi, perm
|
|
}
|
|
|
|
switch ret.Mode {
|
|
case objectlock.RetGovernance:
|
|
govPerm := isPutRetentionAllowed(bucket, object, days,
|
|
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
|
|
}
|
|
}
|
|
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
|
|
}
|
|
compliancePerm := isPutRetentionAllowed(bucket, object,
|
|
days, objRetention.RetainUntilDate.Time, objRetention.Mode,
|
|
false, r, cred, owner, claims)
|
|
return oi, compliancePerm
|
|
}
|
|
return oi, ErrNone
|
|
} // No pre-existing retention metadata present.
|
|
|
|
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
|
|
// for requests with WORM headers
|
|
// See https://docs.aws.amazon.com/AmazonS3/latest/dev/object-lock-managing.html for the spec.
|
|
// For non-existing objects with object retention headers set, this method returns ErrNone if bucket has
|
|
// locking enabled and user has requisite permissions (s3:PutObjectRetention)
|
|
// If object exists on object store and site wide WORM enabled - this method
|
|
// returns an error. For objects in "Governance" mode, overwrite is allowed if the retention date has expired.
|
|
// For objects in "Compliance" mode, retention date cannot be shortened, and mode cannot be altered.
|
|
// For objects with legal hold header set, the s3:PutObjectLegalHold permission is expected to be set
|
|
// Both legal hold and retention can be applied independently on an object
|
|
func checkPutObjectLockAllowed(ctx context.Context, r *http.Request, bucket, object string, getObjectInfoFn GetObjectInfoFn, retentionPermErr, legalHoldPermErr APIErrorCode) (objectlock.RetMode, objectlock.RetentionDate, objectlock.ObjectLegalHold, APIErrorCode) {
|
|
var mode objectlock.RetMode
|
|
var retainDate objectlock.RetentionDate
|
|
var legalHold objectlock.ObjectLegalHold
|
|
|
|
retentionRequested := objectlock.IsObjectLockRetentionRequested(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
|
|
opts, err := getOpts(ctx, r, bucket, object)
|
|
if err != nil {
|
|
return mode, retainDate, legalHold, toAPIErrorCode(ctx, err)
|
|
}
|
|
|
|
t, err := objectlock.UTCNowNTP()
|
|
if err != nil {
|
|
logger.LogIf(ctx, err)
|
|
return mode, retainDate, legalHold, ErrObjectLocked
|
|
}
|
|
|
|
if objInfo, err := getObjectInfoFn(ctx, bucket, object, opts); err == nil {
|
|
objExists = true
|
|
r := objectlock.GetObjectRetentionMeta(objInfo.UserDefined)
|
|
if globalWORMEnabled || ((r.Mode == objectlock.RetCompliance) && r.RetainUntilDate.After(t)) {
|
|
return mode, retainDate, legalHold, ErrObjectLocked
|
|
}
|
|
mode = r.Mode
|
|
retainDate = r.RetainUntilDate
|
|
legalHold = objectlock.GetObjectLegalHoldMeta(objInfo.UserDefined)
|
|
// Disallow overwriting an object on legal hold
|
|
if legalHold.Status == objectlock.LegalHoldOn {
|
|
return mode, retainDate, legalHold, ErrObjectLocked
|
|
}
|
|
}
|
|
|
|
if legalHoldRequested {
|
|
var lerr error
|
|
if legalHold, lerr = objectlock.ParseObjectLockLegalHoldHeaders(r.Header); lerr != nil {
|
|
return mode, retainDate, legalHold, toAPIErrorCode(ctx, err)
|
|
}
|
|
}
|
|
|
|
if retentionRequested {
|
|
legalHold, err := objectlock.ParseObjectLockLegalHoldHeaders(r.Header)
|
|
if err != nil {
|
|
return mode, retainDate, legalHold, toAPIErrorCode(ctx, err)
|
|
}
|
|
rMode, rDate, err := objectlock.ParseObjectLockRetentionHeaders(r.Header)
|
|
if err != nil {
|
|
return mode, retainDate, legalHold, toAPIErrorCode(ctx, err)
|
|
}
|
|
if objExists && retainDate.After(t) {
|
|
return mode, retainDate, legalHold, ErrObjectLocked
|
|
}
|
|
if retentionPermErr != ErrNone {
|
|
return mode, retainDate, legalHold, retentionPermErr
|
|
}
|
|
return rMode, rDate, legalHold, ErrNone
|
|
}
|
|
|
|
if !retentionRequested && isWORMBucket {
|
|
if retentionPermErr != ErrNone {
|
|
return mode, retainDate, legalHold, retentionPermErr
|
|
}
|
|
t, err := objectlock.UTCNowNTP()
|
|
if err != nil {
|
|
logger.LogIf(ctx, err)
|
|
return mode, retainDate, legalHold, ErrObjectLocked
|
|
}
|
|
// AWS S3 just creates a new version of object when an object is being overwritten.
|
|
if objExists && retainDate.After(t) {
|
|
return mode, retainDate, legalHold, ErrObjectLocked
|
|
}
|
|
if !legalHoldRequested && !retentionCfg.IsEmpty() {
|
|
// inherit retention from bucket configuration
|
|
return retentionCfg.Mode, objectlock.RetentionDate{Time: t.Add(retentionCfg.Validity)}, legalHold, ErrNone
|
|
}
|
|
return "", objectlock.RetentionDate{}, legalHold, ErrNone
|
|
}
|
|
return mode, retainDate, legalHold, ErrNone
|
|
}
|
|
|
|
func initBucketObjectLockConfig(buckets []BucketInfo, objAPI ObjectLayer) error {
|
|
for _, bucket := range buckets {
|
|
ctx := logger.SetReqInfo(GlobalContext, &logger.ReqInfo{BucketName: bucket.Name})
|
|
configFile := path.Join(bucketConfigPrefix, bucket.Name, bucketObjectLockEnabledConfigFile)
|
|
bucketObjLockData, err := readConfig(ctx, objAPI, configFile)
|
|
if err != nil {
|
|
if errors.Is(err, errConfigNotFound) {
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
|
|
if string(bucketObjLockData) != bucketObjectLockEnabledConfig {
|
|
// this should never happen
|
|
logger.LogIf(ctx, objectlock.ErrMalformedBucketObjectConfig)
|
|
continue
|
|
}
|
|
|
|
configFile = path.Join(bucketConfigPrefix, bucket.Name, objectLockConfig)
|
|
configData, err := readConfig(ctx, objAPI, configFile)
|
|
if err != nil {
|
|
if errors.Is(err, errConfigNotFound) {
|
|
globalBucketObjectLockConfig.Set(bucket.Name, objectlock.Retention{})
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
|
|
config, err := objectlock.ParseObjectLockConfig(bytes.NewReader(configData))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
retention := objectlock.Retention{}
|
|
if config.Rule != nil {
|
|
retention = config.ToRetention()
|
|
}
|
|
globalBucketObjectLockConfig.Set(bucket.Name, retention)
|
|
}
|
|
return nil
|
|
}
|
|
|