// Copyright (c) 2015-2021 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "errors" "math" "net/http" "github.com/minio/minio/internal/auth" objectlock "github.com/minio/minio/internal/bucket/object/lock" "github.com/minio/minio/internal/bucket/replication" xhttp "github.com/minio/minio/internal/http" "github.com/minio/minio/internal/logger" "github.com/minio/pkg/v2/policy" ) // BucketObjectLockSys - map of bucket and retention configuration. type BucketObjectLockSys struct{} // Get - Get retention configuration. func (sys *BucketObjectLockSys) Get(bucketName string) (r objectlock.Retention, err error) { config, _, err := globalBucketMetadataSys.GetObjectLockConfig(bucketName) if err != nil { if errors.Is(err, BucketObjectLockConfigNotFound{Bucket: bucketName}) { return r, nil } if errors.Is(err, errInvalidArgument) { return r, err } logger.CriticalIf(context.Background(), err) return r, err } return config.ToRetention(), nil } // enforceRetentionForDeletion checks if it is appropriate to remove an // object according to locking configuration when this is lifecycle/ bucket quota asking. func enforceRetentionForDeletion(ctx context.Context, objInfo ObjectInfo) (locked bool) { if objInfo.DeleteMarker { return false } 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 string, object ObjectToDelete, oi ObjectInfo, gerr error) error { if gerr != nil { // error from GetObjectInfo if _, ok := gerr.(MethodNotAllowed); ok { // This happens usually for a delete marker if oi.DeleteMarker || !oi.VersionPurgeStatus.Empty() { // Delete marker should be present and valid. return nil } } if isErrObjectNotFound(gerr) || isErrVersionNotFound(gerr) { return nil } return gerr } lhold := objectlock.GetObjectLegalHoldMeta(oi.UserDefined) if lhold.Status.Valid() && lhold.Status == objectlock.LegalHoldOn { return ObjectLocked{} } 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 ObjectLocked{} } if !ret.RetainUntilDate.Before(t) { return ObjectLocked{} } return nil 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 ObjectLocked{} } if !ret.RetainUntilDate.Before(t) { return ObjectLocked{} } return nil } // 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, the operation will succeed. if checkRequestAuthType(ctx, r, policy.BypassGovernanceRetentionAction, bucket, object.ObjectName) != ErrNone { return errAuthentication } } } return nil } // 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, oi ObjectInfo, objRetention *objectlock.ObjectRetention, cred auth.Credentials, owner bool) error { byPassSet := objectlock.IsObjectLockGovernanceBypassSet(r.Header) t, err := objectlock.UTCNowNTP() if err != nil { logger.LogIf(ctx, err) return ObjectLocked{Bucket: oi.Bucket, Object: oi.Name, VersionID: oi.VersionID} } // 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) { apiErr := isPutRetentionAllowed(oi.Bucket, oi.Name, days, objRetention.RetainUntilDate.Time, objRetention.Mode, byPassSet, r, cred, owner) if apiErr == ErrAccessDenied { return errAuthentication } return nil } switch ret.Mode { case objectlock.RetGovernance: govPerm := isPutRetentionAllowed(oi.Bucket, oi.Name, days, objRetention.RetainUntilDate.Time, objRetention.Mode, byPassSet, r, cred, owner) // 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 ObjectLocked{Bucket: oi.Bucket, Object: oi.Name, VersionID: oi.VersionID} } } if govPerm == ErrAccessDenied { return errAuthentication } return nil 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 ObjectLocked{Bucket: oi.Bucket, Object: oi.Name, VersionID: oi.VersionID} } apiErr := isPutRetentionAllowed(oi.Bucket, oi.Name, days, objRetention.RetainUntilDate.Time, objRetention.Mode, false, r, cred, owner) if apiErr == ErrAccessDenied { return errAuthentication } return nil } return nil } // No pre-existing retention metadata present. apiErr := isPutRetentionAllowed(oi.Bucket, oi.Name, days, objRetention.RetainUntilDate.Time, objRetention.Mode, byPassSet, r, cred, owner) if apiErr == ErrAccessDenied { return errAuthentication } return nil } // 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, rq *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(rq.Header) legalHoldRequested := objectlock.IsObjectLockLegalHoldRequested(rq.Header) retentionCfg, err := globalBucketObjectLockSys.Get(bucket) if err != nil { return mode, retainDate, legalHold, ErrInvalidBucketObjectLockConfiguration } if !retentionCfg.LockEnabled { 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 } opts, err := getOpts(ctx, rq, bucket, object) if err != nil { return mode, retainDate, legalHold, toAPIErrorCode(ctx, err) } replica := rq.Header.Get(xhttp.AmzBucketReplicationStatus) == replication.Replica.String() if opts.VersionID != "" && !replica { if objInfo, err := getObjectInfoFn(ctx, bucket, object, opts); err == nil { r := objectlock.GetObjectRetentionMeta(objInfo.UserDefined) t, err := objectlock.UTCNowNTP() if err != nil { logger.LogIf(ctx, err) return mode, retainDate, legalHold, ErrObjectLocked } if 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(rq.Header); lerr != nil { return mode, retainDate, legalHold, toAPIErrorCode(ctx, err) } } if retentionRequested { legalHold, err := objectlock.ParseObjectLockLegalHoldHeaders(rq.Header) if err != nil { return mode, retainDate, legalHold, toAPIErrorCode(ctx, err) } rMode, rDate, err := objectlock.ParseObjectLockRetentionHeaders(rq.Header) if err != nil && !(replica && rMode == "" && rDate.IsZero()) { return mode, retainDate, legalHold, toAPIErrorCode(ctx, err) } if retentionPermErr != ErrNone { return mode, retainDate, legalHold, retentionPermErr } return rMode, rDate, legalHold, ErrNone } if replica { // replica inherits retention metadata only from source return "", objectlock.RetentionDate{}, legalHold, ErrNone } if !retentionRequested && retentionCfg.Validity > 0 { if retentionPermErr != ErrNone { return mode, retainDate, legalHold, retentionPermErr } t, err := objectlock.UTCNowNTP() if err != nil { logger.LogIf(ctx, err) return mode, retainDate, legalHold, ErrObjectLocked } if !legalHoldRequested && retentionCfg.LockEnabled { // 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 } // NewBucketObjectLockSys returns initialized BucketObjectLockSys func NewBucketObjectLockSys() *BucketObjectLockSys { return &BucketObjectLockSys{} }