/*
 * 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"
	"net/http"
	"path"

	"github.com/minio/minio/cmd/logger"
	objectlock "github.com/minio/minio/pkg/bucket/object/lock"
)

// 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, govBypassPerm APIErrorCode) (oi ObjectInfo, s3Err APIErrorCode) {
	if globalWORMEnabled {
		return oi, ErrObjectLocked
	}
	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" {
			oi.UserDefined = map[string]string{}
			return oi, ErrNone
		}
		return oi, toAPIErrorCode(ctx, err)
	}
	ret := objectlock.GetObjectRetentionMeta(oi.UserDefined)
	lhold := objectlock.GetObjectLegalHoldMeta(oi.UserDefined)
	if lhold.Status == objectlock.ON {
		return oi, ErrObjectLocked
	}
	// Here bucket does not support object lock
	if ret.Mode == objectlock.Invalid {
		return oi, ErrNone
	}
	if ret.Mode != objectlock.Compliance && ret.Mode != objectlock.Governance {
		return oi, ErrUnknownWORMModeDirective
	}
	t, err := objectlock.UTCNowNTP()
	if err != nil {
		logger.LogIf(ctx, err)
		return oi, ErrObjectLocked
	}
	if ret.RetainUntilDate.Before(t) {
		return oi, ErrNone
	}
	if objectlock.IsObjectLockGovernanceBypassSet(r.Header) && ret.Mode == objectlock.Governance && govBypassPerm == ErrNone {
		return oi, ErrNone
	}
	return oi, ErrObjectLocked
}

// 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, govBypassPerm APIErrorCode, objRetention *objectlock.ObjectRetention) (oi ObjectInfo, s3Err APIErrorCode) {
	if globalWORMEnabled {
		return oi, ErrObjectLocked
	}

	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" {
			oi.UserDefined = map[string]string{}
			return oi, ErrNone
		}
		return oi, toAPIErrorCode(ctx, err)
	}

	ret := objectlock.GetObjectRetentionMeta(oi.UserDefined)
	// no retention metadata on object
	if ret.Mode == objectlock.Invalid {
		if _, isWORMBucket := globalBucketObjectLockConfig.Get(bucket); !isWORMBucket {
			return oi, ErrInvalidBucketObjectLockConfiguration
		}
		return oi, ErrNone
	}
	t, err := objectlock.UTCNowNTP()
	if err != nil {
		logger.LogIf(ctx, err)
		return oi, ErrObjectLocked
	}

	if ret.Mode == objectlock.Compliance {
		// Compliance retention mode cannot be changed and retention period cannot be shortened as per
		// https://docs.aws.amazon.com/AmazonS3/latest/dev/object-lock-overview.html#object-lock-retention-modes
		if objRetention.Mode != objectlock.Compliance || objRetention.RetainUntilDate.Before(ret.RetainUntilDate.Time) {
			return oi, ErrObjectLocked
		}
		if objRetention.RetainUntilDate.Before(t) {
			return oi, ErrInvalidRetentionDate
		}
		return oi, ErrNone
	}

	if ret.Mode == objectlock.Governance {
		if !objectlock.IsObjectLockGovernanceBypassSet(r.Header) {
			if objRetention.RetainUntilDate.Before(t) {
				return oi, ErrInvalidRetentionDate
			}
			if objRetention.RetainUntilDate.Before((ret.RetainUntilDate.Time)) {
				return oi, ErrObjectLocked
			}
			return oi, ErrNone
		}
		return oi, govBypassPerm
	}
	return oi, ErrNone
}

// 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.Mode, objectlock.RetentionDate, objectlock.ObjectLegalHold, APIErrorCode) {
	var mode objectlock.Mode
	var retainDate objectlock.RetentionDate
	var legalHold objectlock.ObjectLegalHold

	retention, isWORMBucket := globalBucketObjectLockConfig.Get(bucket)

	retentionRequested := objectlock.IsObjectLockRetentionRequested(r.Header)
	legalHoldRequested := objectlock.IsObjectLockLegalHoldRequested(r.Header)

	var objExists bool
	opts, err := getOpts(ctx, r, bucket, object)
	if err != nil {
		return mode, retainDate, legalHold, toAPIErrorCode(ctx, err)
	}
	if objInfo, err := getObjectInfoFn(ctx, bucket, object, opts); err == nil {
		objExists = true
		r := objectlock.GetObjectRetentionMeta(objInfo.UserDefined)
		if globalWORMEnabled || r.Mode == objectlock.Compliance {
			return mode, retainDate, legalHold, ErrObjectLocked
		}
		mode = r.Mode
		retainDate = r.RetainUntilDate
		legalHold = objectlock.GetObjectLegalHoldMeta(objInfo.UserDefined)
		// Disallow overwriting an object on legal hold
		if legalHold.Status == "ON" {
			return mode, retainDate, legalHold, ErrObjectLocked
		}
	}
	if legalHoldRequested {
		if !isWORMBucket {
			return mode, retainDate, legalHold, ErrInvalidBucketObjectLockConfiguration
		}
		var lerr error
		if legalHold, lerr = objectlock.ParseObjectLockLegalHoldHeaders(r.Header); lerr != nil {
			return mode, retainDate, legalHold, toAPIErrorCode(ctx, err)
		}
	}
	if retentionRequested {
		if !isWORMBucket {
			return mode, retainDate, legalHold, ErrInvalidBucketObjectLockConfiguration
		}
		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)
		}
		// AWS S3 just creates a new version of object when an object is being overwritten.
		t, err := objectlock.UTCNowNTP()
		if err != nil {
			logger.LogIf(ctx, err)
			return mode, retainDate, legalHold, ErrObjectLocked
		}
		if objExists && retainDate.After(t) {
			return mode, retainDate, legalHold, ErrObjectLocked
		}
		if rMode == objectlock.Invalid {
			return mode, retainDate, legalHold, toAPIErrorCode(ctx, objectlock.ErrObjectLockInvalidHeaders)
		}
		if retentionPermErr != ErrNone {
			return mode, retainDate, legalHold, retentionPermErr
		}
		return rMode, rDate, legalHold, ErrNone
	}

	if !retentionRequested && isWORMBucket {
		if retention.IsEmpty() && (mode == objectlock.Compliance || mode == objectlock.Governance) {
			return mode, retainDate, legalHold, ErrObjectLocked
		}
		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 {
			// inherit retention from bucket configuration
			return retention.Mode, objectlock.RetentionDate{Time: t.Add(retention.Validity)}, legalHold, ErrNone
		}
	}
	return mode, retainDate, legalHold, ErrNone
}

func initBucketObjectLockConfig(buckets []BucketInfo, objAPI ObjectLayer) error {
	for _, bucket := range buckets {
		ctx := logger.SetReqInfo(context.Background(), &logger.ReqInfo{BucketName: bucket.Name})
		configFile := path.Join(bucketConfigPrefix, bucket.Name, bucketObjectLockEnabledConfigFile)
		bucketObjLockData, err := readConfig(ctx, objAPI, configFile)
		if err != nil {
			if 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 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
}
