From 8cad7fd6d900c201e96189db7ca7700a0b78f3c2 Mon Sep 17 00:00:00 2001 From: Ben McClelland Date: Fri, 29 Aug 2025 17:08:48 -0700 Subject: [PATCH] feat: add response header overrides for GetObject GetObject allows overriding response headers with the following paramters: response-cache-control response-content-disposition response-content-encoding response-content-language response-content-type response-expires This is only valid for signed (and pre-singed) requests. An error is returned for anonymous requests if these are set. More info on the GetObject overrides can be found in the GetObject API reference. This also clarifies the naming of the AccessOptions IsPublicBucket to IsPublicRequest to indicate this is a public access request and not just accessing a bucket that allows public access. Fixes #1501 --- auth/access-control.go | 22 +- s3api/controllers/bucket-delete.go | 48 ++-- s3api/controllers/bucket-get.go | 192 +++++++------- s3api/controllers/bucket-head.go | 16 +- s3api/controllers/bucket-post.go | 16 +- s3api/controllers/bucket-put.go | 64 ++--- s3api/controllers/object-delete.go | 54 ++-- s3api/controllers/object-get.go | 169 +++++++----- s3api/controllers/object-head.go | 18 +- s3api/controllers/object-post.go | 54 ++-- s3api/controllers/object-put.go | 106 ++++---- s3api/middlewares/public-bucket.go | 2 +- s3api/utils/utils.go | 17 ++ s3err/s3err.go | 6 + tests/integration/group-tests.go | 10 +- tests/integration/tests.go | 403 ++++++++++++++++++++++++++++- 16 files changed, 827 insertions(+), 370 deletions(-) diff --git a/auth/access-control.go b/auth/access-control.go index 81cc0e2..283a290 100644 --- a/auth/access-control.go +++ b/auth/access-control.go @@ -70,20 +70,20 @@ func VerifyObjectCopyAccess(ctx context.Context, be backend.Backend, copySource } type AccessOptions struct { - Acl ACL - AclPermission Permission - IsRoot bool - Acc Account - Bucket string - Object string - Action Action - Readonly bool - IsBucketPublic bool + Acl ACL + AclPermission Permission + IsRoot bool + Acc Account + Bucket string + Object string + Action Action + Readonly bool + IsPublicRequest bool } func VerifyAccess(ctx context.Context, be backend.Backend, opts AccessOptions) error { - // Skip the access check for public buckets - if opts.IsBucketPublic { + // Skip the access check for public bucket requests + if opts.IsPublicRequest { return nil } if opts.Readonly { diff --git a/s3api/controllers/bucket-delete.go b/s3api/controllers/bucket-delete.go index 22ef928..7b062b0 100644 --- a/s3api/controllers/bucket-delete.go +++ b/s3api/controllers/bucket-delete.go @@ -31,14 +31,14 @@ func (c S3ApiController) DeleteBucketTagging(ctx *fiber.Ctx) (*Response, error) err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.PutBucketTaggingAction, - IsBucketPublic: IsBucketPublic, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.PutBucketTaggingAction, + IsPublicRequest: IsBucketPublic, }) if err != nil { return &Response{ @@ -132,14 +132,14 @@ func (c S3ApiController) DeleteBucketCors(ctx *fiber.Ctx) (*Response, error) { err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.PutBucketCorsAction, - IsBucketPublic: IsBucketPublic, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.PutBucketCorsAction, + IsPublicRequest: IsBucketPublic, }) if err != nil { return &Response{ @@ -167,14 +167,14 @@ func (c S3ApiController) DeleteBucket(ctx *fiber.Ctx) (*Response, error) { err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.DeleteBucketAction, - IsBucketPublic: IsBucketPublic, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.DeleteBucketAction, + IsPublicRequest: IsBucketPublic, }) if err != nil { return &Response{ diff --git a/s3api/controllers/bucket-get.go b/s3api/controllers/bucket-get.go index c3116c5..2e3c75d 100644 --- a/s3api/controllers/bucket-get.go +++ b/s3api/controllers/bucket-get.go @@ -35,14 +35,14 @@ func (c S3ApiController) GetBucketTagging(ctx *fiber.Ctx) (*Response, error) { parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.GetBucketTaggingAction, - IsBucketPublic: isPublicBucket, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.GetBucketTaggingAction, + IsPublicRequest: isPublicBucket, }) if err != nil { return &Response{ @@ -87,14 +87,14 @@ func (c S3ApiController) GetBucketOwnershipControls(ctx *fiber.Ctx) (*Response, parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.GetBucketOwnershipControlsAction, - IsBucketPublic: isPublicBucket, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.GetBucketOwnershipControlsAction, + IsPublicRequest: isPublicBucket, }) if err != nil { return &Response{ @@ -127,14 +127,14 @@ func (c S3ApiController) GetBucketVersioning(ctx *fiber.Ctx) (*Response, error) parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.GetBucketVersioningAction, - IsBucketPublic: isPublicBucket, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.GetBucketVersioningAction, + IsPublicRequest: isPublicBucket, }) if err != nil { return &Response{ @@ -169,14 +169,14 @@ func (c S3ApiController) GetBucketCors(ctx *fiber.Ctx) (*Response, error) { parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.GetBucketCorsAction, - IsBucketPublic: isPublicBucket, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.GetBucketCorsAction, + IsPublicRequest: isPublicBucket, }) if err != nil { return &Response{ @@ -212,14 +212,14 @@ func (c S3ApiController) GetBucketPolicy(ctx *fiber.Ctx) (*Response, error) { parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.GetBucketPolicyAction, - IsBucketPublic: isPublicBucket, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.GetBucketPolicyAction, + IsPublicRequest: isPublicBucket, }) if err != nil { return &Response{ @@ -246,14 +246,14 @@ func (c S3ApiController) GetBucketPolicyStatus(ctx *fiber.Ctx) (*Response, error parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.GetBucketPolicyStatusAction, - IsBucketPublic: isPublicBucket, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.GetBucketPolicyStatusAction, + IsPublicRequest: isPublicBucket, }) if err != nil { return &Response{ @@ -307,14 +307,14 @@ func (c S3ApiController) ListObjectVersions(ctx *fiber.Ctx) (*Response, error) { parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.ListBucketVersionsAction, - IsBucketPublic: isPublicBucket, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.ListBucketVersionsAction, + IsPublicRequest: isPublicBucket, }) if err != nil { return &Response{ @@ -362,14 +362,14 @@ func (c S3ApiController) GetObjectLockConfiguration(ctx *fiber.Ctx) (*Response, parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.GetBucketObjectLockConfigurationAction, - IsBucketPublic: isPublicBucket, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.GetBucketObjectLockConfigurationAction, + IsPublicRequest: isPublicBucket, }) if err != nil { return &Response{ @@ -407,14 +407,14 @@ func (c S3ApiController) GetBucketAcl(ctx *fiber.Ctx) (*Response, error) { parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionReadAcp, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.GetBucketAclAction, - IsBucketPublic: isPublicBucket, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionReadAcp, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.GetBucketAclAction, + IsPublicRequest: isPublicBucket, }) if err != nil { return &Response{ @@ -458,14 +458,14 @@ func (c S3ApiController) ListMultipartUploads(ctx *fiber.Ctx) (*Response, error) parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.ListBucketMultipartUploadsAction, - IsBucketPublic: isPublicBucket, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.ListBucketMultipartUploadsAction, + IsPublicRequest: isPublicBucket, }) if err != nil { return &Response{ @@ -517,14 +517,14 @@ func (c S3ApiController) ListObjectsV2(ctx *fiber.Ctx) (*Response, error) { parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.ListBucketAction, - IsBucketPublic: isPublicBucket, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.ListBucketAction, + IsPublicRequest: isPublicBucket, }) if err != nil { return &Response{ @@ -576,14 +576,14 @@ func (c S3ApiController) ListObjects(ctx *fiber.Ctx) (*Response, error) { parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.ListBucketAction, - IsBucketPublic: isPublicBucket, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.ListBucketAction, + IsPublicRequest: isPublicBucket, }) if err != nil { return &Response{ diff --git a/s3api/controllers/bucket-head.go b/s3api/controllers/bucket-head.go index f4f063d..9d3d58e 100644 --- a/s3api/controllers/bucket-head.go +++ b/s3api/controllers/bucket-head.go @@ -31,14 +31,14 @@ func (c S3ApiController) HeadBucket(ctx *fiber.Ctx) (*Response, error) { err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.ListBucketAction, - IsBucketPublic: isPublicBucket, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.ListBucketAction, + IsPublicRequest: isPublicBucket, }) if err != nil { return &Response{ diff --git a/s3api/controllers/bucket-post.go b/s3api/controllers/bucket-post.go index 22e5f2f..a78340e 100644 --- a/s3api/controllers/bucket-post.go +++ b/s3api/controllers/bucket-post.go @@ -39,14 +39,14 @@ func (c S3ApiController) DeleteObjects(ctx *fiber.Ctx) (*Response, error) { err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.DeleteObjectAction, - IsBucketPublic: IsBucketPublic, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.DeleteObjectAction, + IsPublicRequest: IsBucketPublic, }) if err != nil { return &Response{ diff --git a/s3api/controllers/bucket-put.go b/s3api/controllers/bucket-put.go index 2290993..d55315e 100644 --- a/s3api/controllers/bucket-put.go +++ b/s3api/controllers/bucket-put.go @@ -40,14 +40,14 @@ func (c S3ApiController) PutBucketTagging(ctx *fiber.Ctx) (*Response, error) { isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx) err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.PutBucketTaggingAction, - IsBucketPublic: isPublicBucket, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.PutBucketTaggingAction, + IsPublicRequest: isPublicBucket, }) if err != nil { return &Response{ @@ -136,14 +136,14 @@ func (c S3ApiController) PutBucketVersioning(ctx *fiber.Ctx) (*Response, error) isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx) err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.PutBucketVersioningAction, - IsBucketPublic: isPublicBucket, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.PutBucketVersioningAction, + IsPublicRequest: isPublicBucket, }) if err != nil { return &Response{ @@ -190,14 +190,14 @@ func (c S3ApiController) PutObjectLockConfiguration(ctx *fiber.Ctx) (*Response, isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx) if err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.PutBucketObjectLockConfigurationAction, - IsBucketPublic: isPublicBucket, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.PutBucketObjectLockConfigurationAction, + IsPublicRequest: isPublicBucket, }); err != nil { return &Response{ MetaOpts: &MetaOptions{ @@ -231,14 +231,14 @@ func (c S3ApiController) PutBucketCors(ctx *fiber.Ctx) (*Response, error) { isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx) err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Action: auth.PutBucketCorsAction, - IsBucketPublic: isPublicBucket, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Action: auth.PutBucketCorsAction, + IsPublicRequest: isPublicBucket, }) if err != nil { return &Response{ diff --git a/s3api/controllers/object-delete.go b/s3api/controllers/object-delete.go index 76dfb16..d625fe6 100644 --- a/s3api/controllers/object-delete.go +++ b/s3api/controllers/object-delete.go @@ -37,15 +37,15 @@ func (c S3ApiController) DeleteObjectTagging(ctx *fiber.Ctx) (*Response, error) err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: key, - Action: auth.DeleteObjectTaggingAction, - IsBucketPublic: isBucketPublic, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.DeleteObjectTaggingAction, + IsPublicRequest: isBucketPublic, }) if err != nil { return &Response{ @@ -76,15 +76,15 @@ func (c S3ApiController) AbortMultipartUpload(ctx *fiber.Ctx) (*Response, error) err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: key, - Action: auth.AbortMultipartUploadAction, - IsBucketPublic: isBucketPublic, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.AbortMultipartUploadAction, + IsPublicRequest: isBucketPublic, }) if err != nil { return &Response{ @@ -123,15 +123,15 @@ func (c S3ApiController) DeleteObject(ctx *fiber.Ctx) (*Response, error) { err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: key, - Action: auth.DeleteObjectAction, - IsBucketPublic: isBucketPublic, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.DeleteObjectAction, + IsPublicRequest: isBucketPublic, }) if err != nil { return &Response{ diff --git a/s3api/controllers/object-get.go b/s3api/controllers/object-get.go index 422d578..e87f29d 100644 --- a/s3api/controllers/object-get.go +++ b/s3api/controllers/object-get.go @@ -41,15 +41,15 @@ func (c S3ApiController) GetObjectTagging(ctx *fiber.Ctx) (*Response, error) { isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx) err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: key, - Action: auth.GetObjectTaggingAction, - IsBucketPublic: isPublicBucket, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.GetObjectTaggingAction, + IsPublicRequest: isPublicBucket, }) if err != nil { return &Response{ @@ -95,15 +95,15 @@ func (c S3ApiController) GetObjectRetention(ctx *fiber.Ctx) (*Response, error) { isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx) err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: key, - Action: auth.GetObjectRetentionAction, - IsBucketPublic: isPublicBucket, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.GetObjectRetentionAction, + IsPublicRequest: isPublicBucket, }) if err != nil { return &Response{ @@ -142,15 +142,15 @@ func (c S3ApiController) GetObjectLegalHold(ctx *fiber.Ctx) (*Response, error) { isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx) err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: key, - Action: auth.GetObjectLegalHoldAction, - IsBucketPublic: isPublicBucket, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.GetObjectLegalHoldAction, + IsPublicRequest: isPublicBucket, }) if err != nil { return &Response{ @@ -179,15 +179,15 @@ func (c S3ApiController) GetObjectAcl(ctx *fiber.Ctx) (*Response, error) { isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx) err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionReadAcp, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: key, - Action: auth.GetObjectAclAction, - IsBucketPublic: isPublicBucket, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionReadAcp, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.GetObjectAclAction, + IsPublicRequest: isPublicBucket, }) if err != nil { return &Response{ @@ -221,15 +221,15 @@ func (c S3ApiController) ListParts(ctx *fiber.Ctx) (*Response, error) { isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx) err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: key, - Action: auth.ListMultipartUploadPartsAction, - IsBucketPublic: isPublicBucket, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.ListMultipartUploadPartsAction, + IsPublicRequest: isPublicBucket, }) if err != nil { return &Response{ @@ -294,15 +294,15 @@ func (c S3ApiController) GetObjectAttributes(ctx *fiber.Ctx) (*Response, error) isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx) err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: key, - Action: auth.GetObjectAttributesAction, - IsBucketPublic: isPublicBucket, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.GetObjectAttributesAction, + IsPublicRequest: isPublicBucket, }) if err != nil { return &Response{ @@ -380,28 +380,57 @@ func (c S3ApiController) GetObject(ctx *fiber.Ctx) (*Response, error) { versionId := ctx.Query("versionId") acceptRange := ctx.Get("Range") checksumMode := types.ChecksumMode(ctx.Get("x-amz-checksum-mode")) + + // Extract response override query parameters + responseOverrides := map[string]*string{ + "Cache-Control": utils.GetQueryParam(ctx, "response-cache-control"), + "Content-Disposition": utils.GetQueryParam(ctx, "response-content-disposition"), + "Content-Encoding": utils.GetQueryParam(ctx, "response-content-encoding"), + "Content-Language": utils.GetQueryParam(ctx, "response-content-language"), + "Content-Type": utils.GetQueryParam(ctx, "response-content-type"), + "Expires": utils.GetQueryParam(ctx, "response-expires"), + } + + // Check if any response override parameters are present + hasResponseOverrides := false + for _, override := range responseOverrides { + if override != nil { + hasResponseOverrides = true + break + } + } + // context locals acct := utils.ContextKeyAccount.Get(ctx).(auth.Account) isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool) parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) - isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx) + isPublicBucketRequest := utils.ContextKeyPublicBucket.IsSet(ctx) utils.ContextKeySkipResBodyLog.Set(ctx, true) + // Validate that response override parameters are not used with anonymous requests + if hasResponseOverrides && isPublicBucketRequest { + return &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: parsedAcl.Owner, + }, + }, s3err.GetAPIError(s3err.ErrAnonymousResponseHeaders) + } + action := auth.GetObjectAction if ctx.Request().URI().QueryArgs().Has("versionId") { action = auth.GetObjectVersionAction } err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: key, - Action: action, - IsBucketPublic: isPublicBucket, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: action, + IsPublicRequest: isPublicBucketRequest, }) if err != nil { return &Response{ @@ -478,17 +507,17 @@ func (c S3ApiController) GetObject(ctx *fiber.Ctx) (*Response, error) { "x-amz-restore": res.Restore, "accept-ranges": res.AcceptRanges, "Content-Range": res.ContentRange, - "Content-Disposition": res.ContentDisposition, - "Content-Encoding": res.ContentEncoding, - "Content-Language": res.ContentLanguage, - "Cache-Control": res.CacheControl, - "Expires": res.ExpiresString, + "Content-Disposition": utils.ApplyOverride(res.ContentDisposition, responseOverrides["Content-Disposition"]), + "Content-Encoding": utils.ApplyOverride(res.ContentEncoding, responseOverrides["Content-Encoding"]), + "Content-Language": utils.ApplyOverride(res.ContentLanguage, responseOverrides["Content-Language"]), + "Cache-Control": utils.ApplyOverride(res.CacheControl, responseOverrides["Cache-Control"]), + "Expires": utils.ApplyOverride(res.ExpiresString, responseOverrides["Expires"]), "x-amz-checksum-crc32": res.ChecksumCRC32, "x-amz-checksum-crc64nvme": res.ChecksumCRC64NVME, "x-amz-checksum-crc32c": res.ChecksumCRC32C, "x-amz-checksum-sha1": res.ChecksumSHA1, "x-amz-checksum-sha256": res.ChecksumSHA256, - "Content-Type": res.ContentType, + "Content-Type": utils.ApplyOverride(res.ContentType, responseOverrides["Content-Type"]), "x-amz-version-id": res.VersionId, "Content-Length": utils.ConvertPtrToStringPtr(res.ContentLength), "x-amz-mp-parts-count": utils.ConvertPtrToStringPtr(res.PartsCount), diff --git a/s3api/controllers/object-head.go b/s3api/controllers/object-head.go index 5599574..1361603 100644 --- a/s3api/controllers/object-head.go +++ b/s3api/controllers/object-head.go @@ -48,15 +48,15 @@ func (c S3ApiController) HeadObject(ctx *fiber.Ctx) (*Response, error) { err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: key, - Action: action, - IsBucketPublic: isPublicBucket, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: action, + IsPublicRequest: isPublicBucket, }) if err != nil { return &Response{ diff --git a/s3api/controllers/object-post.go b/s3api/controllers/object-post.go index 18b9280..0244a9c 100644 --- a/s3api/controllers/object-post.go +++ b/s3api/controllers/object-post.go @@ -41,15 +41,15 @@ func (c S3ApiController) RestoreObject(ctx *fiber.Ctx) (*Response, error) { err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: key, - Action: auth.RestoreObjectAction, - IsBucketPublic: isBucketPublic, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.RestoreObjectAction, + IsPublicRequest: isBucketPublic, }) if err != nil { return &Response{ @@ -92,15 +92,15 @@ func (c S3ApiController) SelectObjectContent(ctx *fiber.Ctx) (*Response, error) err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionRead, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: key, - Action: auth.GetObjectAction, - IsBucketPublic: isBucketPublic, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionRead, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.GetObjectAction, + IsPublicRequest: isBucketPublic, }) if err != nil { return &Response{ @@ -243,15 +243,15 @@ func (c S3ApiController) CompleteMultipartUpload(ctx *fiber.Ctx) (*Response, err err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: key, - Action: auth.PutObjectAction, - IsBucketPublic: isBucketPublic, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.PutObjectAction, + IsPublicRequest: isBucketPublic, }) if err != nil { return &Response{ diff --git a/s3api/controllers/object-put.go b/s3api/controllers/object-put.go index b2f8f9d..d5afd40 100644 --- a/s3api/controllers/object-put.go +++ b/s3api/controllers/object-put.go @@ -43,15 +43,15 @@ func (c S3ApiController) PutObjectTagging(ctx *fiber.Ctx) (*Response, error) { parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: key, - Action: auth.PutObjectTaggingAction, - IsBucketPublic: IsBucketPublic, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.PutObjectTaggingAction, + IsPublicRequest: IsBucketPublic, }) if err != nil { return &Response{ @@ -90,15 +90,15 @@ func (c S3ApiController) PutObjectRetention(ctx *fiber.Ctx) (*Response, error) { parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) if err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: key, - Action: auth.PutObjectRetentionAction, - IsBucketPublic: IsBucketPublic, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.PutObjectRetentionAction, + IsPublicRequest: IsBucketPublic, }); err != nil { return &Response{ MetaOpts: &MetaOptions{ @@ -146,15 +146,15 @@ func (c S3ApiController) PutObjectLegalHold(ctx *fiber.Ctx) (*Response, error) { parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL) if err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: key, - Action: auth.PutObjectLegalHoldAction, - IsBucketPublic: IsBucketPublic, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.PutObjectLegalHoldAction, + IsPublicRequest: IsBucketPublic, }); err != nil { return &Response{ MetaOpts: &MetaOptions{ @@ -214,15 +214,15 @@ func (c S3ApiController) UploadPart(ctx *fiber.Ctx) (*Response, error) { err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: key, - Action: auth.PutObjectAction, - IsBucketPublic: IsBucketPublic, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.PutObjectAction, + IsPublicRequest: IsBucketPublic, }) if err != nil { return &Response{ @@ -329,14 +329,14 @@ func (c S3ApiController) UploadPartCopy(ctx *fiber.Ctx) (*Response, error) { err = auth.VerifyObjectCopyAccess(ctx.Context(), c.be, copySource, auth.AccessOptions{ - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: key, - Action: auth.PutObjectAction, - IsBucketPublic: IsBucketPublic, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.PutObjectAction, + IsPublicRequest: IsBucketPublic, }) if err != nil { return &Response{ @@ -628,15 +628,15 @@ func (c S3ApiController) PutObject(ctx *fiber.Ctx) (*Response, error) { err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ - Readonly: c.readonly, - Acl: parsedAcl, - AclPermission: auth.PermissionWrite, - IsRoot: isRoot, - Acc: acct, - Bucket: bucket, - Object: key, - Action: auth.PutObjectAction, - IsBucketPublic: IsBucketPublic, + Readonly: c.readonly, + Acl: parsedAcl, + AclPermission: auth.PermissionWrite, + IsRoot: isRoot, + Acc: acct, + Bucket: bucket, + Object: key, + Action: auth.PutObjectAction, + IsPublicRequest: IsBucketPublic, }) if err != nil { return &Response{ diff --git a/s3api/middlewares/public-bucket.go b/s3api/middlewares/public-bucket.go index 5238308..11a62ec 100644 --- a/s3api/middlewares/public-bucket.go +++ b/s3api/middlewares/public-bucket.go @@ -30,7 +30,7 @@ import ( // access to anonymous requesters func AuthorizePublicBucketAccess(be backend.Backend, s3action string, policyPermission auth.Action, permission auth.Permission) fiber.Handler { return func(ctx *fiber.Ctx) error { - // skip for auhtneicated requests + // skip for authenticated requests if ctx.Query("X-Amz-Algorithm") != "" || ctx.Get("Authorization") != "" { return nil } diff --git a/s3api/utils/utils.go b/s3api/utils/utils.go index 9ce6670..e327810 100644 --- a/s3api/utils/utils.go +++ b/s3api/utils/utils.go @@ -779,3 +779,20 @@ func ValidateCopySource(copysource string) error { return nil } + +// GetQueryParam returns a pointer to the query parameter value if it exists +func GetQueryParam(ctx *fiber.Ctx, key string) *string { + value := ctx.Query(key) + if value == "" { + return nil + } + return &value +} + +// ApplyOverride returns the override value if it exists and status is 200, otherwise returns original +func ApplyOverride(original, override *string) *string { + if override != nil { + return override + } + return original +} diff --git a/s3err/s3err.go b/s3err/s3err.go index ce249fa..7b4d499 100644 --- a/s3err/s3err.go +++ b/s3err/s3err.go @@ -64,6 +64,7 @@ const ( ErrAnonymousCopyObject ErrAnonymousPutBucketOwnership ErrAnonymousGetBucketOwnership + ErrAnonymousResponseHeaders ErrMethodNotAllowed ErrBucketNotEmpty ErrVersionedBucketNotEmpty @@ -225,6 +226,11 @@ var errorCodeResponse = map[ErrorCode]APIError{ Description: "s3:GetBucketOwnershipControls does not support Anonymous requests!", HTTPStatusCode: http.StatusForbidden, }, + ErrAnonymousResponseHeaders: { + Code: "InvalidRequest", + Description: "Request specific response headers cannot be used for anonymous GET requests.", + HTTPStatusCode: http.StatusBadRequest, + }, ErrMethodNotAllowed: { Code: "MethodNotAllowed", Description: "The specified method is not allowed against this resource.", diff --git a/tests/integration/group-tests.go b/tests/integration/group-tests.go index 764aa21..b1be74b 100644 --- a/tests/integration/group-tests.go +++ b/tests/integration/group-tests.go @@ -211,6 +211,9 @@ func TestGetObject(s *S3Conf) { GetObject_directory_success(s) GetObject_by_range_resp_status(s) GetObject_non_existing_dir_object(s) + GetObject_overrides_success(s) + GetObject_overrides_presign_success(s) + GetObject_overrides_fail_public(s) } func TestListObjects(s *S3Conf) { @@ -896,7 +899,7 @@ func TestAccessControl(s *S3Conf) { } func TestPublicBuckets(s *S3Conf) { - PublicBucket_default_privet_bucket(s) + PublicBucket_default_private_bucket(s) PublicBucket_public_bucket_policy(s) PublicBucket_public_object_policy(s) PublicBucket_public_acl(s) @@ -1133,6 +1136,9 @@ func GetIntTests() IntTests { "GetObject_directory_success": GetObject_directory_success, "GetObject_by_range_resp_status": GetObject_by_range_resp_status, "GetObject_non_existing_dir_object": GetObject_non_existing_dir_object, + "GetObject_overrides_success": GetObject_overrides_success, + "GetObject_overrides_presign_success": GetObject_overrides_presign_success, + "GetObject_overrides_fail_public": GetObject_overrides_fail_public, "ListObjects_non_existing_bucket": ListObjects_non_existing_bucket, "ListObjects_with_prefix": ListObjects_with_prefix, "ListObjects_truncated": ListObjects_truncated, @@ -1484,7 +1490,7 @@ 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, - "PublicBucket_default_privet_bucket": PublicBucket_default_privet_bucket, + "PublicBucket_default_private_bucket": PublicBucket_default_private_bucket, "PublicBucket_public_bucket_policy": PublicBucket_public_bucket_policy, "PublicBucket_public_object_policy": PublicBucket_public_object_policy, "PublicBucket_public_acl": PublicBucket_public_acl, diff --git a/tests/integration/tests.go b/tests/integration/tests.go index 07fd0b9..bc70f32 100644 --- a/tests/integration/tests.go +++ b/tests/integration/tests.go @@ -4773,6 +4773,405 @@ func GetObject_non_existing_dir_object(s *S3Conf) error { }) } +func GetObject_overrides_success(s *S3Conf) error { + testName := "GetObject_overrides_success" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + // Test data + objKey := "test-object" + objContent := "test content for response overrides" + exp := time.Now() + + // Put an object first + _, err := s3client.PutObject(context.Background(), &s3.PutObjectInput{ + Bucket: &bucket, + Key: &objKey, + Body: strings.NewReader(objContent), + }) + if err != nil { + return fmt.Errorf("failed to put object: %v", err) + } + + for _, test := range []PublicBucketTestCase{ + { + Action: "GetObject", + Call: func(ctx context.Context) error { + _, err := s3client.GetObject(ctx, &s3.GetObjectInput{ + Bucket: &bucket, + Key: &objKey, + ResponseCacheControl: getPtr("max-age=90"), + }) + return err + }, + ExpectedErr: nil, + }, + { + Action: "GetObject", + Call: func(ctx context.Context) error { + _, err := s3client.GetObject(ctx, &s3.GetObjectInput{ + Bucket: &bucket, + Key: &objKey, + ResponseContentDisposition: getPtr("inline"), + }) + return err + }, + ExpectedErr: nil, + }, + { + Action: "GetObject", + Call: func(ctx context.Context) error { + _, err := s3client.GetObject(ctx, &s3.GetObjectInput{ + Bucket: &bucket, + Key: &objKey, + ResponseContentEncoding: getPtr("txt"), + }) + return err + }, + ExpectedErr: nil, + }, + { + Action: "GetObject", + Call: func(ctx context.Context) error { + _, err := s3client.GetObject(ctx, &s3.GetObjectInput{ + Bucket: &bucket, + Key: &objKey, + ResponseContentLanguage: getPtr("en"), + }) + return err + }, + ExpectedErr: nil, + }, + { + Action: "GetObject", + Call: func(ctx context.Context) error { + _, err := s3client.GetObject(ctx, &s3.GetObjectInput{ + Bucket: &bucket, + Key: &objKey, + ResponseContentType: getPtr("application/json"), + }) + return err + }, + ExpectedErr: nil, + }, + { + Action: "GetObject", + Call: func(ctx context.Context) error { + _, err := s3client.GetObject(ctx, &s3.GetObjectInput{ + Bucket: &bucket, + Key: &objKey, + ResponseExpires: &exp, + }) + return err + }, + ExpectedErr: nil, + }, + } { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + err := test.Call(ctx) + cancel() + if err == nil && test.ExpectedErr != nil { + return fmt.Errorf("%v: expected err %v, instead got successful response", test.Action, test.ExpectedErr) + } + if err != nil { + if test.ExpectedErr == nil { + return fmt.Errorf("%v: expected no error, instead got %v", test.Action, err) + } + + apiErr, ok := test.ExpectedErr.(s3err.APIError) + if !ok { + return fmt.Errorf("invalid error type provided in the test, expected s3err.APIError") + } + + if err := checkApiErr(err, apiErr); err != nil { + return err + } + } + } + + return nil + }) +} + +func GetObject_overrides_presign_success(s *S3Conf) error { + testName := "GetObject_overrides_presign_success" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + // Test data + objKey := "test-object" + objContent := "test content for response overrides" + + // Put an object first + _, err := s3client.PutObject(context.Background(), &s3.PutObjectInput{ + Bucket: &bucket, + Key: &objKey, + Body: strings.NewReader(objContent), + }) + if err != nil { + return fmt.Errorf("failed to put object: %v", err) + } + + // Test cases for each response override parameter + testCases := []struct { + name string + queryParam string + expectedHeader string + expectedValue string + }{ + { + name: "response-cache-control", + queryParam: "response-cache-control=no-cache", + expectedHeader: "Cache-Control", + expectedValue: "no-cache", + }, + { + name: "response-content-disposition", + queryParam: "response-content-disposition=attachment%3B%20filename%3D%22test.txt%22", + expectedHeader: "Content-Disposition", + expectedValue: "attachment; filename=\"test.txt\"", + }, + { + name: "response-content-encoding", + queryParam: "response-content-encoding=txt", + expectedHeader: "Content-Encoding", + expectedValue: "txt", + }, + { + name: "response-content-language", + queryParam: "response-content-language=en-US", + expectedHeader: "Content-Language", + expectedValue: "en-US", + }, + { + name: "response-content-type", + queryParam: "response-content-type=text%2Fplain", + expectedHeader: "Content-Type", + expectedValue: "text/plain", + }, + { + name: "response-expires", + queryParam: "response-expires=Thu%2C%2001%20Dec%202024%2016%3A00%3A00%20GMT", + expectedHeader: "Expires", + expectedValue: "Thu, 01 Dec 2024 16:00:00 GMT", + }, + } + + // Test each override parameter individually + for _, tc := range testCases { + // Create a signed request with the response override parameter + req, err := createSignedReq( + http.MethodGet, + s.endpoint, + fmt.Sprintf("%s/%s?%s", bucket, objKey, tc.queryParam), + s.awsID, + s.awsSecret, + "s3", + s.awsRegion, + nil, + time.Now(), + nil, + ) + if err != nil { + return fmt.Errorf("failed to create signed request for %s: %v", tc.name, err) + } + + resp, err := s.httpClient.Do(req) + if err != nil { + return fmt.Errorf("failed to execute request for %s: %v", tc.name, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("expected status 200 for %s, got %d", tc.name, resp.StatusCode) + } + + // Verify the response override header is set correctly + actualValue := resp.Header.Get(tc.expectedHeader) + if actualValue != tc.expectedValue { + return fmt.Errorf("expected %s header to be %q for %s, got %q", + tc.expectedHeader, tc.expectedValue, tc.name, actualValue) + } + + // Verify content is still correct + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to read response body for %s: %v", tc.name, err) + } + + if string(body) != objContent { + return fmt.Errorf("expected content %q for %s, got %q", objContent, tc.name, string(body)) + } + } + + // Test multiple override parameters together + multiParam := "response-cache-control=max-age%3D3600&response-content-type=application%2Fjson&response-content-disposition=inline" + req, err := createSignedReq( + http.MethodGet, + s.endpoint, + fmt.Sprintf("%s/%s?%s", bucket, objKey, multiParam), + s.awsID, + s.awsSecret, + "s3", + s.awsRegion, + nil, + time.Now(), + nil, + ) + if err != nil { + return fmt.Errorf("failed to create signed request for multiple overrides: %v", err) + } + + resp, err := s.httpClient.Do(req) + if err != nil { + return fmt.Errorf("failed to execute request for multiple overrides: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("expected status 200 for multiple overrides, got %d", resp.StatusCode) + } + + // Verify all override headers are set correctly + expectedHeaders := map[string]string{ + "Cache-Control": "max-age=3600", + "Content-Type": "application/json", + "Content-Disposition": "inline", + } + + for headerName, expectedValue := range expectedHeaders { + actualValue := resp.Header.Get(headerName) + if actualValue != expectedValue { + return fmt.Errorf("expected %s header to be %q for multiple overrides, got %q", + headerName, expectedValue, actualValue) + } + } + + return nil + }) +} + +func GetObject_overrides_fail_public(s *S3Conf) error { + testName := "GetObject_overrides_fail_public" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + rootClient := s.GetClient() + // Grant public access to the bucket for bucket operations + err := grantPublicBucketPolicy(rootClient, bucket, policyTypeObject) + if err != nil { + return err + } + + // Test data + objKey := "test-object" + objContent := "test content for response overrides" + exp := time.Now() + + // Put an object first + _, err = rootClient.PutObject(context.Background(), &s3.PutObjectInput{ + Bucket: &bucket, + Key: &objKey, + Body: strings.NewReader(objContent), + }) + if err != nil { + return fmt.Errorf("failed to put object: %v", err) + } + + for _, test := range []PublicBucketTestCase{ + { + Action: "GetObject", + Call: func(ctx context.Context) error { + _, err := s3client.GetObject(ctx, &s3.GetObjectInput{ + Bucket: &bucket, + Key: &objKey, + ResponseCacheControl: getPtr("max-age=90"), + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousResponseHeaders), + }, + { + Action: "GetObject", + Call: func(ctx context.Context) error { + _, err := s3client.GetObject(ctx, &s3.GetObjectInput{ + Bucket: &bucket, + Key: &objKey, + ResponseContentDisposition: getPtr("inline"), + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousResponseHeaders), + }, + { + Action: "GetObject", + Call: func(ctx context.Context) error { + _, err := s3client.GetObject(ctx, &s3.GetObjectInput{ + Bucket: &bucket, + Key: &objKey, + ResponseContentEncoding: getPtr("txt"), + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousResponseHeaders), + }, + { + Action: "GetObject", + Call: func(ctx context.Context) error { + _, err := s3client.GetObject(ctx, &s3.GetObjectInput{ + Bucket: &bucket, + Key: &objKey, + ResponseContentLanguage: getPtr("en"), + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousResponseHeaders), + }, + { + Action: "GetObject", + Call: func(ctx context.Context) error { + _, err := s3client.GetObject(ctx, &s3.GetObjectInput{ + Bucket: &bucket, + Key: &objKey, + ResponseContentType: getPtr("application/json"), + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousResponseHeaders), + }, + { + Action: "GetObject", + Call: func(ctx context.Context) error { + _, err := s3client.GetObject(ctx, &s3.GetObjectInput{ + Bucket: &bucket, + Key: &objKey, + ResponseExpires: &exp, + }) + return err + }, + ExpectedErr: s3err.GetAPIError(s3err.ErrAnonymousResponseHeaders), + }, + } { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + err := test.Call(ctx) + cancel() + if err == nil && test.ExpectedErr != nil { + return fmt.Errorf("%v: expected err %v, instead got successful response", test.Action, test.ExpectedErr) + } + if err != nil { + if test.ExpectedErr == nil { + return fmt.Errorf("%v: expected no error, instead got %v", test.Action, err) + } + + apiErr, ok := test.ExpectedErr.(s3err.APIError) + if !ok { + return fmt.Errorf("invalid error type provided in the test, expected s3err.APIError") + } + + if err := checkApiErr(err, apiErr); err != nil { + return err + } + } + } + + return nil + }, withAnonymousClient()) +} + func ListObjects_non_existing_bucket(s *S3Conf) error { testName := "ListObjects_non_existing_bucket" return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { @@ -17194,8 +17593,8 @@ func AccessControl_copy_object_with_starting_slash_for_user(s *S3Conf) error { } // Public bucket tests -func PublicBucket_default_privet_bucket(s *S3Conf) error { - testName := "PublicBucket_default_privet_bucket" +func PublicBucket_default_private_bucket(s *S3Conf) error { + testName := "PublicBucket_default_private_bucket" return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { partNumber := int32(1)