fix: check PutObjectTagging/LegalHold/Retention permissions on PutObject,CopyObject and CreateMultipartUpload

Fixes #1986

When a client includes tagging, legal hold, or retention headers in a PutObject, CopyObject or CreateMultipartUpload request, the corresponding bucket policy permissions must be verified in addition to s3:PutObject:

`X-Amz-Tagging` - `s3:PutObjectTagging`
`X-Amz-Object-Lock-Legal-Hold` - `s3:PutObjectLegalHold`
`X-Amz-Object-Lock-Mode` - `s3:PutObjectRetention`

Previously, only s3:PutObject was checked, allowing users to set tagging, legal hold, and retention without having the required permissions. Now each action permission is check, if user tries to add them.

For CopyObject these permissions are checked on destination object.
This commit is contained in:
niksis02
2026-04-03 00:08:13 +04:00
parent b7b5a347ae
commit 8d5b2be0b2
14 changed files with 679 additions and 57 deletions

View File

@@ -61,7 +61,7 @@ func VerifyObjectCopyAccess(ctx context.Context, be backend.Backend, copySource
Acc: opts.Acc,
Bucket: srcBucket,
Object: srcObject,
Action: GetObjectAction,
Actions: []Action{GetObjectAction},
}); err != nil {
return err
}
@@ -76,7 +76,7 @@ type AccessOptions struct {
Acc Account
Bucket string
Object string
Action Action
Actions []Action
Readonly bool
IsPublicRequest bool
DisableACL bool
@@ -105,7 +105,7 @@ func VerifyAccess(ctx context.Context, be backend.Backend, opts AccessOptions) e
return policyErr
}
} else {
return VerifyBucketPolicy(policy, opts.Acc.Access, opts.Bucket, opts.Object, opts.Action)
return VerifyBucketPolicy(policy, opts.Acc.Access, opts.Bucket, opts.Object, opts.Actions...)
}
if err := verifyACL(opts.Acl, opts.Acc.Access, opts.AclPermission, opts.DisableACL); err != nil {

View File

@@ -234,7 +234,11 @@ func ValidatePolicyDocument(policyBin []byte, bucket string, iam IAMService) err
return nil
}
func VerifyBucketPolicy(policy []byte, access, bucket, object string, action Action) error {
func VerifyBucketPolicy(policy []byte, access, bucket, object string, actions ...Action) error {
if len(actions) == 0 {
return s3err.GetAPIError(s3err.ErrAccessDenied)
}
var bucketPolicy BucketPolicy
if err := json.Unmarshal(policy, &bucketPolicy); err != nil {
return fmt.Errorf("failed to parse the bucket policy: %w", err)
@@ -245,8 +249,10 @@ func VerifyBucketPolicy(policy []byte, access, bucket, object string, action Act
resource += "/" + object
}
if !bucketPolicy.isAllowed(access, action, resource) {
return s3err.GetAPIError(s3err.ErrAccessDenied)
for _, action := range actions {
if !bucketPolicy.isAllowed(access, action, resource) {
return s3err.GetAPIError(s3err.ErrAccessDenied)
}
}
return nil

View File

@@ -37,7 +37,7 @@ func (c S3ApiController) DeleteBucketTagging(ctx *fiber.Ctx) (*Response, error)
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Action: auth.PutBucketTaggingAction,
Actions: []auth.Action{auth.PutBucketTaggingAction},
IsPublicRequest: IsBucketPublic,
DisableACL: c.disableACL,
})
@@ -72,7 +72,7 @@ func (c S3ApiController) DeleteBucketOwnershipControls(ctx *fiber.Ctx) (*Respons
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Action: auth.PutBucketOwnershipControlsAction,
Actions: []auth.Action{auth.PutBucketOwnershipControlsAction},
DisableACL: c.disableACL,
})
if err != nil {
@@ -106,7 +106,7 @@ func (c S3ApiController) DeleteBucketPolicy(ctx *fiber.Ctx) (*Response, error) {
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Action: auth.DeleteBucketPolicyAction,
Actions: []auth.Action{auth.DeleteBucketPolicyAction},
DisableACL: c.disableACL,
})
if err != nil {
@@ -141,7 +141,7 @@ func (c S3ApiController) DeleteBucketCors(ctx *fiber.Ctx) (*Response, error) {
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Action: auth.PutBucketCorsAction,
Actions: []auth.Action{auth.PutBucketCorsAction},
IsPublicRequest: IsBucketPublic,
DisableACL: c.disableACL,
})
@@ -177,7 +177,7 @@ func (c S3ApiController) DeleteBucket(ctx *fiber.Ctx) (*Response, error) {
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Action: auth.DeleteBucketAction,
Actions: []auth.Action{auth.DeleteBucketAction},
IsPublicRequest: IsBucketPublic,
DisableACL: c.disableACL,
})

View File

