Merge pull request #2011 from versity/sis/putobject-meta-property-permissions

fix: check PutObjectTagging/LegalHold/Retention permissions on PutObject,CopyObject and CreateMultipartUpload
This commit is contained in:
Ben McClelland
2026-04-27 14:42:41 -07:00
committed by GitHub
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,