mirror of
https://github.com/versity/versitygw.git
synced 2026-09-22 07:54:14 +00:00
* fix: omit ObjectLockConfiguration Rule when there is no default retention ParseBucketLockConfigurationOutput always set Rule, so a bucket with object lock enabled and no default retention answered GET ?object-lock with an empty <Rule></Rule>. AWS S3 omits the element, and the AWS SDK v2 clients that read the rule from it follow the empty element with a malformed request. Fixes #2397 * test: cover GetObjectLockConfiguration with no default retention in the integration suite and drop the stale non-nil Rule expectation from the controller unit test --------- Co-authored-by: Tung Lam <lamphamabtung96@gmail.com>
578 lines
21 KiB
Go
578 lines
21 KiB
Go
// Copyright 2023 Versity Software
|
|
// This file is 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 auth
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
|
"github.com/gofiber/fiber/v3"
|
|
"github.com/versity/versitygw/backend"
|
|
"github.com/versity/versitygw/debuglogger"
|
|
"github.com/versity/versitygw/s3err"
|
|
"github.com/versity/versitygw/s3response"
|
|
)
|
|
|
|
type BucketLockConfig struct {
|
|
Enabled bool
|
|
DefaultRetention *types.DefaultRetention
|
|
CreatedAt *time.Time
|
|
}
|
|
|
|
// BypassMode says whether, and on whose authority, a request may override a
|
|
// GOVERNANCE-mode retention. It exists because the two ways that can happen
|
|
// are not equivalent, and collapsing them into one boolean previously let
|
|
// root overwrite locked objects it should not have been able to.
|
|
type BypassMode int
|
|
|
|
const (
|
|
// BypassNone is a request that has not asked to override anything: any
|
|
// unexpired retention blocks it outright.
|
|
BypassNone BypassMode = iota
|
|
|
|
// BypassRequested is a request carrying x-amz-bypass-governance-retention
|
|
// — DeleteObject, DeleteObjects, or PutObjectRetention. Root and admin
|
|
// may always override a GOVERNANCE retention this way, matching real
|
|
// AWS, where the account root can bypass regardless of policy; everyone
|
|
// else needs s3:BypassGovernanceRetention.
|
|
BypassRequested
|
|
|
|
// BypassOverwrite is the gateway's own extension: replacing an existing
|
|
// governance-locked object via PutObject, CopyObject or POST Object,
|
|
// none of which has a bypass header for a client to send. Because the
|
|
// caller never asked to override anything, the permission is required
|
|
// from everyone here — root included — and root's blanket bypass above
|
|
// deliberately does not apply. (Real S3 has no analogue: it only allows
|
|
// object lock on versioned buckets, where an overwrite creates a new
|
|
// version rather than replacing a locked one.)
|
|
BypassOverwrite
|
|
)
|
|
|
|
// allowsGovernanceOverride reports whether this mode permits overriding a
|
|
// GOVERNANCE retention at all, given the permission to do so.
|
|
func (b BypassMode) allowsGovernanceOverride() bool { return b != BypassNone }
|
|
|
|
// BypassModeForRequest maps the presence of the client's
|
|
// x-amz-bypass-governance-retention header onto a BypassMode.
|
|
func BypassModeForRequest(headerPresent bool) BypassMode {
|
|
if headerPresent {
|
|
return BypassRequested
|
|
}
|
|
return BypassNone
|
|
}
|
|
|
|
const (
|
|
maxObjectLockRetentionDays int32 = 36500
|
|
maxObjectLockRetentionYears int32 = 100
|
|
)
|
|
|
|
func ParseBucketLockConfigurationInput(input []byte) ([]byte, error) {
|
|
var lockConfig types.ObjectLockConfiguration
|
|
if err := xml.Unmarshal(input, &lockConfig); err != nil {
|
|
return nil, s3err.GetAPIError(s3err.ErrMalformedXML)
|
|
}
|
|
|
|
if lockConfig.ObjectLockEnabled != types.ObjectLockEnabledEnabled {
|
|
return nil, s3err.GetAPIError(s3err.ErrMalformedXML)
|
|
}
|
|
|
|
config := BucketLockConfig{
|
|
Enabled: lockConfig.ObjectLockEnabled == types.ObjectLockEnabledEnabled,
|
|
}
|
|
|
|
if lockConfig.Rule != nil && lockConfig.Rule.DefaultRetention != nil {
|
|
retention := lockConfig.Rule.DefaultRetention
|
|
|
|
if retention.Mode != types.ObjectLockRetentionModeCompliance && retention.Mode != types.ObjectLockRetentionModeGovernance {
|
|
return nil, s3err.GetAPIError(s3err.ErrMalformedXML)
|
|
}
|
|
if retention.Years != nil && retention.Days != nil {
|
|
return nil, s3err.GetAPIError(s3err.ErrMalformedXML)
|
|
}
|
|
|
|
if retention.Days != nil && *retention.Days <= 0 {
|
|
return nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgObjectLockRetentionDays, fmt.Sprint(*retention.Days))
|
|
}
|
|
if retention.Days != nil && *retention.Days > maxObjectLockRetentionDays {
|
|
return nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgObjectLockRetentionDaysTooLarge, fmt.Sprint(*retention.Days))
|
|
}
|
|
if retention.Years != nil && *retention.Years <= 0 {
|
|
return nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgObjectLockRetentionYears, fmt.Sprint(*retention.Years))
|
|
}
|
|
if retention.Years != nil && *retention.Years > maxObjectLockRetentionYears {
|
|
return nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgObjectLockRetentionYearsTooLarge, fmt.Sprint(*retention.Years))
|
|
}
|
|
|
|
config.DefaultRetention = retention
|
|
now := time.Now()
|
|
config.CreatedAt = &now
|
|
}
|
|
|
|
return json.Marshal(config)
|
|
}
|
|
|
|
func ParseBucketLockConfigurationOutput(input []byte) (*types.ObjectLockConfiguration, error) {
|
|
var config BucketLockConfig
|
|
if err := json.Unmarshal(input, &config); err != nil {
|
|
return nil, fmt.Errorf("parse object lock config: %w", err)
|
|
}
|
|
|
|
result := &types.ObjectLockConfiguration{}
|
|
|
|
if config.DefaultRetention != nil {
|
|
result.Rule = &types.ObjectLockRule{
|
|
DefaultRetention: config.DefaultRetention,
|
|
}
|
|
}
|
|
|
|
if config.Enabled {
|
|
result.ObjectLockEnabled = types.ObjectLockEnabledEnabled
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func ParseObjectLockRetentionInput(input []byte) (*s3response.PutObjectRetentionInput, error) {
|
|
var retention s3response.PutObjectRetentionInput
|
|
if err := xml.Unmarshal(input, &retention); err != nil {
|
|
debuglogger.Logf("invalid object lock retention request body: %v", err)
|
|
return nil, s3err.GetAPIError(s3err.ErrMalformedXML)
|
|
}
|
|
|
|
if retention.RetainUntilDate.Before(time.Now()) {
|
|
debuglogger.Logf("object lock retain until date must be in the future")
|
|
return nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgPastObjectLockRetainDate, retention.RetainUntilDate.Format(time.RFC3339))
|
|
}
|
|
switch retention.Mode {
|
|
case types.ObjectLockRetentionModeCompliance:
|
|
case types.ObjectLockRetentionModeGovernance:
|
|
default:
|
|
debuglogger.Logf("invalid object lock retention mode: %s", retention.Mode)
|
|
return nil, s3err.GetAPIError(s3err.ErrMalformedXML)
|
|
}
|
|
|
|
return &retention, nil
|
|
}
|
|
|
|
func ParseObjectLockRetentionInputToJSON(input *s3response.PutObjectRetentionInput) ([]byte, error) {
|
|
data, err := json.Marshal(input)
|
|
if err != nil {
|
|
debuglogger.Logf("parse object lock retention to JSON: %v", err)
|
|
return nil, fmt.Errorf("parse object lock retention: %w", err)
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
// IsObjectLockRetentionPutAllowed checks if the object lock retention PUT request
|
|
// is allowed against the current state of the object lock
|
|
func IsObjectLockRetentionPutAllowed(ctx fiber.Ctx, be backend.Backend, iam IAMService, bucket, object, versionId string, acc Account, input *s3response.PutObjectRetentionInput, bypass bool) error {
|
|
ret, err := be.GetObjectRetention(ctx.RequestCtx(), bucket, object, versionId)
|
|
if errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchObjectLockConfiguration)) {
|
|
// if object lock configuration is not set
|
|
// allow the retention modification without any checks
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
debuglogger.Logf("failed to get object retention: %v", err)
|
|
return err
|
|
}
|
|
|
|
retention, err := ParseObjectLockRetentionOutput(ret)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Pushing the date further out only ever strengthens the lock, so it
|
|
// needs nothing beyond s3:PutObjectRetention — in either mode. Anything
|
|
// that weakens it, an earlier date or a mode change, does not.
|
|
//
|
|
// A stored retention carrying no date can't be compared, so it counts as
|
|
// weakenable rather than being assumed an extension — the fail-closed
|
|
// direction.
|
|
isExtension := retention.Mode == input.Mode &&
|
|
retention.RetainUntilDate != nil &&
|
|
!input.RetainUntilDate.Time.Before(*retention.RetainUntilDate)
|
|
if isExtension {
|
|
return nil
|
|
}
|
|
|
|
if retention.Mode == types.ObjectLockRetentionModeCompliance {
|
|
// COMPLIANCE is absolute until it expires: it can be extended (above)
|
|
// but never shortened, and never downgraded to GOVERNANCE — by
|
|
// anyone, with any permission, including the account root. That
|
|
// immutability is the whole point of the mode, and real AWS rejects
|
|
// a shortening PutObjectRetention on a COMPLIANCE object even with
|
|
// the bypass header present.
|
|
debuglogger.Logf("weakening a 'COMPLIANCE' object lock retention is not allowed")
|
|
return s3err.GetAPIError(s3err.ErrObjectLocked)
|
|
}
|
|
|
|
if !bypass {
|
|
// if x-amz-bypass-governance-retention is not provided
|
|
// return error: object is locked
|
|
debuglogger.Logf("weakening a 'GOVERNANCE' object lock retention is not allowed without the bypass governance header")
|
|
return s3err.GetAPIError(s3err.ErrObjectLocked)
|
|
}
|
|
|
|
// What's left is weakening a GOVERNANCE retention — shortening its date,
|
|
// or switching it to COMPLIANCE — with the bypass header. That needs
|
|
// s3:BypassGovernanceRetention, via the bucket policy and/or (when
|
|
// configured) the IAM identity policy.
|
|
if err := verifyBypassGovernancePermission(ctx.RequestCtx(), be, iam, acc, bucket, object, BypassRequested, false, requestConditionContext(ctx, []Action{BypassGovernanceRetentionAction})); err != nil {
|
|
debuglogger.Logf("the user is missing 's3:BypassGovernanceRetention' permission: %v", err)
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// verifyBypassGovernancePermission decides whether acc may use
|
|
// x-amz-bypass-governance-retention to override a GOVERNANCE-mode lock on
|
|
// bucket/key. For a public (anonymous) request it consults only the
|
|
// bucket's public policy grant, wrapped in the generic ErrObjectLocked. For
|
|
// an authenticated request it combines the bucket policy decision with an
|
|
// identity-policy decision from iam when it implements PolicyEvaluator
|
|
// (currently only the standalone IAM service client), using the same
|
|
// explicit-deny-wins precedence as VerifyAccess. Unlike the "no header"
|
|
// case, a failed permission check here is reported as the specific
|
|
// AccessDenied error naming s3:BypassGovernanceRetention, not the generic
|
|
// "object protected by object lock" message — that message is reserved for
|
|
// when the bypass header itself is absent, or for backends with no
|
|
// identity-policy layer at all, where it preserves the existing behavior.
|
|
func verifyBypassGovernancePermission(ctx context.Context, be backend.Backend, iam IAMService, acc Account, bucket, key string, mode BypassMode, isBucketPublic bool, condCtx map[string][]string) error {
|
|
// Root and admin override a GOVERNANCE retention unconditionally when
|
|
// the client actually asked to — matching real AWS, where the account
|
|
// root can bypass whatever the policies say.
|
|
//
|
|
// This deliberately does not extend to BypassOverwrite: there the
|
|
// caller never requested a bypass (no S3 write API has a header for
|
|
// it), so there is nothing to grant root on their behalf, and letting
|
|
// it through would mean root silently replacing locked objects. See
|
|
// BypassMode.
|
|
if mode == BypassRequested && acc.Role == RoleAdmin {
|
|
return nil
|
|
}
|
|
|
|
if isBucketPublic {
|
|
policy, err := be.GetBucketPolicy(ctx, bucket)
|
|
if errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchBucketPolicy)) {
|
|
return s3err.GetAPIError(s3err.ErrObjectLocked)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := VerifyPublicBucketPolicy(policy, bucket, key, condCtx, be.NormalizeObjectKey, BypassGovernanceRetentionAction); err != nil {
|
|
return s3err.GetAPIError(s3err.ErrObjectLocked)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var resourceDecision policyDecision
|
|
policy, err := be.GetBucketPolicy(ctx, bucket)
|
|
switch {
|
|
case errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchBucketPolicy)):
|
|
resourceDecision = policyDecisionNoMatch
|
|
case err != nil:
|
|
return err
|
|
default:
|
|
resourceDecision, _, err = verifyBucketPolicy(policy, acc, bucket, key, condCtx, be.NormalizeObjectKey, BypassGovernanceRetentionAction)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
resourceArn := objectPolicyArn(bucket, key, be.NormalizeObjectKey)
|
|
|
|
if resourceDecision == policyDecisionDeny {
|
|
return s3err.GetExplicitDenyAccessErr(principalName(acc), string(BypassGovernanceRetentionAction), resourceArn, "a resource-based policy")
|
|
}
|
|
|
|
pe, hasPolicyEvaluator := iam.(PolicyEvaluator)
|
|
|
|
// Only BypassOverwrite reaches here as root — BypassRequested already
|
|
// returned above. Root has no identity policy to evaluate: with the
|
|
// standalone IAM backend it is not an IAM user at all, so asking that
|
|
// service about it would fail with ErrNoSuchUser rather than return a
|
|
// decision. It therefore falls back to the bucket-policy decision alone,
|
|
// exactly as a backend with no identity-policy layer does, and so still
|
|
// needs an explicit grant to replace a locked object.
|
|
if !hasPolicyEvaluator || acc.Role == RoleAdmin {
|
|
// No identity-policy layer for this backend: preserve today's exact
|
|
// behavior for every internal/LDAP/Vault/IPA deployment.
|
|
if resourceDecision == policyDecisionAllow {
|
|
return nil
|
|
}
|
|
return s3err.GetAPIError(s3err.ErrObjectLocked)
|
|
}
|
|
|
|
identity, err := identityPolicyDecisions(pe, AccessOptions{
|
|
Acc: acc,
|
|
Bucket: bucket,
|
|
Object: key,
|
|
Actions: []Action{BypassGovernanceRetentionAction},
|
|
}, []string{key}, be.NormalizeObjectKey, condCtx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
identityDecision := identity.Decisions[0].Decision
|
|
sessionDenies := identity.HasSessionPolicy && identity.SessionDecisions[0].Decision == policyDecisionDeny
|
|
// A session policy filters this permission the same way it filters any
|
|
// other: it can only take away what the role or the bucket policy grants.
|
|
sessionWithholds := identity.HasSessionPolicy && identity.SessionDecisions[0].Decision != policyDecisionAllow
|
|
|
|
if identityDecision == policyDecisionDeny || sessionDenies {
|
|
principal := identity.PrincipalArn
|
|
if principal == "" {
|
|
principal = principalName(acc)
|
|
}
|
|
return s3err.GetExplicitDenyAccessErr(principal, string(BypassGovernanceRetentionAction), resourceArn, "an identity-based policy")
|
|
}
|
|
if !sessionWithholds &&
|
|
(resourceDecision == policyDecisionAllow || identityDecision == policyDecisionAllow) {
|
|
return nil
|
|
}
|
|
|
|
principal := identity.PrincipalArn
|
|
if principal == "" {
|
|
principal = principalName(acc)
|
|
}
|
|
return s3err.GetImplicitDenyAccessErr(principal, string(BypassGovernanceRetentionAction), resourceArn)
|
|
}
|
|
|
|
func ParseObjectLockRetentionOutput(input []byte) (*types.ObjectLockRetention, error) {
|
|
var retention types.ObjectLockRetention
|
|
if err := json.Unmarshal(input, &retention); err != nil {
|
|
debuglogger.Logf("parse object lock retention output: %v", err)
|
|
return nil, fmt.Errorf("parse object lock retention: %w", err)
|
|
}
|
|
|
|
return &retention, nil
|
|
}
|
|
|
|
func ParseObjectLegalHoldOutput(status *bool) *s3response.GetObjectLegalHoldResult {
|
|
if status == nil {
|
|
return nil
|
|
}
|
|
|
|
if *status {
|
|
return &s3response.GetObjectLegalHoldResult{
|
|
Status: types.ObjectLockLegalHoldStatusOn,
|
|
}
|
|
}
|
|
|
|
return &s3response.GetObjectLegalHoldResult{
|
|
Status: types.ObjectLockLegalHoldStatusOff,
|
|
}
|
|
}
|
|
|
|
// CheckObjectAccess enforces the object locks protecting objects, for the
|
|
// single-object write paths. The multi-object delete path uses
|
|
// VerifyObjectsAccess instead, which folds this together with the
|
|
// authorization check into one pass.
|
|
func CheckObjectAccess(ctx fiber.Ctx, bucket string, acc Account, objects []types.ObjectIdentifier, bypass BypassMode, isBucketPublic bool, be backend.Backend, iam IAMService, isOverwrite bool) error {
|
|
rctx := ctx.RequestCtx()
|
|
state, err := loadObjectLockState(rctx, be, bucket, isOverwrite)
|
|
if err != nil || !state.applies {
|
|
return err
|
|
}
|
|
|
|
condCtx := requestConditionContext(ctx, []Action{BypassGovernanceRetentionAction})
|
|
for _, obj := range objects {
|
|
if err := state.checkObject(rctx, be, iam, acc, bucket, obj, bypass, isBucketPublic, condCtx); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// objectLockState is the bucket-level object-lock configuration a request is
|
|
// evaluated against, resolved once so a request naming many objects doesn't
|
|
// re-fetch it per key.
|
|
type objectLockState struct {
|
|
// applies is false when nothing about this bucket can block the request:
|
|
// object lock is off, unconfigured, or the write creates a new version
|
|
// rather than replacing anything.
|
|
applies bool
|
|
// defaultRetention is the bucket's default retention, only set when it
|
|
// is configured and still in force.
|
|
defaultRetention *types.DefaultRetention
|
|
// versioningEnabled makes a delete without a version id a new delete
|
|
// marker, which no retention protects against.
|
|
versioningEnabled bool
|
|
}
|
|
|
|
func loadObjectLockState(ctx context.Context, be backend.Backend, bucket string, isOverwrite bool) (objectLockState, error) {
|
|
var state objectLockState
|
|
|
|
if isOverwrite {
|
|
// if bucket versioning is enabled, any overwrite request
|
|
// should be enabled, as it leads to a new object version
|
|
// creation
|
|
res, err := be.GetBucketVersioning(ctx, bucket)
|
|
if err == nil && res.Status != nil && *res.Status == types.BucketVersioningStatusEnabled {
|
|
return state, nil
|
|
}
|
|
}
|
|
|
|
data, err := be.GetObjectLockConfiguration(ctx, bucket)
|
|
if err != nil {
|
|
if errors.Is(err, s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound)) {
|
|
return state, nil
|
|
}
|
|
|
|
return state, err
|
|
}
|
|
|
|
var bucketLockConfig BucketLockConfig
|
|
if err := json.Unmarshal(data, &bucketLockConfig); err != nil {
|
|
return state, fmt.Errorf("parse object lock config: %w", err)
|
|
}
|
|
|
|
if !bucketLockConfig.Enabled {
|
|
return state, nil
|
|
}
|
|
state.applies = true
|
|
|
|
if bucketLockConfig.DefaultRetention != nil && bucketLockConfig.CreatedAt != nil {
|
|
expirationDate := *bucketLockConfig.CreatedAt
|
|
if bucketLockConfig.DefaultRetention.Days != nil {
|
|
expirationDate = expirationDate.AddDate(0, 0, int(*bucketLockConfig.DefaultRetention.Days))
|
|
}
|
|
if bucketLockConfig.DefaultRetention.Years != nil {
|
|
expirationDate = expirationDate.AddDate(int(*bucketLockConfig.DefaultRetention.Years), 0, 0)
|
|
}
|
|
|
|
if expirationDate.After(time.Now()) {
|
|
state.defaultRetention = bucketLockConfig.DefaultRetention
|
|
}
|
|
}
|
|
|
|
vers, err := be.GetBucketVersioning(ctx, bucket)
|
|
if err == nil && vers.Status != nil {
|
|
state.versioningEnabled = *vers.Status == types.BucketVersioningStatusEnabled
|
|
}
|
|
|
|
return state, nil
|
|
}
|
|
|
|
// checkObject reports whether one object's retention or legal hold blocks
|
|
// this request. A nil error means this object is writable; it says nothing
|
|
// about any other object in the same request.
|
|
func (s objectLockState) checkObject(ctx context.Context, be backend.Backend, iam IAMService, acc Account, bucket string, obj types.ObjectIdentifier, bypass BypassMode, isBucketPublic bool, condCtx map[string][]string) error {
|
|
var key, versionId string
|
|
if obj.Key != nil {
|
|
key = *obj.Key
|
|
}
|
|
if obj.VersionId != nil {
|
|
versionId = *obj.VersionId
|
|
}
|
|
|
|
// if bucket versioning is enabled and versionId isn't provided
|
|
// no lock check is needed, as it leads to a new delete marker creation
|
|
if s.versioningEnabled && versionId == "" {
|
|
return nil
|
|
}
|
|
|
|
checkRetention := true
|
|
retentionData, err := be.GetObjectRetention(ctx, bucket, key, versionId)
|
|
if errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchKey)) {
|
|
return nil
|
|
}
|
|
// the object is a delete marker, if a `MethodNotAllowed` error is returned
|
|
// no object lock check is needed
|
|
if errors.Is(err, s3err.GetAPIError(s3err.ErrMethodNotAllowed)) {
|
|
return nil
|
|
}
|
|
if errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchObjectLockConfiguration)) {
|
|
checkRetention = false
|
|
}
|
|
if err != nil && checkRetention {
|
|
return err
|
|
}
|
|
|
|
if checkRetention {
|
|
retention, err := ParseObjectLockRetentionOutput(retentionData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if retention.Mode != "" && retention.RetainUntilDate != nil {
|
|
// An expired retention protects nothing, and an object's own
|
|
// retention supersedes the bucket default, so this object is
|
|
// past its lock. Note this also skips the legal-hold check
|
|
// below, preserving long-standing behavior; it returns for
|
|
// this object only, where the same statement previously
|
|
// short-circuited the caller's whole request and let every
|
|
// remaining object through unchecked.
|
|
if retention.RetainUntilDate.Before(time.Now()) {
|
|
return nil
|
|
}
|
|
|
|
if err := s.checkRetentionMode(ctx, be, iam, acc, bucket, key, retention.Mode, bypass, isBucketPublic, condCtx); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
checkLegalHold := true
|
|
|
|
status, err := be.GetObjectLegalHold(ctx, bucket, key, versionId)
|
|
if err != nil {
|
|
if errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchKey)) {
|
|
return nil
|
|
}
|
|
if errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchObjectLockConfiguration)) {
|
|
checkLegalHold = false
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if checkLegalHold && *status {
|
|
return s3err.GetAPIError(s3err.ErrObjectLocked)
|
|
}
|
|
|
|
if s.defaultRetention != nil {
|
|
return s.checkRetentionMode(ctx, be, iam, acc, bucket, key, s.defaultRetention.Mode, bypass, isBucketPublic, condCtx)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// checkRetentionMode applies one retention mode's rule: COMPLIANCE blocks
|
|
// unconditionally, GOVERNANCE blocks unless the request both asked to
|
|
// override it and is permitted to.
|
|
func (s objectLockState) checkRetentionMode(ctx context.Context, be backend.Backend, iam IAMService, acc Account, bucket, key string, mode types.ObjectLockRetentionMode, bypass BypassMode, isBucketPublic bool, condCtx map[string][]string) error {
|
|
switch mode {
|
|
case types.ObjectLockRetentionModeGovernance:
|
|
if !bypass.allowsGovernanceOverride() {
|
|
return s3err.GetAPIError(s3err.ErrObjectLocked)
|
|
}
|
|
return verifyBypassGovernancePermission(ctx, be, iam, acc, bucket, key, bypass, isBucketPublic, condCtx)
|
|
case types.ObjectLockRetentionModeCompliance:
|
|
return s3err.GetAPIError(s3err.ErrObjectLocked)
|
|
}
|
|
|
|
return nil
|
|
}
|