@@ -39,7 +39,7 @@ func (c S3ApiController) GetBucketTagging(ctx *fiber.Ctx) (*Response, error) {
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Action: auth.GetBucketTaggingAction,
Actions: []auth.Action{auth.GetBucketTaggingAction},
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
@@ -92,7 +92,7 @@ func (c S3ApiController) GetBucketOwnershipControls(ctx *fiber.Ctx) (*Response,
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Action: auth.GetBucketOwnershipControlsAction,
Actions: []auth.Action{auth.GetBucketOwnershipControlsAction},
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
@@ -133,7 +133,7 @@ func (c S3ApiController) GetBucketVersioning(ctx *fiber.Ctx) (*Response, error)
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Action: auth.GetBucketVersioningAction,
Actions: []auth.Action{auth.GetBucketVersioningAction},
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
@@ -176,7 +176,7 @@ func (c S3ApiController) GetBucketCors(ctx *fiber.Ctx) (*Response, error) {
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Action: auth.GetBucketCorsAction,
Actions: []auth.Action{auth.GetBucketCorsAction},
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
@@ -220,7 +220,7 @@ func (c S3ApiController) GetBucketPolicy(ctx *fiber.Ctx) (*Response, error) {
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Action: auth.GetBucketPolicyAction,
Actions: []auth.Action{auth.GetBucketPolicyAction},
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
@@ -255,7 +255,7 @@ func (c S3ApiController) GetBucketPolicyStatus(ctx *fiber.Ctx) (*Response, error
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Action: auth.GetBucketPolicyStatusAction,
Actions: []auth.Action{auth.GetBucketPolicyStatusAction},
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
@@ -317,7 +317,7 @@ func (c S3ApiController) ListObjectVersions(ctx *fiber.Ctx) (*Response, error) {
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Action: auth.ListBucketVersionsAction,
Actions: []auth.Action{auth.ListBucketVersionsAction},
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
@@ -371,7 +371,7 @@ func (c S3ApiController) GetObjectLockConfiguration(ctx *fiber.Ctx) (*Response,
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Action: auth.GetBucketObjectLockConfigurationAction,
Actions: []auth.Action{auth.GetBucketObjectLockConfigurationAction},
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
@@ -417,7 +417,7 @@ func (c S3ApiController) GetBucketAcl(ctx *fiber.Ctx) (*Response, error) {
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Action: auth.GetBucketAclAction,
Actions: []auth.Action{auth.GetBucketAclAction},
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
@@ -469,7 +469,7 @@ func (c S3ApiController) ListMultipartUploads(ctx *fiber.Ctx) (*Response, error)
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Action: auth.ListBucketMultipartUploadsAction,
Actions: []auth.Action{auth.ListBucketMultipartUploadsAction},
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
@@ -531,7 +531,7 @@ func (c S3ApiController) ListObjectsV2(ctx *fiber.Ctx) (*Response, error) {
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Action: auth.ListBucketAction,
Actions: []auth.Action{auth.ListBucketAction},
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
@@ -604,7 +604,7 @@ func (c S3ApiController) ListObjects(ctx *fiber.Ctx) (*Response, error) {
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Action: auth.ListBucketAction,
Actions: []auth.Action{auth.ListBucketAction},
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
@@ -667,7 +667,7 @@ func (c S3ApiController) GetBucketLocation(ctx *fiber.Ctx) (*Response, error) {
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Action: auth.GetBucketLocationAction,
Actions: []auth.Action{auth.GetBucketLocationAction},
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})

View File

@@ -40,7 +40,7 @@ func (c S3ApiController) HeadBucket(ctx *fiber.Ctx) (*Response, error) {
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Action: auth.ListBucketAction,
Actions: []auth.Action{auth.ListBucketAction},
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})

View File

@@ -48,7 +48,7 @@ func (c S3ApiController) DeleteObjects(ctx *fiber.Ctx) (*Response, error) {
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Action: auth.DeleteObjectAction,
Actions: []auth.Action{auth.DeleteObjectAction},
IsPublicRequest: IsBucketPublic,
DisableACL: c.disableACL,
})
@@ -130,7 +130,7 @@ func (c S3ApiController) POSTObject(ctx *fiber.Ctx) (*Response, error) {
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Action: auth.PutObjectAction,
Actions: []auth.Action{auth.PutObjectAction},
IsPublicRequest: IsBucketPublic,
DisableACL: c.disableACL,
})

View File

@@ -45,7 +45,7 @@ func (c S3ApiController) PutBucketTagging(ctx *fiber.Ctx) (*Response, error) {
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Action: auth.PutBucketTaggingAction,
Actions: []auth.Action{auth.PutBucketTaggingAction},
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
@@ -88,7 +88,7 @@ func (c S3ApiController) PutBucketOwnershipControls(ctx *fiber.Ctx) (*Response,
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Action: auth.PutBucketOwnershipControlsAction,
Actions: []auth.Action{auth.PutBucketOwnershipControlsAction},
DisableACL: c.disableACL,
}); err != nil {
return &Response{
@@ -143,7 +143,7 @@ func (c S3ApiController) PutBucketVersioning(ctx *fiber.Ctx) (*Response, error)
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Action: auth.PutBucketVersioningAction,
Actions: []auth.Action{auth.PutBucketVersioningAction},
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
@@ -198,7 +198,7 @@ func (c S3ApiController) PutObjectLockConfiguration(ctx *fiber.Ctx) (*Response,
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Action: auth.PutBucketObjectLockConfigurationAction,
Actions: []auth.Action{auth.PutBucketObjectLockConfigurationAction},
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
}); err != nil {
@@ -240,7 +240,7 @@ func (c S3ApiController) PutBucketCors(ctx *fiber.Ctx) (*Response, error) {
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Action: auth.PutBucketCorsAction,
Actions: []auth.Action{auth.PutBucketCorsAction},
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
@@ -296,7 +296,7 @@ func (c S3ApiController) PutBucketPolicy(ctx *fiber.Ctx) (*Response, error) {
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Action: auth.PutBucketPolicyAction,
Actions: []auth.Action{auth.PutBucketPolicyAction},
DisableACL: c.disableACL,
})
if err != nil {
@@ -349,7 +349,7 @@ func (c S3ApiController) PutBucketAcl(ctx *fiber.Ctx) (*Response, error) {
IsRoot: isRoot,
Acc: acct,
Bucket: bucket,
Action: auth.PutBucketAclAction,
Actions: []auth.Action{auth.PutBucketAclAction},
DisableACL: c.disableACL,
})
if err != nil {

View File

@@ -50,7 +50,7 @@ func (c S3ApiController) DeleteObjectTagging(ctx *fiber.Ctx) (*Response, error)
Acc: acct,
Bucket: bucket,
Object: key,
Action: action,
Actions: []auth.Action{action},
IsPublicRequest: isBucketPublic,
DisableACL: c.disableACL,
})
@@ -94,7 +94,7 @@ func (c S3ApiController) AbortMultipartUpload(ctx *fiber.Ctx) (*Response, error)
Acc: acct,
Bucket: bucket,
Object: key,
Action: auth.AbortMultipartUploadAction,
Actions: []auth.Action{auth.AbortMultipartUploadAction},
IsPublicRequest: isBucketPublic,
DisableACL: c.disableACL,
})
@@ -149,7 +149,7 @@ func (c S3ApiController) DeleteObject(ctx *fiber.Ctx) (*Response, error) {
Acc: acct,
Bucket: bucket,
Object: key,
Action: action,
Actions: []auth.Action{action},
IsPublicRequest: isBucketPublic,
DisableACL: c.disableACL,
})

View File

@@ -53,7 +53,7 @@ func (c S3ApiController) GetObjectTagging(ctx *fiber.Ctx) (*Response, error) {
Acc: acct,
Bucket: bucket,
Object: key,
Action: action,
Actions: []auth.Action{action},
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
@@ -111,7 +111,7 @@ func (c S3ApiController) GetObjectRetention(ctx *fiber.Ctx) (*Response, error) {
Acc: acct,
Bucket: bucket,
Object: key,
Action: auth.GetObjectRetentionAction,
Actions: []auth.Action{auth.GetObjectRetentionAction},
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
@@ -159,7 +159,7 @@ func (c S3ApiController) GetObjectLegalHold(ctx *fiber.Ctx) (*Response, error) {
Acc: acct,
Bucket: bucket,
Object: key,
Action: auth.GetObjectLegalHoldAction,
Actions: []auth.Action{auth.GetObjectLegalHoldAction},
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
@@ -197,7 +197,7 @@ func (c S3ApiController) GetObjectAcl(ctx *fiber.Ctx) (*Response, error) {
Acc: acct,
Bucket: bucket,
Object: key,
Action: auth.GetObjectAclAction,
Actions: []auth.Action{auth.GetObjectAclAction},
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
@@ -240,7 +240,7 @@ func (c S3ApiController) ListParts(ctx *fiber.Ctx) (*Response, error) {
Acc: acct,
Bucket: bucket,
Object: key,
Action: auth.ListMultipartUploadPartsAction,
Actions: []auth.Action{auth.ListMultipartUploadPartsAction},
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
@@ -312,7 +312,7 @@ func (c S3ApiController) GetObjectAttributes(ctx *fiber.Ctx) (*Response, error)
Acc: acct,
Bucket: bucket,
Object: key,
Action: action,
Actions: []auth.Action{action},
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})
@@ -437,7 +437,7 @@ func (c S3ApiController) GetObject(ctx *fiber.Ctx) (*Response, error) {
Acc: acct,
Bucket: bucket,
Object: key,
Action: action,
Actions: []auth.Action{action},
IsPublicRequest: isPublicBucketRequest,
DisableACL: c.disableACL,
})

View File

@@ -85,7 +85,7 @@ func (c S3ApiController) HeadObject(ctx *fiber.Ctx) (*Response, error) {
Acc: acct,
Bucket: bucket,
Object: key,
Action: action,
Actions: []auth.Action{action},
IsPublicRequest: isPublicBucket,
DisableACL: c.disableACL,
})

View File

@@ -48,7 +48,7 @@ func (c S3ApiController) RestoreObject(ctx *fiber.Ctx) (*Response, error) {
Acc: acct,
Bucket: bucket,
Object: key,
Action: auth.RestoreObjectAction,
Actions: []auth.Action{auth.RestoreObjectAction},
IsPublicRequest: isBucketPublic,
DisableACL: c.disableACL,
})
@@ -100,7 +100,7 @@ func (c S3ApiController) SelectObjectContent(ctx *fiber.Ctx) (*Response, error)
Acc: acct,
Bucket: bucket,
Object: key,
Action: auth.GetObjectAction,
Actions: []auth.Action{auth.GetObjectAction},
IsPublicRequest: isBucketPublic,
DisableACL: c.disableACL,
})
@@ -154,11 +154,26 @@ func (c S3ApiController) CreateMultipartUpload(ctx *fiber.Ctx) (*Response, error
contentEncoding := ctx.Get("Content-Encoding")
tagging := ctx.Get("X-Amz-Tagging")
expires := ctx.Get("Expires")
legalHoldHdr := ctx.Get("X-Amz-Object-Lock-Legal-Hold")
lockModeHdr := ctx.Get("X-Amz-Object-Lock-Mode")
objLockDate := ctx.Get("X-Amz-Object-Lock-Retain-Until-Date")
// context locals
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
actions := []auth.Action{auth.PutObjectAction}
if tagging != "" {
actions = append(actions, auth.PutObjectTaggingAction)
}
if legalHoldHdr != "" {
actions = append(actions, auth.PutObjectLegalHoldAction)
}
if lockModeHdr != "" || objLockDate != "" {
actions = append(actions, auth.PutObjectRetentionAction)
}
err := auth.VerifyAccess(ctx.Context(), c.be,
auth.AccessOptions{
Readonly: c.readonly,
@@ -168,7 +183,7 @@ func (c S3ApiController) CreateMultipartUpload(ctx *fiber.Ctx) (*Response, error
Acc: acct,
Bucket: bucket,
Object: key,
Action: auth.PutObjectAction,
Actions: actions,
DisableACL: c.disableACL,
})
if err != nil {
@@ -261,7 +276,7 @@ func (c S3ApiController) CompleteMultipartUpload(ctx *fiber.Ctx) (*Response, err
Acc: acct,
Bucket: bucket,
Object: key,
Action: auth.PutObjectAction,
Actions: []auth.Action{auth.PutObjectAction},
IsPublicRequest: isBucketPublic,
DisableACL: c.disableACL,
})

View File

@@ -55,7 +55,7 @@ func (c S3ApiController) PutObjectTagging(ctx *fiber.Ctx) (*Response, error) {
Acc: acct,
Bucket: bucket,
Object: key,
Action: action,
Actions: []auth.Action{action},
IsPublicRequest: IsBucketPublic,
DisableACL: c.disableACL,
})
@@ -106,7 +106,7 @@ func (c S3ApiController) PutObjectRetention(ctx *fiber.Ctx) (*Response, error) {
Acc: acct,
Bucket: bucket,
Object: key,
Action: auth.PutObjectRetentionAction,
Actions: []auth.Action{auth.PutObjectRetentionAction},
IsPublicRequest: IsBucketPublic,
DisableACL: c.disableACL,
})
@@ -173,7 +173,7 @@ func (c S3ApiController) PutObjectLegalHold(ctx *fiber.Ctx) (*Response, error) {
Acc: acct,
Bucket: bucket,
Object: key,
Action: auth.PutObjectLegalHoldAction,
Actions: []auth.Action{auth.PutObjectLegalHoldAction},
IsPublicRequest: IsBucketPublic,
DisableACL: c.disableACL,
})
@@ -243,7 +243,7 @@ func (c S3ApiController) UploadPart(ctx *fiber.Ctx) (*Response, error) {
Acc: acct,
Bucket: bucket,
Object: key,
Action: auth.PutObjectAction,
Actions: []auth.Action{auth.PutObjectAction},
IsPublicRequest: IsBucketPublic,
DisableACL: c.disableACL,
})
@@ -359,7 +359,7 @@ func (c S3ApiController) UploadPartCopy(ctx *fiber.Ctx) (*Response, error) {
Acc: acct,
Bucket: bucket,
Object: key,
Action: auth.PutObjectAction,
Actions: []auth.Action{auth.PutObjectAction},
IsPublicRequest: IsBucketPublic,
DisableACL: c.disableACL,
})
@@ -443,7 +443,7 @@ func (c S3ApiController) PutObjectAcl(ctx *fiber.Ctx) (*Response, error) {
Acc: acct,
Bucket: bucket,
Object: key,
Action: auth.PutObjectAclAction,
Actions: []auth.Action{auth.PutObjectAclAction},
})
if err != nil {
return &Response{
@@ -486,6 +486,9 @@ func (c S3ApiController) CopyObject(ctx *fiber.Ctx) (*Response, error) {
expires := ctx.Get("Expires")
tagging := ctx.Get("x-amz-tagging")
storageClass := ctx.Get("X-Amz-Storage-Class")
legalHoldHdr := ctx.Get("X-Amz-Object-Lock-Legal-Hold")
lockModeHdr := ctx.Get("X-Amz-Object-Lock-Mode")
objLockDate := ctx.Get("X-Amz-Object-Lock-Retain-Until-Date")
// context locals
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
@@ -500,6 +503,17 @@ func (c S3ApiController) CopyObject(ctx *fiber.Ctx) (*Response, error) {
}, err
}
actions := []auth.Action{auth.PutObjectAction}
if tagging != "" {
actions = append(actions, auth.PutObjectTaggingAction)
}
if legalHoldHdr != "" {
actions = append(actions, auth.PutObjectLegalHoldAction)
}
if lockModeHdr != "" || objLockDate != "" {
actions = append(actions, auth.PutObjectRetentionAction)
}
err = auth.VerifyObjectCopyAccess(ctx.Context(), c.be, copySource,
auth.AccessOptions{
Acl: parsedAcl,
@@ -508,7 +522,7 @@ func (c S3ApiController) CopyObject(ctx *fiber.Ctx) (*Response, error) {
Acc: acct,
Bucket: bucket,
Object: key,
Action: auth.PutObjectAction,
Actions: actions,
})
if err != nil {
return &Response{
@@ -642,6 +656,9 @@ func (c S3ApiController) PutObject(ctx *fiber.Ctx) (*Response, error) {
cacheControl := ctx.Get("Cache-Control")
expires := ctx.Get("Expires")
tagging := ctx.Get("x-amz-tagging")
legalHoldHdr := ctx.Get("X-Amz-Object-Lock-Legal-Hold")
lockModeHdr := ctx.Get("X-Amz-Object-Lock-Mode")
objLockDate := ctx.Get("X-Amz-Object-Lock-Retain-Until-Date")
// context locals
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
@@ -660,6 +677,17 @@ func (c S3ApiController) PutObject(ctx *fiber.Ctx) (*Response, error) {
contentLengthStr = decodedLength
}
actions := []auth.Action{auth.PutObjectAction}
if tagging != "" {
actions = append(actions, auth.PutObjectTaggingAction)
}
if legalHoldHdr != "" {
actions = append(actions, auth.PutObjectLegalHoldAction)
}
if lockModeHdr != "" || objLockDate != "" {
actions = append(actions, auth.PutObjectRetentionAction)
}
err := auth.VerifyAccess(ctx.Context(), c.be,
auth.AccessOptions{
Readonly: c.readonly,
@@ -669,7 +697,7 @@ func (c S3ApiController) PutObject(ctx *fiber.Ctx) (*Response, error) {
Acc: acct,
Bucket: bucket,
Object: key,
Action: auth.PutObjectAction,
Actions: actions,
IsPublicRequest: IsBucketPublic,
DisableACL: c.disableACL,
})

View File

@@ -17,6 +17,7 @@ package integration
import (
"context"
"fmt"
"time"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
@@ -453,3 +454,557 @@ func AccessControl_copy_object_with_starting_slash_for_user(s *S3Conf) error {
return nil
})
}
func AccessControl_PutObject_with_tagging_policy(s *S3Conf) error {
testName := "AccessControl_PutObject_with_tagging_policy"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-obj"
testuser := getUser("user")
if err := createUsers(s, []user{testuser}); err != nil {
return err
}
objectResource := fmt.Sprintf(`"arn:aws:s3:::%s/*"`, bucket)
// Error path: user has s3:PutObject but not s3:PutObjectTagging
policy := genPolicyDoc("Allow", fmt.Sprintf(`"%s"`, testuser.access), `"s3:PutObject"`, objectResource)
if err := putBucketPolicy(s3client, bucket, policy); err != nil {
return err
}
userClient := s.getUserClient(testuser)
tagging := "key=value"
_, err := putObjectWithData(0, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
Tagging: &tagging,
}, userClient)
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied)); err != nil {
return err
}
// Happy path: user has s3:PutObject and s3:PutObjectTagging
policy = genPolicyDoc("Allow", fmt.Sprintf(`"%s"`, testuser.access), `["s3:PutObject","s3:PutObjectTagging"]`, objectResource)
if err := putBucketPolicy(s3client, bucket, policy); err != nil {
return err
}
_, err = putObjectWithData(0, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
Tagging: &tagging,
}, userClient)
return err
})
}
func AccessControl_PutObject_with_legal_hold_policy(s *S3Conf) error {
testName := "AccessControl_PutObject_with_legal_hold_policy"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-obj"
testuser := getUser("user")
if err := createUsers(s, []user{testuser}); err != nil {
return err
}
objectResource := fmt.Sprintf(`"arn:aws:s3:::%s/*"`, bucket)
// Error path: user has s3:PutObject but not s3:PutObjectLegalHold
policy := genPolicyDoc("Allow", fmt.Sprintf(`"%s"`, testuser.access), `"s3:PutObject"`, objectResource)
if err := putBucketPolicy(s3client, bucket, policy); err != nil {
return err
}
userClient := s.getUserClient(testuser)
_, err := putObjectWithData(0, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
ObjectLockLegalHoldStatus: types.ObjectLockLegalHoldStatusOn,
}, userClient)
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied)); err != nil {
return err
}
// Happy path: user has s3:PutObject and s3:PutObjectLegalHold
policy = genPolicyDoc("Allow", fmt.Sprintf(`"%s"`, testuser.access), `["s3:PutObject","s3:PutObjectLegalHold"]`, objectResource)
if err := putBucketPolicy(s3client, bucket, policy); err != nil {
return err
}
_, err = putObjectWithData(0, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
ObjectLockLegalHoldStatus: types.ObjectLockLegalHoldStatusOn,
}, userClient)
if err != nil {
return err
}
return cleanupLockedObjects(s3client, bucket, []objToDelete{{key: obj, removeLegalHold: true}})
}, withLock())
}
func AccessControl_PutObject_with_retention_policy(s *S3Conf) error {
testName := "AccessControl_PutObject_with_retention_policy"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-obj"
testuser := getUser("user")
if err := createUsers(s, []user{testuser}); err != nil {
return err
}
objectResource := fmt.Sprintf(`"arn:aws:s3:::%s/*"`, bucket)
date := time.Now().Add(time.Hour)
// Error path: user has s3:PutObject but not s3:PutObjectRetention
policy := genPolicyDoc("Allow", fmt.Sprintf(`"%s"`, testuser.access), `"s3:PutObject"`, objectResource)
if err := putBucketPolicy(s3client, bucket, policy); err != nil {
return err
}
userClient := s.getUserClient(testuser)
_, err := putObjectWithData(0, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
ObjectLockMode: types.ObjectLockModeGovernance,
ObjectLockRetainUntilDate: &date,
}, userClient)
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied)); err != nil {
return err
}
// Happy path: user has s3:PutObject and s3:PutObjectRetention
policy = genPolicyDoc("Allow", fmt.Sprintf(`"%s"`, testuser.access), `["s3:PutObject","s3:PutObjectRetention"]`, objectResource)
if err := putBucketPolicy(s3client, bucket, policy); err != nil {
return err
}
_, err = putObjectWithData(0, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
ObjectLockMode: types.ObjectLockModeGovernance,
ObjectLockRetainUntilDate: &date,
}, userClient)
if err != nil {
return err
}
return cleanupLockedObjects(s3client, bucket, []objToDelete{{key: obj}})
}, withLock())
}
func AccessControl_CreateMultipartUpload_with_tagging_policy(s *S3Conf) error {
testName := "AccessControl_CreateMultipartUpload_with_tagging_policy"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-obj"
testuser := getUser("user")
if err := createUsers(s, []user{testuser}); err != nil {
return err
}
objectResource := fmt.Sprintf(`"arn:aws:s3:::%s/*"`, bucket)
// Error path: user has s3:PutObject but not s3:PutObjectTagging
policy := genPolicyDoc("Allow", fmt.Sprintf(`"%s"`, testuser.access), `"s3:PutObject"`, objectResource)
if err := putBucketPolicy(s3client, bucket, policy); err != nil {
return err
}
userClient := s.getUserClient(testuser)
tagging := "key=value"
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := userClient.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{
Bucket: &bucket,
Key: &obj,
Tagging: &tagging,
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied)); err != nil {
return err
}
// Happy path: user has s3:PutObject and s3:PutObjectTagging
policy = genPolicyDoc("Allow", fmt.Sprintf(`"%s"`, testuser.access), `["s3:PutObject","s3:PutObjectTagging"]`, objectResource)
if err := putBucketPolicy(s3client, bucket, policy); err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = userClient.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{
Bucket: &bucket,
Key: &obj,
Tagging: &tagging,
})
cancel()
return err
})
}
func AccessControl_CreateMultipartUpload_with_legal_hold_policy(s *S3Conf) error {
testName := "AccessControl_CreateMultipartUpload_with_legal_hold_policy"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-obj"
testuser := getUser("user")
if err := createUsers(s, []user{testuser}); err != nil {
return err
}
objectResource := fmt.Sprintf(`"arn:aws:s3:::%s/*"`, bucket)
// Error path: user has s3:PutObject but not s3:PutObjectLegalHold
policy := genPolicyDoc("Allow", fmt.Sprintf(`"%s"`, testuser.access), `"s3:PutObject"`, objectResource)
if err := putBucketPolicy(s3client, bucket, policy); err != nil {
return err
}
userClient := s.getUserClient(testuser)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := userClient.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{
Bucket: &bucket,
Key: &obj,
ObjectLockLegalHoldStatus: types.ObjectLockLegalHoldStatusOn,
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied)); err != nil {
return err
}
// Happy path: user has s3:PutObject and s3:PutObjectLegalHold
policy = genPolicyDoc("Allow", fmt.Sprintf(`"%s"`, testuser.access), `["s3:PutObject","s3:PutObjectLegalHold"]`, objectResource)
if err := putBucketPolicy(s3client, bucket, policy); err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = userClient.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{
Bucket: &bucket,
Key: &obj,
ObjectLockLegalHoldStatus: types.ObjectLockLegalHoldStatusOn,
})
cancel()
return err
}, withLock())
}
func AccessControl_CreateMultipartUpload_with_retention_policy(s *S3Conf) error {
testName := "AccessControl_CreateMultipartUpload_with_retention_policy"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-obj"
testuser := getUser("user")
if err := createUsers(s, []user{testuser}); err != nil {
return err
}
objectResource := fmt.Sprintf(`"arn:aws:s3:::%s/*"`, bucket)
date := time.Now().Add(time.Hour)
// Error path: user has s3:PutObject but not s3:PutObjectRetention
policy := genPolicyDoc("Allow", fmt.Sprintf(`"%s"`, testuser.access), `"s3:PutObject"`, objectResource)
if err := putBucketPolicy(s3client, bucket, policy); err != nil {
return err
}
userClient := s.getUserClient(testuser)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := userClient.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{
Bucket: &bucket,
Key: &obj,
ObjectLockMode: types.ObjectLockModeGovernance,
ObjectLockRetainUntilDate: &date,
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied)); err != nil {
return err
}
// Happy path: user has s3:PutObject and s3:PutObjectRetention
policy = genPolicyDoc("Allow", fmt.Sprintf(`"%s"`, testuser.access), `["s3:PutObject","s3:PutObjectRetention"]`, objectResource)
if err := putBucketPolicy(s3client, bucket, policy); err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = userClient.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{
Bucket: &bucket,
Key: &obj,
ObjectLockMode: types.ObjectLockModeGovernance,
ObjectLockRetainUntilDate: &date,
})
cancel()
return err
}, withLock())
}
func AccessControl_CopyObject_with_tagging_policy(s *S3Conf) error {
testName := "AccessControl_CopyObject_with_tagging_policy"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
testuser := getUser("user")
if err := createUsers(s, []user{testuser}); err != nil {
return err
}
srcObj, dstObj := "source-object", "dst-object"
_, err := putObjectWithData(0, &s3.PutObjectInput{
Bucket: &bucket,
Key: &srcObj,
}, s3client)
if err != nil {
return err
}
dstObjectResource := fmt.Sprintf(`"arn:aws:s3:::%s/%s"`, bucket, dstObj)
srcObjectResource := fmt.Sprintf(`"arn:aws:s3:::%s/%s"`, bucket, srcObj)
// Error path: user has s3:PutObject, s3:GetObject but not s3:PutObjectTagging
policy := fmt.Sprintf(`{
"Statement": [
{
"Effect": "Allow",
"Principal": "%s",
"Action": "s3:GetObject",
"Resource": %s
},
{
"Effect": "Allow",
"Principal": "%s",
"Action": "s3:PutObject",
"Resource": %s
}
]
}`, testuser.access, srcObjectResource, testuser.access, dstObjectResource)
if err := putBucketPolicy(s3client, bucket, policy); err != nil {
return err
}
userClient := s.getUserClient(testuser)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = userClient.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &bucket,
Key: &dstObj,
CopySource: getPtr(fmt.Sprintf("%s/%s", bucket, srcObj)),
Tagging: getPtr("key=value"),
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied)); err != nil {
return err
}
// Happy path: user has s3:GetObject, s3:PutObject and s3:PutObjectTagging
policy = fmt.Sprintf(`{
"Statement": [
{
"Effect": "Allow",
"Principal": "%s",
"Action": "s3:GetObject",
"Resource": %s
},
{
"Effect": "Allow",
"Principal": "%s",
"Action": ["s3:PutObject","s3:PutObjectTagging"],
"Resource": %s
}
]
}`, testuser.access, srcObjectResource, testuser.access, dstObjectResource)
if err := putBucketPolicy(s3client, bucket, policy); err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = userClient.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &bucket,
Key: &dstObj,
CopySource: getPtr(fmt.Sprintf("%s/%s", bucket, srcObj)),
Tagging: getPtr("key=value"),
})
cancel()
return err
})
}
func AccessControl_CopyObject_with_legal_hold_policy(s *S3Conf) error {
testName := "AccessControl_CopyObject_with_legal_hold_policy"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
testuser := getUser("user")
if err := createUsers(s, []user{testuser}); err != nil {
return err
}
srcObj, dstObj := "source-object", "dst-object"
_, err := putObjectWithData(0, &s3.PutObjectInput{
Bucket: &bucket,
Key: &srcObj,
}, s3client)
if err != nil {
return err
}
dstObjectResource := fmt.Sprintf(`"arn:aws:s3:::%s/%s"`, bucket, dstObj)
srcObjectResource := fmt.Sprintf(`"arn:aws:s3:::%s/%s"`, bucket, srcObj)
// Error path: user has s3:PutObject, s3:GetObject but not s3:PutObjectLegalHold
policy := fmt.Sprintf(`{
"Statement": [
{
"Effect": "Allow",
"Principal": "%s",
"Action": "s3:GetObject",
"Resource": %s
},
{
"Effect": "Allow",
"Principal": "%s",
"Action": "s3:PutObject",
"Resource": %s
}
]
}`, testuser.access, srcObjectResource, testuser.access, dstObjectResource)
if err := putBucketPolicy(s3client, bucket, policy); err != nil {
return err
}
userClient := s.getUserClient(testuser)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = userClient.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &bucket,
Key: &dstObj,
CopySource: getPtr(fmt.Sprintf("%s/%s", bucket, srcObj)),
ObjectLockLegalHoldStatus: types.ObjectLockLegalHoldStatusOn,
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied)); err != nil {
return err
}
// Happy path: user has s3:GetObject, s3:PutObject and s3:PutObjectLegalHold
policy = fmt.Sprintf(`{
"Statement": [
{
"Effect": "Allow",
"Principal": "%s",
"Action": "s3:GetObject",
"Resource": %s
},
{
"Effect": "Allow",
"Principal": "%s",
"Action": ["s3:PutObject","s3:PutObjectLegalHold"],
"Resource": %s
}
]
}`, testuser.access, srcObjectResource, testuser.access, dstObjectResource)
if err := putBucketPolicy(s3client, bucket, policy); err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = userClient.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &bucket,
Key: &dstObj,
CopySource: getPtr(fmt.Sprintf("%s/%s", bucket, srcObj)),
ObjectLockLegalHoldStatus: types.ObjectLockLegalHoldStatusOn,
})
cancel()
if err != nil {
return err
}
return cleanupLockedObjects(s3client, bucket, []objToDelete{{key: dstObj, removeLegalHold: true}})
}, withLock())
}
func AccessControl_CopyObject_with_retention_policy(s *S3Conf) error {
testName := "AccessControl_CopyObject_with_retention_policy"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
testuser := getUser("user")
if err := createUsers(s, []user{testuser}); err != nil {
return err
}
srcObj, dstObj := "source-object", "dst-object"
_, err := putObjectWithData(0, &s3.PutObjectInput{
Bucket: &bucket,
Key: &srcObj,
}, s3client)
if err != nil {
return err
}
dstObjectResource := fmt.Sprintf(`"arn:aws:s3:::%s/%s"`, bucket, dstObj)
srcObjectResource := fmt.Sprintf(`"arn:aws:s3:::%s/%s"`, bucket, srcObj)
// Error path: user has s3:PutObject, s3:GetObject but not s3:PutObjectRetention
policy := fmt.Sprintf(`{
"Statement": [
{
"Effect": "Allow",
"Principal": "%s",
"Action": "s3:GetObject",
"Resource": %s
},
{
"Effect": "Allow",
"Principal": "%s",
"Action": "s3:PutObject",
"Resource": %s
}
]
}`, testuser.access, srcObjectResource, testuser.access, dstObjectResource)
if err := putBucketPolicy(s3client, bucket, policy); err != nil {
return err
}
userClient := s.getUserClient(testuser)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = userClient.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &bucket,
Key: &dstObj,
CopySource: getPtr(fmt.Sprintf("%s/%s", bucket, srcObj)),
ObjectLockMode: types.ObjectLockModeGovernance,
ObjectLockRetainUntilDate: getPtr(time.Now().AddDate(1, 0, 0)),
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied)); err != nil {
return err
}
// Happy path: user has s3:GetObject, s3:PutObject and s3:PutObjectRetention
policy = fmt.Sprintf(`{
"Statement": [
{
"Effect": "Allow",
"Principal": "%s",
"Action": "s3:GetObject",
"Resource": %s
},
{
"Effect": "Allow",
"Principal": "%s",
"Action": ["s3:PutObject","s3:PutObjectRetention"],
"Resource": %s
}
]
}`, testuser.access, srcObjectResource, testuser.access, dstObjectResource)
if err := putBucketPolicy(s3client, bucket, policy); err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = userClient.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &bucket,
Key: &dstObj,
CopySource: getPtr(fmt.Sprintf("%s/%s", bucket, srcObj)),
ObjectLockMode: types.ObjectLockModeGovernance,
ObjectLockRetainUntilDate: getPtr(time.Now().AddDate(1, 0, 0)),
})
cancel()
if err != nil {
return err
}
return cleanupLockedObjects(s3client, bucket, []objToDelete{{key: dstObj}})
}, withLock())
}

