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
This commit is contained in:
Ben McClelland
2025-08-29 17:08:48 -07:00
parent 2015cc1ab2
commit 8cad7fd6d9
16 changed files with 827 additions and 370 deletions

View File

@@ -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 {

View File

@@ -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{

View File

@@ -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{

View File

@@ -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{

View File

@@ -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{

View File

@@ -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{

View File

@@ -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{

View File

@@ -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),

View File

@@ -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{

View File

@@ -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{

View File

@@ -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{

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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.",

View File

@@ -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,

View File

@@ -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)