View File

@@ -1021,6 +1021,15 @@ func TestAccessControl(ts *TestState) {
ts.Run(AccessControl_root_PutBucketAcl)
ts.Run(AccessControl_user_PutBucketAcl_with_policy_access)
ts.Run(AccessControl_copy_object_with_starting_slash_for_user)
ts.Run(AccessControl_PutObject_with_tagging_policy)
ts.Run(AccessControl_PutObject_with_legal_hold_policy)
ts.Run(AccessControl_PutObject_with_retention_policy)
ts.Run(AccessControl_CreateMultipartUpload_with_tagging_policy)
ts.Run(AccessControl_CreateMultipartUpload_with_legal_hold_policy)
ts.Run(AccessControl_CreateMultipartUpload_with_retention_policy)
ts.Run(AccessControl_CopyObject_with_tagging_policy)
ts.Run(AccessControl_CopyObject_with_legal_hold_policy)
ts.Run(AccessControl_CopyObject_with_retention_policy)
}
func TestPublicBuckets(ts *TestState) {
@@ -1861,6 +1870,15 @@ func GetIntTests() IntTests {
"AccessControl_root_PutBucketAcl": AccessControl_root_PutBucketAcl,
"AccessControl_user_PutBucketAcl_with_policy_access": AccessControl_user_PutBucketAcl_with_policy_access,
"AccessControl_copy_object_with_starting_slash_for_user": AccessControl_copy_object_with_starting_slash_for_user,
"AccessControl_PutObject_with_tagging_policy": AccessControl_PutObject_with_tagging_policy,
"AccessControl_PutObject_with_legal_hold_policy": AccessControl_PutObject_with_legal_hold_policy,
"AccessControl_PutObject_with_retention_policy": AccessControl_PutObject_with_retention_policy,
"AccessControl_CreateMultipartUpload_with_tagging_policy": AccessControl_CreateMultipartUpload_with_tagging_policy,
"AccessControl_CreateMultipartUpload_with_legal_hold_policy": AccessControl_CreateMultipartUpload_with_legal_hold_policy,
"AccessControl_CreateMultipartUpload_with_retention_policy": AccessControl_CreateMultipartUpload_with_retention_policy,
"AccessControl_CopyObject_with_tagging_policy": AccessControl_CopyObject_with_tagging_policy,
"AccessControl_CopyObject_with_legal_hold_policy": AccessControl_CopyObject_with_legal_hold_policy,
"AccessControl_CopyObject_with_retention_policy": AccessControl_CopyObject_with_retention_policy,
"PublicBucket_default_private_bucket": PublicBucket_default_private_bucket,
"PublicBucket_public_bucket_policy": PublicBucket_public_bucket_policy,
"PublicBucket_public_object_policy": PublicBucket_public_object_policy,